From 9f06dda8693b473068d6fb5e48e6aa39c8a27cf1 Mon Sep 17 00:00:00 2001 From: Raph Levien Date: Mon, 11 Apr 2022 20:47:25 -0700 Subject: [PATCH 01/28] Initial commit of idiopath experiment This is similar to lasagna but with strongly typed elements. Let's see how it goes. --- Cargo.toml | 1 + idiopath/Cargo.toml | 8 ++ idiopath/src/app.rs | 89 +++++++++++++++++++++ idiopath/src/event.rs | 22 +++++ idiopath/src/id.rs | 34 ++++++++ idiopath/src/main.rs | 146 ++++++++++++++++++++++++++++++++++ idiopath/src/view.rs | 47 +++++++++++ idiopath/src/view/adapt.rs | 92 +++++++++++++++++++++ idiopath/src/view/button.rs | 71 +++++++++++++++++ idiopath/src/view/column.rs | 70 ++++++++++++++++ idiopath/src/view/memoize.rs | 78 ++++++++++++++++++ idiopath/src/view_tuple.rs | 111 ++++++++++++++++++++++++++ idiopath/src/widget.rs | 76 ++++++++++++++++++ idiopath/src/widget/button.rs | 66 +++++++++++++++ idiopath/src/widget/column.rs | 75 +++++++++++++++++ 15 files changed, 986 insertions(+) create mode 100644 idiopath/Cargo.toml create mode 100644 idiopath/src/app.rs create mode 100644 idiopath/src/event.rs create mode 100644 idiopath/src/id.rs create mode 100644 idiopath/src/main.rs create mode 100644 idiopath/src/view.rs create mode 100644 idiopath/src/view/adapt.rs create mode 100644 idiopath/src/view/button.rs create mode 100644 idiopath/src/view/column.rs create mode 100644 idiopath/src/view/memoize.rs create mode 100644 idiopath/src/view_tuple.rs create mode 100644 idiopath/src/widget.rs create mode 100644 idiopath/src/widget/button.rs create mode 100644 idiopath/src/widget/column.rs diff --git a/Cargo.toml b/Cargo.toml index 4815d2d075..ee804ea7e7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ members = [ "druid/examples/web", "druid/examples/hello_web", "druid/examples/value_formatting", + "idiopath", ] default-members = [ "druid", diff --git a/idiopath/Cargo.toml b/idiopath/Cargo.toml new file mode 100644 index 0000000000..148802e512 --- /dev/null +++ b/idiopath/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "idiopath" +version = "0.1.0" +authors = ["Raph Levien "] +edition = "2021" + +[dependencies] +"druid-shell" = { path = "../druid-shell" } diff --git a/idiopath/src/app.rs b/idiopath/src/app.rs new file mode 100644 index 0000000000..a0107c7508 --- /dev/null +++ b/idiopath/src/app.rs @@ -0,0 +1,89 @@ +// Copyright 2022 The Druid Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use druid_shell::{kurbo::Point, piet::Piet}; + +use crate::{ + event::Event, + id::{Id, IdPath}, + view::View, + widget::{RawEvent, Widget}, +}; + +pub struct App, F: FnMut(&mut T) -> V> +where + V::Element: Widget, +{ + data: T, + app_logic: F, + view: V, + id: Id, + state: V::State, + element: V::Element, + events: Vec, + id_path: IdPath, +} + +impl, F: FnMut(&mut T) -> V> App +where + V::Element: Widget, +{ + pub fn new(mut data: T, mut app_logic: F) -> Self { + let mut id_path = IdPath::default(); + let view = (app_logic)(&mut data); + let (id, state, element) = view.build(&mut id_path); + assert!(id_path.is_empty(), "id path imbalance"); + App { + data, + app_logic, + view, + id, + state, + element, + events: Vec::new(), + id_path, + } + } + + pub fn paint(&mut self, piet: &mut Piet) { + self.element.layout(); + self.element.paint(piet, Point::ZERO); + } + + pub fn mouse_down(&mut self, point: Point) { + self.event(RawEvent::MouseDown(point)); + } + + fn event(&mut self, event: RawEvent) { + self.element.event(&event, &mut self.events); + self.run_app_logic(); + } + + pub fn run_app_logic(&mut self) { + for event in self.events.drain(..) { + let id_path = &event.id_path[1..]; + self.view + .event(id_path, &self.state, event.body, &mut self.data); + } + let view = (self.app_logic)(&mut self.data); + view.rebuild( + &mut self.id_path, + &self.view, + &mut self.id, + &mut self.state, + &mut self.element, + ); + self.view = view; + } +} diff --git a/idiopath/src/event.rs b/idiopath/src/event.rs new file mode 100644 index 0000000000..a6e1553a52 --- /dev/null +++ b/idiopath/src/event.rs @@ -0,0 +1,22 @@ +// Copyright 2022 The Druid Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::any::Any; + +use crate::id::IdPath; + +pub struct Event { + pub id_path: IdPath, + pub body: Box, +} diff --git a/idiopath/src/id.rs b/idiopath/src/id.rs new file mode 100644 index 0000000000..2bf6889e7d --- /dev/null +++ b/idiopath/src/id.rs @@ -0,0 +1,34 @@ +// Copyright 2022 The Druid Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::num::NonZeroU64; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Hash)] +/// A stable identifier for an element. +pub struct Id(NonZeroU64); + +pub type IdPath = Vec; + +impl Id { + /// Allocate a new, unique `Id`. + pub fn next() -> Id { + use druid_shell::Counter; + static WIDGET_ID_COUNTER: Counter = Counter::new(); + Id(WIDGET_ID_COUNTER.next_nonzero()) + } + + pub fn to_raw(self) -> u64 { + self.0.into() + } +} diff --git a/idiopath/src/main.rs b/idiopath/src/main.rs new file mode 100644 index 0000000000..24bec960a4 --- /dev/null +++ b/idiopath/src/main.rs @@ -0,0 +1,146 @@ +// Copyright 2022 The Druid Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +mod app; +mod event; +mod id; +mod view; +mod view_tuple; +mod widget; + +use std::any::Any; + +use app::App; +use druid_shell::kurbo::Size; +use druid_shell::piet::{Color, RenderContext}; + +use druid_shell::{ + Application, Cursor, HotKey, Menu, MouseEvent, Region, SysMods, WinHandler, WindowBuilder, + WindowHandle, +}; +use view::button::Button; +use view::View; +use widget::Widget; + +const BG_COLOR: Color = Color::rgb8(0x27, 0x28, 0x22); + +struct MainState, F: FnMut(&mut T) -> V> +where + V::Element: Widget, +{ + size: Size, + handle: WindowHandle, + app: App, +} + +impl + 'static, F: FnMut(&mut T) -> V + 'static> WinHandler + for MainState +where + V::Element: Widget, +{ + fn connect(&mut self, handle: &WindowHandle) { + self.handle = handle.clone(); + } + + fn prepare_paint(&mut self) {} + + fn paint(&mut self, piet: &mut druid_shell::piet::Piet, _: &Region) { + let rect = self.size.to_rect(); + piet.fill(rect, &BG_COLOR); + self.app.paint(piet); + } + + fn command(&mut self, id: u32) { + match id { + 0x100 => { + self.handle.close(); + Application::global().quit() + } + _ => println!("unexpected id {}", id), + } + } + + fn mouse_move(&mut self, _event: &MouseEvent) { + self.handle.set_cursor(&Cursor::Arrow); + } + + fn mouse_down(&mut self, event: &MouseEvent) { + self.app.mouse_down(event.pos); + self.handle.invalidate(); + } + + fn mouse_up(&mut self, _event: &MouseEvent) {} + + fn size(&mut self, size: Size) { + self.size = size; + } + + fn request_close(&mut self) { + self.handle.close(); + } + + fn destroy(&mut self) { + Application::global().quit() + } + + fn as_any(&mut self) -> &mut dyn Any { + self + } +} + +impl, F: FnMut(&mut T) -> V> MainState +where + V::Element: Widget, +{ + fn new(app: App) -> Self { + let state = MainState { + size: Default::default(), + handle: Default::default(), + app, + }; + state + } +} + +fn app_logic(data: &mut u32) -> impl View { + Button::new(format!("count: {}", data), |data| *data += 1) +} + +fn main() { + //tracing_subscriber::fmt().init(); + let mut file_menu = Menu::new(); + file_menu.add_item( + 0x100, + "E&xit", + Some(&HotKey::new(SysMods::Cmd, "q")), + true, + false, + ); + let mut menubar = Menu::new(); + menubar.add_dropdown(Menu::new(), "Application", true); + menubar.add_dropdown(file_menu, "&File", true); + + let app = App::new(0, app_logic); + let druid_app = Application::new().unwrap(); + let mut builder = WindowBuilder::new(druid_app.clone()); + let main_state = MainState::new(app); + builder.set_handler(Box::new(main_state)); + builder.set_title("Idiopath"); + builder.set_menu(menubar); + + let window = builder.build().unwrap(); + window.show(); + + druid_app.run(None); +} diff --git a/idiopath/src/view.rs b/idiopath/src/view.rs new file mode 100644 index 0000000000..004c00a6ef --- /dev/null +++ b/idiopath/src/view.rs @@ -0,0 +1,47 @@ +// Copyright 2022 The Druid Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +pub mod adapt; +pub mod button; +pub mod column; +pub mod memoize; + +use std::any::Any; + +use crate::id::{Id, IdPath}; + +pub trait View { + type State; + + type Element; + + fn build(&self, id_path: &mut IdPath) -> (Id, Self::State, Self::Element); + + fn rebuild( + &self, + id_path: &mut IdPath, + prev: &Self, + id: &mut Id, + state: &mut Self::State, + element: &mut Self::Element, + ); + + fn event( + &self, + id_path: &[Id], + state: &Self::State, + event: Box, + app_state: &mut T, + ) -> A; +} diff --git a/idiopath/src/view/adapt.rs b/idiopath/src/view/adapt.rs new file mode 100644 index 0000000000..35c4d47dcc --- /dev/null +++ b/idiopath/src/view/adapt.rs @@ -0,0 +1,92 @@ +// Copyright 2022 The Druid Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::{any::Any, marker::PhantomData}; + +use crate::id::{Id, IdPath}; + +use super::View; + +pub struct Adapt) -> A, C: View> { + f: F, + child: C, + phantom: PhantomData<(T, A, U, B)>, +} + +/// A "thunk" which dispatches an event to an adapt node's child. +/// +/// The closure passed to Adapt should call this thunk with the child's +/// app state. +pub struct AdaptThunk<'a, U, B, C: View> { + child: &'a C, + state: &'a C::State, + id_path: &'a [Id], + event: Box, +} + +impl) -> A, C: View> Adapt { + pub fn new(f: F, child: C) -> Self { + Adapt { + f, + child, + phantom: Default::default(), + } + } +} + +impl<'a, U, B, C: View> AdaptThunk<'a, U, B, C> { + pub fn call(self, app_state: &mut U) -> B { + self.child + .event(self.id_path, self.state, self.event, app_state) + } +} + +impl) -> A, C: View> View + for Adapt +{ + type State = C::State; + + type Element = C::Element; + + fn build(&self, id_path: &mut IdPath) -> (Id, Self::State, Self::Element) { + self.child.build(id_path) + } + + fn rebuild( + &self, + id_path: &mut IdPath, + prev: &Self, + id: &mut Id, + state: &mut Self::State, + element: &mut Self::Element, + ) { + self.child.rebuild(id_path, &prev.child, id, state, element); + } + + fn event( + &self, + id_path: &[Id], + state: &Self::State, + event: Box, + app_state: &mut T, + ) -> A { + let thunk = AdaptThunk { + child: &self.child, + state, + id_path, + event, + }; + (self.f)(app_state, thunk) + } +} diff --git a/idiopath/src/view/button.rs b/idiopath/src/view/button.rs new file mode 100644 index 0000000000..300aac7df2 --- /dev/null +++ b/idiopath/src/view/button.rs @@ -0,0 +1,71 @@ +// Copyright 2022 The Druid Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::any::Any; + +use crate::id::{Id, IdPath}; + +use super::View; + +pub struct Button { + label: String, + // consider not boxing + callback: Box A>, +} + +impl Button { + pub fn new(label: impl Into, clicked: impl Fn(&mut T) -> A + 'static) -> Self { + Button { + label: label.into(), + callback: Box::new(clicked), + } + } +} + +impl View for Button { + type State = (); + + type Element = crate::widget::button::Button; + + fn build(&self, id_path: &mut IdPath) -> (Id, Self::State, Self::Element) { + let id = Id::next(); + id_path.push(id); + let element = crate::widget::button::Button::new(&id_path, self.label.clone()); + id_path.pop(); + (id, (), element) + } + + fn rebuild( + &self, + _id_path: &mut crate::id::IdPath, + prev: &Self, + _id: &mut crate::id::Id, + _state: &mut Self::State, + element: &mut Self::Element, + ) { + if prev.label != self.label { + element.set_label(self.label.clone()); + } + } + + fn event( + &self, + _id_path: &[crate::id::Id], + _state: &Self::State, + _event: Box, + app_state: &mut T, + ) -> A { + (self.callback)(app_state) + } +} diff --git a/idiopath/src/view/column.rs b/idiopath/src/view/column.rs new file mode 100644 index 0000000000..291171c789 --- /dev/null +++ b/idiopath/src/view/column.rs @@ -0,0 +1,70 @@ +// Copyright 2022 The Druid Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::{any::Any, marker::PhantomData}; + +use crate::{ + id::{Id, IdPath}, + view_tuple::ViewTuple, + widget::WidgetTuple, +}; + +use super::View; + +pub struct Column> { + children: VT, + phantom: PhantomData<(T, A)>, +} + +impl> View for Column +where + VT::Elements: WidgetTuple, +{ + type State = VT::State; + + type Element = crate::widget::column::Column; + + fn build(&self, id_path: &mut IdPath) -> (Id, Self::State, Self::Element) { + let id = Id::next(); + id_path.push(id); + let (state, elements) = self.children.build(id_path); + id_path.pop(); + let column = crate::widget::column::Column::new(elements); + (id, state, column) + } + + fn rebuild( + &self, + id_path: &mut IdPath, + prev: &Self, + id: &mut Id, + state: &mut Self::State, + element: &mut Self::Element, + ) { + id_path.push(*id); + self.children + .rebuild(id_path, &prev.children, state, element.children_mut()); + id_path.pop(); + } + + fn event( + &self, + id_path: &[Id], + state: &Self::State, + event: Box, + app_state: &mut T, + ) -> A { + self.children.event(id_path, state, event, app_state) + } +} diff --git a/idiopath/src/view/memoize.rs b/idiopath/src/view/memoize.rs new file mode 100644 index 0000000000..9f9553380c --- /dev/null +++ b/idiopath/src/view/memoize.rs @@ -0,0 +1,78 @@ +// Copyright 2022 The Druid Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::any::Any; + +use crate::id::{Id, IdPath}; + +use super::View; + +pub struct Memoize { + data: D, + child_cb: F, +} + +pub struct MemoizeState> { + view: V, + view_state: V::State, +} + +impl V> Memoize { + pub fn new(data: D, child_cb: F) -> Self { + Memoize { data, child_cb } + } +} + +impl, F: Fn(&D) -> V> View + for Memoize +{ + type State = MemoizeState; + + type Element = V::Element; + + fn build(&self, id_path: &mut IdPath) -> (Id, Self::State, Self::Element) { + let view = (self.child_cb)(&self.data); + let (id, view_state, element) = view.build(id_path); + let memoize_state = MemoizeState { + view, + view_state + }; + (id, memoize_state, element) + } + + fn rebuild( + &self, + id_path: &mut IdPath, + prev: &Self, + id: &mut Id, + state: &mut Self::State, + element: &mut Self::Element, + ) { + if prev.data != self.data { + let view = (self.child_cb)(&self.data); + view.rebuild(id_path, &state.view, id, &mut state.view_state, element); + state.view = view; + } + } + + fn event( + &self, + id_path: &[Id], + state: &Self::State, + event: Box, + app_state: &mut T, + ) -> A { + state.view.event(id_path, &state.view_state, event, app_state) + } +} diff --git a/idiopath/src/view_tuple.rs b/idiopath/src/view_tuple.rs new file mode 100644 index 0000000000..a8ce67d4f8 --- /dev/null +++ b/idiopath/src/view_tuple.rs @@ -0,0 +1,111 @@ +// Copyright 2022 The Druid Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::any::Any; + +use crate::{ + id::{Id, IdPath}, + view::View, +}; + +pub trait ViewTuple { + type State; + + type Elements; + + fn build(&self, id_path: &mut IdPath) -> (Self::State, Self::Elements); + + fn rebuild( + &self, + id_path: &mut IdPath, + prev: &Self, + state: &mut Self::State, + els: &mut Self::Elements, + ); + + fn event( + &self, + id_path: &[Id], + state: &Self::State, + event: Box, + app_state: &mut T, + ) -> A; +} + +macro_rules! impl_view_tuple { + ( $n: tt; $( $t:ident),* ; $( $s:tt ),* ) => { + impl ),* > ViewTuple for ( $( $t, )* ) { + type State = ( $( $t::State, )* [Id; $n]); + + type Elements = ( $( $t::Element, )* ); + + fn build(&self, id_path: &mut IdPath) -> (Self::State, Self::Elements) { + let b = ( $( self.$s.build(id_path), )* ); + let state = ( $( b.$s.1, )* [ $( b.$s.0 ),* ]); + let els = ( $( b.$s.2, )* ); + (state, els) + } + + fn rebuild( + &self, + id_path: &mut IdPath, + prev: &Self, + state: &mut Self::State, + els: &mut Self::Elements, + ) { + $( + self.$s + .rebuild(id_path, &prev.$s, &mut state.$n[$s], &mut state.$s, &mut els.$s); + )* + } + + fn event( + &self, + id_path: &[Id], + state: &Self::State, + event: Box, + app_state: &mut T, + ) -> A { + let hd = id_path[0]; + let tl = &id_path[1..]; + $( + if hd == state.$n[$s] { + self.$s.event(tl, &state.$s, event, app_state) + } else )* { + panic!("inconsistent id_path") + } + } + } + } +} + +impl_view_tuple!(1; V0; 0); +impl_view_tuple!(2; V0, V1; 0, 1); +impl_view_tuple!(3; V0, V1, V2; 0, 1, 2); +impl_view_tuple!(4; V0, V1, V2, V3; 0, 1, 2, 3); +impl_view_tuple!(5; V0, V1, V2, V3, V4; 0, 1, 2, 3, 4); +impl_view_tuple!(6; V0, V1, V2, V3, V4, V5; 0, 1, 2, 3, 4, 5); +impl_view_tuple!(7; V0, V1, V2, V3, V4, V5, V6; 0, 1, 2, 3, 4, 5, 6); +impl_view_tuple!(8; + V0, V1, V2, V3, V4, V5, V6, V7; + 0, 1, 2, 3, 4, 5, 6, 7 +); +impl_view_tuple!(9; + V0, V1, V2, V3, V4, V5, V6, V7, V8; + 0, 1, 2, 3, 4, 5, 6, 7, 8 +); +impl_view_tuple!(10; + V0, V1, V2, V3, V4, V5, V6, V7, V8, V9; + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 +); diff --git a/idiopath/src/widget.rs b/idiopath/src/widget.rs new file mode 100644 index 0000000000..85e627db8c --- /dev/null +++ b/idiopath/src/widget.rs @@ -0,0 +1,76 @@ +// Copyright 2022 The Druid Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +pub mod button; +pub mod column; + +use std::any::Any; + +use druid_shell::kurbo::{Point, Size}; +use druid_shell::piet::Piet; + +use crate::event::Event; + +/// A basic widget trait. +pub trait Widget { + fn event(&mut self, event: &RawEvent, events: &mut Vec); + + fn layout(&mut self) -> Size; + + fn paint(&mut self, ctx: &mut Piet, pos: Point); +} + +// consider renaming, may get other stuff +#[derive(Default)] +pub struct Geom { + // probably want id? + size: Size, +} + +pub trait AnyWidget: Widget { + fn as_any(&self) -> &dyn Any; +} + +impl AnyWidget for W { + fn as_any(&self) -> &dyn Any { + self + } +} + +#[derive(Debug)] +pub enum RawEvent { + MouseDown(Point), +} + +pub struct BoxWidget(Box); + +pub trait WidgetTuple { + fn length(&self) -> usize; + + // Follows Panoramix; rethink to reduce allocation + fn widgets_mut(&mut self) -> Vec<&mut dyn AnyWidget>; +} + +impl WidgetTuple for (W0, W1) { + fn length(&self) -> usize { + 2 + } + + fn widgets_mut(&mut self) -> Vec<&mut dyn AnyWidget> { + let mut v: Vec<&mut dyn AnyWidget> = Vec::with_capacity(self.length()); + v.push(&mut self.0); + v.push(&mut self.1); + v + } +} diff --git a/idiopath/src/widget/button.rs b/idiopath/src/widget/button.rs new file mode 100644 index 0000000000..4b1b9b0599 --- /dev/null +++ b/idiopath/src/widget/button.rs @@ -0,0 +1,66 @@ +// Copyright 2022 The Druid Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use druid_shell::{ + kurbo::{Point, Size}, + piet::{Color, Piet, RenderContext, Text, TextLayoutBuilder}, +}; + +use crate::{event::Event, id::IdPath}; + +use super::Widget; + +#[derive(Default)] + +pub struct Button { + id_path: IdPath, + label: String, +} + +impl Button { + pub fn new(id_path: &IdPath, label: String) -> Button { + Button { + id_path: id_path.clone(), + label, + } + } + + pub fn set_label(&mut self, label: String) { + self.label = label; + } +} + +impl Widget for Button { + fn event(&mut self, _event: &super::RawEvent, events: &mut Vec) { + let body = Box::new(()); + events.push(Event { + id_path: self.id_path.clone(), + body, + }) + } + + fn layout(&mut self) -> Size { + Size::new(100., 20.) + } + + fn paint(&mut self, ctx: &mut Piet, pos: Point) { + let layout = ctx + .text() + .new_text_layout(self.label.clone()) + .text_color(Color::WHITE) + .build() + .unwrap(); + ctx.draw_text(&layout, pos); + } +} diff --git a/idiopath/src/widget/column.rs b/idiopath/src/widget/column.rs new file mode 100644 index 0000000000..258e6f5873 --- /dev/null +++ b/idiopath/src/widget/column.rs @@ -0,0 +1,75 @@ +// Copyright 2022 The Druid Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use druid_shell::{ + kurbo::{Point, Size, Vec2}, + piet::Piet, +}; + +use crate::event::Event; + +use super::{Geom, RawEvent, Widget, WidgetTuple}; + +pub struct Column { + children: W, + geoms: Vec, +} + +impl Column { + pub fn new(children: W) -> Self { + let geoms = (0..children.length()).map(|_| Geom::default()).collect(); + Column { children, geoms } + } + + pub fn children_mut(&mut self) -> &mut W { + &mut self.children + } +} + +impl Widget for Column { + fn event(&mut self, event: &super::RawEvent, events: &mut Vec) { + match event { + RawEvent::MouseDown(p) => { + let mut p = *p; + for (child, geom) in self.children.widgets_mut().into_iter().zip(&self.geoms) { + if p.y < geom.size.height { + let child_event = RawEvent::MouseDown(p); + child.event(&child_event, events); + break; + } + p.y -= geom.size.height; + } + } + } + } + + fn layout(&mut self) -> Size { + let mut size = Size::default(); + for (child, geom) in self.children.widgets_mut().into_iter().zip(&mut self.geoms) { + let child_size = child.layout(); + geom.size = child_size; + size.width = size.width.max(child_size.width); + size.height += child_size.height; + } + size + } + + fn paint(&mut self, ctx: &mut Piet, pos: Point) { + let mut child_pos = pos + Vec2::new(10.0, 0.0); + for (child, geom) in self.children.widgets_mut().into_iter().zip(&self.geoms) { + child.paint(ctx, child_pos); + child_pos.y += geom.size.height; + } + } +} From 1caa8cac940413f4c12ab46b9703f98c8293683b Mon Sep 17 00:00:00 2001 From: Raph Levien Date: Tue, 12 Apr 2022 15:02:28 -0700 Subject: [PATCH 02/28] Add AnyView Support for type erasure of views and widgets. --- idiopath/src/app.rs | 4 +- idiopath/src/main.rs | 5 +- idiopath/src/view.rs | 1 + idiopath/src/view/any_view.rs | 146 ++++++++++++++++++++++++++++++++++ idiopath/src/view/memoize.rs | 9 +-- idiopath/src/widget.rs | 23 +++++- 6 files changed, 179 insertions(+), 9 deletions(-) create mode 100644 idiopath/src/view/any_view.rs diff --git a/idiopath/src/app.rs b/idiopath/src/app.rs index a0107c7508..3b9f393e4b 100644 --- a/idiopath/src/app.rs +++ b/idiopath/src/app.rs @@ -43,7 +43,7 @@ where let mut id_path = IdPath::default(); let view = (app_logic)(&mut data); let (id, state, element) = view.build(&mut id_path); - assert!(id_path.is_empty(), "id path imbalance"); + assert!(id_path.is_empty(), "id path imbalance on build"); App { data, app_logic, @@ -76,6 +76,7 @@ where self.view .event(id_path, &self.state, event.body, &mut self.data); } + // Re-rendering should be more lazy. let view = (self.app_logic)(&mut self.data); view.rebuild( &mut self.id_path, @@ -84,6 +85,7 @@ where &mut self.state, &mut self.element, ); + assert!(self.id_path.is_empty(), "id path imbalance on rebuild"); self.view = view; } } diff --git a/idiopath/src/main.rs b/idiopath/src/main.rs index 24bec960a4..8cb5fc1ada 100644 --- a/idiopath/src/main.rs +++ b/idiopath/src/main.rs @@ -29,6 +29,7 @@ use druid_shell::{ Application, Cursor, HotKey, Menu, MouseEvent, Region, SysMods, WinHandler, WindowBuilder, WindowHandle, }; +use view::any_view::AnyView; use view::button::Button; use view::View; use widget::Widget; @@ -114,7 +115,9 @@ where } fn app_logic(data: &mut u32) -> impl View { - Button::new(format!("count: {}", data), |data| *data += 1) + let button = Button::new(format!("count: {}", data), |data| *data += 1); + let boxed: Box> = Box::new(button); + boxed } fn main() { diff --git a/idiopath/src/view.rs b/idiopath/src/view.rs index 004c00a6ef..f8b124419e 100644 --- a/idiopath/src/view.rs +++ b/idiopath/src/view.rs @@ -13,6 +13,7 @@ // limitations under the License. pub mod adapt; +pub mod any_view; pub mod button; pub mod column; pub mod memoize; diff --git a/idiopath/src/view/any_view.rs b/idiopath/src/view/any_view.rs new file mode 100644 index 0000000000..fcb276a8df --- /dev/null +++ b/idiopath/src/view/any_view.rs @@ -0,0 +1,146 @@ +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::{ + any::Any, + ops::{Deref, DerefMut}, +}; + +use crate::{ + id::{Id, IdPath}, + widget::AnyWidget, +}; + +use super::View; + +/// A trait enabling type erasure of views. +/// +/// The name is slightly misleading as it's not any view, but only ones +/// whose element is AnyWidget. +/// +/// Making a trait which is generic over another trait bound appears to +/// be well beyond the capability of Rust's type system. If type-erased +/// views with other bounds are needed, the best approach is probably +/// duplication of the code, probably with a macro. +pub trait AnyView { + fn as_any(&self) -> &dyn Any; + + fn dyn_build(&self, id_path: &mut IdPath) -> (Id, Box, Box); + + fn dyn_rebuild( + &self, + id_path: &mut IdPath, + prev: &dyn AnyView, + id: &mut Id, + state: &mut Box, + element: &mut Box, + ); + + fn dyn_event( + &self, + id_path: &[Id], + state: &dyn Any, + event: Box, + app_state: &mut T, + ) -> A; +} + +impl + 'static> AnyView for V +where + V::State: 'static, + V::Element: AnyWidget + 'static, +{ + fn as_any(&self) -> &dyn Any { + self + } + + fn dyn_build(&self, id_path: &mut IdPath) -> (Id, Box, Box) { + let (id, state, element) = self.build(id_path); + (id, Box::new(state), Box::new(element)) + } + + fn dyn_rebuild( + &self, + id_path: &mut IdPath, + prev: &dyn AnyView, + id: &mut Id, + state: &mut Box, + element: &mut Box, + ) { + if let Some(prev) = prev.as_any().downcast_ref() { + if let Some(state) = state.downcast_mut() { + if let Some(element) = element.deref_mut().as_any_mut().downcast_mut() { + self.rebuild(id_path, prev, id, state, element); + } else { + println!("downcast of element failed in dyn_event"); + } + } else { + println!("downcast of state failed in dyn_event"); + } + } else { + let (new_id, new_state, new_element) = self.build(id_path); + *id = new_id; + *state = Box::new(new_state); + *element = Box::new(new_element); + } + } + + fn dyn_event( + &self, + id_path: &[Id], + state: &dyn Any, + event: Box, + app_state: &mut T, + ) -> A { + if let Some(state) = state.downcast_ref() { + self.event(id_path, state, event, app_state) + } else { + // Possibly softer failure? Would require either Option return or + // Default bound on A. + panic!("downcast error in dyn_event"); + } + } +} + +impl View for Box> { + type State = Box; + + type Element = Box; + + fn build(&self, id_path: &mut IdPath) -> (Id, Self::State, Self::Element) { + self.deref().dyn_build(id_path) + } + + fn rebuild( + &self, + id_path: &mut IdPath, + prev: &Self, + id: &mut Id, + state: &mut Self::State, + element: &mut Self::Element, + ) { + self.deref() + .dyn_rebuild(id_path, prev.deref(), id, state, element); + } + + fn event( + &self, + id_path: &[Id], + state: &Self::State, + event: Box, + app_state: &mut T, + ) -> A { + self.deref() + .dyn_event(id_path, state.deref(), event, app_state) + } +} diff --git a/idiopath/src/view/memoize.rs b/idiopath/src/view/memoize.rs index 9f9553380c..02d66a389e 100644 --- a/idiopath/src/view/memoize.rs +++ b/idiopath/src/view/memoize.rs @@ -44,10 +44,7 @@ impl, F: Fn(&D) -> V> View (Id, Self::State, Self::Element) { let view = (self.child_cb)(&self.data); let (id, view_state, element) = view.build(id_path); - let memoize_state = MemoizeState { - view, - view_state - }; + let memoize_state = MemoizeState { view, view_state }; (id, memoize_state, element) } @@ -73,6 +70,8 @@ impl, F: Fn(&D) -> V> View, app_state: &mut T, ) -> A { - state.view.event(id_path, &state.view_state, event, app_state) + state + .view + .event(id_path, &state.view_state, event, app_state) } } diff --git a/idiopath/src/widget.rs b/idiopath/src/widget.rs index 85e627db8c..ece5718e06 100644 --- a/idiopath/src/widget.rs +++ b/idiopath/src/widget.rs @@ -16,6 +16,7 @@ pub mod button; pub mod column; use std::any::Any; +use std::ops::DerefMut; use druid_shell::kurbo::{Point, Size}; use druid_shell::piet::Piet; @@ -40,12 +41,32 @@ pub struct Geom { pub trait AnyWidget: Widget { fn as_any(&self) -> &dyn Any; + + fn as_any_mut(&mut self) -> &mut dyn Any; } impl AnyWidget for W { fn as_any(&self) -> &dyn Any { self } + + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } +} + +impl Widget for Box { + fn event(&mut self, event: &RawEvent, events: &mut Vec) { + self.deref_mut().event(event, events); + } + + fn layout(&mut self) -> Size { + self.deref_mut().layout() + } + + fn paint(&mut self, ctx: &mut Piet, pos: Point) { + self.deref_mut().paint(ctx, pos); + } } #[derive(Debug)] @@ -53,8 +74,6 @@ pub enum RawEvent { MouseDown(Point), } -pub struct BoxWidget(Box); - pub trait WidgetTuple { fn length(&self) -> usize; From 899feb2648dc3931e0d5ad5bd0596bdc5e4441c7 Mon Sep 17 00:00:00 2001 From: Raph Levien Date: Tue, 12 Apr 2022 17:47:57 -0700 Subject: [PATCH 03/28] Add more widget tuples Bring in more widget tuples with a macro. --- idiopath/src/widget.rs | 39 +++++++++++++++++++++++++++++---------- 1 file changed, 29 insertions(+), 10 deletions(-) diff --git a/idiopath/src/widget.rs b/idiopath/src/widget.rs index ece5718e06..45a171e010 100644 --- a/idiopath/src/widget.rs +++ b/idiopath/src/widget.rs @@ -78,18 +78,37 @@ pub trait WidgetTuple { fn length(&self) -> usize; // Follows Panoramix; rethink to reduce allocation + // Maybe SmallVec? fn widgets_mut(&mut self) -> Vec<&mut dyn AnyWidget>; } -impl WidgetTuple for (W0, W1) { - fn length(&self) -> usize { - 2 - } - - fn widgets_mut(&mut self) -> Vec<&mut dyn AnyWidget> { - let mut v: Vec<&mut dyn AnyWidget> = Vec::with_capacity(self.length()); - v.push(&mut self.0); - v.push(&mut self.1); - v +macro_rules! impl_widget_tuple { + ( $n: tt; $( $WidgetType:ident),* ; $( $index:tt ),* ) => { + impl< $( $WidgetType: AnyWidget ),* > WidgetTuple for ( $( $WidgetType, )* ) { + fn length(&self) -> usize { + $n + } + + fn widgets_mut(&mut self) -> Vec<&mut dyn AnyWidget> { + let mut v: Vec<&mut dyn AnyWidget> = Vec::with_capacity(self.length()); + $( + v.push(&mut self.$index); + )* + v + } + + } } } + +impl_widget_tuple!(1; W0; 0); +impl_widget_tuple!(2; W0, W1; 0, 1); +impl_widget_tuple!(3; W0, W1, W2; 0, 1, 2); +impl_widget_tuple!(4; W0, W1, W2, W3; 0, 1, 2, 3); +impl_widget_tuple!(5; W0, W1, W2, W3, W4; 0, 1, 2, 3, 4); +impl_widget_tuple!(6; W0, W1, W2, W3, W4, W5; 0, 1, 2, 3, 4, 5); +impl_widget_tuple!(7; W0, W1, W2, W3, W4, W5, W6; 0, 1, 2, 3, 4, 5, 6); +impl_widget_tuple!(8; + W0, W1, W2, W3, W4, W5, W6, W7; + 0, 1, 2, 3, 4, 5, 6, 7 +); From fc142ea5dc0ebeffcc8ad832d645f7c3a2fe0776 Mon Sep 17 00:00:00 2001 From: Raph Levien Date: Tue, 12 Apr 2022 17:44:49 -0700 Subject: [PATCH 04/28] Implement not very useful form of use_state Event propagation has no mutable access. --- idiopath/src/view.rs | 1 + idiopath/src/view/use_state.rs | 76 ++++++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+) create mode 100644 idiopath/src/view/use_state.rs diff --git a/idiopath/src/view.rs b/idiopath/src/view.rs index f8b124419e..e44ef38880 100644 --- a/idiopath/src/view.rs +++ b/idiopath/src/view.rs @@ -17,6 +17,7 @@ pub mod any_view; pub mod button; pub mod column; pub mod memoize; +pub mod use_state; use std::any::Any; diff --git a/idiopath/src/view/use_state.rs b/idiopath/src/view/use_state.rs new file mode 100644 index 0000000000..21918e9fa6 --- /dev/null +++ b/idiopath/src/view/use_state.rs @@ -0,0 +1,76 @@ +// Copyright 2022 The Druid Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::{marker::PhantomData, any::Any}; + +use crate::id::{Id, IdPath}; + +use super::View; + +pub struct UseState { + f_init: FInit, + f: F, + phantom: PhantomData<(T, A, S, V)>, +} + +pub struct UseStateState> { + state: S, + view: V, + view_state: V::State, +} + +impl S, F: Fn(&mut S) -> V> UseState { + pub fn new(f_init: FInit, f: F) -> Self { + let phantom = Default::default(); + UseState { + f_init, f, phantom + } + } +} + +impl, FInit: Fn() -> S, F: Fn(&mut S) -> V> View for UseState { + type State = UseStateState; + + type Element = V::Element; + + fn build(&self, id_path: &mut IdPath) -> (Id, Self::State, Self::Element) { + let mut state = (self.f_init)(); + let view = (self.f)(&mut state); + let (id, view_state, element) = view.build(id_path); + let my_state = UseStateState { state, view, view_state }; + (id, my_state, element) + } + fn rebuild( + &self, + id_path: &mut IdPath, + _prev: &Self, + id: &mut Id, + state: &mut Self::State, + element: &mut Self::Element, + ) { + let view = (self.f)(&mut state.state); + view.rebuild(id_path, &state.view, id, &mut state.view_state, element); + state.view = view; + } + + fn event( + &self, + id_path: &[Id], + state: &Self::State, + event: Box, + app_state: &mut T, + ) -> A { + state.view.event(id_path, &state.view_state, event, app_state) + } +} From 08afce9a36589069e191b51d898433d716e6cbbd Mon Sep 17 00:00:00 2001 From: Raph Levien Date: Tue, 12 Apr 2022 18:05:32 -0700 Subject: [PATCH 05/28] A slightly more useful version This passes the app state down through an Rc using a lens-like construction. I'm not sure this is the best construction. Perhaps it should be combined with adapt (the callback could take both mutable state references), but that's 3 callbacks. Also makes state mutable in event propagation, which seems useful. --- idiopath/src/app.rs | 2 +- idiopath/src/view.rs | 2 +- idiopath/src/view/adapt.rs | 4 ++-- idiopath/src/view/any_view.rs | 10 +++++----- idiopath/src/view/button.rs | 2 +- idiopath/src/view/column.rs | 2 +- idiopath/src/view/memoize.rs | 4 ++-- idiopath/src/view/use_state.rs | 36 +++++++++++++++++++++++++--------- idiopath/src/view_tuple.rs | 6 +++--- 9 files changed, 43 insertions(+), 25 deletions(-) diff --git a/idiopath/src/app.rs b/idiopath/src/app.rs index 3b9f393e4b..de8edc4948 100644 --- a/idiopath/src/app.rs +++ b/idiopath/src/app.rs @@ -74,7 +74,7 @@ where for event in self.events.drain(..) { let id_path = &event.id_path[1..]; self.view - .event(id_path, &self.state, event.body, &mut self.data); + .event(id_path, &mut self.state, event.body, &mut self.data); } // Re-rendering should be more lazy. let view = (self.app_logic)(&mut self.data); diff --git a/idiopath/src/view.rs b/idiopath/src/view.rs index e44ef38880..bb596613a0 100644 --- a/idiopath/src/view.rs +++ b/idiopath/src/view.rs @@ -42,7 +42,7 @@ pub trait View { fn event( &self, id_path: &[Id], - state: &Self::State, + state: &mut Self::State, event: Box, app_state: &mut T, ) -> A; diff --git a/idiopath/src/view/adapt.rs b/idiopath/src/view/adapt.rs index 35c4d47dcc..4c66e52f40 100644 --- a/idiopath/src/view/adapt.rs +++ b/idiopath/src/view/adapt.rs @@ -30,7 +30,7 @@ pub struct Adapt) -> A, C: View> { child: &'a C, - state: &'a C::State, + state: &'a mut C::State, id_path: &'a [Id], event: Box, } @@ -77,7 +77,7 @@ impl) -> A, C: View> View, app_state: &mut T, ) -> A { diff --git a/idiopath/src/view/any_view.rs b/idiopath/src/view/any_view.rs index fcb276a8df..26e9a3c92d 100644 --- a/idiopath/src/view/any_view.rs +++ b/idiopath/src/view/any_view.rs @@ -49,7 +49,7 @@ pub trait AnyView { fn dyn_event( &self, id_path: &[Id], - state: &dyn Any, + state: &mut dyn Any, event: Box, app_state: &mut T, ) -> A; @@ -98,11 +98,11 @@ where fn dyn_event( &self, id_path: &[Id], - state: &dyn Any, + state: &mut dyn Any, event: Box, app_state: &mut T, ) -> A { - if let Some(state) = state.downcast_ref() { + if let Some(state) = state.downcast_mut() { self.event(id_path, state, event, app_state) } else { // Possibly softer failure? Would require either Option return or @@ -136,11 +136,11 @@ impl View for Box> { fn event( &self, id_path: &[Id], - state: &Self::State, + state: &mut Self::State, event: Box, app_state: &mut T, ) -> A { self.deref() - .dyn_event(id_path, state.deref(), event, app_state) + .dyn_event(id_path, state.deref_mut(), event, app_state) } } diff --git a/idiopath/src/view/button.rs b/idiopath/src/view/button.rs index 300aac7df2..9eca896f1e 100644 --- a/idiopath/src/view/button.rs +++ b/idiopath/src/view/button.rs @@ -62,7 +62,7 @@ impl View for Button { fn event( &self, _id_path: &[crate::id::Id], - _state: &Self::State, + _state: &mut Self::State, _event: Box, app_state: &mut T, ) -> A { diff --git a/idiopath/src/view/column.rs b/idiopath/src/view/column.rs index 291171c789..8f88968c4b 100644 --- a/idiopath/src/view/column.rs +++ b/idiopath/src/view/column.rs @@ -61,7 +61,7 @@ where fn event( &self, id_path: &[Id], - state: &Self::State, + state: &mut Self::State, event: Box, app_state: &mut T, ) -> A { diff --git a/idiopath/src/view/memoize.rs b/idiopath/src/view/memoize.rs index 02d66a389e..7f4e6b3608 100644 --- a/idiopath/src/view/memoize.rs +++ b/idiopath/src/view/memoize.rs @@ -66,12 +66,12 @@ impl, F: Fn(&D) -> V> View, app_state: &mut T, ) -> A { state .view - .event(id_path, &state.view_state, event, app_state) + .event(id_path, &mut state.view_state, event, app_state) } } diff --git a/idiopath/src/view/use_state.rs b/idiopath/src/view/use_state.rs index 21918e9fa6..7008cdffe8 100644 --- a/idiopath/src/view/use_state.rs +++ b/idiopath/src/view/use_state.rs @@ -12,20 +12,29 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::{marker::PhantomData, any::Any}; +use std::{marker::PhantomData, any::Any, rc::Rc}; use crate::id::{Id, IdPath}; use super::View; +/// An implementation of the "use_state" pattern familiar in reactive UI. +/// +/// This may not be the final form. In this version, the parent app data +/// is `Rc`, and the child is `(Rc, S)` where S is the local state. +/// +/// The first callback creates the initial state (it is called on build but +/// not rebuild). The second callback takes that state as an argument. It +/// is not passed the app state, but since that state is `Rc`, it would be +/// natural to clone it and capture it in a `move` closure. pub struct UseState { f_init: FInit, f: F, phantom: PhantomData<(T, A, S, V)>, } -pub struct UseStateState> { - state: S, +pub struct UseStateState, S), A>> { + state: Option, view: V, view_state: V::State, } @@ -39,7 +48,8 @@ impl S, F: Fn(&mut S) -> V> UseState, FInit: Fn() -> S, F: Fn(&mut S) -> V> View for UseState { +impl, S), A>, FInit: Fn() -> S, F: Fn(&mut S) -> V> View, A> for UseState +{ type State = UseStateState; type Element = V::Element; @@ -48,9 +58,10 @@ impl, FInit: Fn() -> S, F: Fn(&mut S) -> V> View fo let mut state = (self.f_init)(); let view = (self.f)(&mut state); let (id, view_state, element) = view.build(id_path); - let my_state = UseStateState { state, view, view_state }; + let my_state = UseStateState { state: Some(state), view, view_state }; (id, my_state, element) } + fn rebuild( &self, id_path: &mut IdPath, @@ -59,7 +70,7 @@ impl, FInit: Fn() -> S, F: Fn(&mut S) -> V> View fo state: &mut Self::State, element: &mut Self::Element, ) { - let view = (self.f)(&mut state.state); + let view = (self.f)(state.state.as_mut().unwrap()); view.rebuild(id_path, &state.view, id, &mut state.view_state, element); state.view = view; } @@ -67,10 +78,17 @@ impl, FInit: Fn() -> S, F: Fn(&mut S) -> V> View fo fn event( &self, id_path: &[Id], - state: &Self::State, + state: &mut Self::State, event: Box, - app_state: &mut T, + app_state: &mut Rc, ) -> A { - state.view.event(id_path, &state.view_state, event, app_state) + let mut local_state = (app_state.clone(), state.state.take().unwrap()); + let a = state.view.event(id_path, &mut state.view_state, event, &mut local_state); + let (local_app_state, my_state) = local_state; + if !Rc::ptr_eq(app_state, &local_app_state) { + *app_state = local_app_state + } + state.state = Some(my_state); + a } } diff --git a/idiopath/src/view_tuple.rs b/idiopath/src/view_tuple.rs index a8ce67d4f8..756b00ba1b 100644 --- a/idiopath/src/view_tuple.rs +++ b/idiopath/src/view_tuple.rs @@ -37,7 +37,7 @@ pub trait ViewTuple { fn event( &self, id_path: &[Id], - state: &Self::State, + state: &mut Self::State, event: Box, app_state: &mut T, ) -> A; @@ -73,7 +73,7 @@ macro_rules! impl_view_tuple { fn event( &self, id_path: &[Id], - state: &Self::State, + state: &mut Self::State, event: Box, app_state: &mut T, ) -> A { @@ -81,7 +81,7 @@ macro_rules! impl_view_tuple { let tl = &id_path[1..]; $( if hd == state.$n[$s] { - self.$s.event(tl, &state.$s, event, app_state) + self.$s.event(tl, &mut state.$s, event, app_state) } else )* { panic!("inconsistent id_path") } From 8a4b687d6ceee63ade75c66a2a3b7f9aad96b83a Mon Sep 17 00:00:00 2001 From: Raph Levien Date: Tue, 12 Apr 2022 21:50:42 -0700 Subject: [PATCH 06/28] Add README Also trying out a few more node types in the example. --- idiopath/README.md | 77 ++++++++++++++++++++++++++++++++++ idiopath/src/main.rs | 36 +++++++++++++++- idiopath/src/view/column.rs | 10 +++++ idiopath/src/view/use_state.rs | 19 +++++---- 4 files changed, 133 insertions(+), 9 deletions(-) create mode 100644 idiopath/README.md diff --git a/idiopath/README.md b/idiopath/README.md new file mode 100644 index 0000000000..94c9108dc0 --- /dev/null +++ b/idiopath/README.md @@ -0,0 +1,77 @@ +# An experimental Rust architecture for reactive UI + +This repo contains an experimental architecture, implemented with a toy UI. At a very high level, it combines ideas from Flutter, SwiftUI, and Elm. Like all of these, it uses lightweight view objects, diffing them to provide minimal updates to a retained UI. Like SwiftUI, it is strongly typed. + +## Overall program flow + +Like Elm, the app logic contains *centralized state.* On each cycle (meaning, roughly, on each high-level UI interaction such as a button click), the framework calls a closure, giving it mutable access to the app state, and the return value is a *view tree.* This view tree is fairly short-lived; it is used to render the UI, possibly dispatch some events, and be used as a reference for *diffing* by the next cycle, at which point it is dropped. + +We'll use the standard counter example. Here the state is a single integer, and the view tree is a column containing two buttons. + +```rust +fn app_logic(data: &mut u32) -> impl View { + Column::new(( + Button::new(format!("count: {}", data), |data| *data += 1), + Button::new("reset", |data| *data = 0), + )) +} +``` + +These are all just vanilla data structures. The next step is diffing or reconciling against a previous version, now a standard technique. The result is an *element tree.* Each node type in the view tree has a corresponding element as an associated type. The `build` method on a view node creates the element, and the `rebuild` method diffs against the previous version (for example, if the string changes) and updates the element. There's also an associated state tree, not actually needed in this simple example, but would be used for memoization. + +The closures are the interesting part. When they're run, they take a mutable reference to the app data. + +## Components + +A major goal is to support React-like components, where modules that build UI for some fragment of the overall app state are composed together. + +```rust +struct AppData { + count: u32, +} + +fn count_button(count: u32) -> impl View { + Button::new(format!("count: {}", count), |data| *data += 1) +} + +fn app_logic(data: &mut AppData) -> impl View { + Adapt::new(|data: &mut AppData, thunk| thunk.call(&mut data.count), + count_button(data.count)) +} +``` + +This adapt node is very similar to a lens (quite familiar to existing Druid users), and is also very similar to the [Html.map] node in Elm. Note that in this case the data presented to the child component to render, and the mutable app state available in callbacks is the same, but that is not necessarily the case. + +## Memoization + +In the simplest case, the app builds the entire view tree, which is diffed against the previous tree, only to find that most of it hasn't changed. + +When a subtree is a pure function of some data, as is the case for the button above, it makes sense to *memoize.* The data is compared to the previous version, and only when it's changed is the view tree build. The signature of the memoize node is nearly identical to [Html.lazy] in Elm: + +```rust +n app_logic(data: &mut AppData) -> impl View { + Memoize::new(data.count, |count| { + Button::new(format!("count: {}", count), |data: &mut AppData| { + data.count += 1 + }) + }), +} +``` + +The current code uses a `PartialEq` bound, but in practice I think it might be much more useful to use pointer equality on `Rc` and `Arc`. + +The combination of memoization with pointer equality and an adapt node that calls [Rc::make_mut] on the parent type is actually a powerful form of change tracking, similar in scope to Adapton, self-adjusting computation, or the types of binding objects used in SwiftUI. If a piece of data is rendered in two different places, it automatically propagates the change to both of those, without having to do any explicit management of the dependency graph. + +I anticipate it will also be possible to do dirty tracking manually - the app logic can set a dirty flag when a subtree needs re-rendering. + +## Optional type erasure + +By default, view nodes are strongly typed. The type of a container includes the types of its children (through the `ViewTuple` trait), so for a large tree the type can become quite large. In addition, such types don't make for easy dynamic reconfiguration of the UI. SwiftUI has exactly this issue, and provides [AnyView] as the solution. Ours is more or less identical. + +The type erasure of View nodes is not an easy trick, as the trait has two associated types and the `rebuild` method takes the previous view as a `&Self` typed parameter. Nonetheless, it is possible. (As far as I know, Olivier Faure was the first to demonstrate this technique, in [Panoramix], but I'm happy to be further enlightened) + +[Html.lazy]: https://guide.elm-lang.org/optimization/lazy.html +[Html map]: https://package.elm-lang.org/packages/elm/html/latest/Html#map +[Rc::make_mut]: https://doc.rust-lang.org/std/rc/struct.Rc.html#method.make_mut +[AnyView]: https://developer.apple.com/documentation/swiftui/anyview +[Panoramix]: https://github.com/PoignardAzur/panoramix diff --git a/idiopath/src/main.rs b/idiopath/src/main.rs index 8cb5fc1ada..b09790ca53 100644 --- a/idiopath/src/main.rs +++ b/idiopath/src/main.rs @@ -29,8 +29,11 @@ use druid_shell::{ Application, Cursor, HotKey, Menu, MouseEvent, Region, SysMods, WinHandler, WindowBuilder, WindowHandle, }; +use view::adapt::Adapt; use view::any_view::AnyView; use view::button::Button; +use view::column::Column; +use view::memoize::Memoize; use view::View; use widget::Widget; @@ -114,10 +117,39 @@ where } } +/* fn app_logic(data: &mut u32) -> impl View { let button = Button::new(format!("count: {}", data), |data| *data += 1); let boxed: Box> = Box::new(button); - boxed + Column::new((boxed, Button::new("reset", |data| *data = 0))) +} +*/ + +#[derive(Default)] +struct AppData { + count: u32, +} + +fn count_button(count: u32) -> impl View { + Button::new(format!("count: {}", count), |data| *data += 1) +} + +fn app_logic(data: &mut AppData) -> impl View { + Column::new(( + Button::new(format!("count: {}", data.count), |data: &mut AppData| { + data.count += 1 + }), + Button::new("reset", |data: &mut AppData| data.count = 0), + Memoize::new(data.count, |count| { + Button::new(format!("count: {}", count), |data: &mut AppData| { + data.count += 1 + }) + }), + Adapt::new( + |data: &mut AppData, thunk| thunk.call(&mut data.count), + count_button(data.count), + ), + )) } fn main() { @@ -134,7 +166,7 @@ fn main() { menubar.add_dropdown(Menu::new(), "Application", true); menubar.add_dropdown(file_menu, "&File", true); - let app = App::new(0, app_logic); + let app = App::new(AppData::default(), app_logic); let druid_app = Application::new().unwrap(); let mut builder = WindowBuilder::new(druid_app.clone()); let main_state = MainState::new(app); diff --git a/idiopath/src/view/column.rs b/idiopath/src/view/column.rs index 8f88968c4b..97499dc5a6 100644 --- a/idiopath/src/view/column.rs +++ b/idiopath/src/view/column.rs @@ -27,6 +27,16 @@ pub struct Column> { phantom: PhantomData<(T, A)>, } +impl> Column +where + VT::Elements: WidgetTuple, +{ + pub fn new(children: VT) -> Self { + let phantom = Default::default(); + Column { children, phantom } + } +} + impl> View for Column where VT::Elements: WidgetTuple, diff --git a/idiopath/src/view/use_state.rs b/idiopath/src/view/use_state.rs index 7008cdffe8..0c11979abf 100644 --- a/idiopath/src/view/use_state.rs +++ b/idiopath/src/view/use_state.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::{marker::PhantomData, any::Any, rc::Rc}; +use std::{any::Any, marker::PhantomData, rc::Rc}; use crate::id::{Id, IdPath}; @@ -42,13 +42,12 @@ pub struct UseStateState, S), A>> { impl S, F: Fn(&mut S) -> V> UseState { pub fn new(f_init: FInit, f: F) -> Self { let phantom = Default::default(); - UseState { - f_init, f, phantom - } + UseState { f_init, f, phantom } } } -impl, S), A>, FInit: Fn() -> S, F: Fn(&mut S) -> V> View, A> for UseState +impl, S), A>, FInit: Fn() -> S, F: Fn(&mut S) -> V> View, A> + for UseState { type State = UseStateState; @@ -58,7 +57,11 @@ impl, S), A>, FInit: Fn() -> S, F: Fn(&mut S) -> V> View let mut state = (self.f_init)(); let view = (self.f)(&mut state); let (id, view_state, element) = view.build(id_path); - let my_state = UseStateState { state: Some(state), view, view_state }; + let my_state = UseStateState { + state: Some(state), + view, + view_state, + }; (id, my_state, element) } @@ -83,7 +86,9 @@ impl, S), A>, FInit: Fn() -> S, F: Fn(&mut S) -> V> View app_state: &mut Rc, ) -> A { let mut local_state = (app_state.clone(), state.state.take().unwrap()); - let a = state.view.event(id_path, &mut state.view_state, event, &mut local_state); + let a = state + .view + .event(id_path, &mut state.view_state, event, &mut local_state); let (local_app_state, my_state) = local_state; if !Rc::ptr_eq(app_state, &local_app_state) { *app_state = local_app_state From f30b2bbf047f87b0a85e421d89f8a24bdb4ee3b0 Mon Sep 17 00:00:00 2001 From: "Richard Dodd (dodj)" Date: Sat, 16 Apr 2022 19:05:25 +0100 Subject: [PATCH 07/28] Update README.md Fix very small typo --- idiopath/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/idiopath/README.md b/idiopath/README.md index 94c9108dc0..a92be491df 100644 --- a/idiopath/README.md +++ b/idiopath/README.md @@ -49,7 +49,7 @@ In the simplest case, the app builds the entire view tree, which is diffed again When a subtree is a pure function of some data, as is the case for the button above, it makes sense to *memoize.* The data is compared to the previous version, and only when it's changed is the view tree build. The signature of the memoize node is nearly identical to [Html.lazy] in Elm: ```rust -n app_logic(data: &mut AppData) -> impl View { +fn app_logic(data: &mut AppData) -> impl View { Memoize::new(data.count, |count| { Button::new(format!("count: {}", count), |data: &mut AppData| { data.count += 1 From 52c29e83913797ee3fafea8b0e46a9d0232db768 Mon Sep 17 00:00:00 2001 From: jneem Date: Thu, 21 Apr 2022 13:41:43 -0500 Subject: [PATCH 08/28] Silence clippy. (#2169) --- druid-shell/src/backend/windows/application.rs | 2 +- druid-shell/src/backend/windows/clipboard.rs | 2 +- druid-shell/src/backend/windows/error.rs | 2 +- druid-shell/src/backend/windows/util.rs | 2 +- druid-shell/src/backend/windows/window.rs | 2 +- druid-shell/src/lib.rs | 2 ++ druid/src/widget/tabs.rs | 2 +- 7 files changed, 8 insertions(+), 6 deletions(-) diff --git a/druid-shell/src/backend/windows/application.rs b/druid-shell/src/backend/windows/application.rs index 2b15b78a92..9a85def72b 100644 --- a/druid-shell/src/backend/windows/application.rs +++ b/druid-shell/src/backend/windows/application.rs @@ -200,7 +200,7 @@ impl Application { let len_with_null = unsafe { GetUserDefaultLocaleName(buf.as_mut_ptr(), buf.len() as _) as usize }; let locale = if len_with_null > 0 { - buf.get(..len_with_null - 1).and_then(FromWide::from_wide) + buf.get(..len_with_null - 1).and_then(FromWide::to_string) } else { None }; diff --git a/druid-shell/src/backend/windows/clipboard.rs b/druid-shell/src/backend/windows/clipboard.rs index 023150d5e3..327d9bbc39 100644 --- a/druid-shell/src/backend/windows/clipboard.rs +++ b/druid-shell/src/backend/windows/clipboard.rs @@ -80,7 +80,7 @@ impl Clipboard { None } else { let unic_str = GlobalLock(handle) as LPWSTR; - let result = unic_str.from_wide(); + let result = unic_str.to_string(); GlobalUnlock(handle); result } diff --git a/druid-shell/src/backend/windows/error.rs b/druid-shell/src/backend/windows/error.rs index 933927a859..b2aa0c0371 100644 --- a/druid-shell/src/backend/windows/error.rs +++ b/druid-shell/src/backend/windows/error.rs @@ -59,7 +59,7 @@ fn hresult_description(hr: HRESULT) -> Option { return None; } - let result = message_buffer.from_wide(); + let result = message_buffer.to_string(); LocalFree(message_buffer as HLOCAL); result } diff --git a/druid-shell/src/backend/windows/util.rs b/druid-shell/src/backend/windows/util.rs index 34ac3853d9..8cd865386c 100644 --- a/druid-shell/src/backend/windows/util.rs +++ b/druid-shell/src/backend/windows/util.rs @@ -84,7 +84,7 @@ pub trait FromWide { OsStringExt::from_wide(self.to_u16_slice()) } - fn from_wide(&self) -> Option { + fn to_string(&self) -> Option { String::from_utf16(self.to_u16_slice()).ok() } } diff --git a/druid-shell/src/backend/windows/window.rs b/druid-shell/src/backend/windows/window.rs index ba982146b4..1a0c64bde7 100644 --- a/druid-shell/src/backend/windows/window.rs +++ b/druid-shell/src/backend/windows/window.rs @@ -1569,7 +1569,7 @@ unsafe fn choose_adapter(factory: *mut IDXGIFactory2) -> *mut IDXGIAdapter { debug!( "{:?}: desc = {:?}, vram = {}", adapter, - (&mut desc.Description[0] as LPWSTR).from_wide(), + (&mut desc.Description[0] as LPWSTR).to_string(), desc.DedicatedVideoMemory ); i += 1; diff --git a/druid-shell/src/lib.rs b/druid-shell/src/lib.rs index 22de7548f8..0e2b01d36b 100644 --- a/druid-shell/src/lib.rs +++ b/druid-shell/src/lib.rs @@ -32,6 +32,8 @@ #![doc( html_logo_url = "https://raw.githubusercontent.com/linebender/druid/screenshots/images/doc_logo.png" )] +// This is overeager right now, see https://github.com/rust-lang/rust-clippy/issues/8494 +#![allow(clippy::iter_overeager_cloned)] // Rename `gtk_rs` back to `gtk`. // This allows us to use `gtk` as the feature name. diff --git a/druid/src/widget/tabs.rs b/druid/src/widget/tabs.rs index adde669cb7..24f47eccd7 100644 --- a/druid/src/widget/tabs.rs +++ b/druid/src/widget/tabs.rs @@ -566,7 +566,7 @@ impl TabsBody { // Doesn't take self to allow separate borrowing fn child( - children: &mut Vec<(TP::Key, TabBodyPod)>, + children: &mut [(TP::Key, TabBodyPod)], idx: usize, ) -> Option<&mut TabBodyPod> { children.get_mut(idx).map(|x| &mut x.1) From 677f13ff90a6870165f7d19a24daaa2008f90dc8 Mon Sep 17 00:00:00 2001 From: jneem Date: Thu, 21 Apr 2022 21:08:41 -0500 Subject: [PATCH 09/28] X11 dialogs, take 2. (#2153) Use xdg-desktop-portal's dbus APIs for open/save dialogs on x11. --- druid-shell/Cargo.toml | 14 ++- druid-shell/src/backend/x11/dialog.rs | 166 ++++++++++++++++++++++++++ druid-shell/src/backend/x11/mod.rs | 1 + druid-shell/src/backend/x11/window.rs | 46 ++++--- 4 files changed, 209 insertions(+), 18 deletions(-) create mode 100644 druid-shell/src/backend/x11/dialog.rs diff --git a/druid-shell/Cargo.toml b/druid-shell/Cargo.toml index cb6081bcaf..e8c066cb6f 100755 --- a/druid-shell/Cargo.toml +++ b/druid-shell/Cargo.toml @@ -16,7 +16,15 @@ default-target = "x86_64-pc-windows-msvc" [features] default = ["gtk"] gtk = ["gdk-sys", "glib-sys", "gtk-sys", "gtk-rs"] -x11 = ["x11rb", "nix", "cairo-sys-rs", "bindgen", "pkg-config"] +x11 = [ + "ashpd", + "bindgen", + "cairo-sys-rs", + "futures", + "nix", + "pkg-config", + "x11rb", +] wayland = [ "wayland-client", "wayland-protocols/client", @@ -68,7 +76,7 @@ keyboard-types = { version = "0.6.2", default_features = false } # Optional dependencies image = { version = "0.23.12", optional = true, default_features = false } -raw-window-handle = { version = "0.3.3", optional = true, default_features = false } +raw-window-handle = { version = "0.4.2", optional = true, default_features = false } [target.'cfg(target_os="windows")'.dependencies] scopeguard = "1.1.0" @@ -90,9 +98,11 @@ foreign-types = "0.3.2" bitflags = "1.2.1" [target.'cfg(any(target_os="linux", target_os="openbsd"))'.dependencies] +ashpd = { version = "0.3.0", optional = true } # TODO(x11/dependencies): only use feature "xcb" if using X11 cairo-rs = { version = "0.14.0", default_features = false, features = ["xcb"] } cairo-sys-rs = { version = "0.14.0", default_features = false, optional = true } +futures = { version = "0.3.21", optional = true, features = ["executor"]} gdk-sys = { version = "0.14.0", optional = true } # `gtk` gets renamed to `gtk-rs` so that we can use `gtk` as the feature name. gtk-rs = { version = "0.14.0", features = ["v3_22"], package = "gtk", optional = true } diff --git a/druid-shell/src/backend/x11/dialog.rs b/druid-shell/src/backend/x11/dialog.rs new file mode 100644 index 0000000000..b77bae59a2 --- /dev/null +++ b/druid-shell/src/backend/x11/dialog.rs @@ -0,0 +1,166 @@ +//! This module contains functions for opening file dialogs using DBus. + +use ashpd::desktop::file_chooser; +use ashpd::{zbus, WindowIdentifier}; +use futures::executor::block_on; +use tracing::warn; + +use crate::{FileDialogOptions, FileDialogToken, FileInfo}; + +use super::window::IdleHandle; + +pub(crate) fn open_file( + window: u32, + idle: IdleHandle, + options: FileDialogOptions, +) -> FileDialogToken { + dialog(window, idle, options, true) +} + +pub(crate) fn save_file( + window: u32, + idle: IdleHandle, + options: FileDialogOptions, +) -> FileDialogToken { + dialog(window, idle, options, false) +} + +fn dialog( + window: u32, + idle: IdleHandle, + mut options: FileDialogOptions, + open: bool, +) -> FileDialogToken { + let tok = FileDialogToken::next(); + + std::thread::spawn(move || { + if let Err(e) = block_on(async { + let conn = zbus::Connection::session().await?; + let proxy = file_chooser::FileChooserProxy::new(&conn).await?; + let id = WindowIdentifier::from_xid(window as u64); + let multi = options.multi_selection; + + let title_owned = options.title.take(); + let title = match (open, options.select_directories) { + (true, true) => "Open Folder", + (true, false) => "Open File", + (false, _) => "Save File", + }; + let title = title_owned.as_deref().unwrap_or(title); + let open_result; + let save_result; + let uris = if open { + open_result = proxy.open_file(&id, title, options.into()).await?; + open_result.uris() + } else { + save_result = proxy.save_file(&id, title, options.into()).await?; + save_result.uris() + }; + + let mut paths = uris.iter().filter_map(|s| { + s.strip_prefix("file://").or_else(|| { + warn!("expected path '{}' to start with 'file://'", s); + None + }) + }); + if multi && open { + let infos = paths + .map(|p| FileInfo { + path: p.into(), + format: None, + }) + .collect(); + idle.add_idle_callback(move |handler| handler.open_files(tok, infos)); + } else if !multi { + if uris.len() > 2 { + warn!( + "expected one path (got {}), returning only the first", + uris.len() + ); + } + let info = paths.next().map(|p| FileInfo { + path: p.into(), + format: None, + }); + if open { + idle.add_idle_callback(move |handler| handler.open_file(tok, info)); + } else { + idle.add_idle_callback(move |handler| handler.save_as(tok, info)); + } + } else { + warn!("cannot save multiple paths"); + } + + Ok(()) as ashpd::Result<()> + }) { + warn!("error while opening file dialog: {}", e); + } + }); + + tok +} + +impl From for file_chooser::FileFilter { + fn from(spec: crate::FileSpec) -> file_chooser::FileFilter { + let mut filter = file_chooser::FileFilter::new(spec.name); + for ext in spec.extensions { + filter = filter.glob(&format!("*.{}", ext)); + } + filter + } +} + +impl From for file_chooser::OpenFileOptions { + fn from(opts: crate::FileDialogOptions) -> file_chooser::OpenFileOptions { + let mut fc = file_chooser::OpenFileOptions::default() + .modal(true) + .multiple(opts.multi_selection) + .directory(opts.select_directories); + + if let Some(label) = &opts.button_text { + fc = fc.accept_label(label); + } + + if let Some(filters) = opts.allowed_types { + for f in filters { + fc = fc.add_filter(f.into()); + } + } + + if let Some(filter) = opts.default_type { + fc = fc.current_filter(filter.into()); + } + + fc + } +} + +impl From for file_chooser::SaveFileOptions { + fn from(opts: crate::FileDialogOptions) -> file_chooser::SaveFileOptions { + let mut fc = file_chooser::SaveFileOptions::default().modal(true); + + if let Some(name) = &opts.default_name { + fc = fc.current_name(name); + } + + if let Some(label) = &opts.button_text { + fc = fc.accept_label(label); + } + + if let Some(filters) = opts.allowed_types { + for f in filters { + fc = fc.add_filter(f.into()); + } + } + + if let Some(filter) = opts.default_type { + fc = fc.current_filter(filter.into()); + } + + if let Some(dir) = &opts.starting_directory { + fc = fc.current_folder(dir); + } + + fc + } +} diff --git a/druid-shell/src/backend/x11/mod.rs b/druid-shell/src/backend/x11/mod.rs index 6451f6ac52..ae2e9b0927 100644 --- a/druid-shell/src/backend/x11/mod.rs +++ b/druid-shell/src/backend/x11/mod.rs @@ -35,6 +35,7 @@ mod util; pub mod application; pub mod clipboard; +pub mod dialog; pub mod error; pub mod menu; pub mod screen; diff --git a/druid-shell/src/backend/x11/window.rs b/druid-shell/src/backend/x11/window.rs index ffafc9831c..db61b5d2c6 100644 --- a/druid-shell/src/backend/x11/window.rs +++ b/druid-shell/src/backend/x11/window.rs @@ -61,6 +61,7 @@ use crate::window::{ use crate::{window, KeyEvent, ScaledArea}; use super::application::Application; +use super::dialog; use super::menu::Menu; /// A version of XCB's `xcb_visualtype_t` struct. This was copied from the [example] in x11rb; it @@ -1557,23 +1558,22 @@ impl IdleHandle { } pub(crate) fn schedule_redraw(&self) { - self.queue.lock().unwrap().push(IdleKind::Redraw); - self.wake(); + self.add_idle(IdleKind::Redraw); } pub fn add_idle_callback(&self, callback: F) where F: FnOnce(&mut dyn WinHandler) + Send + 'static, { - self.queue - .lock() - .unwrap() - .push(IdleKind::Callback(Box::new(callback))); - self.wake(); + self.add_idle(IdleKind::Callback(Box::new(callback))); } pub fn add_idle_token(&self, token: IdleToken) { - self.queue.lock().unwrap().push(IdleKind::Token(token)); + self.add_idle(IdleKind::Token(token)); + } + + fn add_idle(&self, idle: IdleKind) { + self.queue.lock().unwrap().push(idle); self.wake(); } } @@ -1795,16 +1795,30 @@ impl WindowHandle { } } - pub fn open_file(&mut self, _options: FileDialogOptions) -> Option { - // TODO(x11/file_dialogs): implement WindowHandle::open_file - warn!("WindowHandle::open_file is currently unimplemented for X11 backend."); - None + pub fn open_file(&mut self, options: FileDialogOptions) -> Option { + if let Some(w) = self.window.upgrade() { + if let Some(idle) = self.get_idle_handle() { + Some(dialog::open_file(w.id, idle, options)) + } else { + warn!("Couldn't open file because no idle handle available"); + None + } + } else { + None + } } - pub fn save_as(&mut self, _options: FileDialogOptions) -> Option { - // TODO(x11/file_dialogs): implement WindowHandle::save_as - warn!("WindowHandle::save_as is currently unimplemented for X11 backend."); - None + pub fn save_as(&mut self, options: FileDialogOptions) -> Option { + if let Some(w) = self.window.upgrade() { + if let Some(idle) = self.get_idle_handle() { + Some(dialog::save_file(w.id, idle, options)) + } else { + warn!("Couldn't save file because no idle handle available"); + None + } + } else { + None + } } pub fn show_context_menu(&self, _menu: Menu, _pos: Point) { From ecbcb0ff2b470a78d7f360921a048a85b2164451 Mon Sep 17 00:00:00 2001 From: Daniel <61119347+dakata1337@users.noreply.github.com> Date: Fri, 22 Apr 2022 02:12:35 +0000 Subject: [PATCH 10/28] Fixed readme.md links (#2167) --- druid/examples/readme.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/druid/examples/readme.md b/druid/examples/readme.md index 17ede03902..fedeb912ff 100644 --- a/druid/examples/readme.md +++ b/druid/examples/readme.md @@ -15,7 +15,7 @@ This example shows how to make a simple animation using `Event::AnimFrame`. ``` cargo run --example async_event ``` -Demonstrates receiving data from some outside source, and updating the UI in response. This is similar to [blocking function](#Blocking Function) but here the data source is fully independent, and runs for the lifetime of the program. +Demonstrates receiving data from some outside source, and updating the UI in response. This is similar to [blocking function](#blocking-function) but here the data source is fully independent, and runs for the lifetime of the program. ## Blocking Function ``` @@ -24,7 +24,7 @@ cargo run --example blocking_function Sometimes you need to fetch some data from disk or from the internet, but you should never block the UI thread with long running operations! Instead you should run this task in a separate thread, and have it send -you the data as it arrives. This is very similar to [async event](#Async Event) +you the data as it arrives. This is very similar to [async event](#async-event) except the event is initiated by the main thread. ## Cursor @@ -60,7 +60,7 @@ For more info and prerequistes see [druid/examples/hello_web/README.md](druid/ex cd druid/examples/hello_web wasm-pack build --out-dir pkg --out-name hello_web ``` -[View at http://localhost:8000](http://localhost:8000]. +[View at http://localhost:8000](http://localhost:8000). This is an example of how to get almost any druid application can be used on the web. This is just the hello_world example but should work for all of them. @@ -70,7 +70,7 @@ For more info and prerequistes see [druid/examples/web/README.md](druid/examples cd druid/examples/web wasm-pack build --out-dir pkg --out-name web ``` -[View at http://localhost:8000](http://localhost:8000]. +[View at http://localhost:8000](http://localhost:8000). Simple web app. From cfa7380a0fdfc25ccb8c39705d60f539d7138274 Mon Sep 17 00:00:00 2001 From: Raph Levien Date: Thu, 14 Apr 2022 18:22:42 -0700 Subject: [PATCH 11/28] Introduce Cx for reconciliation This commit was derived from the async experiment, which required adding a waker to the reconciliation context. We will definitely be adding more things to Cx so that should be the type of View methods, but also we'll iterate a while on the core architecture before re-introducing async. Note, it also bounds the element type for View to Widget, which is less flexible but more ergonomic. --- idiopath/src/app.rs | 81 +++++++++++++++++++++------------- idiopath/src/event.rs | 9 ++++ idiopath/src/main.rs | 6 +-- idiopath/src/view.rs | 40 +++++++++++++++-- idiopath/src/view/adapt.rs | 12 ++--- idiopath/src/view/any_view.rs | 29 ++++++------ idiopath/src/view/button.rs | 14 +++--- idiopath/src/view/column.rs | 24 +++++----- idiopath/src/view/memoize.rs | 12 ++--- idiopath/src/view/use_state.rs | 12 ++--- idiopath/src/view_tuple.rs | 22 ++++----- idiopath/src/widget/button.rs | 6 +-- 12 files changed, 156 insertions(+), 111 deletions(-) diff --git a/idiopath/src/app.rs b/idiopath/src/app.rs index de8edc4948..06ed5c7a53 100644 --- a/idiopath/src/app.rs +++ b/idiopath/src/app.rs @@ -12,12 +12,12 @@ // See the License for the specific language governing permissions and // limitations under the License. -use druid_shell::{kurbo::Point, piet::Piet}; +use druid_shell::{kurbo::Point, piet::Piet, WindowHandle}; use crate::{ event::Event, - id::{Id, IdPath}, - view::View, + id::Id, + view::{Cx, View}, widget::{RawEvent, Widget}, }; @@ -27,38 +27,53 @@ where { data: T, app_logic: F, - view: V, - id: Id, - state: V::State, - element: V::Element, + view: Option, + id: Option, + state: Option, + element: Option, events: Vec, - id_path: IdPath, + cx: Cx, } impl, F: FnMut(&mut T) -> V> App where V::Element: Widget, { - pub fn new(mut data: T, mut app_logic: F) -> Self { - let mut id_path = IdPath::default(); - let view = (app_logic)(&mut data); - let (id, state, element) = view.build(&mut id_path); - assert!(id_path.is_empty(), "id path imbalance on build"); + pub fn new(data: T, app_logic: F) -> Self { + let cx = Cx::new(); App { data, app_logic, - view, - id, - state, - element, + view: None, + id: None, + state: None, + element: None, events: Vec::new(), - id_path, + cx, } } + pub fn ensure_app(&mut self) { + if self.view.is_none() { + let view = (self.app_logic)(&mut self.data); + let (id, state, element) = view.build(&mut self.cx); + self.view = Some(view); + self.id = Some(id); + self.state = Some(state); + self.element = Some(element); + } + } + + pub fn connect(&mut self, _window_handle: WindowHandle) { + // This will be needed for wiring up async but is a stub for now. + //self.cx.set_handle(window_handle.get_idle_handle()); + } + pub fn paint(&mut self, piet: &mut Piet) { - self.element.layout(); - self.element.paint(piet, Point::ZERO); + self.ensure_app(); + let element = self.element.as_mut().unwrap(); + element.layout(); + element.paint(piet, Point::ZERO); } pub fn mouse_down(&mut self, point: Point) { @@ -66,26 +81,32 @@ where } fn event(&mut self, event: RawEvent) { - self.element.event(&event, &mut self.events); + self.ensure_app(); + let element = self.element.as_mut().unwrap(); + element.event(&event, &mut self.events); self.run_app_logic(); } pub fn run_app_logic(&mut self) { for event in self.events.drain(..) { let id_path = &event.id_path[1..]; - self.view - .event(id_path, &mut self.state, event.body, &mut self.data); + self.view.as_ref().unwrap().event( + id_path, + self.state.as_mut().unwrap(), + event.body, + &mut self.data, + ); } // Re-rendering should be more lazy. let view = (self.app_logic)(&mut self.data); view.rebuild( - &mut self.id_path, - &self.view, - &mut self.id, - &mut self.state, - &mut self.element, + &mut self.cx, + self.view.as_ref().unwrap(), + self.id.as_mut().unwrap(), + self.state.as_mut().unwrap(), + self.element.as_mut().unwrap(), ); - assert!(self.id_path.is_empty(), "id path imbalance on rebuild"); - self.view = view; + assert!(self.cx.is_empty(), "id path imbalance on rebuild"); + self.view = Some(view); } } diff --git a/idiopath/src/event.rs b/idiopath/src/event.rs index a6e1553a52..b86ee3684e 100644 --- a/idiopath/src/event.rs +++ b/idiopath/src/event.rs @@ -20,3 +20,12 @@ pub struct Event { pub id_path: IdPath, pub body: Box, } + +impl Event { + pub fn new(id_path: IdPath, event: impl Any) -> Event { + Event { + id_path, + body: Box::new(event), + } + } +} diff --git a/idiopath/src/main.rs b/idiopath/src/main.rs index b09790ca53..d52a1501ec 100644 --- a/idiopath/src/main.rs +++ b/idiopath/src/main.rs @@ -30,7 +30,6 @@ use druid_shell::{ WindowHandle, }; use view::adapt::Adapt; -use view::any_view::AnyView; use view::button::Button; use view::column::Column; use view::memoize::Memoize; @@ -55,6 +54,7 @@ where { fn connect(&mut self, handle: &WindowHandle) { self.handle = handle.clone(); + self.app.connect(handle.clone()); } fn prepare_paint(&mut self) {} @@ -130,11 +130,11 @@ struct AppData { count: u32, } -fn count_button(count: u32) -> impl View { +fn count_button(count: u32) -> impl View { Button::new(format!("count: {}", count), |data| *data += 1) } -fn app_logic(data: &mut AppData) -> impl View { +fn app_logic(data: &mut AppData) -> impl View { Column::new(( Button::new(format!("count: {}", data.count), |data: &mut AppData| { data.count += 1 diff --git a/idiopath/src/view.rs b/idiopath/src/view.rs index bb596613a0..44186749aa 100644 --- a/idiopath/src/view.rs +++ b/idiopath/src/view.rs @@ -21,18 +21,21 @@ pub mod use_state; use std::any::Any; -use crate::id::{Id, IdPath}; +use crate::{ + id::{Id, IdPath}, + widget::Widget, +}; pub trait View { type State; - type Element; + type Element: Widget; - fn build(&self, id_path: &mut IdPath) -> (Id, Self::State, Self::Element); + fn build(&self, cx: &mut Cx) -> (Id, Self::State, Self::Element); fn rebuild( &self, - id_path: &mut IdPath, + cx: &mut Cx, prev: &Self, id: &mut Id, state: &mut Self::State, @@ -47,3 +50,32 @@ pub trait View { app_state: &mut T, ) -> A; } + +#[derive(Clone)] +pub struct Cx { + id_path: IdPath, +} + +impl Cx { + pub fn new() -> Self { + Cx { + id_path: Vec::new(), + } + } + + pub fn push(&mut self, id: Id) { + self.id_path.push(id); + } + + pub fn pop(&mut self) { + self.id_path.pop(); + } + + pub fn is_empty(&self) -> bool { + self.id_path.is_empty() + } + + pub fn id_path(&self) -> &IdPath { + &self.id_path + } +} diff --git a/idiopath/src/view/adapt.rs b/idiopath/src/view/adapt.rs index 4c66e52f40..8accd4cd3a 100644 --- a/idiopath/src/view/adapt.rs +++ b/idiopath/src/view/adapt.rs @@ -14,9 +14,9 @@ use std::{any::Any, marker::PhantomData}; -use crate::id::{Id, IdPath}; +use crate::id::Id; -use super::View; +use super::{Cx, View}; pub struct Adapt) -> A, C: View> { f: F, @@ -59,19 +59,19 @@ impl) -> A, C: View> View (Id, Self::State, Self::Element) { - self.child.build(id_path) + fn build(&self, cx: &mut Cx) -> (Id, Self::State, Self::Element) { + self.child.build(cx) } fn rebuild( &self, - id_path: &mut IdPath, + cx: &mut Cx, prev: &Self, id: &mut Id, state: &mut Self::State, element: &mut Self::Element, ) { - self.child.rebuild(id_path, &prev.child, id, state, element); + self.child.rebuild(cx, &prev.child, id, state, element); } fn event( diff --git a/idiopath/src/view/any_view.rs b/idiopath/src/view/any_view.rs index 26e9a3c92d..570d7492ab 100644 --- a/idiopath/src/view/any_view.rs +++ b/idiopath/src/view/any_view.rs @@ -16,12 +16,9 @@ use std::{ ops::{Deref, DerefMut}, }; -use crate::{ - id::{Id, IdPath}, - widget::AnyWidget, -}; +use crate::{id::Id, widget::AnyWidget}; -use super::View; +use super::{Cx, View}; /// A trait enabling type erasure of views. /// @@ -35,11 +32,11 @@ use super::View; pub trait AnyView { fn as_any(&self) -> &dyn Any; - fn dyn_build(&self, id_path: &mut IdPath) -> (Id, Box, Box); + fn dyn_build(&self, cx: &mut Cx) -> (Id, Box, Box); fn dyn_rebuild( &self, - id_path: &mut IdPath, + cx: &mut Cx, prev: &dyn AnyView, id: &mut Id, state: &mut Box, @@ -64,14 +61,14 @@ where self } - fn dyn_build(&self, id_path: &mut IdPath) -> (Id, Box, Box) { - let (id, state, element) = self.build(id_path); + fn dyn_build(&self, cx: &mut Cx) -> (Id, Box, Box) { + let (id, state, element) = self.build(cx); (id, Box::new(state), Box::new(element)) } fn dyn_rebuild( &self, - id_path: &mut IdPath, + cx: &mut Cx, prev: &dyn AnyView, id: &mut Id, state: &mut Box, @@ -80,7 +77,7 @@ where if let Some(prev) = prev.as_any().downcast_ref() { if let Some(state) = state.downcast_mut() { if let Some(element) = element.deref_mut().as_any_mut().downcast_mut() { - self.rebuild(id_path, prev, id, state, element); + self.rebuild(cx, prev, id, state, element); } else { println!("downcast of element failed in dyn_event"); } @@ -88,7 +85,7 @@ where println!("downcast of state failed in dyn_event"); } } else { - let (new_id, new_state, new_element) = self.build(id_path); + let (new_id, new_state, new_element) = self.build(cx); *id = new_id; *state = Box::new(new_state); *element = Box::new(new_element); @@ -117,20 +114,20 @@ impl View for Box> { type Element = Box; - fn build(&self, id_path: &mut IdPath) -> (Id, Self::State, Self::Element) { - self.deref().dyn_build(id_path) + fn build(&self, cx: &mut Cx) -> (Id, Self::State, Self::Element) { + self.deref().dyn_build(cx) } fn rebuild( &self, - id_path: &mut IdPath, + cx: &mut Cx, prev: &Self, id: &mut Id, state: &mut Self::State, element: &mut Self::Element, ) { self.deref() - .dyn_rebuild(id_path, prev.deref(), id, state, element); + .dyn_rebuild(cx, prev.deref(), id, state, element); } fn event( diff --git a/idiopath/src/view/button.rs b/idiopath/src/view/button.rs index 9eca896f1e..30d0d6e2a3 100644 --- a/idiopath/src/view/button.rs +++ b/idiopath/src/view/button.rs @@ -14,9 +14,9 @@ use std::any::Any; -use crate::id::{Id, IdPath}; +use crate::id::Id; -use super::View; +use super::{Cx, View}; pub struct Button { label: String, @@ -38,17 +38,17 @@ impl View for Button { type Element = crate::widget::button::Button; - fn build(&self, id_path: &mut IdPath) -> (Id, Self::State, Self::Element) { + fn build(&self, cx: &mut Cx) -> (Id, Self::State, Self::Element) { let id = Id::next(); - id_path.push(id); - let element = crate::widget::button::Button::new(&id_path, self.label.clone()); - id_path.pop(); + cx.push(id); + let element = crate::widget::button::Button::new(cx.id_path(), self.label.clone()); + cx.pop(); (id, (), element) } fn rebuild( &self, - _id_path: &mut crate::id::IdPath, + _cx: &mut Cx, prev: &Self, _id: &mut crate::id::Id, _state: &mut Self::State, diff --git a/idiopath/src/view/column.rs b/idiopath/src/view/column.rs index 97499dc5a6..310c9f6628 100644 --- a/idiopath/src/view/column.rs +++ b/idiopath/src/view/column.rs @@ -14,13 +14,9 @@ use std::{any::Any, marker::PhantomData}; -use crate::{ - id::{Id, IdPath}, - view_tuple::ViewTuple, - widget::WidgetTuple, -}; +use crate::{id::Id, view_tuple::ViewTuple, widget::WidgetTuple}; -use super::View; +use super::{Cx, View}; pub struct Column> { children: VT, @@ -45,27 +41,27 @@ where type Element = crate::widget::column::Column; - fn build(&self, id_path: &mut IdPath) -> (Id, Self::State, Self::Element) { + fn build(&self, cx: &mut Cx) -> (Id, Self::State, Self::Element) { let id = Id::next(); - id_path.push(id); - let (state, elements) = self.children.build(id_path); - id_path.pop(); + cx.push(id); + let (state, elements) = self.children.build(cx); + cx.pop(); let column = crate::widget::column::Column::new(elements); (id, state, column) } fn rebuild( &self, - id_path: &mut IdPath, + cx: &mut Cx, prev: &Self, id: &mut Id, state: &mut Self::State, element: &mut Self::Element, ) { - id_path.push(*id); + cx.push(*id); self.children - .rebuild(id_path, &prev.children, state, element.children_mut()); - id_path.pop(); + .rebuild(cx, &prev.children, state, element.children_mut()); + cx.pop(); } fn event( diff --git a/idiopath/src/view/memoize.rs b/idiopath/src/view/memoize.rs index 7f4e6b3608..4b9f25ba2b 100644 --- a/idiopath/src/view/memoize.rs +++ b/idiopath/src/view/memoize.rs @@ -14,9 +14,9 @@ use std::any::Any; -use crate::id::{Id, IdPath}; +use crate::id::Id; -use super::View; +use super::{Cx, View}; pub struct Memoize { data: D, @@ -41,16 +41,16 @@ impl, F: Fn(&D) -> V> View (Id, Self::State, Self::Element) { + fn build(&self, cx: &mut Cx) -> (Id, Self::State, Self::Element) { let view = (self.child_cb)(&self.data); - let (id, view_state, element) = view.build(id_path); + let (id, view_state, element) = view.build(cx); let memoize_state = MemoizeState { view, view_state }; (id, memoize_state, element) } fn rebuild( &self, - id_path: &mut IdPath, + cx: &mut Cx, prev: &Self, id: &mut Id, state: &mut Self::State, @@ -58,7 +58,7 @@ impl, F: Fn(&D) -> V> View, S), A>, FInit: Fn() -> S, F: Fn(&mut S) -> V> View type Element = V::Element; - fn build(&self, id_path: &mut IdPath) -> (Id, Self::State, Self::Element) { + fn build(&self, cx: &mut Cx) -> (Id, Self::State, Self::Element) { let mut state = (self.f_init)(); let view = (self.f)(&mut state); - let (id, view_state, element) = view.build(id_path); + let (id, view_state, element) = view.build(cx); let my_state = UseStateState { state: Some(state), view, @@ -67,14 +67,14 @@ impl, S), A>, FInit: Fn() -> S, F: Fn(&mut S) -> V> View fn rebuild( &self, - id_path: &mut IdPath, + cx: &mut Cx, _prev: &Self, id: &mut Id, state: &mut Self::State, element: &mut Self::Element, ) { let view = (self.f)(state.state.as_mut().unwrap()); - view.rebuild(id_path, &state.view, id, &mut state.view_state, element); + view.rebuild(cx, &state.view, id, &mut state.view_state, element); state.view = view; } diff --git a/idiopath/src/view_tuple.rs b/idiopath/src/view_tuple.rs index 756b00ba1b..3d85e98ccb 100644 --- a/idiopath/src/view_tuple.rs +++ b/idiopath/src/view_tuple.rs @@ -15,8 +15,8 @@ use std::any::Any; use crate::{ - id::{Id, IdPath}, - view::View, + id::Id, + view::{Cx, View}, }; pub trait ViewTuple { @@ -24,15 +24,9 @@ pub trait ViewTuple { type Elements; - fn build(&self, id_path: &mut IdPath) -> (Self::State, Self::Elements); + fn build(&self, cx: &mut Cx) -> (Self::State, Self::Elements); - fn rebuild( - &self, - id_path: &mut IdPath, - prev: &Self, - state: &mut Self::State, - els: &mut Self::Elements, - ); + fn rebuild(&self, cx: &mut Cx, prev: &Self, state: &mut Self::State, els: &mut Self::Elements); fn event( &self, @@ -50,8 +44,8 @@ macro_rules! impl_view_tuple { type Elements = ( $( $t::Element, )* ); - fn build(&self, id_path: &mut IdPath) -> (Self::State, Self::Elements) { - let b = ( $( self.$s.build(id_path), )* ); + fn build(&self, cx: &mut Cx) -> (Self::State, Self::Elements) { + let b = ( $( self.$s.build(cx), )* ); let state = ( $( b.$s.1, )* [ $( b.$s.0 ),* ]); let els = ( $( b.$s.2, )* ); (state, els) @@ -59,14 +53,14 @@ macro_rules! impl_view_tuple { fn rebuild( &self, - id_path: &mut IdPath, + cx: &mut Cx, prev: &Self, state: &mut Self::State, els: &mut Self::Elements, ) { $( self.$s - .rebuild(id_path, &prev.$s, &mut state.$n[$s], &mut state.$s, &mut els.$s); + .rebuild(cx, &prev.$s, &mut state.$n[$s], &mut state.$s, &mut els.$s); )* } diff --git a/idiopath/src/widget/button.rs b/idiopath/src/widget/button.rs index 4b1b9b0599..25d3d601da 100644 --- a/idiopath/src/widget/button.rs +++ b/idiopath/src/widget/button.rs @@ -43,11 +43,7 @@ impl Button { impl Widget for Button { fn event(&mut self, _event: &super::RawEvent, events: &mut Vec) { - let body = Box::new(()); - events.push(Event { - id_path: self.id_path.clone(), - body, - }) + events.push(Event::new(self.id_path.clone(), ())); } fn layout(&mut self) -> Size { From bb86d0f31fad0642f9b1f37f9cdb3d9a3a6bdf15 Mon Sep 17 00:00:00 2001 From: Raph Levien Date: Tue, 3 May 2022 09:23:23 -0700 Subject: [PATCH 12/28] Minor ergonomic improvements Provide default (unit) action type for View. Add with_id and with_new_id methods to Cx which ensure proper nesting by construction. --- idiopath/src/app.rs | 4 ++-- idiopath/src/id.rs | 1 + idiopath/src/main.rs | 10 +++++----- idiopath/src/view.rs | 23 ++++++++++++++++++++++- idiopath/src/view/any_view.rs | 2 +- idiopath/src/view/button.rs | 6 ++---- idiopath/src/view/column.rs | 13 +++++-------- idiopath/src/view/use_state.rs | 1 + 8 files changed, 39 insertions(+), 21 deletions(-) diff --git a/idiopath/src/app.rs b/idiopath/src/app.rs index 06ed5c7a53..7e57aa4734 100644 --- a/idiopath/src/app.rs +++ b/idiopath/src/app.rs @@ -21,7 +21,7 @@ use crate::{ widget::{RawEvent, Widget}, }; -pub struct App, F: FnMut(&mut T) -> V> +pub struct App, F: FnMut(&mut T) -> V> where V::Element: Widget, { @@ -35,7 +35,7 @@ where cx: Cx, } -impl, F: FnMut(&mut T) -> V> App +impl, F: FnMut(&mut T) -> V> App where V::Element: Widget, { diff --git a/idiopath/src/id.rs b/idiopath/src/id.rs index 2bf6889e7d..666ad6c7c0 100644 --- a/idiopath/src/id.rs +++ b/idiopath/src/id.rs @@ -28,6 +28,7 @@ impl Id { Id(WIDGET_ID_COUNTER.next_nonzero()) } + #[allow(unused)] pub fn to_raw(self) -> u64 { self.0.into() } diff --git a/idiopath/src/main.rs b/idiopath/src/main.rs index d52a1501ec..850f1fbe98 100644 --- a/idiopath/src/main.rs +++ b/idiopath/src/main.rs @@ -38,7 +38,7 @@ use widget::Widget; const BG_COLOR: Color = Color::rgb8(0x27, 0x28, 0x22); -struct MainState, F: FnMut(&mut T) -> V> +struct MainState, F: FnMut(&mut T) -> V> where V::Element: Widget, { @@ -47,7 +47,7 @@ where app: App, } -impl + 'static, F: FnMut(&mut T) -> V + 'static> WinHandler +impl + 'static, F: FnMut(&mut T) -> V + 'static> WinHandler for MainState where V::Element: Widget, @@ -103,7 +103,7 @@ where } } -impl, F: FnMut(&mut T) -> V> MainState +impl, F: FnMut(&mut T) -> V> MainState where V::Element: Widget, { @@ -130,11 +130,11 @@ struct AppData { count: u32, } -fn count_button(count: u32) -> impl View { +fn count_button(count: u32) -> impl View { Button::new(format!("count: {}", count), |data| *data += 1) } -fn app_logic(data: &mut AppData) -> impl View { +fn app_logic(data: &mut AppData) -> impl View { Column::new(( Button::new(format!("count: {}", data.count), |data: &mut AppData| { data.count += 1 diff --git a/idiopath/src/view.rs b/idiopath/src/view.rs index 44186749aa..0ffb64dff4 100644 --- a/idiopath/src/view.rs +++ b/idiopath/src/view.rs @@ -26,7 +26,7 @@ use crate::{ widget::Widget, }; -pub trait View { +pub trait View { type State; type Element: Widget; @@ -78,4 +78,25 @@ impl Cx { pub fn id_path(&self) -> &IdPath { &self.id_path } + + /// Run some logic with an id added to the id path. + /// + /// This is an ergonomic helper that ensures proper nesting of the id path. + pub fn with_id T>(&mut self, id: Id, f: F) -> T { + self.push(id); + let result = f(self); + self.pop(); + result + } + + /// Allocate a new id and run logic with the new id added to the id path. + /// + /// Also an ergonomic helper. + pub fn with_new_id T>(&mut self, f: F) -> (Id, T) { + let id = Id::next(); + self.push(id); + let result = f(self); + self.pop(); + (id, result) + } } diff --git a/idiopath/src/view/any_view.rs b/idiopath/src/view/any_view.rs index 570d7492ab..95abaf946e 100644 --- a/idiopath/src/view/any_view.rs +++ b/idiopath/src/view/any_view.rs @@ -29,7 +29,7 @@ use super::{Cx, View}; /// be well beyond the capability of Rust's type system. If type-erased /// views with other bounds are needed, the best approach is probably /// duplication of the code, probably with a macro. -pub trait AnyView { +pub trait AnyView { fn as_any(&self) -> &dyn Any; fn dyn_build(&self, cx: &mut Cx) -> (Id, Box, Box); diff --git a/idiopath/src/view/button.rs b/idiopath/src/view/button.rs index 30d0d6e2a3..d4faf2a33a 100644 --- a/idiopath/src/view/button.rs +++ b/idiopath/src/view/button.rs @@ -39,10 +39,8 @@ impl View for Button { type Element = crate::widget::button::Button; fn build(&self, cx: &mut Cx) -> (Id, Self::State, Self::Element) { - let id = Id::next(); - cx.push(id); - let element = crate::widget::button::Button::new(cx.id_path(), self.label.clone()); - cx.pop(); + let (id, element) = cx + .with_new_id(|cx| crate::widget::button::Button::new(cx.id_path(), self.label.clone())); (id, (), element) } diff --git a/idiopath/src/view/column.rs b/idiopath/src/view/column.rs index 310c9f6628..da58ce3cd9 100644 --- a/idiopath/src/view/column.rs +++ b/idiopath/src/view/column.rs @@ -42,10 +42,7 @@ where type Element = crate::widget::column::Column; fn build(&self, cx: &mut Cx) -> (Id, Self::State, Self::Element) { - let id = Id::next(); - cx.push(id); - let (state, elements) = self.children.build(cx); - cx.pop(); + let (id, (state, elements)) = cx.with_new_id(|cx| self.children.build(cx)); let column = crate::widget::column::Column::new(elements); (id, state, column) } @@ -58,10 +55,10 @@ where state: &mut Self::State, element: &mut Self::Element, ) { - cx.push(*id); - self.children - .rebuild(cx, &prev.children, state, element.children_mut()); - cx.pop(); + cx.with_id(*id, |cx| { + self.children + .rebuild(cx, &prev.children, state, element.children_mut()) + }); } fn event( diff --git a/idiopath/src/view/use_state.rs b/idiopath/src/view/use_state.rs index c28201489b..1786be6b11 100644 --- a/idiopath/src/view/use_state.rs +++ b/idiopath/src/view/use_state.rs @@ -40,6 +40,7 @@ pub struct UseStateState, S), A>> { } impl S, F: Fn(&mut S) -> V> UseState { + #[allow(unused)] pub fn new(f_init: FInit, f: F) -> Self { let phantom = Default::default(); UseState { f_init, f, phantom } From c607825bf4f6dfeb4e53140a5d940ea6e2551735 Mon Sep 17 00:00:00 2001 From: Raph Levien Date: Tue, 3 May 2022 15:57:52 -0700 Subject: [PATCH 13/28] Return "changed" indication from rebuild Tweak signature of rebuild method to return a boolean indicating whether anything has been changed. This is not used yet, but will be for invalidation. --- idiopath/src/view.rs | 27 ++++++++++++++++++++++++++- idiopath/src/view/adapt.rs | 4 ++-- idiopath/src/view/any_view.rs | 13 ++++++++----- idiopath/src/view/button.rs | 5 ++++- idiopath/src/view/column.rs | 4 ++-- idiopath/src/view/memoize.rs | 7 +++++-- idiopath/src/view/use_state.rs | 5 +++-- idiopath/src/view_tuple.rs | 14 +++++++++++--- 8 files changed, 61 insertions(+), 18 deletions(-) diff --git a/idiopath/src/view.rs b/idiopath/src/view.rs index 0ffb64dff4..7c071f68c0 100644 --- a/idiopath/src/view.rs +++ b/idiopath/src/view.rs @@ -26,13 +26,34 @@ use crate::{ widget::Widget, }; +/// A view object representing a node in the UI. +/// +/// This is a central trait for representing UI. An app will generate a tree of +/// these objects (the view tree) as the primary interface for expressing UI. +/// The view tree is transitory and is retained only long enough to dispatch +/// events and then serve as a reference for diffing for the next view tree. +/// +/// The framework will then run methods on these views to create the associated +/// state tree and widget tree, as well as incremental updates and event +/// propagation. +/// +/// The `View` trait is parameterized by `T`, which is known as the "app state", +/// and also a type for actions which are passed up the tree in event +/// propagation. During event handling, mutable access to the app state is +/// given to view nodes, which in turn can make expose it to callbacks. pub trait View { + /// Associated state for the view. type State; + /// The associated widget for the view. type Element: Widget; + /// Build the associated widget and initialize state. fn build(&self, cx: &mut Cx) -> (Id, Self::State, Self::Element); + /// Update the associated widget. + /// + /// Returns `true` when anything has changed. fn rebuild( &self, cx: &mut Cx, @@ -40,8 +61,12 @@ pub trait View { id: &mut Id, state: &mut Self::State, element: &mut Self::Element, - ); + ) -> bool; + /// Propagate an event. + /// + /// Handle an event, propagating to children if needed. Here, `id_path` is a slice + /// of ids beginning at a child of this view. fn event( &self, id_path: &[Id], diff --git a/idiopath/src/view/adapt.rs b/idiopath/src/view/adapt.rs index 8accd4cd3a..28318fc4a3 100644 --- a/idiopath/src/view/adapt.rs +++ b/idiopath/src/view/adapt.rs @@ -70,8 +70,8 @@ impl) -> A, C: View> View bool { + self.child.rebuild(cx, &prev.child, id, state, element) } fn event( diff --git a/idiopath/src/view/any_view.rs b/idiopath/src/view/any_view.rs index 95abaf946e..70274bba96 100644 --- a/idiopath/src/view/any_view.rs +++ b/idiopath/src/view/any_view.rs @@ -41,7 +41,7 @@ pub trait AnyView { id: &mut Id, state: &mut Box, element: &mut Box, - ); + ) -> bool; fn dyn_event( &self, @@ -73,22 +73,25 @@ where id: &mut Id, state: &mut Box, element: &mut Box, - ) { + ) -> bool { if let Some(prev) = prev.as_any().downcast_ref() { if let Some(state) = state.downcast_mut() { if let Some(element) = element.deref_mut().as_any_mut().downcast_mut() { - self.rebuild(cx, prev, id, state, element); + self.rebuild(cx, prev, id, state, element) } else { println!("downcast of element failed in dyn_event"); + false } } else { println!("downcast of state failed in dyn_event"); + false } } else { let (new_id, new_state, new_element) = self.build(cx); *id = new_id; *state = Box::new(new_state); *element = Box::new(new_element); + true } } @@ -125,9 +128,9 @@ impl View for Box> { id: &mut Id, state: &mut Self::State, element: &mut Self::Element, - ) { + ) -> bool { self.deref() - .dyn_rebuild(cx, prev.deref(), id, state, element); + .dyn_rebuild(cx, prev.deref(), id, state, element) } fn event( diff --git a/idiopath/src/view/button.rs b/idiopath/src/view/button.rs index d4faf2a33a..72ffcd7eab 100644 --- a/idiopath/src/view/button.rs +++ b/idiopath/src/view/button.rs @@ -51,9 +51,12 @@ impl View for Button { _id: &mut crate::id::Id, _state: &mut Self::State, element: &mut Self::Element, - ) { + ) -> bool { if prev.label != self.label { element.set_label(self.label.clone()); + true + } else { + false } } diff --git a/idiopath/src/view/column.rs b/idiopath/src/view/column.rs index da58ce3cd9..27dc18543b 100644 --- a/idiopath/src/view/column.rs +++ b/idiopath/src/view/column.rs @@ -54,11 +54,11 @@ where id: &mut Id, state: &mut Self::State, element: &mut Self::Element, - ) { + ) -> bool { cx.with_id(*id, |cx| { self.children .rebuild(cx, &prev.children, state, element.children_mut()) - }); + }) } fn event( diff --git a/idiopath/src/view/memoize.rs b/idiopath/src/view/memoize.rs index 4b9f25ba2b..bab968e686 100644 --- a/idiopath/src/view/memoize.rs +++ b/idiopath/src/view/memoize.rs @@ -55,11 +55,14 @@ impl, F: Fn(&D) -> V> View bool { if prev.data != self.data { let view = (self.child_cb)(&self.data); - view.rebuild(cx, &state.view, id, &mut state.view_state, element); + let changed = view.rebuild(cx, &state.view, id, &mut state.view_state, element); state.view = view; + changed + } else { + false } } diff --git a/idiopath/src/view/use_state.rs b/idiopath/src/view/use_state.rs index 1786be6b11..556b374687 100644 --- a/idiopath/src/view/use_state.rs +++ b/idiopath/src/view/use_state.rs @@ -73,10 +73,11 @@ impl, S), A>, FInit: Fn() -> S, F: Fn(&mut S) -> V> View id: &mut Id, state: &mut Self::State, element: &mut Self::Element, - ) { + ) -> bool { let view = (self.f)(state.state.as_mut().unwrap()); - view.rebuild(cx, &state.view, id, &mut state.view_state, element); + let changed = view.rebuild(cx, &state.view, id, &mut state.view_state, element); state.view = view; + changed } fn event( diff --git a/idiopath/src/view_tuple.rs b/idiopath/src/view_tuple.rs index 3d85e98ccb..e2ce0d77e7 100644 --- a/idiopath/src/view_tuple.rs +++ b/idiopath/src/view_tuple.rs @@ -26,7 +26,13 @@ pub trait ViewTuple { fn build(&self, cx: &mut Cx) -> (Self::State, Self::Elements); - fn rebuild(&self, cx: &mut Cx, prev: &Self, state: &mut Self::State, els: &mut Self::Elements); + fn rebuild( + &self, + cx: &mut Cx, + prev: &Self, + state: &mut Self::State, + els: &mut Self::Elements, + ) -> bool; fn event( &self, @@ -57,11 +63,13 @@ macro_rules! impl_view_tuple { prev: &Self, state: &mut Self::State, els: &mut Self::Elements, - ) { + ) -> bool { + let mut changed = false; $( - self.$s + changed |= self.$s .rebuild(cx, &prev.$s, &mut state.$n[$s], &mut state.$s, &mut els.$s); )* + changed } fn event( From a69bb1b309a91fc1f446dbd9bd70f46e6669234f Mon Sep 17 00:00:00 2001 From: Raph Levien Date: Tue, 3 May 2022 16:39:41 -0700 Subject: [PATCH 14/28] Add EventResult Let event handlers do different things than return user-specified actions. This may not be used immediately but is important for implementing environment. --- idiopath/src/event.rs | 29 +++++++++++++++++++++++++++++ idiopath/src/view.rs | 5 +++-- idiopath/src/view/adapt.rs | 14 ++++++++------ idiopath/src/view/any_view.rs | 11 +++++------ idiopath/src/view/button.rs | 6 +++--- idiopath/src/view/column.rs | 4 ++-- idiopath/src/view/memoize.rs | 21 +++++++++++++++------ idiopath/src/view/use_state.rs | 4 ++-- idiopath/src/view_tuple.rs | 7 ++++--- 9 files changed, 71 insertions(+), 30 deletions(-) diff --git a/idiopath/src/event.rs b/idiopath/src/event.rs index b86ee3684e..5279846770 100644 --- a/idiopath/src/event.rs +++ b/idiopath/src/event.rs @@ -21,6 +21,35 @@ pub struct Event { pub body: Box, } +/// A result wrapper type for event handlers. +pub enum EventResult { + /// The event handler was invoked and returned an action. + Action(A), + /// The event handler received a change request that requests a rebuild. + #[allow(unused)] + RequestRebuild, + /// The event handler discarded the event. + #[allow(unused)] + Nop, + /// The event was addressed to an id path no longer in the tree. + /// + /// This is a normal outcome for async operation when the tree is changing + /// dynamically, but otherwise indicates a logic error. + Stale, +} + +impl EventResult { + #[allow(unused)] + pub fn map(self, f: impl FnOnce(A) -> B) -> EventResult { + match self { + EventResult::Action(a) => EventResult::Action(f(a)), + EventResult::RequestRebuild => EventResult::RequestRebuild, + EventResult::Stale => EventResult::Stale, + EventResult::Nop => EventResult::Nop, + } + } +} + impl Event { pub fn new(id_path: IdPath, event: impl Any) -> Event { Event { diff --git a/idiopath/src/view.rs b/idiopath/src/view.rs index 7c071f68c0..b202c2ea65 100644 --- a/idiopath/src/view.rs +++ b/idiopath/src/view.rs @@ -22,6 +22,7 @@ pub mod use_state; use std::any::Any; use crate::{ + event::EventResult, id::{Id, IdPath}, widget::Widget, }; @@ -36,7 +37,7 @@ use crate::{ /// The framework will then run methods on these views to create the associated /// state tree and widget tree, as well as incremental updates and event /// propagation. -/// +/// /// The `View` trait is parameterized by `T`, which is known as the "app state", /// and also a type for actions which are passed up the tree in event /// propagation. During event handling, mutable access to the app state is @@ -73,7 +74,7 @@ pub trait View { state: &mut Self::State, event: Box, app_state: &mut T, - ) -> A; + ) -> EventResult; } #[derive(Clone)] diff --git a/idiopath/src/view/adapt.rs b/idiopath/src/view/adapt.rs index 28318fc4a3..2a34b994fa 100644 --- a/idiopath/src/view/adapt.rs +++ b/idiopath/src/view/adapt.rs @@ -14,11 +14,11 @@ use std::{any::Any, marker::PhantomData}; -use crate::id::Id; +use crate::{event::EventResult, id::Id}; use super::{Cx, View}; -pub struct Adapt) -> A, C: View> { +pub struct Adapt) -> EventResult, C: View> { f: F, child: C, phantom: PhantomData<(T, A, U, B)>, @@ -35,7 +35,9 @@ pub struct AdaptThunk<'a, U, B, C: View> { event: Box, } -impl) -> A, C: View> Adapt { +impl) -> EventResult, C: View> + Adapt +{ pub fn new(f: F, child: C) -> Self { Adapt { f, @@ -46,13 +48,13 @@ impl) -> A, C: View> Adapt> AdaptThunk<'a, U, B, C> { - pub fn call(self, app_state: &mut U) -> B { + pub fn call(self, app_state: &mut U) -> EventResult { self.child .event(self.id_path, self.state, self.event, app_state) } } -impl) -> A, C: View> View +impl) -> EventResult, C: View> View for Adapt { type State = C::State; @@ -80,7 +82,7 @@ impl) -> A, C: View> View, app_state: &mut T, - ) -> A { + ) -> EventResult { let thunk = AdaptThunk { child: &self.child, state, diff --git a/idiopath/src/view/any_view.rs b/idiopath/src/view/any_view.rs index 70274bba96..f1c4ff26cb 100644 --- a/idiopath/src/view/any_view.rs +++ b/idiopath/src/view/any_view.rs @@ -16,7 +16,7 @@ use std::{ ops::{Deref, DerefMut}, }; -use crate::{id::Id, widget::AnyWidget}; +use crate::{event::EventResult, id::Id, widget::AnyWidget}; use super::{Cx, View}; @@ -49,7 +49,7 @@ pub trait AnyView { state: &mut dyn Any, event: Box, app_state: &mut T, - ) -> A; + ) -> EventResult; } impl + 'static> AnyView for V @@ -101,12 +101,11 @@ where state: &mut dyn Any, event: Box, app_state: &mut T, - ) -> A { + ) -> EventResult { if let Some(state) = state.downcast_mut() { self.event(id_path, state, event, app_state) } else { - // Possibly softer failure? Would require either Option return or - // Default bound on A. + // Possibly softer failure? panic!("downcast error in dyn_event"); } } @@ -139,7 +138,7 @@ impl View for Box> { state: &mut Self::State, event: Box, app_state: &mut T, - ) -> A { + ) -> EventResult { self.deref() .dyn_event(id_path, state.deref_mut(), event, app_state) } diff --git a/idiopath/src/view/button.rs b/idiopath/src/view/button.rs index 72ffcd7eab..47059c104b 100644 --- a/idiopath/src/view/button.rs +++ b/idiopath/src/view/button.rs @@ -14,7 +14,7 @@ use std::any::Any; -use crate::id::Id; +use crate::{event::EventResult, id::Id}; use super::{Cx, View}; @@ -66,7 +66,7 @@ impl View for Button { _state: &mut Self::State, _event: Box, app_state: &mut T, - ) -> A { - (self.callback)(app_state) + ) -> EventResult { + EventResult::Action((self.callback)(app_state)) } } diff --git a/idiopath/src/view/column.rs b/idiopath/src/view/column.rs index 27dc18543b..848a852e4a 100644 --- a/idiopath/src/view/column.rs +++ b/idiopath/src/view/column.rs @@ -14,7 +14,7 @@ use std::{any::Any, marker::PhantomData}; -use crate::{id::Id, view_tuple::ViewTuple, widget::WidgetTuple}; +use crate::{event::EventResult, id::Id, view_tuple::ViewTuple, widget::WidgetTuple}; use super::{Cx, View}; @@ -67,7 +67,7 @@ where state: &mut Self::State, event: Box, app_state: &mut T, - ) -> A { + ) -> EventResult { self.children.event(id_path, state, event, app_state) } } diff --git a/idiopath/src/view/memoize.rs b/idiopath/src/view/memoize.rs index bab968e686..c0543c64dc 100644 --- a/idiopath/src/view/memoize.rs +++ b/idiopath/src/view/memoize.rs @@ -14,7 +14,7 @@ use std::any::Any; -use crate::id::Id; +use crate::{event::EventResult, id::Id}; use super::{Cx, View}; @@ -26,6 +26,7 @@ pub struct Memoize { pub struct MemoizeState> { view: V, view_state: V::State, + dirty: bool, } impl V> Memoize { @@ -44,7 +45,11 @@ impl, F: Fn(&D) -> V> View (Id, Self::State, Self::Element) { let view = (self.child_cb)(&self.data); let (id, view_state, element) = view.build(cx); - let memoize_state = MemoizeState { view, view_state }; + let memoize_state = MemoizeState { + view, + view_state, + dirty: false, + }; (id, memoize_state, element) } @@ -56,7 +61,7 @@ impl, F: Fn(&D) -> V> View bool { - if prev.data != self.data { + if std::mem::take(&mut state.dirty) || prev.data != self.data { let view = (self.child_cb)(&self.data); let changed = view.rebuild(cx, &state.view, id, &mut state.view_state, element); state.view = view; @@ -72,9 +77,13 @@ impl, F: Fn(&D) -> V> View, app_state: &mut T, - ) -> A { - state + ) -> EventResult { + let r = state .view - .event(id_path, &mut state.view_state, event, app_state) + .event(id_path, &mut state.view_state, event, app_state); + if matches!(r, EventResult::RequestRebuild) { + state.dirty = true; + } + r } } diff --git a/idiopath/src/view/use_state.rs b/idiopath/src/view/use_state.rs index 556b374687..b276b22341 100644 --- a/idiopath/src/view/use_state.rs +++ b/idiopath/src/view/use_state.rs @@ -14,7 +14,7 @@ use std::{any::Any, marker::PhantomData, rc::Rc}; -use crate::id::Id; +use crate::{event::EventResult, id::Id}; use super::{Cx, View}; @@ -86,7 +86,7 @@ impl, S), A>, FInit: Fn() -> S, F: Fn(&mut S) -> V> View state: &mut Self::State, event: Box, app_state: &mut Rc, - ) -> A { + ) -> EventResult { let mut local_state = (app_state.clone(), state.state.take().unwrap()); let a = state .view diff --git a/idiopath/src/view_tuple.rs b/idiopath/src/view_tuple.rs index e2ce0d77e7..0f7f4fdadb 100644 --- a/idiopath/src/view_tuple.rs +++ b/idiopath/src/view_tuple.rs @@ -15,6 +15,7 @@ use std::any::Any; use crate::{ + event::EventResult, id::Id, view::{Cx, View}, }; @@ -40,7 +41,7 @@ pub trait ViewTuple { state: &mut Self::State, event: Box, app_state: &mut T, - ) -> A; + ) -> EventResult; } macro_rules! impl_view_tuple { @@ -78,14 +79,14 @@ macro_rules! impl_view_tuple { state: &mut Self::State, event: Box, app_state: &mut T, - ) -> A { + ) -> EventResult { let hd = id_path[0]; let tl = &id_path[1..]; $( if hd == state.$n[$s] { self.$s.event(tl, &mut state.$s, event, app_state) } else )* { - panic!("inconsistent id_path") + crate::event::EventResult::Stale } } } From 8682d90cdb20d837bf93afb4893ea48a0c9fdc7f Mon Sep 17 00:00:00 2001 From: Raph Levien Date: Tue, 3 May 2022 19:38:22 -0700 Subject: [PATCH 15/28] Start implementing a widget hierarchy The prototype had an extremely skeletal implementation of the widget hierarchy, but with this commit we start building more of a real one. It's based to a large exttent on existing Druid widgets, but some parts may be done differently, in particular an exploration into layout more similar to SwiftUI. --- idiopath/Cargo.toml | 1 + idiopath/src/app.rs | 32 ++++- idiopath/src/main.rs | 25 ++-- idiopath/src/view/any_view.rs | 4 +- idiopath/src/view/button.rs | 7 + idiopath/src/view/column.rs | 17 +-- idiopath/src/{view_tuple.rs => view_seq.rs} | 36 +++-- idiopath/src/widget.rs | 140 ++++++++++++++++++-- idiopath/src/widget/button.rs | 10 +- idiopath/src/widget/column.rs | 47 +++---- 10 files changed, 236 insertions(+), 83 deletions(-) rename idiopath/src/{view_tuple.rs => view_seq.rs} (71%) diff --git a/idiopath/Cargo.toml b/idiopath/Cargo.toml index 148802e512..f4c9bb2241 100644 --- a/idiopath/Cargo.toml +++ b/idiopath/Cargo.toml @@ -6,3 +6,4 @@ edition = "2021" [dependencies] "druid-shell" = { path = "../druid-shell" } +bitflags = "1.3.2" diff --git a/idiopath/src/app.rs b/idiopath/src/app.rs index 7e57aa4734..2ab08f7518 100644 --- a/idiopath/src/app.rs +++ b/idiopath/src/app.rs @@ -12,8 +12,11 @@ // See the License for the specific language governing permissions and // limitations under the License. -use druid_shell::{kurbo::Point, piet::Piet, WindowHandle}; +use druid_shell::kurbo::Size; +use druid_shell::piet::{Color, Piet, RenderContext}; +use druid_shell::{kurbo::Point, WindowHandle}; +use crate::widget::{CxState, LayoutCx, PaintCx, WidgetState}; use crate::{ event::Event, id::Id, @@ -32,9 +35,14 @@ where state: Option, element: Option, events: Vec, + window_handle: WindowHandle, + root_state: WidgetState, + size: Size, cx: Cx, } +const BG_COLOR: Color = Color::rgb8(0x27, 0x28, 0x22); + impl, F: FnMut(&mut T) -> V> App where V::Element: Widget, @@ -49,6 +57,9 @@ where state: None, element: None, events: Vec::new(), + window_handle: Default::default(), + root_state: Default::default(), + size: Default::default(), cx, } } @@ -64,16 +75,29 @@ where } } - pub fn connect(&mut self, _window_handle: WindowHandle) { + pub fn connect(&mut self, window_handle: WindowHandle) { + self.window_handle = window_handle.clone(); // This will be needed for wiring up async but is a stub for now. //self.cx.set_handle(window_handle.get_idle_handle()); } + pub fn size(&mut self, size: Size) { + self.size = size; + } + pub fn paint(&mut self, piet: &mut Piet) { + let rect = self.size.to_rect(); + piet.fill(rect, &BG_COLOR); + self.ensure_app(); let element = self.element.as_mut().unwrap(); - element.layout(); - element.paint(piet, Point::ZERO); + let text = piet.text(); + let mut cx_state = CxState::new(&self.window_handle, text.clone()); + let mut layout_cx = LayoutCx::new(&mut cx_state, &mut self.root_state); + let proposed_size = self.size; + element.layout(&mut layout_cx, proposed_size); + let mut paint_cx = PaintCx::new(&mut cx_state, piet); + element.paint(&mut paint_cx); } pub fn mouse_down(&mut self, point: Point) { diff --git a/idiopath/src/main.rs b/idiopath/src/main.rs index 850f1fbe98..3420f03238 100644 --- a/idiopath/src/main.rs +++ b/idiopath/src/main.rs @@ -16,33 +16,29 @@ mod app; mod event; mod id; mod view; -mod view_tuple; +mod view_seq; mod widget; use std::any::Any; use app::App; use druid_shell::kurbo::Size; -use druid_shell::piet::{Color, RenderContext}; use druid_shell::{ Application, Cursor, HotKey, Menu, MouseEvent, Region, SysMods, WinHandler, WindowBuilder, WindowHandle, }; use view::adapt::Adapt; -use view::button::Button; -use view::column::Column; +use view::button::button; +use view::column::column; use view::memoize::Memoize; use view::View; use widget::Widget; -const BG_COLOR: Color = Color::rgb8(0x27, 0x28, 0x22); - struct MainState, F: FnMut(&mut T) -> V> where V::Element: Widget, { - size: Size, handle: WindowHandle, app: App, } @@ -60,8 +56,6 @@ where fn prepare_paint(&mut self) {} fn paint(&mut self, piet: &mut druid_shell::piet::Piet, _: &Region) { - let rect = self.size.to_rect(); - piet.fill(rect, &BG_COLOR); self.app.paint(piet); } @@ -87,7 +81,7 @@ where fn mouse_up(&mut self, _event: &MouseEvent) {} fn size(&mut self, size: Size) { - self.size = size; + self.app.size(size); } fn request_close(&mut self) { @@ -109,7 +103,6 @@ where { fn new(app: App) -> Self { let state = MainState { - size: Default::default(), handle: Default::default(), app, }; @@ -131,17 +124,17 @@ struct AppData { } fn count_button(count: u32) -> impl View { - Button::new(format!("count: {}", count), |data| *data += 1) + button(format!("count: {}", count), |data| *data += 1) } fn app_logic(data: &mut AppData) -> impl View { - Column::new(( - Button::new(format!("count: {}", data.count), |data: &mut AppData| { + column(( + button(format!("count: {}", data.count), |data: &mut AppData| { data.count += 1 }), - Button::new("reset", |data: &mut AppData| data.count = 0), + button("reset", |data: &mut AppData| data.count = 0), Memoize::new(data.count, |count| { - Button::new(format!("count: {}", count), |data: &mut AppData| { + button(format!("count: {}", count), |data: &mut AppData| { data.count += 1 }) }), diff --git a/idiopath/src/view/any_view.rs b/idiopath/src/view/any_view.rs index f1c4ff26cb..8c79635a66 100644 --- a/idiopath/src/view/any_view.rs +++ b/idiopath/src/view/any_view.rs @@ -79,11 +79,11 @@ where if let Some(element) = element.deref_mut().as_any_mut().downcast_mut() { self.rebuild(cx, prev, id, state, element) } else { - println!("downcast of element failed in dyn_event"); + println!("downcast of element failed in dyn_rebuild"); false } } else { - println!("downcast of state failed in dyn_event"); + println!("downcast of state failed in dyn_rebuild"); false } } else { diff --git a/idiopath/src/view/button.rs b/idiopath/src/view/button.rs index 47059c104b..6bf7263427 100644 --- a/idiopath/src/view/button.rs +++ b/idiopath/src/view/button.rs @@ -24,6 +24,13 @@ pub struct Button { callback: Box A>, } +pub fn button( + label: impl Into, + clicked: impl Fn(&mut T) -> A + 'static, +) -> Button { + Button::new(label, clicked) +} + impl Button { pub fn new(label: impl Into, clicked: impl Fn(&mut T) -> A + 'static) -> Self { Button { diff --git a/idiopath/src/view/column.rs b/idiopath/src/view/column.rs index 848a852e4a..29e25b398f 100644 --- a/idiopath/src/view/column.rs +++ b/idiopath/src/view/column.rs @@ -14,32 +14,33 @@ use std::{any::Any, marker::PhantomData}; -use crate::{event::EventResult, id::Id, view_tuple::ViewTuple, widget::WidgetTuple}; +use crate::{event::EventResult, id::Id, view_seq::ViewSequence, widget::WidgetTuple}; use super::{Cx, View}; -pub struct Column> { +pub struct Column> { children: VT, phantom: PhantomData<(T, A)>, } -impl> Column -where - VT::Elements: WidgetTuple, -{ +pub fn column>(children: VT) -> Column { + Column::new(children) +} + +impl> Column { pub fn new(children: VT) -> Self { let phantom = Default::default(); Column { children, phantom } } } -impl> View for Column +impl> View for Column where VT::Elements: WidgetTuple, { type State = VT::State; - type Element = crate::widget::column::Column; + type Element = crate::widget::column::Column; fn build(&self, cx: &mut Cx) -> (Id, Self::State, Self::Element) { let (id, (state, elements)) = cx.with_new_id(|cx| self.children.build(cx)); diff --git a/idiopath/src/view_tuple.rs b/idiopath/src/view_seq.rs similarity index 71% rename from idiopath/src/view_tuple.rs rename to idiopath/src/view_seq.rs index 0f7f4fdadb..2cf5fffb3f 100644 --- a/idiopath/src/view_tuple.rs +++ b/idiopath/src/view_seq.rs @@ -18,21 +18,22 @@ use crate::{ event::EventResult, id::Id, view::{Cx, View}, + widget::Pod, }; -pub trait ViewTuple { +pub trait ViewSequence { type State; type Elements; - fn build(&self, cx: &mut Cx) -> (Self::State, Self::Elements); + fn build(&self, cx: &mut Cx) -> (Self::State, Vec); fn rebuild( &self, cx: &mut Cx, prev: &Self, state: &mut Self::State, - els: &mut Self::Elements, + els: &mut Vec, ) -> bool; fn event( @@ -45,16 +46,18 @@ pub trait ViewTuple { } macro_rules! impl_view_tuple { - ( $n: tt; $( $t:ident),* ; $( $s:tt ),* ) => { - impl ),* > ViewTuple for ( $( $t, )* ) { + ( $n: tt; $( $t:ident),* ; $( $i:tt ),* ) => { + impl ),* > ViewSequence for ( $( $t, )* ) + where $( <$t as View>::Element: 'static ),* + { type State = ( $( $t::State, )* [Id; $n]); type Elements = ( $( $t::Element, )* ); - fn build(&self, cx: &mut Cx) -> (Self::State, Self::Elements) { - let b = ( $( self.$s.build(cx), )* ); - let state = ( $( b.$s.1, )* [ $( b.$s.0 ),* ]); - let els = ( $( b.$s.2, )* ); + fn build(&self, cx: &mut Cx) -> (Self::State, Vec) { + let b = ( $( self.$i.build(cx), )* ); + let state = ( $( b.$i.1, )* [ $( b.$i.0 ),* ]); + let els = vec![ $( Pod::new(b.$i.2) ),* ]; (state, els) } @@ -63,12 +66,17 @@ macro_rules! impl_view_tuple { cx: &mut Cx, prev: &Self, state: &mut Self::State, - els: &mut Self::Elements, + els: &mut Vec, ) -> bool { let mut changed = false; $( - changed |= self.$s - .rebuild(cx, &prev.$s, &mut state.$n[$s], &mut state.$s, &mut els.$s); + if self.$i + .rebuild(cx, &prev.$i, &mut state.$n[$i], &mut state.$i, + els[$i].downcast_mut().unwrap()) + { + els[$i].request_update(); + changed = true; + } )* changed } @@ -83,8 +91,8 @@ macro_rules! impl_view_tuple { let hd = id_path[0]; let tl = &id_path[1..]; $( - if hd == state.$n[$s] { - self.$s.event(tl, &mut state.$s, event, app_state) + if hd == state.$n[$i] { + self.$i.event(tl, &mut state.$i, event, app_state) } else )* { crate::event::EventResult::Stale } diff --git a/idiopath/src/widget.rs b/idiopath/src/widget.rs index 45a171e010..6c310ca5c5 100644 --- a/idiopath/src/widget.rs +++ b/idiopath/src/widget.rs @@ -16,10 +16,13 @@ pub mod button; pub mod column; use std::any::Any; -use std::ops::DerefMut; +use std::ops::{Deref, DerefMut}; -use druid_shell::kurbo::{Point, Size}; -use druid_shell::piet::Piet; +use bitflags::bitflags; + +use druid_shell::kurbo::{Affine, Point, Size}; +use druid_shell::piet::{Piet, PietText, RenderContext}; +use druid_shell::WindowHandle; use crate::event::Event; @@ -27,9 +30,48 @@ use crate::event::Event; pub trait Widget { fn event(&mut self, event: &RawEvent, events: &mut Vec); - fn layout(&mut self) -> Size; + fn layout(&mut self, cx: &mut LayoutCx, proposed_size: Size) -> Size; + + fn paint(&mut self, ctx: &mut PaintCx); +} + +// These contexts loosely follow Druid. +pub struct CxState<'a> { + window: &'a WindowHandle, + text: PietText, +} + +pub struct LayoutCx<'a, 'b> { + cx_state: &'a mut CxState<'b>, + widget_state: &'a mut WidgetState, +} - fn paint(&mut self, ctx: &mut Piet, pos: Point); +pub struct PaintCx<'a, 'b, 'c> { + cx_state: &'a mut CxState<'b>, + piet: &'a mut Piet<'c>, +} + +bitflags! { + #[derive(Default)] + struct PodFlags: u32 { + const REQUEST_UPDATE = 1; + const REQUEST_LAYOUT = 2; + const REQUEST_PAINT = 4; + } +} + +/// A pod that contains a widget (in a container). +pub struct Pod { + state: WidgetState, + widget: Box, +} + +#[derive(Default)] +pub struct WidgetState { + flags: PodFlags, + origin: Point, + size: Size, + proposed_size: Size, } // consider renaming, may get other stuff @@ -60,12 +102,12 @@ impl Widget for Box { self.deref_mut().event(event, events); } - fn layout(&mut self) -> Size { - self.deref_mut().layout() + fn layout(&mut self, cx: &mut LayoutCx, proposed_size: Size) -> Size { + self.deref_mut().layout(cx, proposed_size) } - fn paint(&mut self, ctx: &mut Piet, pos: Point) { - self.deref_mut().paint(ctx, pos); + fn paint(&mut self, cx: &mut PaintCx) { + self.deref_mut().paint(cx); } } @@ -112,3 +154,83 @@ impl_widget_tuple!(8; W0, W1, W2, W3, W4, W5, W6, W7; 0, 1, 2, 3, 4, 5, 6, 7 ); + +impl<'a> CxState<'a> { + pub fn new(window: &'a WindowHandle, text: PietText) -> Self { + CxState { window, text } + } +} + +impl<'a, 'b> LayoutCx<'a, 'b> { + pub fn new(cx_state: &'a mut CxState<'b>, root_state: &'a mut WidgetState) -> Self { + LayoutCx { + cx_state, + widget_state: root_state, + } + } +} + +impl<'a, 'b, 'c> PaintCx<'a, 'b, 'c> { + pub fn new(cx_state: &'a mut CxState<'b>, piet: &'a mut Piet<'c>) -> Self { + PaintCx { cx_state, piet } + } + + pub fn with_save(&mut self, f: impl FnOnce(&mut PaintCx)) { + self.piet.save().unwrap(); + f(self); + self.piet.restore().unwrap(); + } +} + +impl<'c> Deref for PaintCx<'_, '_, 'c> { + type Target = Piet<'c>; + + fn deref(&self) -> &Self::Target { + self.piet + } +} + +impl<'c> DerefMut for PaintCx<'_, '_, 'c> { + fn deref_mut(&mut self) -> &mut Self::Target { + self.piet + } +} + +impl Pod { + pub fn new(widget: impl Widget + 'static) -> Self { + Pod { + state: Default::default(), + widget: Box::new(widget), + } + } + + pub fn downcast_mut(&mut self) -> Option<&mut T> { + (*self.widget).as_any_mut().downcast_mut() + } + + pub fn request_update(&mut self) { + self.state.flags |= PodFlags::REQUEST_UPDATE; + } + + pub fn event(&mut self, event: &RawEvent, events: &mut Vec) { + self.widget.event(event, events); + } + + pub fn layout(&mut self, cx: &mut LayoutCx, proposed_size: Size) -> Size { + let mut child_cx = LayoutCx { + cx_state: cx.cx_state, + widget_state: &mut self.state, + }; + let new_size = self.widget.layout(&mut child_cx, proposed_size); + self.state.size = new_size; + new_size + } + + pub fn paint(&mut self, cx: &mut PaintCx) { + cx.with_save(|cx| { + cx.piet + .transform(Affine::translate(self.state.origin.to_vec2())); + self.widget.paint(cx); + }); + } +} diff --git a/idiopath/src/widget/button.rs b/idiopath/src/widget/button.rs index 25d3d601da..0422af6ab3 100644 --- a/idiopath/src/widget/button.rs +++ b/idiopath/src/widget/button.rs @@ -14,12 +14,12 @@ use druid_shell::{ kurbo::{Point, Size}, - piet::{Color, Piet, RenderContext, Text, TextLayoutBuilder}, + piet::{Color, RenderContext, Text, TextLayoutBuilder}, }; use crate::{event::Event, id::IdPath}; -use super::Widget; +use super::{LayoutCx, PaintCx, Widget}; #[derive(Default)] @@ -46,17 +46,17 @@ impl Widget for Button { events.push(Event::new(self.id_path.clone(), ())); } - fn layout(&mut self) -> Size { + fn layout(&mut self, _cx: &mut LayoutCx, _proposed_size: Size) -> Size { Size::new(100., 20.) } - fn paint(&mut self, ctx: &mut Piet, pos: Point) { + fn paint(&mut self, ctx: &mut PaintCx) { let layout = ctx .text() .new_text_layout(self.label.clone()) .text_color(Color::WHITE) .build() .unwrap(); - ctx.draw_text(&layout, pos); + ctx.draw_text(&layout, Point::ZERO); } } diff --git a/idiopath/src/widget/column.rs b/idiopath/src/widget/column.rs index 258e6f5873..4159c5568b 100644 --- a/idiopath/src/widget/column.rs +++ b/idiopath/src/widget/column.rs @@ -13,63 +13,60 @@ // limitations under the License. use druid_shell::{ - kurbo::{Point, Size, Vec2}, + kurbo::{Point, Rect, Size, Vec2}, piet::Piet, }; use crate::event::Event; -use super::{Geom, RawEvent, Widget, WidgetTuple}; +use super::{Geom, LayoutCx, PaintCx, Pod, RawEvent, Widget}; -pub struct Column { - children: W, - geoms: Vec, +pub struct Column { + children: Vec, } -impl Column { - pub fn new(children: W) -> Self { - let geoms = (0..children.length()).map(|_| Geom::default()).collect(); - Column { children, geoms } +impl Column { + pub fn new(children: Vec) -> Self { + Column { children } } - pub fn children_mut(&mut self) -> &mut W { + pub fn children_mut(&mut self) -> &mut Vec { &mut self.children } } -impl Widget for Column { +impl Widget for Column { fn event(&mut self, event: &super::RawEvent, events: &mut Vec) { match event { RawEvent::MouseDown(p) => { - let mut p = *p; - for (child, geom) in self.children.widgets_mut().into_iter().zip(&self.geoms) { - if p.y < geom.size.height { - let child_event = RawEvent::MouseDown(p); + for child in &mut self.children { + let rect = Rect::from_origin_size(child.state.origin, child.state.size); + if rect.contains(*p) { + let child_event = RawEvent::MouseDown(*p - child.state.origin.to_vec2()); child.event(&child_event, events); break; } - p.y -= geom.size.height; } } } } - fn layout(&mut self) -> Size { + fn layout(&mut self, cx: &mut LayoutCx, proposed_size: Size) -> Size { let mut size = Size::default(); - for (child, geom) in self.children.widgets_mut().into_iter().zip(&mut self.geoms) { - let child_size = child.layout(); - geom.size = child_size; + let mut offset = Point::ZERO; + for child in &mut self.children { + let child_size = child.layout(cx, proposed_size); + child.state.origin = offset; size.width = size.width.max(child_size.width); size.height += child_size.height; + offset.y += child_size.height; } size } - fn paint(&mut self, ctx: &mut Piet, pos: Point) { - let mut child_pos = pos + Vec2::new(10.0, 0.0); - for (child, geom) in self.children.widgets_mut().into_iter().zip(&self.geoms) { - child.paint(ctx, child_pos); - child_pos.y += geom.size.height; + fn paint(&mut self, cx: &mut PaintCx) { + for child in &mut self.children { + child.paint(cx); } } } From a2acd92b28a42ae416ef32e91eca515e03b70ab4 Mon Sep 17 00:00:00 2001 From: Raph Levien Date: Tue, 10 May 2022 18:21:27 -0700 Subject: [PATCH 16/28] Start implementing layout Add some layout methods. Rename Column to VStack. Put in alignment mechanisms. Much of the core layout protocol is implemented, but it's not exercised very deeply. A lot of view wrappers like "frame" in SwiftUI need to get implemented. I think I want to get the core widget trait more or less stable first (events need work in particular). --- idiopath/src/app.rs | 50 +++--- idiopath/src/main.rs | 8 +- idiopath/src/view.rs | 3 +- idiopath/src/view/text.rs | 56 ++++++ idiopath/src/view/{column.rs => vstack.rs} | 16 +- idiopath/src/widget.rs | 187 +++++++++++++++++++-- idiopath/src/widget/align.rs | 169 +++++++++++++++++++ idiopath/src/widget/button.rs | 15 +- idiopath/src/widget/column.rs | 72 -------- idiopath/src/widget/text.rs | 112 ++++++++++++ idiopath/src/widget/vstack.rs | 122 ++++++++++++++ 11 files changed, 682 insertions(+), 128 deletions(-) create mode 100644 idiopath/src/view/text.rs rename idiopath/src/view/{column.rs => vstack.rs} (80%) create mode 100644 idiopath/src/widget/align.rs delete mode 100644 idiopath/src/widget/column.rs create mode 100644 idiopath/src/widget/text.rs create mode 100644 idiopath/src/widget/vstack.rs diff --git a/idiopath/src/app.rs b/idiopath/src/app.rs index 2ab08f7518..023f4360b3 100644 --- a/idiopath/src/app.rs +++ b/idiopath/src/app.rs @@ -16,7 +16,7 @@ use druid_shell::kurbo::Size; use druid_shell::piet::{Color, Piet, RenderContext}; use druid_shell::{kurbo::Point, WindowHandle}; -use crate::widget::{CxState, LayoutCx, PaintCx, WidgetState}; +use crate::widget::{CxState, LayoutCx, PaintCx, Pod, UpdateCx, WidgetState}; use crate::{ event::Event, id::Id, @@ -24,19 +24,16 @@ use crate::{ widget::{RawEvent, Widget}, }; -pub struct App, F: FnMut(&mut T) -> V> -where - V::Element: Widget, -{ +pub struct App, F: FnMut(&mut T) -> V> { data: T, app_logic: F, view: Option, id: Option, state: Option, - element: Option, events: Vec, window_handle: WindowHandle, root_state: WidgetState, + root_pod: Option, size: Size, cx: Cx, } @@ -45,7 +42,7 @@ const BG_COLOR: Color = Color::rgb8(0x27, 0x28, 0x22); impl, F: FnMut(&mut T) -> V> App where - V::Element: Widget, + V::Element: Widget + 'static, { pub fn new(data: T, app_logic: F) -> Self { let cx = Cx::new(); @@ -55,7 +52,7 @@ where view: None, id: None, state: None, - element: None, + root_pod: None, events: Vec::new(), window_handle: Default::default(), root_state: Default::default(), @@ -68,10 +65,11 @@ where if self.view.is_none() { let view = (self.app_logic)(&mut self.data); let (id, state, element) = view.build(&mut self.cx); + let root_pod = Pod::new(element); self.view = Some(view); self.id = Some(id); self.state = Some(state); - self.element = Some(element); + self.root_pod = Some(root_pod); } } @@ -90,14 +88,17 @@ where piet.fill(rect, &BG_COLOR); self.ensure_app(); - let element = self.element.as_mut().unwrap(); + let root_pod = self.root_pod.as_mut().unwrap(); let text = piet.text(); let mut cx_state = CxState::new(&self.window_handle, text.clone()); + let mut update_cx = UpdateCx::new(&mut cx_state, &mut self.root_state); + root_pod.update(&mut update_cx); let mut layout_cx = LayoutCx::new(&mut cx_state, &mut self.root_state); + root_pod.prelayout(&mut layout_cx); let proposed_size = self.size; - element.layout(&mut layout_cx, proposed_size); + root_pod.layout(&mut layout_cx, proposed_size); let mut paint_cx = PaintCx::new(&mut cx_state, piet); - element.paint(&mut paint_cx); + root_pod.paint(&mut paint_cx); } pub fn mouse_down(&mut self, point: Point) { @@ -106,8 +107,8 @@ where fn event(&mut self, event: RawEvent) { self.ensure_app(); - let element = self.element.as_mut().unwrap(); - element.event(&event, &mut self.events); + let root_pod = self.root_pod.as_mut().unwrap(); + root_pod.event(&event, &mut self.events); self.run_app_logic(); } @@ -123,14 +124,19 @@ where } // Re-rendering should be more lazy. let view = (self.app_logic)(&mut self.data); - view.rebuild( - &mut self.cx, - self.view.as_ref().unwrap(), - self.id.as_mut().unwrap(), - self.state.as_mut().unwrap(), - self.element.as_mut().unwrap(), - ); - assert!(self.cx.is_empty(), "id path imbalance on rebuild"); + if let Some(element) = self.root_pod.as_mut().unwrap().downcast_mut() { + let changed = view.rebuild( + &mut self.cx, + self.view.as_ref().unwrap(), + self.id.as_mut().unwrap(), + self.state.as_mut().unwrap(), + element, + ); + if changed { + self.root_pod.as_mut().unwrap().request_update(); + } + assert!(self.cx.is_empty(), "id path imbalance on rebuild"); + } self.view = Some(view); } } diff --git a/idiopath/src/main.rs b/idiopath/src/main.rs index 3420f03238..92fcb591ef 100644 --- a/idiopath/src/main.rs +++ b/idiopath/src/main.rs @@ -30,8 +30,8 @@ use druid_shell::{ }; use view::adapt::Adapt; use view::button::button; -use view::column::column; use view::memoize::Memoize; +use view::vstack::v_stack; use view::View; use widget::Widget; @@ -128,10 +128,8 @@ fn count_button(count: u32) -> impl View { } fn app_logic(data: &mut AppData) -> impl View { - column(( - button(format!("count: {}", data.count), |data: &mut AppData| { - data.count += 1 - }), + v_stack(( + format!("count: {}", data.count), button("reset", |data: &mut AppData| data.count = 0), Memoize::new(data.count, |count| { button(format!("count: {}", count), |data: &mut AppData| { diff --git a/idiopath/src/view.rs b/idiopath/src/view.rs index b202c2ea65..87680b9896 100644 --- a/idiopath/src/view.rs +++ b/idiopath/src/view.rs @@ -15,9 +15,10 @@ pub mod adapt; pub mod any_view; pub mod button; -pub mod column; pub mod memoize; +pub mod text; pub mod use_state; +pub mod vstack; use std::any::Any; diff --git a/idiopath/src/view/text.rs b/idiopath/src/view/text.rs new file mode 100644 index 0000000000..ed901e89ba --- /dev/null +++ b/idiopath/src/view/text.rs @@ -0,0 +1,56 @@ +// Copyright 2022 The Druid Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::any::Any; + +use crate::{event::EventResult, id::Id}; + +use super::{Cx, View}; + +impl View for String { + type State = (); + + type Element = crate::widget::text::TextWidget; + + fn build(&self, cx: &mut Cx) -> (Id, Self::State, Self::Element) { + let (id, element) = cx.with_new_id(|_| crate::widget::text::TextWidget::new(self.clone())); + (id, (), element) + } + + fn rebuild( + &self, + _cx: &mut Cx, + prev: &Self, + _id: &mut crate::id::Id, + _state: &mut Self::State, + element: &mut Self::Element, + ) -> bool { + if prev != self { + element.set_text(self.clone()); + true + } else { + false + } + } + + fn event( + &self, + _id_path: &[crate::id::Id], + _state: &mut Self::State, + _event: Box, + _app_state: &mut T, + ) -> EventResult { + EventResult::Stale + } +} diff --git a/idiopath/src/view/column.rs b/idiopath/src/view/vstack.rs similarity index 80% rename from idiopath/src/view/column.rs rename to idiopath/src/view/vstack.rs index 29e25b398f..bd7a81ec27 100644 --- a/idiopath/src/view/column.rs +++ b/idiopath/src/view/vstack.rs @@ -18,33 +18,33 @@ use crate::{event::EventResult, id::Id, view_seq::ViewSequence, widget::WidgetTu use super::{Cx, View}; -pub struct Column> { +pub struct VStack> { children: VT, phantom: PhantomData<(T, A)>, } -pub fn column>(children: VT) -> Column { - Column::new(children) +pub fn v_stack>(children: VT) -> VStack { + VStack::new(children) } -impl> Column { +impl> VStack { pub fn new(children: VT) -> Self { let phantom = Default::default(); - Column { children, phantom } + VStack { children, phantom } } } -impl> View for Column +impl> View for VStack where VT::Elements: WidgetTuple, { type State = VT::State; - type Element = crate::widget::column::Column; + type Element = crate::widget::vstack::VStack; fn build(&self, cx: &mut Cx) -> (Id, Self::State, Self::Element) { let (id, (state, elements)) = cx.with_new_id(|cx| self.children.build(cx)); - let column = crate::widget::column::Column::new(elements); + let column = crate::widget::vstack::VStack::new(elements); (id, state, column) } diff --git a/idiopath/src/widget.rs b/idiopath/src/widget.rs index 6c310ca5c5..22dd51fae5 100644 --- a/idiopath/src/widget.rs +++ b/idiopath/src/widget.rs @@ -12,8 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. +pub mod align; pub mod button; -pub mod column; +pub mod text; +pub mod vstack; use std::any::Any; use std::ops::{Deref, DerefMut}; @@ -26,13 +28,32 @@ use druid_shell::WindowHandle; use crate::event::Event; +use self::align::{AlignResult, OneAlignment}; + /// A basic widget trait. pub trait Widget { fn event(&mut self, event: &RawEvent, events: &mut Vec); + fn update(&mut self, cx: &mut UpdateCx); + + /// Compute intrinsic sizes. + /// + /// This method will be called once on widget creation and then on + /// REQUEST_UPDATE. + fn prelayout(&mut self, cx: &mut LayoutCx) -> (Size, Size); + + /// Compute size given proposed size. + /// + /// The value will be memoized given the proposed size, invalidated + /// on REQUEST_UPDATE. It can count on prelayout being completed. fn layout(&mut self, cx: &mut LayoutCx, proposed_size: Size) -> Size; - fn paint(&mut self, ctx: &mut PaintCx); + /// Query for an alignment. + /// + /// This method can count on layout already having been completed. + fn align(&self, cx: &mut AlignCx, alignment: &OneAlignment) {} + + fn paint(&mut self, cx: &mut PaintCx); } // These contexts loosely follow Druid. @@ -41,11 +62,21 @@ pub struct CxState<'a> { text: PietText, } +pub struct UpdateCx<'a, 'b> { + cx_state: &'a mut CxState<'b>, + widget_state: &'a mut WidgetState, +} + pub struct LayoutCx<'a, 'b> { cx_state: &'a mut CxState<'b>, widget_state: &'a mut WidgetState, } +pub struct AlignCx<'a> { + widget_state: &'a WidgetState, + align_result: &'a mut AlignResult, +} + pub struct PaintCx<'a, 'b, 'c> { cx_state: &'a mut CxState<'b>, piet: &'a mut Piet<'c>, @@ -57,6 +88,9 @@ bitflags! { const REQUEST_UPDATE = 1; const REQUEST_LAYOUT = 2; const REQUEST_PAINT = 4; + + const UPWARD_FLAGS = Self::REQUEST_LAYOUT.bits | Self::REQUEST_PAINT.bits; + const INIT_FLAGS = Self::REQUEST_UPDATE.bits | Self::REQUEST_LAYOUT.bits | Self::REQUEST_PAINT.bits; } } @@ -66,18 +100,17 @@ pub struct Pod { widget: Box, } -#[derive(Default)] +#[derive(Default, Debug)] pub struct WidgetState { flags: PodFlags, origin: Point, - size: Size, + /// The minimum intrinsic size of the widget. + min_size: Size, + /// The maximum intrinsic size of the widget. + max_size: Size, + /// The size proposed by the widget's container. proposed_size: Size, -} - -// consider renaming, may get other stuff -#[derive(Default)] -pub struct Geom { - // probably want id? + /// The size of the widget. size: Size, } @@ -102,6 +135,14 @@ impl Widget for Box { self.deref_mut().event(event, events); } + fn update(&mut self, cx: &mut UpdateCx) { + self.deref_mut().update(cx); + } + + fn prelayout(&mut self, cx: &mut LayoutCx) -> (Size, Size) { + self.deref_mut().prelayout(cx) + } + fn layout(&mut self, cx: &mut LayoutCx, proposed_size: Size) -> Size { self.deref_mut().layout(cx, proposed_size) } @@ -161,6 +202,19 @@ impl<'a> CxState<'a> { } } +impl<'a, 'b> UpdateCx<'a, 'b> { + pub fn new(cx_state: &'a mut CxState<'b>, root_state: &'a mut WidgetState) -> Self { + UpdateCx { + cx_state, + widget_state: root_state, + } + } + + pub fn request_layout(&mut self) { + self.widget_state.flags |= PodFlags::REQUEST_LAYOUT; + } +} + impl<'a, 'b> LayoutCx<'a, 'b> { pub fn new(cx_state: &'a mut CxState<'b>, root_state: &'a mut WidgetState) -> Self { LayoutCx { @@ -168,6 +222,16 @@ impl<'a, 'b> LayoutCx<'a, 'b> { widget_state: root_state, } } + + pub fn text(&mut self) -> &mut PietText { + &mut self.cx_state.text + } +} + +impl<'a> AlignCx<'a> { + pub fn aggregate(&mut self, alignment: &OneAlignment, value: f64) { + alignment.aggregate(&mut self.align_result, value); + } } impl<'a, 'b, 'c> PaintCx<'a, 'b, 'c> { @@ -196,10 +260,44 @@ impl<'c> DerefMut for PaintCx<'_, '_, 'c> { } } +impl WidgetState { + fn merge_up(&mut self, child_state: &mut WidgetState) { + self.flags |= child_state.flags & PodFlags::UPWARD_FLAGS; + } + + fn request(&mut self, flags: PodFlags) { + self.flags |= flags + } + + fn get_alignment(&self, widget: &dyn AnyWidget, alignment: &OneAlignment) -> f64 { + match alignment { + // Note: will have to swap left/right when we do BiDi. + OneAlignment::Horiz(align::HorizAlignment::Leading) => 0.0, + OneAlignment::Horiz(align::HorizAlignment::Center) => self.size.width * 0.5, + OneAlignment::Horiz(align::HorizAlignment::Trailing) => self.size.width, + OneAlignment::Vert(align::VertAlignment::Top) => 0.0, + OneAlignment::Vert(align::VertAlignment::Center) => self.size.height * 0.5, + OneAlignment::Vert(align::VertAlignment::Bottom) => self.size.height, + _ => { + let mut align_result = AlignResult::default(); + let mut align_cx = AlignCx { + widget_state: self, + align_result: &mut align_result, + }; + widget.align(&mut align_cx, alignment); + align_result.reap(alignment) + } + } + } +} + impl Pod { pub fn new(widget: impl Widget + 'static) -> Self { Pod { - state: Default::default(), + state: WidgetState { + flags: PodFlags::INIT_FLAGS, + ..Default::default() + }, widget: Box::new(widget), } } @@ -209,21 +307,66 @@ impl Pod { } pub fn request_update(&mut self) { - self.state.flags |= PodFlags::REQUEST_UPDATE; + self.state.request(PodFlags::REQUEST_UPDATE); } pub fn event(&mut self, event: &RawEvent, events: &mut Vec) { self.widget.event(event, events); } + /// Propagate an update cycle. + pub fn update(&mut self, cx: &mut UpdateCx) { + if self.state.flags.contains(PodFlags::REQUEST_UPDATE) { + let mut child_cx = UpdateCx { + cx_state: cx.cx_state, + widget_state: &mut self.state, + }; + self.widget.update(&mut child_cx); + self.state.flags.remove(PodFlags::REQUEST_UPDATE); + cx.widget_state.merge_up(&mut self.state); + } + } + + pub fn prelayout(&mut self, cx: &mut LayoutCx) -> (Size, Size) { + if self.state.flags.contains(PodFlags::REQUEST_LAYOUT) { + let mut child_cx = LayoutCx { + cx_state: cx.cx_state, + widget_state: &mut self.state, + }; + let (min_size, max_size) = self.widget.prelayout(&mut child_cx); + self.state.min_size = min_size; + self.state.max_size = max_size; + // Don't remove REQUEST_LAYOUT here, that will be done in layout. + } + (self.state.min_size, self.state.max_size) + } + pub fn layout(&mut self, cx: &mut LayoutCx, proposed_size: Size) -> Size { - let mut child_cx = LayoutCx { - cx_state: cx.cx_state, - widget_state: &mut self.state, + if self.state.flags.contains(PodFlags::REQUEST_LAYOUT) + || proposed_size != self.state.proposed_size + { + let mut child_cx = LayoutCx { + cx_state: cx.cx_state, + widget_state: &mut self.state, + }; + let new_size = self.widget.layout(&mut child_cx, proposed_size); + self.state.proposed_size = proposed_size; + self.state.size = new_size; + self.state.flags.remove(PodFlags::REQUEST_LAYOUT); + } + self.state.size + } + + /// Propagate alignment query to children. + /// + /// This call aggregates all instances of the alignment, so cost may be + /// proportional to the number of descendants. + pub fn align(&mut self, cx: &mut AlignCx, alignment: &OneAlignment) { + let mut child_cx = AlignCx { + widget_state: &self.state, + align_result: cx.align_result, }; - let new_size = self.widget.layout(&mut child_cx, proposed_size); - self.state.size = new_size; - new_size + self.widget.align(&mut child_cx, alignment); } pub fn paint(&mut self, cx: &mut PaintCx) { @@ -233,4 +376,12 @@ impl Pod { self.widget.paint(cx); }); } + + pub fn height_flexibility(&self) -> f64 { + self.state.max_size.height - self.state.min_size.height + } + + pub fn get_alignment(&self, alignment: &OneAlignment) -> f64 { + self.state.get_alignment(&self.widget, alignment) + } } diff --git a/idiopath/src/widget/align.rs b/idiopath/src/widget/align.rs new file mode 100644 index 0000000000..2d8cfc68bd --- /dev/null +++ b/idiopath/src/widget/align.rs @@ -0,0 +1,169 @@ +use super::{AlignCx, AnyWidget, Widget, WidgetState}; + +// Copyright 2022 The Druid Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#[derive(Clone, PartialEq, Debug)] +pub enum HorizAlignment { + // Note: actually "left" until we do BiDi. + Leading, + Center, + Trailing, + // We might switch to TinyStr. + Custom(&'static str), +} + +#[derive(Clone, PartialEq, Debug)] +pub enum VertAlignment { + Top, + Center, + Bottom, + FirstBaseline, + LastBaseline, + Custom(&'static str), +} + +#[derive(Clone, PartialEq, Debug)] +pub enum OneAlignment { + Horiz(HorizAlignment), + Vert(VertAlignment), +} + +#[derive(Default)] +pub struct AlignResult { + value: f64, + count: usize, +} + +impl HorizAlignment { + fn aggregate(&self, result: &mut AlignResult, value: f64) { + result.count += 1; + match self { + HorizAlignment::Leading => result.value = result.value.min(value), + HorizAlignment::Trailing => result.value = result.value.max(value), + _ => result.value += value, + } + } +} + +impl VertAlignment { + fn aggregate(&self, result: &mut AlignResult, value: f64) { + result.count += 1; + match self { + VertAlignment::Top | VertAlignment::FirstBaseline => { + result.value = result.value.min(value) + } + VertAlignment::Bottom | VertAlignment::LastBaseline => { + result.value = result.value.max(value) + } + _ => result.value += value, + } + } +} + +impl OneAlignment { + pub fn aggregate(&self, result: &mut AlignResult, value: f64) { + match self { + Self::Horiz(h) => h.aggregate(result, value), + Self::Vert(v) => v.aggregate(result, value), + } + } +} + +impl AlignResult { + pub fn reap(&self, alignment: &OneAlignment) -> f64 { + match alignment { + OneAlignment::Horiz(HorizAlignment::Center) + | OneAlignment::Horiz(HorizAlignment::Custom(_)) + | OneAlignment::Vert(VertAlignment::Center) + | OneAlignment::Vert(VertAlignment::Custom(_)) => { + if self.count == 0 { + 0.0 + } else { + self.value / self.count as f64 + } + } + _ => self.value, + } + } +} +// AlignmentGuide widget + +/// A proxy that can be queried for alignments. +struct AlignmentProxy<'a> { + widget_state: &'a WidgetState, + widget: &'a dyn AnyWidget, +} + +struct AlignmentGuide { + alignment: OneAlignment, + callback: F, + child: Box, +} + +impl<'a> AlignmentProxy<'a> { + pub fn get_alignment(&self, alignment: &OneAlignment) -> f64 { + self.widget_state.get_alignment(self.widget, alignment) + } + + pub fn width(&self) -> f64 { + self.widget_state.size.width + } + + pub fn height(&self) -> f64 { + self.widget_state.size.height + } +} + +impl f64 + 'static> Widget for AlignmentGuide { + fn event(&mut self, event: &super::RawEvent, events: &mut Vec) { + self.child.event(event, events); + } + + fn update(&mut self, cx: &mut super::UpdateCx) { + self.child.update(cx); + } + + fn prelayout( + &mut self, + cx: &mut super::LayoutCx, + ) -> (druid_shell::kurbo::Size, druid_shell::kurbo::Size) { + self.child.prelayout(cx) + } + + fn layout( + &mut self, + cx: &mut super::LayoutCx, + proposed_size: druid_shell::kurbo::Size, + ) -> druid_shell::kurbo::Size { + self.child.layout(cx, proposed_size) + } + + fn align(&self, cx: &mut AlignCx, alignment: &OneAlignment) { + if *alignment == self.alignment { + let proxy = AlignmentProxy { + widget_state: cx.widget_state, + widget: self, + }; + let value = (self.callback)(proxy); + alignment.aggregate(cx.align_result, value); + } else { + self.child.align(cx, alignment); + } + } + + fn paint(&mut self, cx: &mut super::PaintCx) { + self.child.paint(cx); + } +} diff --git a/idiopath/src/widget/button.rs b/idiopath/src/widget/button.rs index 0422af6ab3..2092caa317 100644 --- a/idiopath/src/widget/button.rs +++ b/idiopath/src/widget/button.rs @@ -19,7 +19,7 @@ use druid_shell::{ use crate::{event::Event, id::IdPath}; -use super::{LayoutCx, PaintCx, Widget}; +use super::{LayoutCx, PaintCx, UpdateCx, Widget}; #[derive(Default)] @@ -41,13 +41,24 @@ impl Button { } } +const FIXED_SIZE: Size = Size::new(100., 20.); + impl Widget for Button { + fn update(&mut self, cx: &mut UpdateCx) { + // TODO: probably want to request layout when string changes + } + fn event(&mut self, _event: &super::RawEvent, events: &mut Vec) { events.push(Event::new(self.id_path.clone(), ())); } + fn prelayout(&mut self, _cx: &mut LayoutCx) -> (Size, Size) { + // TODO: do text layout here. + (FIXED_SIZE, FIXED_SIZE) + } + fn layout(&mut self, _cx: &mut LayoutCx, _proposed_size: Size) -> Size { - Size::new(100., 20.) + FIXED_SIZE } fn paint(&mut self, ctx: &mut PaintCx) { diff --git a/idiopath/src/widget/column.rs b/idiopath/src/widget/column.rs deleted file mode 100644 index 4159c5568b..0000000000 --- a/idiopath/src/widget/column.rs +++ /dev/null @@ -1,72 +0,0 @@ -// Copyright 2022 The Druid Authors. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use druid_shell::{ - kurbo::{Point, Rect, Size, Vec2}, - piet::Piet, -}; - -use crate::event::Event; - -use super::{Geom, LayoutCx, PaintCx, Pod, RawEvent, Widget}; - -pub struct Column { - children: Vec, -} - -impl Column { - pub fn new(children: Vec) -> Self { - Column { children } - } - - pub fn children_mut(&mut self) -> &mut Vec { - &mut self.children - } -} - -impl Widget for Column { - fn event(&mut self, event: &super::RawEvent, events: &mut Vec) { - match event { - RawEvent::MouseDown(p) => { - for child in &mut self.children { - let rect = Rect::from_origin_size(child.state.origin, child.state.size); - if rect.contains(*p) { - let child_event = RawEvent::MouseDown(*p - child.state.origin.to_vec2()); - child.event(&child_event, events); - break; - } - } - } - } - } - - fn layout(&mut self, cx: &mut LayoutCx, proposed_size: Size) -> Size { - let mut size = Size::default(); - let mut offset = Point::ZERO; - for child in &mut self.children { - let child_size = child.layout(cx, proposed_size); - child.state.origin = offset; - size.width = size.width.max(child_size.width); - size.height += child_size.height; - offset.y += child_size.height; - } - size - } - - fn paint(&mut self, cx: &mut PaintCx) { - for child in &mut self.children { - child.paint(cx); - } - } -} diff --git a/idiopath/src/widget/text.rs b/idiopath/src/widget/text.rs new file mode 100644 index 0000000000..412769a5dc --- /dev/null +++ b/idiopath/src/widget/text.rs @@ -0,0 +1,112 @@ +// Copyright 2022 The Druid Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use druid_shell::{ + kurbo::{Point, Size}, + piet::{Color, PietTextLayout, RenderContext, Text, TextLayout, TextLayoutBuilder}, +}; + +use crate::event::Event; + +use super::{ + align::{OneAlignment, VertAlignment}, + AlignCx, LayoutCx, PaintCx, UpdateCx, Widget, +}; + +pub struct TextWidget { + text: String, + color: Color, + layout: Option, + is_wrapped: bool, +} + +impl TextWidget { + pub fn new(text: String) -> TextWidget { + TextWidget { + text, + color: Color::WHITE, + layout: None, + is_wrapped: false, + } + } + + pub fn set_text(&mut self, text: String) { + self.text = text; + self.layout = None; + } +} + +impl Widget for TextWidget { + fn update(&mut self, cx: &mut UpdateCx) { + // All changes potentially require layout. Note: we could be finer + // grained, maybe color changes wouldn't. + cx.request_layout(); + } + + fn event(&mut self, _event: &super::RawEvent, _events: &mut Vec) {} + + fn prelayout(&mut self, cx: &mut LayoutCx) -> (Size, Size) { + let layout = cx + .text() + .new_text_layout(self.text.clone()) + .text_color(self.color.clone()) + .build() + .unwrap(); + let min_size = Size::ZERO; + let max_size = layout.size(); + self.layout = Some(layout); + self.is_wrapped = false; + (min_size, max_size) + } + + fn layout(&mut self, cx: &mut LayoutCx, proposed_size: Size) -> Size { + let needs_wrap = proposed_size.width < cx.widget_state.max_size.width; + if self.is_wrapped || needs_wrap { + let layout = cx + .text() + .new_text_layout(self.text.clone()) + .max_width(proposed_size.width) + .text_color(self.color.clone()) + .build() + .unwrap(); + let size = layout.size(); + self.layout = Some(layout); + self.is_wrapped = needs_wrap; + size + } else { + cx.widget_state.max_size + } + } + + fn align(&self, cx: &mut AlignCx, alignment: &OneAlignment) { + match alignment { + &OneAlignment::Vert(VertAlignment::FirstBaseline) => { + if let Some(metric) = self.layout.as_ref().unwrap().line_metric(0) { + cx.aggregate(alignment, metric.baseline); + } + } + &OneAlignment::Vert(VertAlignment::LastBaseline) => { + let i = self.layout.as_ref().unwrap().line_count() - 1; + if let Some(metric) = self.layout.as_ref().unwrap().line_metric(i) { + cx.aggregate(alignment, metric.y_offset + metric.baseline); + } + } + _ => (), + } + } + + fn paint(&mut self, cx: &mut PaintCx) { + cx.draw_text(self.layout.as_ref().unwrap(), Point::ZERO); + } +} diff --git a/idiopath/src/widget/vstack.rs b/idiopath/src/widget/vstack.rs new file mode 100644 index 0000000000..c69199e590 --- /dev/null +++ b/idiopath/src/widget/vstack.rs @@ -0,0 +1,122 @@ +// Copyright 2022 The Druid Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use druid_shell::kurbo::{Point, Rect, Size}; + +use crate::event::Event; + +use super::{ + align::{HorizAlignment, OneAlignment}, + LayoutCx, PaintCx, Pod, RawEvent, UpdateCx, Widget, +}; + +pub struct VStack { + children: Vec, + alignment: HorizAlignment, + spacing: f64, +} + +impl VStack { + pub fn new(children: Vec) -> Self { + let alignment = HorizAlignment::Center; + let spacing = 0.0; + VStack { + children, + alignment, + spacing, + } + } + + pub fn children_mut(&mut self) -> &mut Vec { + &mut self.children + } +} + +impl Widget for VStack { + fn event(&mut self, event: &super::RawEvent, events: &mut Vec) { + match event { + RawEvent::MouseDown(p) => { + for child in &mut self.children { + let rect = Rect::from_origin_size(child.state.origin, child.state.size); + if rect.contains(*p) { + let child_event = RawEvent::MouseDown(*p - child.state.origin.to_vec2()); + child.event(&child_event, events); + break; + } + } + } + } + } + + fn update(&mut self, cx: &mut UpdateCx) { + for child in &mut self.children { + child.update(cx); + } + } + + fn prelayout(&mut self, cx: &mut LayoutCx) -> (Size, Size) { + let mut min_size = Size::ZERO; + let mut max_size = Size::ZERO; + for child in &mut self.children { + let (child_min, child_max) = child.prelayout(cx); + min_size.width = min_size.width.max(child_min.width); + min_size.height += child_min.height; + max_size.width = max_size.width.max(child_max.width); + max_size.height += child_max.height; + } + (min_size, max_size) + } + + fn layout(&mut self, cx: &mut LayoutCx, proposed_size: Size) -> Size { + // First, sort children in order of increasing flexibility + let mut child_order: Vec<_> = (0..self.children.len()).collect(); + child_order.sort_by_key(|ix| self.children[*ix].height_flexibility().to_bits()); + let mut n_remaining = self.children.len(); + let mut height_remaining = proposed_size.height - (n_remaining - 1) as f64 * self.spacing; + for ix in child_order { + let child_height = (height_remaining / n_remaining as f64).max(0.0); + let child_proposed = Size::new(proposed_size.width, child_height); + let child_size = self.children[ix].layout(cx, child_proposed); + height_remaining -= height_remaining - child_size.height; + n_remaining -= 1; + } + let one_alignment = OneAlignment::Horiz(self.alignment.clone()); + let alignments: Vec = self + .children + .iter() + .map(|child| child.get_alignment(&one_alignment)) + .collect(); + let max_align = alignments + .iter() + .copied() + .reduce(f64::max) + .unwrap_or_default(); + let mut size = Size::default(); + let mut y = 0.0; + for (child, align) in self.children.iter_mut().zip(alignments) { + let child_size = child.state.size; + child.state.origin = Point::new(max_align - align, y); + size.width = size.width.max(child_size.width); + size.height += child_size.height; + y += child_size.height + self.spacing; + } + size + } + + fn paint(&mut self, cx: &mut PaintCx) { + for child in &mut self.children { + child.paint(cx); + } + } +} From 3e703fb73c09a7b5ae57c46965450ae1893360fc Mon Sep 17 00:00:00 2001 From: Raph Levien Date: Thu, 12 May 2022 20:44:36 -0700 Subject: [PATCH 17/28] Fix alignment min/max aggregation --- idiopath/src/widget/align.rs | 32 ++++++++++++++++++++++++++------ 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/idiopath/src/widget/align.rs b/idiopath/src/widget/align.rs index 2d8cfc68bd..70c9414d1c 100644 --- a/idiopath/src/widget/align.rs +++ b/idiopath/src/widget/align.rs @@ -48,27 +48,47 @@ pub struct AlignResult { impl HorizAlignment { fn aggregate(&self, result: &mut AlignResult, value: f64) { - result.count += 1; match self { - HorizAlignment::Leading => result.value = result.value.min(value), - HorizAlignment::Trailing => result.value = result.value.max(value), + HorizAlignment::Leading => { + if result.count == 0 { + result.value = value + } else { + result.value = result.value.min(value) + } + } + HorizAlignment::Trailing => { + if result.count == 0 { + result.value = value + } else { + result.value = result.value.max(value) + } + } _ => result.value += value, } + result.count += 1; } } impl VertAlignment { fn aggregate(&self, result: &mut AlignResult, value: f64) { - result.count += 1; match self { VertAlignment::Top | VertAlignment::FirstBaseline => { - result.value = result.value.min(value) + if result.count == 0 { + result.value = value + } else { + result.value = result.value.min(value) + } } VertAlignment::Bottom | VertAlignment::LastBaseline => { - result.value = result.value.max(value) + if result.count == 0 { + result.value = value + } else { + result.value = result.value.max(value) + } } _ => result.value += value, } + result.count += 1; } } From 163f75673c541448c4a726ef1af00f27f686f63a Mon Sep 17 00:00:00 2001 From: Raph Levien Date: Thu, 12 May 2022 21:12:02 -0700 Subject: [PATCH 18/28] Use trait rather than enum for alignments --- idiopath/src/widget.rs | 52 ++++----- idiopath/src/widget/align.rs | 197 ++++++++++++++++++++-------------- idiopath/src/widget/text.rs | 26 ++--- idiopath/src/widget/vstack.rs | 9 +- 4 files changed, 162 insertions(+), 122 deletions(-) diff --git a/idiopath/src/widget.rs b/idiopath/src/widget.rs index 22dd51fae5..7a4b2405d1 100644 --- a/idiopath/src/widget.rs +++ b/idiopath/src/widget.rs @@ -28,7 +28,10 @@ use druid_shell::WindowHandle; use crate::event::Event; -use self::align::{AlignResult, OneAlignment}; +use self::align::{ + AlignResult, Bottom, HorizAlignment, HorizCenter, Leading, OneAlignment, Top, Trailing, + VertAlignment, VertCenter, +}; /// A basic widget trait. pub trait Widget { @@ -51,7 +54,7 @@ pub trait Widget { /// Query for an alignment. /// /// This method can count on layout already having been completed. - fn align(&self, cx: &mut AlignCx, alignment: &OneAlignment) {} + fn align(&self, cx: &mut AlignCx, alignment: OneAlignment) {} fn paint(&mut self, cx: &mut PaintCx); } @@ -229,8 +232,8 @@ impl<'a, 'b> LayoutCx<'a, 'b> { } impl<'a> AlignCx<'a> { - pub fn aggregate(&mut self, alignment: &OneAlignment, value: f64) { - alignment.aggregate(&mut self.align_result, value); + pub fn aggregate(&mut self, alignment: OneAlignment, value: f64) { + self.align_result.aggregate(alignment, value); } } @@ -269,24 +272,25 @@ impl WidgetState { self.flags |= flags } - fn get_alignment(&self, widget: &dyn AnyWidget, alignment: &OneAlignment) -> f64 { - match alignment { - // Note: will have to swap left/right when we do BiDi. - OneAlignment::Horiz(align::HorizAlignment::Leading) => 0.0, - OneAlignment::Horiz(align::HorizAlignment::Center) => self.size.width * 0.5, - OneAlignment::Horiz(align::HorizAlignment::Trailing) => self.size.width, - OneAlignment::Vert(align::VertAlignment::Top) => 0.0, - OneAlignment::Vert(align::VertAlignment::Center) => self.size.height * 0.5, - OneAlignment::Vert(align::VertAlignment::Bottom) => self.size.height, - _ => { - let mut align_result = AlignResult::default(); - let mut align_cx = AlignCx { - widget_state: self, - align_result: &mut align_result, - }; - widget.align(&mut align_cx, alignment); - align_result.reap(alignment) - } + fn get_alignment(&self, widget: &dyn AnyWidget, alignment: OneAlignment) -> f64 { + if alignment.id == Leading.id() || alignment.id == Top.id() { + 0.0 + } else if alignment.id == HorizCenter.id() { + self.size.width * 0.5 + } else if alignment.id == Trailing.id() { + self.size.width + } else if alignment.id == VertCenter.id() { + self.size.height * 0.5 + } else if alignment.id == Bottom.id() { + self.size.height + } else { + let mut align_result = AlignResult::default(); + let mut align_cx = AlignCx { + widget_state: self, + align_result: &mut align_result, + }; + widget.align(&mut align_cx, alignment); + align_result.reap(alignment) } } } @@ -361,7 +365,7 @@ impl Pod { /// /// This call aggregates all instances of the alignment, so cost may be /// proportional to the number of descendants. - pub fn align(&mut self, cx: &mut AlignCx, alignment: &OneAlignment) { + pub fn align(&mut self, cx: &mut AlignCx, alignment: OneAlignment) { let mut child_cx = AlignCx { widget_state: &self.state, align_result: cx.align_result, @@ -381,7 +385,7 @@ impl Pod { self.state.max_size.height - self.state.min_size.height } - pub fn get_alignment(&self, alignment: &OneAlignment) -> f64 { + pub fn get_alignment(&self, alignment: OneAlignment) -> f64 { self.state.get_alignment(&self.widget, alignment) } } diff --git a/idiopath/src/widget/align.rs b/idiopath/src/widget/align.rs index 70c9414d1c..a25d80a709 100644 --- a/idiopath/src/widget/align.rs +++ b/idiopath/src/widget/align.rs @@ -14,100 +14,141 @@ use super::{AlignCx, AnyWidget, Widget, WidgetState}; // See the License for the specific language governing permissions and // limitations under the License. -#[derive(Clone, PartialEq, Debug)] -pub enum HorizAlignment { - // Note: actually "left" until we do BiDi. - Leading, - Center, - Trailing, - // We might switch to TinyStr. - Custom(&'static str), +#[derive(Clone, Copy, PartialEq)] +pub enum AlignmentMerge { + Min, + Mean, + Max, } -#[derive(Clone, PartialEq, Debug)] -pub enum VertAlignment { - Top, - Center, - Bottom, - FirstBaseline, - LastBaseline, - Custom(&'static str), +pub trait HorizAlignment: 'static { + fn id(&self) -> std::any::TypeId { + std::any::TypeId::of::() + } + + fn merge(&self) -> AlignmentMerge { + AlignmentMerge::Mean + } } -#[derive(Clone, PartialEq, Debug)] -pub enum OneAlignment { - Horiz(HorizAlignment), - Vert(VertAlignment), +pub trait VertAlignment: 'static { + fn id(&self) -> std::any::TypeId { + std::any::TypeId::of::() + } + + fn merge(&self) -> AlignmentMerge { + AlignmentMerge::Mean + } } -#[derive(Default)] -pub struct AlignResult { - value: f64, - count: usize, +pub struct Leading; + +impl HorizAlignment for Leading { + fn merge(&self) -> AlignmentMerge { + AlignmentMerge::Min + } } +pub struct HorizCenter; -impl HorizAlignment { - fn aggregate(&self, result: &mut AlignResult, value: f64) { - match self { - HorizAlignment::Leading => { - if result.count == 0 { - result.value = value - } else { - result.value = result.value.min(value) - } - } - HorizAlignment::Trailing => { - if result.count == 0 { - result.value = value - } else { - result.value = result.value.max(value) - } - } - _ => result.value += value, +impl HorizAlignment for HorizCenter {} + +pub struct Trailing; + +impl HorizAlignment for Trailing { + fn merge(&self) -> AlignmentMerge { + AlignmentMerge::Max + } +} + +pub struct Top; + +impl VertAlignment for Top { + fn merge(&self) -> AlignmentMerge { + AlignmentMerge::Min + } +} + +pub struct VertCenter; + +impl VertAlignment for VertCenter {} + +pub struct Bottom; + +impl VertAlignment for Bottom { + fn merge(&self) -> AlignmentMerge { + AlignmentMerge::Max + } +} + +pub struct FirstBaseline; + +impl VertAlignment for FirstBaseline { + fn merge(&self) -> AlignmentMerge { + AlignmentMerge::Min + } +} + +pub struct LastBaseline; + +impl VertAlignment for LastBaseline { + fn merge(&self) -> AlignmentMerge { + AlignmentMerge::Max + } +} + +#[derive(Clone, Copy)] +pub struct OneAlignment { + pub id: std::any::TypeId, + merge: AlignmentMerge, +} + +impl OneAlignment { + pub fn from_horiz(h: &impl HorizAlignment) -> OneAlignment { + OneAlignment { + id: h.id(), + merge: h.merge(), + } + } + + pub fn from_vert(v: &impl VertAlignment) -> OneAlignment { + OneAlignment { + id: v.id(), + merge: v.merge(), } - result.count += 1; } } -impl VertAlignment { - fn aggregate(&self, result: &mut AlignResult, value: f64) { - match self { - VertAlignment::Top | VertAlignment::FirstBaseline => { - if result.count == 0 { - result.value = value +#[derive(Default)] +pub struct AlignResult { + value: f64, + count: usize, +} + +impl AlignResult { + pub fn aggregate(&mut self, alignment: OneAlignment, value: f64) { + match alignment.merge { + AlignmentMerge::Max => { + if self.count == 0 { + self.value = value; } else { - result.value = result.value.min(value) + self.value = self.value.max(value) } } - VertAlignment::Bottom | VertAlignment::LastBaseline => { - if result.count == 0 { - result.value = value + AlignmentMerge::Min => { + if self.count == 0 { + self.value = value; } else { - result.value = result.value.max(value) + self.value = self.value.min(value) } } - _ => result.value += value, + AlignmentMerge::Mean => self.value += value, } - result.count += 1; + self.count += 1; } -} -impl OneAlignment { - pub fn aggregate(&self, result: &mut AlignResult, value: f64) { - match self { - Self::Horiz(h) => h.aggregate(result, value), - Self::Vert(v) => v.aggregate(result, value), - } - } -} - -impl AlignResult { - pub fn reap(&self, alignment: &OneAlignment) -> f64 { - match alignment { - OneAlignment::Horiz(HorizAlignment::Center) - | OneAlignment::Horiz(HorizAlignment::Custom(_)) - | OneAlignment::Vert(VertAlignment::Center) - | OneAlignment::Vert(VertAlignment::Custom(_)) => { + pub fn reap(&self, alignment: OneAlignment) -> f64 { + match alignment.merge { + AlignmentMerge::Mean => { if self.count == 0 { 0.0 } else { @@ -127,13 +168,13 @@ struct AlignmentProxy<'a> { } struct AlignmentGuide { - alignment: OneAlignment, + alignment_id: std::any::TypeId, callback: F, child: Box, } impl<'a> AlignmentProxy<'a> { - pub fn get_alignment(&self, alignment: &OneAlignment) -> f64 { + pub fn get_alignment(&self, alignment: OneAlignment) -> f64 { self.widget_state.get_alignment(self.widget, alignment) } @@ -170,14 +211,14 @@ impl f64 + 'static> Widget for AlignmentGuide { self.child.layout(cx, proposed_size) } - fn align(&self, cx: &mut AlignCx, alignment: &OneAlignment) { - if *alignment == self.alignment { + fn align(&self, cx: &mut AlignCx, alignment: OneAlignment) { + if alignment.id == self.alignment_id { let proxy = AlignmentProxy { widget_state: cx.widget_state, widget: self, }; let value = (self.callback)(proxy); - alignment.aggregate(cx.align_result, value); + cx.align_result.aggregate(alignment, value); } else { self.child.align(cx, alignment); } diff --git a/idiopath/src/widget/text.rs b/idiopath/src/widget/text.rs index 412769a5dc..b93c327f96 100644 --- a/idiopath/src/widget/text.rs +++ b/idiopath/src/widget/text.rs @@ -20,7 +20,7 @@ use druid_shell::{ use crate::event::Event; use super::{ - align::{OneAlignment, VertAlignment}, + align::{FirstBaseline, LastBaseline, OneAlignment, VertAlignment}, AlignCx, LayoutCx, PaintCx, UpdateCx, Widget, }; @@ -48,14 +48,14 @@ impl TextWidget { } impl Widget for TextWidget { + fn event(&mut self, _event: &super::RawEvent, _events: &mut Vec) {} + fn update(&mut self, cx: &mut UpdateCx) { // All changes potentially require layout. Note: we could be finer // grained, maybe color changes wouldn't. cx.request_layout(); } - fn event(&mut self, _event: &super::RawEvent, _events: &mut Vec) {} - fn prelayout(&mut self, cx: &mut LayoutCx) -> (Size, Size) { let layout = cx .text() @@ -89,20 +89,16 @@ impl Widget for TextWidget { } } - fn align(&self, cx: &mut AlignCx, alignment: &OneAlignment) { - match alignment { - &OneAlignment::Vert(VertAlignment::FirstBaseline) => { - if let Some(metric) = self.layout.as_ref().unwrap().line_metric(0) { - cx.aggregate(alignment, metric.baseline); - } + fn align(&self, cx: &mut AlignCx, alignment: OneAlignment) { + if alignment.id == FirstBaseline.id() { + if let Some(metric) = self.layout.as_ref().unwrap().line_metric(0) { + cx.aggregate(alignment, metric.baseline); } - &OneAlignment::Vert(VertAlignment::LastBaseline) => { - let i = self.layout.as_ref().unwrap().line_count() - 1; - if let Some(metric) = self.layout.as_ref().unwrap().line_metric(i) { - cx.aggregate(alignment, metric.y_offset + metric.baseline); - } + } else if alignment.id == LastBaseline.id() { + let i = self.layout.as_ref().unwrap().line_count() - 1; + if let Some(metric) = self.layout.as_ref().unwrap().line_metric(i) { + cx.aggregate(alignment, metric.y_offset + metric.baseline); } - _ => (), } } diff --git a/idiopath/src/widget/vstack.rs b/idiopath/src/widget/vstack.rs index c69199e590..4059b1d796 100644 --- a/idiopath/src/widget/vstack.rs +++ b/idiopath/src/widget/vstack.rs @@ -17,19 +17,19 @@ use druid_shell::kurbo::{Point, Rect, Size}; use crate::event::Event; use super::{ - align::{HorizAlignment, OneAlignment}, + align::{HorizCenter, OneAlignment}, LayoutCx, PaintCx, Pod, RawEvent, UpdateCx, Widget, }; pub struct VStack { children: Vec, - alignment: HorizAlignment, + alignment: OneAlignment, spacing: f64, } impl VStack { pub fn new(children: Vec) -> Self { - let alignment = HorizAlignment::Center; + let alignment = OneAlignment::from_horiz(&HorizCenter); let spacing = 0.0; VStack { children, @@ -91,11 +91,10 @@ impl Widget for VStack { height_remaining -= height_remaining - child_size.height; n_remaining -= 1; } - let one_alignment = OneAlignment::Horiz(self.alignment.clone()); let alignments: Vec = self .children .iter() - .map(|child| child.get_alignment(&one_alignment)) + .map(|child| child.get_alignment(self.alignment)) .collect(); let max_align = alignments .iter() From ebe51a6f86467aad20e6dd4d37619a24cae78e4b Mon Sep 17 00:00:00 2001 From: Raph Levien Date: Fri, 13 May 2022 08:02:18 -0700 Subject: [PATCH 19/28] Continued work on alignment Adds axis to SingleAlignment (renamed from OneAlignment). Incorporates layout offsets into alignment results. --- idiopath/src/widget.rs | 38 +++++++++++------- idiopath/src/widget/align.rs | 73 +++++++++++++++++++++++++++++------ idiopath/src/widget/button.rs | 2 + idiopath/src/widget/text.rs | 8 ++-- idiopath/src/widget/vstack.rs | 12 ++++-- 5 files changed, 102 insertions(+), 31 deletions(-) diff --git a/idiopath/src/widget.rs b/idiopath/src/widget.rs index 7a4b2405d1..7e40680c52 100644 --- a/idiopath/src/widget.rs +++ b/idiopath/src/widget.rs @@ -29,8 +29,8 @@ use druid_shell::WindowHandle; use crate::event::Event; use self::align::{ - AlignResult, Bottom, HorizAlignment, HorizCenter, Leading, OneAlignment, Top, Trailing, - VertAlignment, VertCenter, + AlignResult, Bottom, HorizAlignment, HorizCenter, Leading, SingleAlignment, Top, Trailing, + VertAlignment, VertCenter, AlignmentAxis, }; /// A basic widget trait. @@ -54,7 +54,7 @@ pub trait Widget { /// Query for an alignment. /// /// This method can count on layout already having been completed. - fn align(&self, cx: &mut AlignCx, alignment: OneAlignment) {} + fn align(&self, cx: &mut AlignCx, alignment: SingleAlignment) {} fn paint(&mut self, cx: &mut PaintCx); } @@ -78,6 +78,7 @@ pub struct LayoutCx<'a, 'b> { pub struct AlignCx<'a> { widget_state: &'a WidgetState, align_result: &'a mut AlignResult, + origin: Point, } pub struct PaintCx<'a, 'b, 'c> { @@ -232,8 +233,12 @@ impl<'a, 'b> LayoutCx<'a, 'b> { } impl<'a> AlignCx<'a> { - pub fn aggregate(&mut self, alignment: OneAlignment, value: f64) { - self.align_result.aggregate(alignment, value); + pub fn aggregate(&mut self, alignment: SingleAlignment, value: f64) { + let origin_value = match alignment.axis() { + AlignmentAxis::Horizontal => self.origin.x, + AlignmentAxis::Vertical => self.origin.y, + }; + self.align_result.aggregate(alignment, value + origin_value); } } @@ -272,22 +277,26 @@ impl WidgetState { self.flags |= flags } - fn get_alignment(&self, widget: &dyn AnyWidget, alignment: OneAlignment) -> f64 { - if alignment.id == Leading.id() || alignment.id == Top.id() { + /// Get alignment value. + /// + /// The value is in the coordinate system of the parent widget. + fn get_alignment(&self, widget: &dyn AnyWidget, alignment: SingleAlignment) -> f64 { + if alignment.id() == Leading.id() || alignment.id() == Top.id() { 0.0 - } else if alignment.id == HorizCenter.id() { + } else if alignment.id() == HorizCenter.id() { self.size.width * 0.5 - } else if alignment.id == Trailing.id() { + } else if alignment.id() == Trailing.id() { self.size.width - } else if alignment.id == VertCenter.id() { + } else if alignment.id() == VertCenter.id() { self.size.height * 0.5 - } else if alignment.id == Bottom.id() { + } else if alignment.id() == Bottom.id() { self.size.height } else { let mut align_result = AlignResult::default(); let mut align_cx = AlignCx { widget_state: self, align_result: &mut align_result, + origin: self.origin, }; widget.align(&mut align_cx, alignment); align_result.reap(alignment) @@ -365,10 +374,11 @@ impl Pod { /// /// This call aggregates all instances of the alignment, so cost may be /// proportional to the number of descendants. - pub fn align(&mut self, cx: &mut AlignCx, alignment: OneAlignment) { + pub fn align(&self, cx: &mut AlignCx, alignment: SingleAlignment) { let mut child_cx = AlignCx { widget_state: &self.state, align_result: cx.align_result, + origin: cx.origin + self.state.origin.to_vec2(), }; self.widget.align(&mut child_cx, alignment); } @@ -385,7 +395,9 @@ impl Pod { self.state.max_size.height - self.state.min_size.height } - pub fn get_alignment(&self, alignment: OneAlignment) -> f64 { + /// The returned value is in the coordinate space of the parent that + /// owns this pod. + pub fn get_alignment(&self, alignment: SingleAlignment) -> f64 { self.state.get_alignment(&self.widget, alignment) } } diff --git a/idiopath/src/widget/align.rs b/idiopath/src/widget/align.rs index a25d80a709..ba823fe5c6 100644 --- a/idiopath/src/widget/align.rs +++ b/idiopath/src/widget/align.rs @@ -1,3 +1,5 @@ +use druid_shell::kurbo::Point; + use super::{AlignCx, AnyWidget, Widget, WidgetState}; // Copyright 2022 The Druid Authors. @@ -21,6 +23,12 @@ pub enum AlignmentMerge { Max, } +#[derive(Clone, Copy, PartialEq)] +pub enum AlignmentAxis { + Horizontal, + Vertical, +} + pub trait HorizAlignment: 'static { fn id(&self) -> std::any::TypeId { std::any::TypeId::of::() @@ -97,23 +105,58 @@ impl VertAlignment for LastBaseline { } #[derive(Clone, Copy)] -pub struct OneAlignment { - pub id: std::any::TypeId, +pub struct SingleAlignment { + id: std::any::TypeId, merge: AlignmentMerge, + axis: AlignmentAxis, } -impl OneAlignment { - pub fn from_horiz(h: &impl HorizAlignment) -> OneAlignment { - OneAlignment { +impl SingleAlignment { + pub fn id(&self) -> std::any::TypeId { + self.id + } + + pub fn axis(&self) -> AlignmentAxis { + self.axis + } + + // Maybe these should all be dyn + pub fn from_horiz(h: &impl HorizAlignment) -> SingleAlignment { + SingleAlignment { + id: h.id(), + merge: h.merge(), + axis: AlignmentAxis::Horizontal, + } + } + + pub fn from_dyn_horiz(h: &dyn HorizAlignment) -> SingleAlignment { + SingleAlignment { id: h.id(), merge: h.merge(), + axis: AlignmentAxis::Horizontal, } } - pub fn from_vert(v: &impl VertAlignment) -> OneAlignment { - OneAlignment { + pub fn from_vert(v: &impl VertAlignment) -> SingleAlignment { + SingleAlignment { id: v.id(), merge: v.merge(), + axis: AlignmentAxis::Vertical, + } + } + + pub fn from_dyn_vert(v: &dyn VertAlignment) -> SingleAlignment { + SingleAlignment { + id: v.id(), + merge: v.merge(), + axis: AlignmentAxis::Vertical, + } + } + + pub fn apply_offset(&self, offset: Point, value: f64) -> f64 { + match self.axis { + AlignmentAxis::Horizontal => value + offset.x, + AlignmentAxis::Vertical => value + offset.y, } } } @@ -125,7 +168,7 @@ pub struct AlignResult { } impl AlignResult { - pub fn aggregate(&mut self, alignment: OneAlignment, value: f64) { + pub fn aggregate(&mut self, alignment: SingleAlignment, value: f64) { match alignment.merge { AlignmentMerge::Max => { if self.count == 0 { @@ -146,7 +189,7 @@ impl AlignResult { self.count += 1; } - pub fn reap(&self, alignment: OneAlignment) -> f64 { + pub fn reap(&self, alignment: SingleAlignment) -> f64 { match alignment.merge { AlignmentMerge::Mean => { if self.count == 0 { @@ -174,10 +217,18 @@ struct AlignmentGuide { } impl<'a> AlignmentProxy<'a> { - pub fn get_alignment(&self, alignment: OneAlignment) -> f64 { + pub fn get_alignment(&self, alignment: SingleAlignment) -> f64 { self.widget_state.get_alignment(self.widget, alignment) } + pub fn get_horiz(&self, alignment: &dyn HorizAlignment) -> f64 { + self.get_alignment(SingleAlignment::from_dyn_horiz(alignment)) + } + + pub fn get_vert(&self, alignment: &dyn VertAlignment) -> f64 { + self.get_alignment(SingleAlignment::from_dyn_vert(alignment)) + } + pub fn width(&self) -> f64 { self.widget_state.size.width } @@ -211,7 +262,7 @@ impl f64 + 'static> Widget for AlignmentGuide { self.child.layout(cx, proposed_size) } - fn align(&self, cx: &mut AlignCx, alignment: OneAlignment) { + fn align(&self, cx: &mut AlignCx, alignment: SingleAlignment) { if alignment.id == self.alignment_id { let proxy = AlignmentProxy { widget_state: cx.widget_state, diff --git a/idiopath/src/widget/button.rs b/idiopath/src/widget/button.rs index 2092caa317..d025c9c352 100644 --- a/idiopath/src/widget/button.rs +++ b/idiopath/src/widget/button.rs @@ -61,6 +61,8 @@ impl Widget for Button { FIXED_SIZE } + // TODO: alignment + fn paint(&mut self, ctx: &mut PaintCx) { let layout = ctx .text() diff --git a/idiopath/src/widget/text.rs b/idiopath/src/widget/text.rs index b93c327f96..4102cb077a 100644 --- a/idiopath/src/widget/text.rs +++ b/idiopath/src/widget/text.rs @@ -20,7 +20,7 @@ use druid_shell::{ use crate::event::Event; use super::{ - align::{FirstBaseline, LastBaseline, OneAlignment, VertAlignment}, + align::{FirstBaseline, LastBaseline, SingleAlignment, VertAlignment}, AlignCx, LayoutCx, PaintCx, UpdateCx, Widget, }; @@ -89,12 +89,12 @@ impl Widget for TextWidget { } } - fn align(&self, cx: &mut AlignCx, alignment: OneAlignment) { - if alignment.id == FirstBaseline.id() { + fn align(&self, cx: &mut AlignCx, alignment: SingleAlignment) { + if alignment.id() == FirstBaseline.id() { if let Some(metric) = self.layout.as_ref().unwrap().line_metric(0) { cx.aggregate(alignment, metric.baseline); } - } else if alignment.id == LastBaseline.id() { + } else if alignment.id() == LastBaseline.id() { let i = self.layout.as_ref().unwrap().line_count() - 1; if let Some(metric) = self.layout.as_ref().unwrap().line_metric(i) { cx.aggregate(alignment, metric.y_offset + metric.baseline); diff --git a/idiopath/src/widget/vstack.rs b/idiopath/src/widget/vstack.rs index 4059b1d796..f9d9a5bbe7 100644 --- a/idiopath/src/widget/vstack.rs +++ b/idiopath/src/widget/vstack.rs @@ -17,19 +17,19 @@ use druid_shell::kurbo::{Point, Rect, Size}; use crate::event::Event; use super::{ - align::{HorizCenter, OneAlignment}, + align::{HorizCenter, SingleAlignment}, LayoutCx, PaintCx, Pod, RawEvent, UpdateCx, Widget, }; pub struct VStack { children: Vec, - alignment: OneAlignment, + alignment: SingleAlignment, spacing: f64, } impl VStack { pub fn new(children: Vec) -> Self { - let alignment = OneAlignment::from_horiz(&HorizCenter); + let alignment = SingleAlignment::from_horiz(&HorizCenter); let spacing = 0.0; VStack { children, @@ -113,6 +113,12 @@ impl Widget for VStack { size } + fn align(&self, cx: &mut super::AlignCx, alignment: SingleAlignment) { + for child in &self.children { + child.align(cx, alignment); + } + } + fn paint(&mut self, cx: &mut PaintCx) { for child in &mut self.children { child.paint(cx); From 064d6397c3896ca43018a950374bf972fd64c8ea Mon Sep 17 00:00:00 2001 From: Raph Levien Date: Fri, 13 May 2022 18:31:33 -0700 Subject: [PATCH 20/28] Use single Center alignment for horiz and vert In SwiftUI, HorizontalAlignment.center and VerticalAlignment.center are two different structs, but in Rust it's just as convenient, if not more so, to let the same struct just impl the two different traits. --- idiopath/src/widget.rs | 13 +++++++------ idiopath/src/widget/align.rs | 14 ++++++++------ idiopath/src/widget/vstack.rs | 4 ++-- 3 files changed, 17 insertions(+), 14 deletions(-) diff --git a/idiopath/src/widget.rs b/idiopath/src/widget.rs index 7e40680c52..848ad16e21 100644 --- a/idiopath/src/widget.rs +++ b/idiopath/src/widget.rs @@ -29,8 +29,8 @@ use druid_shell::WindowHandle; use crate::event::Event; use self::align::{ - AlignResult, Bottom, HorizAlignment, HorizCenter, Leading, SingleAlignment, Top, Trailing, - VertAlignment, VertCenter, AlignmentAxis, + AlignResult, AlignmentAxis, Bottom, Center, HorizAlignment, Leading, SingleAlignment, Top, + Trailing, VertAlignment, }; /// A basic widget trait. @@ -283,12 +283,13 @@ impl WidgetState { fn get_alignment(&self, widget: &dyn AnyWidget, alignment: SingleAlignment) -> f64 { if alignment.id() == Leading.id() || alignment.id() == Top.id() { 0.0 - } else if alignment.id() == HorizCenter.id() { - self.size.width * 0.5 + } else if alignment.id() ==
::id(&Center) { + match alignment.axis() { + AlignmentAxis::Horizontal => self.size.width * 0.5, + AlignmentAxis::Vertical => self.size.height * 0.5, + } } else if alignment.id() == Trailing.id() { self.size.width - } else if alignment.id() == VertCenter.id() { - self.size.height * 0.5 } else if alignment.id() == Bottom.id() { self.size.height } else { diff --git a/idiopath/src/widget/align.rs b/idiopath/src/widget/align.rs index ba823fe5c6..ad79870e3a 100644 --- a/idiopath/src/widget/align.rs +++ b/idiopath/src/widget/align.rs @@ -56,9 +56,15 @@ impl HorizAlignment for Leading { AlignmentMerge::Min } } -pub struct HorizCenter; -impl HorizAlignment for HorizCenter {} +/// Center alignment. +/// +/// Note that this alignment can be used for both horizontal and vertical +/// alignment. +pub struct Center; + +impl HorizAlignment for Center {} +impl VertAlignment for Center {} pub struct Trailing; @@ -76,10 +82,6 @@ impl VertAlignment for Top { } } -pub struct VertCenter; - -impl VertAlignment for VertCenter {} - pub struct Bottom; impl VertAlignment for Bottom { diff --git a/idiopath/src/widget/vstack.rs b/idiopath/src/widget/vstack.rs index f9d9a5bbe7..129c36ec8d 100644 --- a/idiopath/src/widget/vstack.rs +++ b/idiopath/src/widget/vstack.rs @@ -17,7 +17,7 @@ use druid_shell::kurbo::{Point, Rect, Size}; use crate::event::Event; use super::{ - align::{HorizCenter, SingleAlignment}, + align::{Center, SingleAlignment}, LayoutCx, PaintCx, Pod, RawEvent, UpdateCx, Widget, }; @@ -29,7 +29,7 @@ pub struct VStack { impl VStack { pub fn new(children: Vec) -> Self { - let alignment = SingleAlignment::from_horiz(&HorizCenter); + let alignment = SingleAlignment::from_horiz(&Center); let spacing = 0.0; VStack { children, From a1706b86f65e5d1a338312cb1520040238d68e9f Mon Sep 17 00:00:00 2001 From: Raph Levien Date: Fri, 13 May 2022 19:57:47 -0700 Subject: [PATCH 21/28] Add context to event method This brings the event method in line with other widget methods, and also existing Druid. Also adds mutable access to the event queue to the base context state, so other contexts can add events. This will be useful for LayoutObserver (event generated from layout context). Actual event propagation is still very under-developed, but it should be possible to build that out without big signature changes. --- idiopath/src/app.rs | 9 +++++---- idiopath/src/widget.rs | 37 ++++++++++++++++++++++++++++------- idiopath/src/widget/align.rs | 12 ++++++------ idiopath/src/widget/button.rs | 6 +++--- idiopath/src/widget/text.rs | 6 ++---- idiopath/src/widget/vstack.rs | 8 +++----- 6 files changed, 49 insertions(+), 29 deletions(-) diff --git a/idiopath/src/app.rs b/idiopath/src/app.rs index 023f4360b3..e955c3ff0b 100644 --- a/idiopath/src/app.rs +++ b/idiopath/src/app.rs @@ -16,7 +16,7 @@ use druid_shell::kurbo::Size; use druid_shell::piet::{Color, Piet, RenderContext}; use druid_shell::{kurbo::Point, WindowHandle}; -use crate::widget::{CxState, LayoutCx, PaintCx, Pod, UpdateCx, WidgetState}; +use crate::widget::{CxState, EventCx, LayoutCx, PaintCx, Pod, UpdateCx, WidgetState}; use crate::{ event::Event, id::Id, @@ -89,8 +89,7 @@ where self.ensure_app(); let root_pod = self.root_pod.as_mut().unwrap(); - let text = piet.text(); - let mut cx_state = CxState::new(&self.window_handle, text.clone()); + let mut cx_state = CxState::new(&self.window_handle, &mut self.events); let mut update_cx = UpdateCx::new(&mut cx_state, &mut self.root_state); root_pod.update(&mut update_cx); let mut layout_cx = LayoutCx::new(&mut cx_state, &mut self.root_state); @@ -108,7 +107,9 @@ where fn event(&mut self, event: RawEvent) { self.ensure_app(); let root_pod = self.root_pod.as_mut().unwrap(); - root_pod.event(&event, &mut self.events); + let mut cx_state = CxState::new(&self.window_handle, &mut self.events); + let mut event_cx = EventCx::new(&mut cx_state, &mut self.root_state); + root_pod.event(&mut event_cx, &event); self.run_app_logic(); } diff --git a/idiopath/src/widget.rs b/idiopath/src/widget.rs index 848ad16e21..ddd7bee269 100644 --- a/idiopath/src/widget.rs +++ b/idiopath/src/widget.rs @@ -35,7 +35,7 @@ use self::align::{ /// A basic widget trait. pub trait Widget { - fn event(&mut self, event: &RawEvent, events: &mut Vec); + fn event(&mut self, cx: &mut EventCx, event: &RawEvent); fn update(&mut self, cx: &mut UpdateCx); @@ -63,6 +63,12 @@ pub trait Widget { pub struct CxState<'a> { window: &'a WindowHandle, text: PietText, + events: &'a mut Vec, +} + +pub struct EventCx<'a, 'b> { + cx_state: &'a mut CxState<'b>, + widget_state: &'a mut WidgetState, } pub struct UpdateCx<'a, 'b> { @@ -135,8 +141,8 @@ impl AnyWidget for W { } impl Widget for Box { - fn event(&mut self, event: &RawEvent, events: &mut Vec) { - self.deref_mut().event(event, events); + fn event(&mut self, cx: &mut EventCx, event: &RawEvent) { + self.deref_mut().event(cx, event); } fn update(&mut self, cx: &mut UpdateCx) { @@ -201,8 +207,25 @@ impl_widget_tuple!(8; ); impl<'a> CxState<'a> { - pub fn new(window: &'a WindowHandle, text: PietText) -> Self { - CxState { window, text } + pub fn new(window: &'a WindowHandle, events: &'a mut Vec) -> Self { + CxState { + window, + text: window.text(), + events, + } + } +} + +impl<'a, 'b> EventCx<'a, 'b> { + pub fn new(cx_state: &'a mut CxState<'b>, root_state: &'a mut WidgetState) -> Self { + EventCx { + cx_state, + widget_state: root_state, + } + } + + pub fn add_event(&mut self, event: Event) { + self.cx_state.events.push(event); } } @@ -324,8 +347,8 @@ impl Pod { self.state.request(PodFlags::REQUEST_UPDATE); } - pub fn event(&mut self, event: &RawEvent, events: &mut Vec) { - self.widget.event(event, events); + pub fn event(&mut self, cx: &mut EventCx, event: &RawEvent) { + self.widget.event(cx, event); } /// Propagate an update cycle. diff --git a/idiopath/src/widget/align.rs b/idiopath/src/widget/align.rs index ad79870e3a..6028d13e04 100644 --- a/idiopath/src/widget/align.rs +++ b/idiopath/src/widget/align.rs @@ -1,7 +1,3 @@ -use druid_shell::kurbo::Point; - -use super::{AlignCx, AnyWidget, Widget, WidgetState}; - // Copyright 2022 The Druid Authors. // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,6 +12,10 @@ use super::{AlignCx, AnyWidget, Widget, WidgetState}; // See the License for the specific language governing permissions and // limitations under the License. +use druid_shell::kurbo::Point; + +use super::{AlignCx, AnyWidget, EventCx, Widget, WidgetState}; + #[derive(Clone, Copy, PartialEq)] pub enum AlignmentMerge { Min, @@ -241,8 +241,8 @@ impl<'a> AlignmentProxy<'a> { } impl f64 + 'static> Widget for AlignmentGuide { - fn event(&mut self, event: &super::RawEvent, events: &mut Vec) { - self.child.event(event, events); + fn event(&mut self, cx: &mut EventCx, event: &super::RawEvent) { + self.child.event(cx, event); } fn update(&mut self, cx: &mut super::UpdateCx) { diff --git a/idiopath/src/widget/button.rs b/idiopath/src/widget/button.rs index d025c9c352..8eac70ed1f 100644 --- a/idiopath/src/widget/button.rs +++ b/idiopath/src/widget/button.rs @@ -19,7 +19,7 @@ use druid_shell::{ use crate::{event::Event, id::IdPath}; -use super::{LayoutCx, PaintCx, UpdateCx, Widget}; +use super::{EventCx, LayoutCx, PaintCx, UpdateCx, Widget}; #[derive(Default)] @@ -48,8 +48,8 @@ impl Widget for Button { // TODO: probably want to request layout when string changes } - fn event(&mut self, _event: &super::RawEvent, events: &mut Vec) { - events.push(Event::new(self.id_path.clone(), ())); + fn event(&mut self, cx: &mut EventCx, _event: &super::RawEvent) { + cx.add_event(Event::new(self.id_path.clone(), ())); } fn prelayout(&mut self, _cx: &mut LayoutCx) -> (Size, Size) { diff --git a/idiopath/src/widget/text.rs b/idiopath/src/widget/text.rs index 4102cb077a..df93962ff2 100644 --- a/idiopath/src/widget/text.rs +++ b/idiopath/src/widget/text.rs @@ -17,11 +17,9 @@ use druid_shell::{ piet::{Color, PietTextLayout, RenderContext, Text, TextLayout, TextLayoutBuilder}, }; -use crate::event::Event; - use super::{ align::{FirstBaseline, LastBaseline, SingleAlignment, VertAlignment}, - AlignCx, LayoutCx, PaintCx, UpdateCx, Widget, + AlignCx, EventCx, LayoutCx, PaintCx, UpdateCx, Widget, }; pub struct TextWidget { @@ -48,7 +46,7 @@ impl TextWidget { } impl Widget for TextWidget { - fn event(&mut self, _event: &super::RawEvent, _events: &mut Vec) {} + fn event(&mut self, _cx: &mut EventCx, _event: &super::RawEvent) {} fn update(&mut self, cx: &mut UpdateCx) { // All changes potentially require layout. Note: we could be finer diff --git a/idiopath/src/widget/vstack.rs b/idiopath/src/widget/vstack.rs index 129c36ec8d..9f62f23c66 100644 --- a/idiopath/src/widget/vstack.rs +++ b/idiopath/src/widget/vstack.rs @@ -14,11 +14,9 @@ use druid_shell::kurbo::{Point, Rect, Size}; -use crate::event::Event; - use super::{ align::{Center, SingleAlignment}, - LayoutCx, PaintCx, Pod, RawEvent, UpdateCx, Widget, + EventCx, LayoutCx, PaintCx, Pod, RawEvent, UpdateCx, Widget, }; pub struct VStack { @@ -44,14 +42,14 @@ impl VStack { } impl Widget for VStack { - fn event(&mut self, event: &super::RawEvent, events: &mut Vec) { + fn event(&mut self, cx: &mut EventCx, event: &super::RawEvent) { match event { RawEvent::MouseDown(p) => { for child in &mut self.children { let rect = Rect::from_origin_size(child.state.origin, child.state.size); if rect.contains(*p) { let child_event = RawEvent::MouseDown(*p - child.state.origin.to_vec2()); - child.event(&child_event, events); + child.event(cx, &child_event); break; } } From e678cc40dddd4aa1e30ca0de93e867c88bad34b1 Mon Sep 17 00:00:00 2001 From: Raph Levien Date: Sat, 14 May 2022 08:07:11 -0700 Subject: [PATCH 22/28] Implement layout observer Implement the "layout observer" concept which is very similar to GeometryReader in SwiftUI. Also fix some problems in vstack layout computation. --- idiopath/src/app.rs | 29 ++++-- idiopath/src/main.rs | 2 + idiopath/src/view.rs | 1 + idiopath/src/view/layout_observer.rs | 125 +++++++++++++++++++++++++ idiopath/src/widget.rs | 15 ++- idiopath/src/widget/layout_observer.rs | 96 +++++++++++++++++++ idiopath/src/widget/vstack.rs | 22 +++-- 7 files changed, 273 insertions(+), 17 deletions(-) create mode 100644 idiopath/src/view/layout_observer.rs create mode 100644 idiopath/src/widget/layout_observer.rs diff --git a/idiopath/src/app.rs b/idiopath/src/app.rs index e955c3ff0b..98521dcd9d 100644 --- a/idiopath/src/app.rs +++ b/idiopath/src/app.rs @@ -88,16 +88,25 @@ where piet.fill(rect, &BG_COLOR); self.ensure_app(); - let root_pod = self.root_pod.as_mut().unwrap(); - let mut cx_state = CxState::new(&self.window_handle, &mut self.events); - let mut update_cx = UpdateCx::new(&mut cx_state, &mut self.root_state); - root_pod.update(&mut update_cx); - let mut layout_cx = LayoutCx::new(&mut cx_state, &mut self.root_state); - root_pod.prelayout(&mut layout_cx); - let proposed_size = self.size; - root_pod.layout(&mut layout_cx, proposed_size); - let mut paint_cx = PaintCx::new(&mut cx_state, piet); - root_pod.paint(&mut paint_cx); + loop { + let root_pod = self.root_pod.as_mut().unwrap(); + let mut cx_state = CxState::new(&self.window_handle, &mut self.events); + let mut update_cx = UpdateCx::new(&mut cx_state, &mut self.root_state); + root_pod.update(&mut update_cx); + let mut layout_cx = LayoutCx::new(&mut cx_state, &mut self.root_state); + root_pod.prelayout(&mut layout_cx); + let proposed_size = self.size; + root_pod.layout(&mut layout_cx, proposed_size); + if cx_state.has_events() { + // We might want some debugging here if the number of iterations + // becomes extreme. + self.run_app_logic(); + } else { + let mut paint_cx = PaintCx::new(&mut cx_state, piet); + root_pod.paint(&mut paint_cx); + break; + } + } } pub fn mouse_down(&mut self, point: Point) { diff --git a/idiopath/src/main.rs b/idiopath/src/main.rs index 92fcb591ef..784018e906 100644 --- a/idiopath/src/main.rs +++ b/idiopath/src/main.rs @@ -30,6 +30,7 @@ use druid_shell::{ }; use view::adapt::Adapt; use view::button::button; +use view::layout_observer::LayoutObserver; use view::memoize::Memoize; use view::vstack::v_stack; use view::View; @@ -140,6 +141,7 @@ fn app_logic(data: &mut AppData) -> impl View { |data: &mut AppData, thunk| thunk.call(&mut data.count), count_button(data.count), ), + LayoutObserver::new(|size| format!("size: {:?}", size)), )) } diff --git a/idiopath/src/view.rs b/idiopath/src/view.rs index 87680b9896..692244c877 100644 --- a/idiopath/src/view.rs +++ b/idiopath/src/view.rs @@ -15,6 +15,7 @@ pub mod adapt; pub mod any_view; pub mod button; +pub mod layout_observer; pub mod memoize; pub mod text; pub mod use_state; diff --git a/idiopath/src/view/layout_observer.rs b/idiopath/src/view/layout_observer.rs new file mode 100644 index 0000000000..0ed5373c02 --- /dev/null +++ b/idiopath/src/view/layout_observer.rs @@ -0,0 +1,125 @@ +use std::{alloc::Layout, any::Any, marker::PhantomData}; + +// Copyright 2022 The Druid Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use druid_shell::kurbo::Size; + +use crate::{event::EventResult, id::Id}; + +use super::{Cx, View}; + +pub struct LayoutObserver { + callback: F, + phantom: PhantomData<(T, A, V)>, +} + +pub struct LayoutObserverState> { + size: Option, + child_id: Option, + child_view: Option, + child_state: Option, +} + +impl LayoutObserver { + pub fn new(callback: F) -> Self { + LayoutObserver { + callback, + phantom: Default::default(), + } + } +} + +impl V, V: View> View for LayoutObserver +where + V::Element: 'static, +{ + type State = LayoutObserverState; + + type Element = crate::widget::layout_observer::LayoutObserver; + + fn build(&self, cx: &mut Cx) -> (Id, Self::State, Self::Element) { + let (id, element) = + cx.with_new_id(|cx| crate::widget::layout_observer::LayoutObserver::new(cx.id_path())); + let child_state = LayoutObserverState { + size: None, + child_id: None, + child_view: None, + child_state: None, + }; + (id, child_state, element) + } + + fn rebuild( + &self, + cx: &mut Cx, + _prev: &Self, + id: &mut crate::id::Id, + state: &mut Self::State, + element: &mut Self::Element, + ) -> bool { + if let Some(size) = &state.size { + let view = (self.callback)(*size); + cx.with_id(*id, |cx| { + if let (Some(id), Some(prev_view), Some(child_state)) = ( + &mut state.child_id, + &state.child_view, + &mut state.child_state, + ) { + let child_pod = element.child_mut().as_mut().unwrap(); + let child_element = child_pod.downcast_mut().unwrap(); + let changed = view.rebuild(cx, prev_view, id, child_state, child_element); + state.child_view = Some(view); + if changed { + child_pod.request_update(); + } + changed + } else { + let (child_id, child_state, child_element) = view.build(cx); + element.set_child(Box::new(child_element)); + state.child_id = Some(child_id); + state.child_state = Some(child_state); + state.child_view = Some(view); + true + } + }) + } else { + false + } + } + + fn event( + &self, + id_path: &[crate::id::Id], + state: &mut Self::State, + event: Box, + app_state: &mut T, + ) -> EventResult { + if id_path.is_empty() { + if let Ok(size) = event.downcast() { + state.size = Some(*size); + } + EventResult::RequestRebuild + } else { + let tl = &id_path[1..]; + if let (Some(child_view), Some(child_state)) = + (&state.child_view, &mut state.child_state) + { + child_view.event(tl, child_state, event, app_state) + } else { + EventResult::Stale + } + } + } +} diff --git a/idiopath/src/widget.rs b/idiopath/src/widget.rs index ddd7bee269..f5af6d7d44 100644 --- a/idiopath/src/widget.rs +++ b/idiopath/src/widget.rs @@ -14,6 +14,7 @@ pub mod align; pub mod button; +pub mod layout_observer; pub mod text; pub mod vstack; @@ -214,6 +215,10 @@ impl<'a> CxState<'a> { events, } } + + pub(crate) fn has_events(&self) -> bool { + !self.events.is_empty() + } } impl<'a, 'b> EventCx<'a, 'b> { @@ -253,6 +258,10 @@ impl<'a, 'b> LayoutCx<'a, 'b> { pub fn text(&mut self) -> &mut PietText { &mut self.cx_state.text } + + pub fn add_event(&mut self, event: Event) { + self.cx_state.events.push(event); + } } impl<'a> AlignCx<'a> { @@ -330,12 +339,16 @@ impl WidgetState { impl Pod { pub fn new(widget: impl Widget + 'static) -> Self { + Self::new_from_box(Box::new(widget)) + } + + pub fn new_from_box(widget: Box) -> Self { Pod { state: WidgetState { flags: PodFlags::INIT_FLAGS, ..Default::default() }, - widget: Box::new(widget), + widget, } } diff --git a/idiopath/src/widget/layout_observer.rs b/idiopath/src/widget/layout_observer.rs new file mode 100644 index 0000000000..ecbabf3b5c --- /dev/null +++ b/idiopath/src/widget/layout_observer.rs @@ -0,0 +1,96 @@ +// Copyright 2022 The Druid Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! The widget-side implementation of layout observers. +//! +//! This concept is very similar to GeometryReader in SwiftUI. + +use druid_shell::kurbo::Size; + +use crate::{event::Event, id::IdPath}; + +use super::{ + align::SingleAlignment, AlignCx, AnyWidget, EventCx, LayoutCx, PaintCx, Pod, RawEvent, + UpdateCx, Widget, +}; + +pub struct LayoutObserver { + id_path: IdPath, + size: Option, + child: Option, +} + +impl LayoutObserver { + pub fn new(id_path: &IdPath) -> LayoutObserver { + LayoutObserver { + id_path: id_path.clone(), + size: None, + child: None, + } + } + + pub fn set_child(&mut self, child: Box) { + self.child = Some(Pod::new_from_box(child)); + } + + pub fn child_mut(&mut self) -> &mut Option { + &mut self.child + } +} + +impl Widget for LayoutObserver { + fn update(&mut self, cx: &mut UpdateCx) { + // Need to make sure we do layout on child when set. + cx.request_layout(); + if let Some(child) = &mut self.child { + child.update(cx); + } + } + + fn event(&mut self, cx: &mut EventCx, event: &RawEvent) { + if let Some(child) = &mut self.child { + child.event(cx, event); + } + } + + fn prelayout(&mut self, cx: &mut LayoutCx) -> (Size, Size) { + if let Some(child) = &mut self.child { + let _ = child.prelayout(cx); + } + (Size::ZERO, Size::new(1e9, 1e9)) + } + + fn layout(&mut self, cx: &mut LayoutCx, proposed_size: Size) -> Size { + if Some(proposed_size) != self.size { + cx.add_event(Event::new(self.id_path.clone(), proposed_size)); + self.size = Some(proposed_size); + } + if let Some(child) = &mut self.child { + let _ = child.layout(cx, proposed_size); + } + proposed_size + } + + fn align(&self, cx: &mut AlignCx, alignment: SingleAlignment) { + if let Some(child) = &self.child { + child.align(cx, alignment); + } + } + + fn paint(&mut self, cx: &mut PaintCx) { + if let Some(child) = &mut self.child { + child.paint(cx); + } + } +} diff --git a/idiopath/src/widget/vstack.rs b/idiopath/src/widget/vstack.rs index 9f62f23c66..82dd089b9f 100644 --- a/idiopath/src/widget/vstack.rs +++ b/idiopath/src/widget/vstack.rs @@ -73,6 +73,9 @@ impl Widget for VStack { max_size.width = max_size.width.max(child_max.width); max_size.height += child_max.height; } + let spacing = self.spacing * (self.children.len() - 1) as f64; + min_size.height += spacing; + max_size.height += spacing; (min_size, max_size) } @@ -80,15 +83,17 @@ impl Widget for VStack { // First, sort children in order of increasing flexibility let mut child_order: Vec<_> = (0..self.children.len()).collect(); child_order.sort_by_key(|ix| self.children[*ix].height_flexibility().to_bits()); + // Offer remaining height to each child let mut n_remaining = self.children.len(); let mut height_remaining = proposed_size.height - (n_remaining - 1) as f64 * self.spacing; for ix in child_order { let child_height = (height_remaining / n_remaining as f64).max(0.0); let child_proposed = Size::new(proposed_size.width, child_height); let child_size = self.children[ix].layout(cx, child_proposed); - height_remaining -= height_remaining - child_size.height; + height_remaining -= child_size.height; n_remaining -= 1; } + // Get alignments from children let alignments: Vec = self .children .iter() @@ -99,15 +104,20 @@ impl Widget for VStack { .copied() .reduce(f64::max) .unwrap_or_default(); + // Place children, using computed height and alignments let mut size = Size::default(); let mut y = 0.0; - for (child, align) in self.children.iter_mut().zip(alignments) { + for (i, (child, align)) in self.children.iter_mut().zip(alignments).enumerate() { + if i != 0 { + y += self.spacing; + } let child_size = child.state.size; - child.state.origin = Point::new(max_align - align, y); - size.width = size.width.max(child_size.width); - size.height += child_size.height; - y += child_size.height + self.spacing; + let origin = Point::new(max_align - align, y); + child.state.origin = origin; + size.width = size.width.max(child_size.width + origin.x); + y += child_size.height; } + size.height = y; size } From 6c6b0f2fb2a0a4bf7ec993a525bf34500e58ec44 Mon Sep 17 00:00:00 2001 From: Raph Levien Date: Wed, 18 May 2022 14:55:48 -0700 Subject: [PATCH 23/28] A bit of reorganization Move relevant bits into core and contexts files, mirroring the structure of existing Druid (which hopefully will make it easier to navigate when adapting existing Druid code to the new architecture). --- idiopath/src/widget.rs | 168 ++------------------------------ idiopath/src/widget/contexts.rs | 159 ++++++++++++++++++++++++++++++ idiopath/src/widget/core.rs | 55 +++++++++++ 3 files changed, 220 insertions(+), 162 deletions(-) create mode 100644 idiopath/src/widget/contexts.rs create mode 100644 idiopath/src/widget/core.rs diff --git a/idiopath/src/widget.rs b/idiopath/src/widget.rs index f5af6d7d44..58a9021fac 100644 --- a/idiopath/src/widget.rs +++ b/idiopath/src/widget.rs @@ -14,20 +14,22 @@ pub mod align; pub mod button; +mod contexts; +mod core; pub mod layout_observer; pub mod text; pub mod vstack; use std::any::Any; -use std::ops::{Deref, DerefMut}; - -use bitflags::bitflags; +use std::ops::DerefMut; use druid_shell::kurbo::{Affine, Point, Size}; use druid_shell::piet::{Piet, PietText, RenderContext}; use druid_shell::WindowHandle; -use crate::event::Event; +pub use self::contexts::{AlignCx, CxState, EventCx, LayoutCx, PaintCx, UpdateCx}; +pub use self::core::Pod; +pub(crate) use self::core::{PodFlags, WidgetState}; use self::align::{ AlignResult, AlignmentAxis, Bottom, Center, HorizAlignment, Leading, SingleAlignment, Top, @@ -60,71 +62,6 @@ pub trait Widget { fn paint(&mut self, cx: &mut PaintCx); } -// These contexts loosely follow Druid. -pub struct CxState<'a> { - window: &'a WindowHandle, - text: PietText, - events: &'a mut Vec, -} - -pub struct EventCx<'a, 'b> { - cx_state: &'a mut CxState<'b>, - widget_state: &'a mut WidgetState, -} - -pub struct UpdateCx<'a, 'b> { - cx_state: &'a mut CxState<'b>, - widget_state: &'a mut WidgetState, -} - -pub struct LayoutCx<'a, 'b> { - cx_state: &'a mut CxState<'b>, - widget_state: &'a mut WidgetState, -} - -pub struct AlignCx<'a> { - widget_state: &'a WidgetState, - align_result: &'a mut AlignResult, - origin: Point, -} - -pub struct PaintCx<'a, 'b, 'c> { - cx_state: &'a mut CxState<'b>, - piet: &'a mut Piet<'c>, -} - -bitflags! { - #[derive(Default)] - struct PodFlags: u32 { - const REQUEST_UPDATE = 1; - const REQUEST_LAYOUT = 2; - const REQUEST_PAINT = 4; - - const UPWARD_FLAGS = Self::REQUEST_LAYOUT.bits | Self::REQUEST_PAINT.bits; - const INIT_FLAGS = Self::REQUEST_UPDATE.bits | Self::REQUEST_LAYOUT.bits | Self::REQUEST_PAINT.bits; - } -} - -/// A pod that contains a widget (in a container). -pub struct Pod { - state: WidgetState, - widget: Box, -} - -#[derive(Default, Debug)] -pub struct WidgetState { - flags: PodFlags, - origin: Point, - /// The minimum intrinsic size of the widget. - min_size: Size, - /// The maximum intrinsic size of the widget. - max_size: Size, - /// The size proposed by the widget's container. - proposed_size: Size, - /// The size of the widget. - size: Size, -} - pub trait AnyWidget: Widget { fn as_any(&self) -> &dyn Any; @@ -207,99 +144,6 @@ impl_widget_tuple!(8; 0, 1, 2, 3, 4, 5, 6, 7 ); -impl<'a> CxState<'a> { - pub fn new(window: &'a WindowHandle, events: &'a mut Vec) -> Self { - CxState { - window, - text: window.text(), - events, - } - } - - pub(crate) fn has_events(&self) -> bool { - !self.events.is_empty() - } -} - -impl<'a, 'b> EventCx<'a, 'b> { - pub fn new(cx_state: &'a mut CxState<'b>, root_state: &'a mut WidgetState) -> Self { - EventCx { - cx_state, - widget_state: root_state, - } - } - - pub fn add_event(&mut self, event: Event) { - self.cx_state.events.push(event); - } -} - -impl<'a, 'b> UpdateCx<'a, 'b> { - pub fn new(cx_state: &'a mut CxState<'b>, root_state: &'a mut WidgetState) -> Self { - UpdateCx { - cx_state, - widget_state: root_state, - } - } - - pub fn request_layout(&mut self) { - self.widget_state.flags |= PodFlags::REQUEST_LAYOUT; - } -} - -impl<'a, 'b> LayoutCx<'a, 'b> { - pub fn new(cx_state: &'a mut CxState<'b>, root_state: &'a mut WidgetState) -> Self { - LayoutCx { - cx_state, - widget_state: root_state, - } - } - - pub fn text(&mut self) -> &mut PietText { - &mut self.cx_state.text - } - - pub fn add_event(&mut self, event: Event) { - self.cx_state.events.push(event); - } -} - -impl<'a> AlignCx<'a> { - pub fn aggregate(&mut self, alignment: SingleAlignment, value: f64) { - let origin_value = match alignment.axis() { - AlignmentAxis::Horizontal => self.origin.x, - AlignmentAxis::Vertical => self.origin.y, - }; - self.align_result.aggregate(alignment, value + origin_value); - } -} - -impl<'a, 'b, 'c> PaintCx<'a, 'b, 'c> { - pub fn new(cx_state: &'a mut CxState<'b>, piet: &'a mut Piet<'c>) -> Self { - PaintCx { cx_state, piet } - } - - pub fn with_save(&mut self, f: impl FnOnce(&mut PaintCx)) { - self.piet.save().unwrap(); - f(self); - self.piet.restore().unwrap(); - } -} - -impl<'c> Deref for PaintCx<'_, '_, 'c> { - type Target = Piet<'c>; - - fn deref(&self) -> &Self::Target { - self.piet - } -} - -impl<'c> DerefMut for PaintCx<'_, '_, 'c> { - fn deref_mut(&mut self) -> &mut Self::Target { - self.piet - } -} - impl WidgetState { fn merge_up(&mut self, child_state: &mut WidgetState) { self.flags |= child_state.flags & PodFlags::UPWARD_FLAGS; diff --git a/idiopath/src/widget/contexts.rs b/idiopath/src/widget/contexts.rs new file mode 100644 index 0000000000..5cd239b86e --- /dev/null +++ b/idiopath/src/widget/contexts.rs @@ -0,0 +1,159 @@ +// Copyright 2022 The Druid Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Contexts for the widget system. +//! +//! Note: the organization of this code roughly follows the existing Druid +//! widget system, particularly its contexts.rs. + +use std::ops::{Deref, DerefMut}; + +use druid_shell::{ + kurbo::Point, + piet::{Piet, PietText, RenderContext}, + WindowHandle, +}; + +use crate::event::Event; + +use super::{ + align::{AlignResult, AlignmentAxis, SingleAlignment}, + PodFlags, WidgetState, +}; + +// These contexts loosely follow Druid. +pub struct CxState<'a> { + window: &'a WindowHandle, + text: PietText, + events: &'a mut Vec, +} + +pub struct EventCx<'a, 'b> { + pub(crate) cx_state: &'a mut CxState<'b>, + pub(crate) widget_state: &'a mut WidgetState, +} + +pub struct UpdateCx<'a, 'b> { + pub(crate) cx_state: &'a mut CxState<'b>, + pub(crate) widget_state: &'a mut WidgetState, +} + +pub struct LayoutCx<'a, 'b> { + pub(crate) cx_state: &'a mut CxState<'b>, + pub(crate) widget_state: &'a mut WidgetState, +} + +pub struct AlignCx<'a> { + pub(crate) widget_state: &'a WidgetState, + pub(crate) align_result: &'a mut AlignResult, + pub(crate) origin: Point, +} + +pub struct PaintCx<'a, 'b, 'c> { + pub(crate) cx_state: &'a mut CxState<'b>, + pub(crate) piet: &'a mut Piet<'c>, +} + +impl<'a> CxState<'a> { + pub fn new(window: &'a WindowHandle, events: &'a mut Vec) -> Self { + CxState { + window, + text: window.text(), + events, + } + } + + pub(crate) fn has_events(&self) -> bool { + !self.events.is_empty() + } +} + +impl<'a, 'b> EventCx<'a, 'b> { + pub(crate) fn new(cx_state: &'a mut CxState<'b>, root_state: &'a mut WidgetState) -> Self { + EventCx { + cx_state, + widget_state: root_state, + } + } + + pub fn add_event(&mut self, event: Event) { + self.cx_state.events.push(event); + } +} + +impl<'a, 'b> UpdateCx<'a, 'b> { + pub(crate) fn new(cx_state: &'a mut CxState<'b>, root_state: &'a mut WidgetState) -> Self { + UpdateCx { + cx_state, + widget_state: root_state, + } + } + + pub fn request_layout(&mut self) { + self.widget_state.flags |= PodFlags::REQUEST_LAYOUT; + } +} + +impl<'a, 'b> LayoutCx<'a, 'b> { + pub(crate) fn new(cx_state: &'a mut CxState<'b>, root_state: &'a mut WidgetState) -> Self { + LayoutCx { + cx_state, + widget_state: root_state, + } + } + + pub fn text(&mut self) -> &mut PietText { + &mut self.cx_state.text + } + + pub fn add_event(&mut self, event: Event) { + self.cx_state.events.push(event); + } +} + +impl<'a> AlignCx<'a> { + pub fn aggregate(&mut self, alignment: SingleAlignment, value: f64) { + let origin_value = match alignment.axis() { + AlignmentAxis::Horizontal => self.origin.x, + AlignmentAxis::Vertical => self.origin.y, + }; + self.align_result.aggregate(alignment, value + origin_value); + } +} + +impl<'a, 'b, 'c> PaintCx<'a, 'b, 'c> { + pub fn new(cx_state: &'a mut CxState<'b>, piet: &'a mut Piet<'c>) -> Self { + PaintCx { cx_state, piet } + } + + pub fn with_save(&mut self, f: impl FnOnce(&mut PaintCx)) { + self.piet.save().unwrap(); + f(self); + self.piet.restore().unwrap(); + } +} + +impl<'c> Deref for PaintCx<'_, '_, 'c> { + type Target = Piet<'c>; + + fn deref(&self) -> &Self::Target { + self.piet + } +} + +impl<'c> DerefMut for PaintCx<'_, '_, 'c> { + fn deref_mut(&mut self) -> &mut Self::Target { + self.piet + } +} diff --git a/idiopath/src/widget/core.rs b/idiopath/src/widget/core.rs new file mode 100644 index 0000000000..7da07f12dd --- /dev/null +++ b/idiopath/src/widget/core.rs @@ -0,0 +1,55 @@ +// Copyright 2022 The Druid Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Core types and mechanisms for the widget hierarchy. +//! +//! //! Note: the organization of this code roughly follows the existing Druid +//! widget system, particularly its core.rs. + +use bitflags::bitflags; +use druid_shell::kurbo::{Point, Size}; + +use super::AnyWidget; + +bitflags! { + #[derive(Default)] + pub(crate) struct PodFlags: u32 { + const REQUEST_UPDATE = 1; + const REQUEST_LAYOUT = 2; + const REQUEST_PAINT = 4; + + const UPWARD_FLAGS = Self::REQUEST_LAYOUT.bits | Self::REQUEST_PAINT.bits; + const INIT_FLAGS = Self::REQUEST_UPDATE.bits | Self::REQUEST_LAYOUT.bits | Self::REQUEST_PAINT.bits; + } +} + +/// A pod that contains a widget (in a container). +pub struct Pod { + pub(crate) state: WidgetState, + pub(crate) widget: Box, +} + +#[derive(Default, Debug)] +pub(crate) struct WidgetState { + pub(crate) flags: PodFlags, + pub(crate) origin: Point, + /// The minimum intrinsic size of the widget. + pub(crate) min_size: Size, + /// The maximum intrinsic size of the widget. + pub(crate) max_size: Size, + /// The size proposed by the widget's container. + pub(crate) proposed_size: Size, + /// The size of the widget. + pub(crate) size: Size, +} From 5aaec6d0beb6b5c9a80651b2853e2a1071f50f8a Mon Sep 17 00:00:00 2001 From: Raph Levien Date: Wed, 18 May 2022 16:18:59 -0700 Subject: [PATCH 24/28] Make into library The widget set really should be structured as a library, and it will no doubt be useful to have multiple examples to exercise various aspects of that. This patch changes from a single main executable to a library with an examples dir. --- idiopath/examples/counter.rs | 46 ++++++++ idiopath/src/{main.rs => app_main.rs} | 133 +++++++++------------- idiopath/src/lib.rs | 37 +++++++ idiopath/src/view/layout_observer.rs | 2 +- idiopath/src/widget.rs | 149 +------------------------ idiopath/src/widget/align.rs | 2 +- idiopath/src/widget/button.rs | 2 +- idiopath/src/widget/core.rs | 154 +++++++++++++++++++++++++- 8 files changed, 292 insertions(+), 233 deletions(-) create mode 100644 idiopath/examples/counter.rs rename idiopath/src/{main.rs => app_main.rs} (50%) create mode 100644 idiopath/src/lib.rs diff --git a/idiopath/examples/counter.rs b/idiopath/examples/counter.rs new file mode 100644 index 0000000000..722463d68f --- /dev/null +++ b/idiopath/examples/counter.rs @@ -0,0 +1,46 @@ +// Copyright 2022 The Druid Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use idiopath::{button, v_stack, Adapt, App, AppLauncher, LayoutObserver, Memoize, View}; + +#[derive(Default)] +struct AppData { + count: u32, +} + +fn count_button(count: u32) -> impl View { + button(format!("count: {}", count), |data| *data += 1) +} + +fn app_logic(data: &mut AppData) -> impl View { + v_stack(( + format!("count: {}", data.count), + button("reset", |data: &mut AppData| data.count = 0), + Memoize::new(data.count, |count| { + button(format!("count: {}", count), |data: &mut AppData| { + data.count += 1 + }) + }), + Adapt::new( + |data: &mut AppData, thunk| thunk.call(&mut data.count), + count_button(data.count), + ), + LayoutObserver::new(|size| format!("size: {:?}", size)), + )) +} + +pub fn main() { + let app = App::new(AppData::default(), app_logic); + AppLauncher::new(app).run(); +} diff --git a/idiopath/src/main.rs b/idiopath/src/app_main.rs similarity index 50% rename from idiopath/src/main.rs rename to idiopath/src/app_main.rs index 784018e906..250e280054 100644 --- a/idiopath/src/main.rs +++ b/idiopath/src/app_main.rs @@ -12,29 +12,21 @@ // See the License for the specific language governing permissions and // limitations under the License. -mod app; -mod event; -mod id; -mod view; -mod view_seq; -mod widget; - use std::any::Any; -use app::App; -use druid_shell::kurbo::Size; - use druid_shell::{ - Application, Cursor, HotKey, Menu, MouseEvent, Region, SysMods, WinHandler, WindowBuilder, - WindowHandle, + kurbo::Size, Application, Cursor, HotKey, Menu, MouseEvent, Region, SysMods, WinHandler, + WindowBuilder, WindowHandle, }; -use view::adapt::Adapt; -use view::button::button; -use view::layout_observer::LayoutObserver; -use view::memoize::Memoize; -use view::vstack::v_stack; -use view::View; -use widget::Widget; + +use crate::{app::App, View, Widget}; + +// This is a bit of a hack just to get a window launched. The real version +// would deal with multiple windows and have other ways to configure things. +pub struct AppLauncher, F: FnMut(&mut T) -> V> { + title: String, + app: App, +} struct MainState, F: FnMut(&mut T) -> V> where @@ -44,6 +36,45 @@ where app: App, } +const QUIT_MENU_ID: u32 = 0x100; + +impl + 'static, F: FnMut(&mut T) -> V + 'static> AppLauncher { + pub fn new(app: App) -> Self { + AppLauncher { + title: "Xilem app".into(), + app, + } + } + + pub fn title(mut self, title: impl Into) -> Self { + self.title = title.into(); + self + } + + pub fn run(self) { + let mut file_menu = Menu::new(); + file_menu.add_item( + QUIT_MENU_ID, + "E&xit", + Some(&HotKey::new(SysMods::Cmd, "q")), + true, + false, + ); + let mut menubar = Menu::new(); + menubar.add_dropdown(Menu::new(), "Application", true); + menubar.add_dropdown(file_menu, "&File", true); + let druid_app = Application::new().unwrap(); + let mut builder = WindowBuilder::new(druid_app.clone()); + let main_state = MainState::new(self.app); + builder.set_handler(Box::new(main_state)); + builder.set_title(self.title); + builder.set_menu(menubar); + let window = builder.build().unwrap(); + window.show(); + druid_app.run(None); + } +} + impl + 'static, F: FnMut(&mut T) -> V + 'static> WinHandler for MainState where @@ -62,7 +93,7 @@ where fn command(&mut self, id: u32) { match id { - 0x100 => { + QUIT_MENU_ID => { self.handle.close(); Application::global().quit() } @@ -110,65 +141,3 @@ where state } } - -/* -fn app_logic(data: &mut u32) -> impl View { - let button = Button::new(format!("count: {}", data), |data| *data += 1); - let boxed: Box> = Box::new(button); - Column::new((boxed, Button::new("reset", |data| *data = 0))) -} -*/ - -#[derive(Default)] -struct AppData { - count: u32, -} - -fn count_button(count: u32) -> impl View { - button(format!("count: {}", count), |data| *data += 1) -} - -fn app_logic(data: &mut AppData) -> impl View { - v_stack(( - format!("count: {}", data.count), - button("reset", |data: &mut AppData| data.count = 0), - Memoize::new(data.count, |count| { - button(format!("count: {}", count), |data: &mut AppData| { - data.count += 1 - }) - }), - Adapt::new( - |data: &mut AppData, thunk| thunk.call(&mut data.count), - count_button(data.count), - ), - LayoutObserver::new(|size| format!("size: {:?}", size)), - )) -} - -fn main() { - //tracing_subscriber::fmt().init(); - let mut file_menu = Menu::new(); - file_menu.add_item( - 0x100, - "E&xit", - Some(&HotKey::new(SysMods::Cmd, "q")), - true, - false, - ); - let mut menubar = Menu::new(); - menubar.add_dropdown(Menu::new(), "Application", true); - menubar.add_dropdown(file_menu, "&File", true); - - let app = App::new(AppData::default(), app_logic); - let druid_app = Application::new().unwrap(); - let mut builder = WindowBuilder::new(druid_app.clone()); - let main_state = MainState::new(app); - builder.set_handler(Box::new(main_state)); - builder.set_title("Idiopath"); - builder.set_menu(menubar); - - let window = builder.build().unwrap(); - window.show(); - - druid_app.run(None); -} diff --git a/idiopath/src/lib.rs b/idiopath/src/lib.rs new file mode 100644 index 0000000000..c018d0ede9 --- /dev/null +++ b/idiopath/src/lib.rs @@ -0,0 +1,37 @@ +// Copyright 2022 The Druid Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Prototype implementation of Xilem architecture. +//! +//! This is a skeletal, proof-of-concept UI toolkit to prove out the Xilem +//! architectural ideas. + +mod app; +mod app_main; +mod event; +mod id; +mod view; +mod view_seq; +mod widget; + +pub use app::App; +pub use app_main::AppLauncher; +pub use view::adapt::Adapt; +pub use view::button::button; +pub use view::layout_observer::LayoutObserver; +pub use view::memoize::Memoize; +pub use view::vstack::v_stack; +pub use view::View; +pub use widget::align::{AlignmentAxis, AlignmentProxy, HorizAlignment, VertAlignment}; +pub use widget::Widget; diff --git a/idiopath/src/view/layout_observer.rs b/idiopath/src/view/layout_observer.rs index 0ed5373c02..9928e38704 100644 --- a/idiopath/src/view/layout_observer.rs +++ b/idiopath/src/view/layout_observer.rs @@ -1,4 +1,4 @@ -use std::{alloc::Layout, any::Any, marker::PhantomData}; +use std::{any::Any, marker::PhantomData}; // Copyright 2022 The Druid Authors. // diff --git a/idiopath/src/widget.rs b/idiopath/src/widget.rs index 58a9021fac..2388e6d226 100644 --- a/idiopath/src/widget.rs +++ b/idiopath/src/widget.rs @@ -23,18 +23,13 @@ pub mod vstack; use std::any::Any; use std::ops::DerefMut; -use druid_shell::kurbo::{Affine, Point, Size}; -use druid_shell::piet::{Piet, PietText, RenderContext}; -use druid_shell::WindowHandle; +use druid_shell::kurbo::{Point, Size}; pub use self::contexts::{AlignCx, CxState, EventCx, LayoutCx, PaintCx, UpdateCx}; pub use self::core::Pod; pub(crate) use self::core::{PodFlags, WidgetState}; -use self::align::{ - AlignResult, AlignmentAxis, Bottom, Center, HorizAlignment, Leading, SingleAlignment, Top, - Trailing, VertAlignment, -}; +use self::align::SingleAlignment; /// A basic widget trait. pub trait Widget { @@ -57,6 +52,7 @@ pub trait Widget { /// Query for an alignment. /// /// This method can count on layout already having been completed. + #[allow(unused)] fn align(&self, cx: &mut AlignCx, alignment: SingleAlignment) {} fn paint(&mut self, cx: &mut PaintCx); @@ -143,142 +139,3 @@ impl_widget_tuple!(8; W0, W1, W2, W3, W4, W5, W6, W7; 0, 1, 2, 3, 4, 5, 6, 7 ); - -impl WidgetState { - fn merge_up(&mut self, child_state: &mut WidgetState) { - self.flags |= child_state.flags & PodFlags::UPWARD_FLAGS; - } - - fn request(&mut self, flags: PodFlags) { - self.flags |= flags - } - - /// Get alignment value. - /// - /// The value is in the coordinate system of the parent widget. - fn get_alignment(&self, widget: &dyn AnyWidget, alignment: SingleAlignment) -> f64 { - if alignment.id() == Leading.id() || alignment.id() == Top.id() { - 0.0 - } else if alignment.id() ==
::id(&Center) { - match alignment.axis() { - AlignmentAxis::Horizontal => self.size.width * 0.5, - AlignmentAxis::Vertical => self.size.height * 0.5, - } - } else if alignment.id() == Trailing.id() { - self.size.width - } else if alignment.id() == Bottom.id() { - self.size.height - } else { - let mut align_result = AlignResult::default(); - let mut align_cx = AlignCx { - widget_state: self, - align_result: &mut align_result, - origin: self.origin, - }; - widget.align(&mut align_cx, alignment); - align_result.reap(alignment) - } - } -} - -impl Pod { - pub fn new(widget: impl Widget + 'static) -> Self { - Self::new_from_box(Box::new(widget)) - } - - pub fn new_from_box(widget: Box) -> Self { - Pod { - state: WidgetState { - flags: PodFlags::INIT_FLAGS, - ..Default::default() - }, - widget, - } - } - - pub fn downcast_mut(&mut self) -> Option<&mut T> { - (*self.widget).as_any_mut().downcast_mut() - } - - pub fn request_update(&mut self) { - self.state.request(PodFlags::REQUEST_UPDATE); - } - - pub fn event(&mut self, cx: &mut EventCx, event: &RawEvent) { - self.widget.event(cx, event); - } - - /// Propagate an update cycle. - pub fn update(&mut self, cx: &mut UpdateCx) { - if self.state.flags.contains(PodFlags::REQUEST_UPDATE) { - let mut child_cx = UpdateCx { - cx_state: cx.cx_state, - widget_state: &mut self.state, - }; - self.widget.update(&mut child_cx); - self.state.flags.remove(PodFlags::REQUEST_UPDATE); - cx.widget_state.merge_up(&mut self.state); - } - } - - pub fn prelayout(&mut self, cx: &mut LayoutCx) -> (Size, Size) { - if self.state.flags.contains(PodFlags::REQUEST_LAYOUT) { - let mut child_cx = LayoutCx { - cx_state: cx.cx_state, - widget_state: &mut self.state, - }; - let (min_size, max_size) = self.widget.prelayout(&mut child_cx); - self.state.min_size = min_size; - self.state.max_size = max_size; - // Don't remove REQUEST_LAYOUT here, that will be done in layout. - } - (self.state.min_size, self.state.max_size) - } - - pub fn layout(&mut self, cx: &mut LayoutCx, proposed_size: Size) -> Size { - if self.state.flags.contains(PodFlags::REQUEST_LAYOUT) - || proposed_size != self.state.proposed_size - { - let mut child_cx = LayoutCx { - cx_state: cx.cx_state, - widget_state: &mut self.state, - }; - let new_size = self.widget.layout(&mut child_cx, proposed_size); - self.state.proposed_size = proposed_size; - self.state.size = new_size; - self.state.flags.remove(PodFlags::REQUEST_LAYOUT); - } - self.state.size - } - - /// Propagate alignment query to children. - /// - /// This call aggregates all instances of the alignment, so cost may be - /// proportional to the number of descendants. - pub fn align(&self, cx: &mut AlignCx, alignment: SingleAlignment) { - let mut child_cx = AlignCx { - widget_state: &self.state, - align_result: cx.align_result, - origin: cx.origin + self.state.origin.to_vec2(), - }; - self.widget.align(&mut child_cx, alignment); - } - - pub fn paint(&mut self, cx: &mut PaintCx) { - cx.with_save(|cx| { - cx.piet - .transform(Affine::translate(self.state.origin.to_vec2())); - self.widget.paint(cx); - }); - } - - pub fn height_flexibility(&self) -> f64 { - self.state.max_size.height - self.state.min_size.height - } - - /// The returned value is in the coordinate space of the parent that - /// owns this pod. - pub fn get_alignment(&self, alignment: SingleAlignment) -> f64 { - self.state.get_alignment(&self.widget, alignment) - } -} diff --git a/idiopath/src/widget/align.rs b/idiopath/src/widget/align.rs index 6028d13e04..eafbed6ffe 100644 --- a/idiopath/src/widget/align.rs +++ b/idiopath/src/widget/align.rs @@ -207,7 +207,7 @@ impl AlignResult { // AlignmentGuide widget /// A proxy that can be queried for alignments. -struct AlignmentProxy<'a> { +pub struct AlignmentProxy<'a> { widget_state: &'a WidgetState, widget: &'a dyn AnyWidget, } diff --git a/idiopath/src/widget/button.rs b/idiopath/src/widget/button.rs index 8eac70ed1f..3b381f5736 100644 --- a/idiopath/src/widget/button.rs +++ b/idiopath/src/widget/button.rs @@ -44,7 +44,7 @@ impl Button { const FIXED_SIZE: Size = Size::new(100., 20.); impl Widget for Button { - fn update(&mut self, cx: &mut UpdateCx) { + fn update(&mut self, _cx: &mut UpdateCx) { // TODO: probably want to request layout when string changes } diff --git a/idiopath/src/widget/core.rs b/idiopath/src/widget/core.rs index 7da07f12dd..e80436841f 100644 --- a/idiopath/src/widget/core.rs +++ b/idiopath/src/widget/core.rs @@ -18,9 +18,20 @@ //! widget system, particularly its core.rs. use bitflags::bitflags; -use druid_shell::kurbo::{Point, Size}; +use druid_shell::{ + kurbo::{Affine, Point, Size}, + piet::RenderContext, +}; -use super::AnyWidget; +use crate::Widget; + +use super::{ + align::{ + AlignResult, AlignmentAxis, Bottom, Center, HorizAlignment, Leading, SingleAlignment, Top, + Trailing, VertAlignment, + }, + AlignCx, AnyWidget, EventCx, LayoutCx, PaintCx, RawEvent, UpdateCx, +}; bitflags! { #[derive(Default)] @@ -53,3 +64,142 @@ pub(crate) struct WidgetState { /// The size of the widget. pub(crate) size: Size, } + +impl WidgetState { + fn merge_up(&mut self, child_state: &mut WidgetState) { + self.flags |= child_state.flags & PodFlags::UPWARD_FLAGS; + } + + fn request(&mut self, flags: PodFlags) { + self.flags |= flags + } + + /// Get alignment value. + /// + /// The value is in the coordinate system of the parent widget. + pub(crate) fn get_alignment(&self, widget: &dyn AnyWidget, alignment: SingleAlignment) -> f64 { + if alignment.id() == Leading.id() || alignment.id() == Top.id() { + 0.0 + } else if alignment.id() ==
::id(&Center) { + match alignment.axis() { + AlignmentAxis::Horizontal => self.size.width * 0.5, + AlignmentAxis::Vertical => self.size.height * 0.5, + } + } else if alignment.id() == Trailing.id() { + self.size.width + } else if alignment.id() == Bottom.id() { + self.size.height + } else { + let mut align_result = AlignResult::default(); + let mut align_cx = AlignCx { + widget_state: self, + align_result: &mut align_result, + origin: self.origin, + }; + widget.align(&mut align_cx, alignment); + align_result.reap(alignment) + } + } +} + +impl Pod { + pub fn new(widget: impl Widget + 'static) -> Self { + Self::new_from_box(Box::new(widget)) + } + + pub fn new_from_box(widget: Box) -> Self { + Pod { + state: WidgetState { + flags: PodFlags::INIT_FLAGS, + ..Default::default() + }, + widget, + } + } + + pub fn downcast_mut(&mut self) -> Option<&mut T> { + (*self.widget).as_any_mut().downcast_mut() + } + + pub fn request_update(&mut self) { + self.state.request(PodFlags::REQUEST_UPDATE); + } + + pub fn event(&mut self, cx: &mut EventCx, event: &RawEvent) { + self.widget.event(cx, event); + } + + /// Propagate an update cycle. + pub fn update(&mut self, cx: &mut UpdateCx) { + if self.state.flags.contains(PodFlags::REQUEST_UPDATE) { + let mut child_cx = UpdateCx { + cx_state: cx.cx_state, + widget_state: &mut self.state, + }; + self.widget.update(&mut child_cx); + self.state.flags.remove(PodFlags::REQUEST_UPDATE); + cx.widget_state.merge_up(&mut self.state); + } + } + + pub fn prelayout(&mut self, cx: &mut LayoutCx) -> (Size, Size) { + if self.state.flags.contains(PodFlags::REQUEST_LAYOUT) { + let mut child_cx = LayoutCx { + cx_state: cx.cx_state, + widget_state: &mut self.state, + }; + let (min_size, max_size) = self.widget.prelayout(&mut child_cx); + self.state.min_size = min_size; + self.state.max_size = max_size; + // Don't remove REQUEST_LAYOUT here, that will be done in layout. + } + (self.state.min_size, self.state.max_size) + } + + pub fn layout(&mut self, cx: &mut LayoutCx, proposed_size: Size) -> Size { + if self.state.flags.contains(PodFlags::REQUEST_LAYOUT) + || proposed_size != self.state.proposed_size + { + let mut child_cx = LayoutCx { + cx_state: cx.cx_state, + widget_state: &mut self.state, + }; + let new_size = self.widget.layout(&mut child_cx, proposed_size); + self.state.proposed_size = proposed_size; + self.state.size = new_size; + self.state.flags.remove(PodFlags::REQUEST_LAYOUT); + } + self.state.size + } + + /// Propagate alignment query to children. + /// + /// This call aggregates all instances of the alignment, so cost may be + /// proportional to the number of descendants. + pub fn align(&self, cx: &mut AlignCx, alignment: SingleAlignment) { + let mut child_cx = AlignCx { + widget_state: &self.state, + align_result: cx.align_result, + origin: cx.origin + self.state.origin.to_vec2(), + }; + self.widget.align(&mut child_cx, alignment); + } + + pub fn paint(&mut self, cx: &mut PaintCx) { + cx.with_save(|cx| { + cx.piet + .transform(Affine::translate(self.state.origin.to_vec2())); + self.widget.paint(cx); + }); + } + + pub fn height_flexibility(&self) -> f64 { + self.state.max_size.height - self.state.min_size.height + } + + /// The returned value is in the coordinate space of the parent that + /// owns this pod. + pub fn get_alignment(&self, alignment: SingleAlignment) -> f64 { + self.state.get_alignment(&self.widget, alignment) + } +} From ab39d1f31a4551706c2b04b85c667b36f30c3d4b Mon Sep 17 00:00:00 2001 From: Raph Levien Date: Thu, 19 May 2022 09:01:39 -0700 Subject: [PATCH 25/28] Rename to xilem I've decided on the name "xilem" rather than "idiopath," and this also matches the blog post (now linked from the README, though that is a bit stale). --- Cargo.toml | 2 +- {idiopath => xilem}/Cargo.toml | 3 ++- {idiopath => xilem}/README.md | 3 +++ {idiopath => xilem}/examples/counter.rs | 2 +- {idiopath => xilem}/src/app.rs | 0 {idiopath => xilem}/src/app_main.rs | 0 {idiopath => xilem}/src/event.rs | 0 {idiopath => xilem}/src/id.rs | 0 {idiopath => xilem}/src/lib.rs | 0 {idiopath => xilem}/src/view.rs | 0 {idiopath => xilem}/src/view/adapt.rs | 0 {idiopath => xilem}/src/view/any_view.rs | 0 {idiopath => xilem}/src/view/button.rs | 0 {idiopath => xilem}/src/view/layout_observer.rs | 0 {idiopath => xilem}/src/view/memoize.rs | 0 {idiopath => xilem}/src/view/text.rs | 0 {idiopath => xilem}/src/view/use_state.rs | 0 {idiopath => xilem}/src/view/vstack.rs | 0 {idiopath => xilem}/src/view_seq.rs | 0 {idiopath => xilem}/src/widget.rs | 0 {idiopath => xilem}/src/widget/align.rs | 0 {idiopath => xilem}/src/widget/button.rs | 0 {idiopath => xilem}/src/widget/contexts.rs | 0 {idiopath => xilem}/src/widget/core.rs | 0 {idiopath => xilem}/src/widget/layout_observer.rs | 0 {idiopath => xilem}/src/widget/text.rs | 0 {idiopath => xilem}/src/widget/vstack.rs | 0 27 files changed, 7 insertions(+), 3 deletions(-) rename {idiopath => xilem}/Cargo.toml (81%) rename {idiopath => xilem}/README.md (95%) rename {idiopath => xilem}/examples/counter.rs (94%) rename {idiopath => xilem}/src/app.rs (100%) rename {idiopath => xilem}/src/app_main.rs (100%) rename {idiopath => xilem}/src/event.rs (100%) rename {idiopath => xilem}/src/id.rs (100%) rename {idiopath => xilem}/src/lib.rs (100%) rename {idiopath => xilem}/src/view.rs (100%) rename {idiopath => xilem}/src/view/adapt.rs (100%) rename {idiopath => xilem}/src/view/any_view.rs (100%) rename {idiopath => xilem}/src/view/button.rs (100%) rename {idiopath => xilem}/src/view/layout_observer.rs (100%) rename {idiopath => xilem}/src/view/memoize.rs (100%) rename {idiopath => xilem}/src/view/text.rs (100%) rename {idiopath => xilem}/src/view/use_state.rs (100%) rename {idiopath => xilem}/src/view/vstack.rs (100%) rename {idiopath => xilem}/src/view_seq.rs (100%) rename {idiopath => xilem}/src/widget.rs (100%) rename {idiopath => xilem}/src/widget/align.rs (100%) rename {idiopath => xilem}/src/widget/button.rs (100%) rename {idiopath => xilem}/src/widget/contexts.rs (100%) rename {idiopath => xilem}/src/widget/core.rs (100%) rename {idiopath => xilem}/src/widget/layout_observer.rs (100%) rename {idiopath => xilem}/src/widget/text.rs (100%) rename {idiopath => xilem}/src/widget/vstack.rs (100%) diff --git a/Cargo.toml b/Cargo.toml index ee804ea7e7..031cf9043c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,7 @@ members = [ "druid/examples/web", "druid/examples/hello_web", "druid/examples/value_formatting", - "idiopath", + "xilem", ] default-members = [ "druid", diff --git a/idiopath/Cargo.toml b/xilem/Cargo.toml similarity index 81% rename from idiopath/Cargo.toml rename to xilem/Cargo.toml index f4c9bb2241..3714ffe304 100644 --- a/idiopath/Cargo.toml +++ b/xilem/Cargo.toml @@ -1,6 +1,7 @@ [package] -name = "idiopath" +name = "xilem" version = "0.1.0" +license = "Apache-2.0" authors = ["Raph Levien "] edition = "2021" diff --git a/idiopath/README.md b/xilem/README.md similarity index 95% rename from idiopath/README.md rename to xilem/README.md index a92be491df..1b1da84f94 100644 --- a/idiopath/README.md +++ b/xilem/README.md @@ -1,5 +1,7 @@ # An experimental Rust architecture for reactive UI +Note: this README is a bit out of date. To understand more of what's going on, please read the blog post, [Xilem: an architecture for UI in Rust]. + This repo contains an experimental architecture, implemented with a toy UI. At a very high level, it combines ideas from Flutter, SwiftUI, and Elm. Like all of these, it uses lightweight view objects, diffing them to provide minimal updates to a retained UI. Like SwiftUI, it is strongly typed. ## Overall program flow @@ -75,3 +77,4 @@ The type erasure of View nodes is not an easy trick, as the trait has two associ [Rc::make_mut]: https://doc.rust-lang.org/std/rc/struct.Rc.html#method.make_mut [AnyView]: https://developer.apple.com/documentation/swiftui/anyview [Panoramix]: https://github.com/PoignardAzur/panoramix +[Xilem: an architecture for UI in Rust]: https://raphlinus.github.io/rust/gui/2022/05/07/ui-architecture.html \ No newline at end of file diff --git a/idiopath/examples/counter.rs b/xilem/examples/counter.rs similarity index 94% rename from idiopath/examples/counter.rs rename to xilem/examples/counter.rs index 722463d68f..96ea46450f 100644 --- a/idiopath/examples/counter.rs +++ b/xilem/examples/counter.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use idiopath::{button, v_stack, Adapt, App, AppLauncher, LayoutObserver, Memoize, View}; +use xilem::{button, v_stack, Adapt, App, AppLauncher, LayoutObserver, Memoize, View}; #[derive(Default)] struct AppData { diff --git a/idiopath/src/app.rs b/xilem/src/app.rs similarity index 100% rename from idiopath/src/app.rs rename to xilem/src/app.rs diff --git a/idiopath/src/app_main.rs b/xilem/src/app_main.rs similarity index 100% rename from idiopath/src/app_main.rs rename to xilem/src/app_main.rs diff --git a/idiopath/src/event.rs b/xilem/src/event.rs similarity index 100% rename from idiopath/src/event.rs rename to xilem/src/event.rs diff --git a/idiopath/src/id.rs b/xilem/src/id.rs similarity index 100% rename from idiopath/src/id.rs rename to xilem/src/id.rs diff --git a/idiopath/src/lib.rs b/xilem/src/lib.rs similarity index 100% rename from idiopath/src/lib.rs rename to xilem/src/lib.rs diff --git a/idiopath/src/view.rs b/xilem/src/view.rs similarity index 100% rename from idiopath/src/view.rs rename to xilem/src/view.rs diff --git a/idiopath/src/view/adapt.rs b/xilem/src/view/adapt.rs similarity index 100% rename from idiopath/src/view/adapt.rs rename to xilem/src/view/adapt.rs diff --git a/idiopath/src/view/any_view.rs b/xilem/src/view/any_view.rs similarity index 100% rename from idiopath/src/view/any_view.rs rename to xilem/src/view/any_view.rs diff --git a/idiopath/src/view/button.rs b/xilem/src/view/button.rs similarity index 100% rename from idiopath/src/view/button.rs rename to xilem/src/view/button.rs diff --git a/idiopath/src/view/layout_observer.rs b/xilem/src/view/layout_observer.rs similarity index 100% rename from idiopath/src/view/layout_observer.rs rename to xilem/src/view/layout_observer.rs diff --git a/idiopath/src/view/memoize.rs b/xilem/src/view/memoize.rs similarity index 100% rename from idiopath/src/view/memoize.rs rename to xilem/src/view/memoize.rs diff --git a/idiopath/src/view/text.rs b/xilem/src/view/text.rs similarity index 100% rename from idiopath/src/view/text.rs rename to xilem/src/view/text.rs diff --git a/idiopath/src/view/use_state.rs b/xilem/src/view/use_state.rs similarity index 100% rename from idiopath/src/view/use_state.rs rename to xilem/src/view/use_state.rs diff --git a/idiopath/src/view/vstack.rs b/xilem/src/view/vstack.rs similarity index 100% rename from idiopath/src/view/vstack.rs rename to xilem/src/view/vstack.rs diff --git a/idiopath/src/view_seq.rs b/xilem/src/view_seq.rs similarity index 100% rename from idiopath/src/view_seq.rs rename to xilem/src/view_seq.rs diff --git a/idiopath/src/widget.rs b/xilem/src/widget.rs similarity index 100% rename from idiopath/src/widget.rs rename to xilem/src/widget.rs diff --git a/idiopath/src/widget/align.rs b/xilem/src/widget/align.rs similarity index 100% rename from idiopath/src/widget/align.rs rename to xilem/src/widget/align.rs diff --git a/idiopath/src/widget/button.rs b/xilem/src/widget/button.rs similarity index 100% rename from idiopath/src/widget/button.rs rename to xilem/src/widget/button.rs diff --git a/idiopath/src/widget/contexts.rs b/xilem/src/widget/contexts.rs similarity index 100% rename from idiopath/src/widget/contexts.rs rename to xilem/src/widget/contexts.rs diff --git a/idiopath/src/widget/core.rs b/xilem/src/widget/core.rs similarity index 100% rename from idiopath/src/widget/core.rs rename to xilem/src/widget/core.rs diff --git a/idiopath/src/widget/layout_observer.rs b/xilem/src/widget/layout_observer.rs similarity index 100% rename from idiopath/src/widget/layout_observer.rs rename to xilem/src/widget/layout_observer.rs diff --git a/idiopath/src/widget/text.rs b/xilem/src/widget/text.rs similarity index 100% rename from idiopath/src/widget/text.rs rename to xilem/src/widget/text.rs diff --git a/idiopath/src/widget/vstack.rs b/xilem/src/widget/vstack.rs similarity index 100% rename from idiopath/src/widget/vstack.rs rename to xilem/src/widget/vstack.rs From c4171f51e83978eb7061796c38f0964225bba280 Mon Sep 17 00:00:00 2001 From: Raph Levien Date: Wed, 18 May 2022 17:56:52 -0700 Subject: [PATCH 26/28] Start building event propagation Import some of the event dispatching logic from Druid. Still fairly minimal, not yet doing real hot/active logic on buttons yet (doing it the same way as Druid would depend on adding lifecycle to the Widget trait). --- xilem/src/app.rs | 8 +-- xilem/src/app_main.rs | 23 +++++-- xilem/src/widget.rs | 9 +-- xilem/src/widget/button.rs | 9 ++- xilem/src/widget/core.rs | 109 ++++++++++++++++++++++++++++++++-- xilem/src/widget/raw_event.rs | 61 +++++++++++++++++++ xilem/src/widget/vstack.rs | 17 ++---- 7 files changed, 198 insertions(+), 38 deletions(-) create mode 100644 xilem/src/widget/raw_event.rs diff --git a/xilem/src/app.rs b/xilem/src/app.rs index 98521dcd9d..0d34c3d03e 100644 --- a/xilem/src/app.rs +++ b/xilem/src/app.rs @@ -14,7 +14,7 @@ use druid_shell::kurbo::Size; use druid_shell::piet::{Color, Piet, RenderContext}; -use druid_shell::{kurbo::Point, WindowHandle}; +use druid_shell::WindowHandle; use crate::widget::{CxState, EventCx, LayoutCx, PaintCx, Pod, UpdateCx, WidgetState}; use crate::{ @@ -109,11 +109,7 @@ where } } - pub fn mouse_down(&mut self, point: Point) { - self.event(RawEvent::MouseDown(point)); - } - - fn event(&mut self, event: RawEvent) { + pub fn window_event(&mut self, event: RawEvent) { self.ensure_app(); let root_pod = self.root_pod.as_mut().unwrap(); let mut cx_state = CxState::new(&self.window_handle, &mut self.events); diff --git a/xilem/src/app_main.rs b/xilem/src/app_main.rs index 250e280054..a60ed0389e 100644 --- a/xilem/src/app_main.rs +++ b/xilem/src/app_main.rs @@ -19,7 +19,7 @@ use druid_shell::{ WindowBuilder, WindowHandle, }; -use crate::{app::App, View, Widget}; +use crate::{app::App, widget::RawEvent, View, Widget}; // This is a bit of a hack just to get a window launched. The real version // would deal with multiple windows and have other ways to configure things. @@ -28,6 +28,7 @@ pub struct AppLauncher, F: FnMut(&mut T) -> V> { app: App, } +// The logic of this struct is mostly parallel to DruidHandler in win_handler.rs. struct MainState, F: FnMut(&mut T) -> V> where V::Element: Widget, @@ -101,16 +102,26 @@ where } } - fn mouse_move(&mut self, _event: &MouseEvent) { - self.handle.set_cursor(&Cursor::Arrow); + fn mouse_down(&mut self, event: &MouseEvent) { + self.app.window_event(RawEvent::MouseDown(event.into())); + self.handle.invalidate(); } - fn mouse_down(&mut self, event: &MouseEvent) { - self.app.mouse_down(event.pos); + fn mouse_up(&mut self, event: &MouseEvent) { + self.app.window_event(RawEvent::MouseUp(event.into())); + self.handle.invalidate(); + } + + fn mouse_move(&mut self, event: &MouseEvent) { + self.app.window_event(RawEvent::MouseMove(event.into())); self.handle.invalidate(); + self.handle.set_cursor(&Cursor::Arrow); } - fn mouse_up(&mut self, _event: &MouseEvent) {} + fn wheel(&mut self, event: &MouseEvent) { + self.app.window_event(RawEvent::MouseWheel(event.into())); + self.handle.invalidate(); + } fn size(&mut self, size: Size) { self.app.size(size); diff --git a/xilem/src/widget.rs b/xilem/src/widget.rs index 2388e6d226..bd19d48de4 100644 --- a/xilem/src/widget.rs +++ b/xilem/src/widget.rs @@ -17,17 +17,19 @@ pub mod button; mod contexts; mod core; pub mod layout_observer; +mod raw_event; pub mod text; pub mod vstack; use std::any::Any; use std::ops::DerefMut; -use druid_shell::kurbo::{Point, Size}; +use druid_shell::kurbo::Size; pub use self::contexts::{AlignCx, CxState, EventCx, LayoutCx, PaintCx, UpdateCx}; pub use self::core::Pod; pub(crate) use self::core::{PodFlags, WidgetState}; +pub use self::raw_event::RawEvent; use self::align::SingleAlignment; @@ -96,11 +98,6 @@ impl Widget for Box { } } -#[derive(Debug)] -pub enum RawEvent { - MouseDown(Point), -} - pub trait WidgetTuple { fn length(&self) -> usize; diff --git a/xilem/src/widget/button.rs b/xilem/src/widget/button.rs index 3b381f5736..ab5b00fd19 100644 --- a/xilem/src/widget/button.rs +++ b/xilem/src/widget/button.rs @@ -19,7 +19,7 @@ use druid_shell::{ use crate::{event::Event, id::IdPath}; -use super::{EventCx, LayoutCx, PaintCx, UpdateCx, Widget}; +use super::{EventCx, LayoutCx, PaintCx, RawEvent, UpdateCx, Widget}; #[derive(Default)] @@ -48,8 +48,11 @@ impl Widget for Button { // TODO: probably want to request layout when string changes } - fn event(&mut self, cx: &mut EventCx, _event: &super::RawEvent) { - cx.add_event(Event::new(self.id_path.clone(), ())); + fn event(&mut self, cx: &mut EventCx, event: &RawEvent) { + match event { + RawEvent::MouseDown(_) => cx.add_event(Event::new(self.id_path.clone(), ())), + _ => (), + }; } fn prelayout(&mut self, _cx: &mut LayoutCx) -> (Size, Size) { diff --git a/xilem/src/widget/core.rs b/xilem/src/widget/core.rs index e80436841f..55ac448d2c 100644 --- a/xilem/src/widget/core.rs +++ b/xilem/src/widget/core.rs @@ -19,7 +19,7 @@ use bitflags::bitflags; use druid_shell::{ - kurbo::{Affine, Point, Size}, + kurbo::{Affine, Point, Rect, Size}, piet::RenderContext, }; @@ -30,7 +30,7 @@ use super::{ AlignResult, AlignmentAxis, Bottom, Center, HorizAlignment, Leading, SingleAlignment, Top, Trailing, VertAlignment, }, - AlignCx, AnyWidget, EventCx, LayoutCx, PaintCx, RawEvent, UpdateCx, + AlignCx, AnyWidget, CxState, EventCx, LayoutCx, PaintCx, RawEvent, UpdateCx, }; bitflags! { @@ -40,7 +40,10 @@ bitflags! { const REQUEST_LAYOUT = 2; const REQUEST_PAINT = 4; - const UPWARD_FLAGS = Self::REQUEST_LAYOUT.bits | Self::REQUEST_PAINT.bits; + const IS_HOT = 8; + const HAS_ACTIVE = 16; + + const UPWARD_FLAGS = Self::REQUEST_LAYOUT.bits | Self::REQUEST_PAINT.bits | Self::HAS_ACTIVE.bits; const INIT_FLAGS = Self::REQUEST_UPDATE.bits | Self::REQUEST_LAYOUT.bits | Self::REQUEST_PAINT.bits; } } @@ -125,8 +128,86 @@ impl Pod { self.state.request(PodFlags::REQUEST_UPDATE); } + /// Propagate a platform event. As in Druid, a great deal of the event + /// dispatching logic is in this function. pub fn event(&mut self, cx: &mut EventCx, event: &RawEvent) { - self.widget.event(cx, event); + let rect = Rect::from_origin_size(self.state.origin, self.state.size); + let mut modified_event = None; + let had_active = self.state.flags.contains(PodFlags::HAS_ACTIVE); + let recurse = match event { + RawEvent::MouseDown(mouse_event) => { + Pod::set_hot_state( + &mut self.widget, + &mut self.state, + cx.cx_state, + rect, + Some(mouse_event.pos), + ); + if had_active || self.state.flags.contains(PodFlags::IS_HOT) { + let mut mouse_event = mouse_event.clone(); + mouse_event.pos -= self.state.origin.to_vec2(); + modified_event = Some(RawEvent::MouseDown(mouse_event)); + true + } else { + false + } + } + RawEvent::MouseUp(mouse_event) => { + Pod::set_hot_state( + &mut self.widget, + &mut self.state, + cx.cx_state, + rect, + Some(mouse_event.pos), + ); + if had_active || self.state.flags.contains(PodFlags::IS_HOT) { + let mut mouse_event = mouse_event.clone(); + mouse_event.pos -= self.state.origin.to_vec2(); + modified_event = Some(RawEvent::MouseUp(mouse_event)); + true + } else { + false + } + } + RawEvent::MouseMove(mouse_event) => { + let hot_changed = Pod::set_hot_state( + &mut self.widget, + &mut self.state, + cx.cx_state, + rect, + Some(mouse_event.pos), + ); + if had_active || self.state.flags.contains(PodFlags::IS_HOT) || hot_changed { + let mut mouse_event = mouse_event.clone(); + mouse_event.pos -= self.state.origin.to_vec2(); + modified_event = Some(RawEvent::MouseUp(mouse_event)); + true + } else { + false + } + } + RawEvent::MouseWheel(mouse_event) => { + Pod::set_hot_state( + &mut self.widget, + &mut self.state, + cx.cx_state, + rect, + Some(mouse_event.pos), + ); + if had_active || self.state.flags.contains(PodFlags::IS_HOT) { + let mut mouse_event = mouse_event.clone(); + mouse_event.pos -= self.state.origin.to_vec2(); + modified_event = Some(RawEvent::MouseWheel(mouse_event)); + true + } else { + false + } + } + }; + if recurse { + self.widget + .event(cx, modified_event.as_ref().unwrap_or(event)); + } } /// Propagate an update cycle. @@ -202,4 +283,24 @@ impl Pod { pub fn get_alignment(&self, alignment: SingleAlignment) -> f64 { self.state.get_alignment(&self.widget, alignment) } + + // Return true if hot state has changed + fn set_hot_state( + _widget: &mut dyn AnyWidget, + state: &mut WidgetState, + _cx_state: &mut CxState, + rect: Rect, + mouse_pos: Option, + ) -> bool { + let had_hot = state.flags.contains(PodFlags::IS_HOT); + state.flags.set( + PodFlags::IS_HOT, + match mouse_pos { + Some(pos) => rect.contains(pos), + None => false, + }, + ); + had_hot != state.flags.contains(PodFlags::IS_HOT) + // TODO: propagate hot changed (is a lifecycle method in Druid) + } } diff --git a/xilem/src/widget/raw_event.rs b/xilem/src/widget/raw_event.rs new file mode 100644 index 0000000000..0be7fb84b8 --- /dev/null +++ b/xilem/src/widget/raw_event.rs @@ -0,0 +1,61 @@ +use druid_shell::{kurbo::{Point, Vec2}, Modifiers, MouseButtons, MouseButton}; + +// Copyright 2022 The Druid Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#[derive(Debug)] +pub enum RawEvent { + MouseDown(MouseEvent), + MouseUp(MouseEvent), + MouseMove(MouseEvent), + MouseWheel(MouseEvent), +} + +#[derive(Debug, Clone)] +pub struct MouseEvent { + /// The position of the mouse in the coordinate space of the receiver. + pub pos: Point, + /// The position of the mose in the window coordinate space. + pub window_pos: Point, + pub buttons: MouseButtons, + pub mods: Modifiers, + pub count: u8, + pub focus: bool, + pub button: MouseButton, + pub wheel_delta: Vec2, +} + +impl<'a> From<&'a druid_shell::MouseEvent> for MouseEvent { + fn from(src: &druid_shell::MouseEvent) -> MouseEvent { + let druid_shell::MouseEvent { + pos, + buttons, + mods, + count, + focus, + button, + wheel_delta, + } = src; + MouseEvent { + pos: *pos, + window_pos: *pos, + buttons: *buttons, + mods: *mods, + count: *count, + focus: *focus, + button: *button, + wheel_delta: *wheel_delta, + } + } +} diff --git a/xilem/src/widget/vstack.rs b/xilem/src/widget/vstack.rs index 82dd089b9f..910a1099d2 100644 --- a/xilem/src/widget/vstack.rs +++ b/xilem/src/widget/vstack.rs @@ -12,11 +12,11 @@ // See the License for the specific language governing permissions and // limitations under the License. -use druid_shell::kurbo::{Point, Rect, Size}; +use druid_shell::kurbo::{Point, Size}; use super::{ align::{Center, SingleAlignment}, - EventCx, LayoutCx, PaintCx, Pod, RawEvent, UpdateCx, Widget, + EventCx, LayoutCx, PaintCx, Pod, UpdateCx, Widget, }; pub struct VStack { @@ -43,17 +43,8 @@ impl VStack { impl Widget for VStack { fn event(&mut self, cx: &mut EventCx, event: &super::RawEvent) { - match event { - RawEvent::MouseDown(p) => { - for child in &mut self.children { - let rect = Rect::from_origin_size(child.state.origin, child.state.size); - if rect.contains(*p) { - let child_event = RawEvent::MouseDown(*p - child.state.origin.to_vec2()); - child.event(cx, &child_event); - break; - } - } - } + for child in &mut self.children { + child.event(cx, event); } } From 19aa2e6d4ea5fa969fdbba1d4df9eb3dcb850697 Mon Sep 17 00:00:00 2001 From: Raph Levien Date: Thu, 19 May 2022 19:23:56 -0700 Subject: [PATCH 27/28] Add lifecycle method Add the lifecycle method to the Widget trait, matching existing Druid. I'm not at all convinced it's necessary - in original Druid, one of the main differences is that it doesn't have mutable access to app state, but that concern isn't really relevant. In any case, this minimizes divergence, and can possibly be cleaned up later. --- xilem/src/widget.rs | 13 +++++++- xilem/src/widget/align.rs | 6 +++- xilem/src/widget/button.rs | 10 +++++- xilem/src/widget/contexts.rs | 18 +++++++++++ xilem/src/widget/core.rs | 48 ++++++++++++++++++++--------- xilem/src/widget/layout_observer.rs | 10 ++++-- xilem/src/widget/raw_event.rs | 9 +++++- xilem/src/widget/text.rs | 7 +++-- xilem/src/widget/vstack.rs | 11 +++++-- 9 files changed, 108 insertions(+), 24 deletions(-) diff --git a/xilem/src/widget.rs b/xilem/src/widget.rs index bd19d48de4..f62a496043 100644 --- a/xilem/src/widget.rs +++ b/xilem/src/widget.rs @@ -26,10 +26,11 @@ use std::ops::DerefMut; use druid_shell::kurbo::Size; +use self::contexts::LifeCycleCx; pub use self::contexts::{AlignCx, CxState, EventCx, LayoutCx, PaintCx, UpdateCx}; pub use self::core::Pod; pub(crate) use self::core::{PodFlags, WidgetState}; -pub use self::raw_event::RawEvent; +pub use self::raw_event::{LifeCycle, RawEvent}; use self::align::SingleAlignment; @@ -37,6 +38,12 @@ use self::align::SingleAlignment; pub trait Widget { fn event(&mut self, cx: &mut EventCx, event: &RawEvent); + /// Propagate a lifecycle event. + /// + /// I am not convinced this needs to be distinct from `event`. For the + /// moment, we're following existing Druid. + fn lifecycle(&mut self, cx: &mut LifeCycleCx, event: &LifeCycle); + fn update(&mut self, cx: &mut UpdateCx); /// Compute intrinsic sizes. @@ -81,6 +88,10 @@ impl Widget for Box { self.deref_mut().event(cx, event); } + fn lifecycle(&mut self, cx: &mut LifeCycleCx, event: &LifeCycle) { + self.deref_mut().lifecycle(cx, event); + } + fn update(&mut self, cx: &mut UpdateCx) { self.deref_mut().update(cx); } diff --git a/xilem/src/widget/align.rs b/xilem/src/widget/align.rs index eafbed6ffe..50fc78040f 100644 --- a/xilem/src/widget/align.rs +++ b/xilem/src/widget/align.rs @@ -14,7 +14,7 @@ use druid_shell::kurbo::Point; -use super::{AlignCx, AnyWidget, EventCx, Widget, WidgetState}; +use super::{contexts::LifeCycleCx, AlignCx, AnyWidget, EventCx, LifeCycle, Widget, WidgetState}; #[derive(Clone, Copy, PartialEq)] pub enum AlignmentMerge { @@ -245,6 +245,10 @@ impl f64 + 'static> Widget for AlignmentGuide { self.child.event(cx, event); } + fn lifecycle(&mut self, cx: &mut LifeCycleCx, event: &LifeCycle) { + self.child.lifecycle(cx, event); + } + fn update(&mut self, cx: &mut super::UpdateCx) { self.child.update(cx); } diff --git a/xilem/src/widget/button.rs b/xilem/src/widget/button.rs index ab5b00fd19..aa98c2808e 100644 --- a/xilem/src/widget/button.rs +++ b/xilem/src/widget/button.rs @@ -19,7 +19,9 @@ use druid_shell::{ use crate::{event::Event, id::IdPath}; -use super::{EventCx, LayoutCx, PaintCx, RawEvent, UpdateCx, Widget}; +use super::{ + contexts::LifeCycleCx, EventCx, LayoutCx, LifeCycle, PaintCx, RawEvent, UpdateCx, Widget, +}; #[derive(Default)] @@ -55,6 +57,12 @@ impl Widget for Button { }; } + fn lifecycle(&mut self, cx: &mut LifeCycleCx, event: &LifeCycle) { + match event { + LifeCycle::HotChanged(_) => cx.request_paint(), + } + } + fn prelayout(&mut self, _cx: &mut LayoutCx) -> (Size, Size) { // TODO: do text layout here. (FIXED_SIZE, FIXED_SIZE) diff --git a/xilem/src/widget/contexts.rs b/xilem/src/widget/contexts.rs index 5cd239b86e..5d361347dd 100644 --- a/xilem/src/widget/contexts.rs +++ b/xilem/src/widget/contexts.rs @@ -44,6 +44,11 @@ pub struct EventCx<'a, 'b> { pub(crate) widget_state: &'a mut WidgetState, } +pub struct LifeCycleCx<'a, 'b> { + pub(crate) cx_state: &'a mut CxState<'b>, + pub(crate) widget_state: &'a mut WidgetState, +} + pub struct UpdateCx<'a, 'b> { pub(crate) cx_state: &'a mut CxState<'b>, pub(crate) widget_state: &'a mut WidgetState, @@ -92,6 +97,19 @@ impl<'a, 'b> EventCx<'a, 'b> { } } +impl<'a, 'b> LifeCycleCx<'a, 'b> { + pub(crate) fn new(cx_state: &'a mut CxState<'b>, root_state: &'a mut WidgetState) -> Self { + LifeCycleCx { + cx_state, + widget_state: root_state, + } + } + + pub fn request_paint(&mut self) { + self.widget_state.flags |= PodFlags::REQUEST_PAINT; + } +} + impl<'a, 'b> UpdateCx<'a, 'b> { pub(crate) fn new(cx_state: &'a mut CxState<'b>, root_state: &'a mut WidgetState) -> Self { UpdateCx { diff --git a/xilem/src/widget/core.rs b/xilem/src/widget/core.rs index 55ac448d2c..ff362701f0 100644 --- a/xilem/src/widget/core.rs +++ b/xilem/src/widget/core.rs @@ -30,7 +30,8 @@ use super::{ AlignResult, AlignmentAxis, Bottom, Center, HorizAlignment, Leading, SingleAlignment, Top, Trailing, VertAlignment, }, - AlignCx, AnyWidget, CxState, EventCx, LayoutCx, PaintCx, RawEvent, UpdateCx, + contexts::LifeCycleCx, + AlignCx, AnyWidget, CxState, EventCx, LayoutCx, LifeCycle, PaintCx, RawEvent, UpdateCx, }; bitflags! { @@ -210,6 +211,19 @@ impl Pod { } } + pub fn lifecycle(&mut self, cx: &mut LifeCycleCx, event: &LifeCycle) { + let recurse = match event { + LifeCycle::HotChanged(_) => false, + }; + let mut child_cx = LifeCycleCx { + cx_state: cx.cx_state, + widget_state: &mut self.state, + }; + if recurse { + self.widget.lifecycle(&mut child_cx, event); + } + } + /// Propagate an update cycle. pub fn update(&mut self, cx: &mut UpdateCx) { if self.state.flags.contains(PodFlags::REQUEST_UPDATE) { @@ -286,21 +300,27 @@ impl Pod { // Return true if hot state has changed fn set_hot_state( - _widget: &mut dyn AnyWidget, - state: &mut WidgetState, - _cx_state: &mut CxState, + widget: &mut dyn AnyWidget, + widget_state: &mut WidgetState, + cx_state: &mut CxState, rect: Rect, mouse_pos: Option, ) -> bool { - let had_hot = state.flags.contains(PodFlags::IS_HOT); - state.flags.set( - PodFlags::IS_HOT, - match mouse_pos { - Some(pos) => rect.contains(pos), - None => false, - }, - ); - had_hot != state.flags.contains(PodFlags::IS_HOT) - // TODO: propagate hot changed (is a lifecycle method in Druid) + let had_hot = widget_state.flags.contains(PodFlags::IS_HOT); + let is_hot = match mouse_pos { + Some(pos) => rect.contains(pos), + None => false, + }; + widget_state.flags.set(PodFlags::IS_HOT, is_hot); + if had_hot != is_hot { + let hot_changed_event = LifeCycle::HotChanged(is_hot); + let mut child_cx = LifeCycleCx { + cx_state, + widget_state, + }; + widget.lifecycle(&mut child_cx, &hot_changed_event); + return true; + } + false } } diff --git a/xilem/src/widget/layout_observer.rs b/xilem/src/widget/layout_observer.rs index ecbabf3b5c..e78f0e6895 100644 --- a/xilem/src/widget/layout_observer.rs +++ b/xilem/src/widget/layout_observer.rs @@ -21,8 +21,8 @@ use druid_shell::kurbo::Size; use crate::{event::Event, id::IdPath}; use super::{ - align::SingleAlignment, AlignCx, AnyWidget, EventCx, LayoutCx, PaintCx, Pod, RawEvent, - UpdateCx, Widget, + align::SingleAlignment, contexts::LifeCycleCx, AlignCx, AnyWidget, EventCx, LayoutCx, + LifeCycle, PaintCx, Pod, RawEvent, UpdateCx, Widget, }; pub struct LayoutObserver { @@ -64,6 +64,12 @@ impl Widget for LayoutObserver { } } + fn lifecycle(&mut self, cx: &mut LifeCycleCx, event: &LifeCycle) { + if let Some(child) = &mut self.child { + child.lifecycle(cx, event); + } + } + fn prelayout(&mut self, cx: &mut LayoutCx) -> (Size, Size) { if let Some(child) = &mut self.child { let _ = child.prelayout(cx); diff --git a/xilem/src/widget/raw_event.rs b/xilem/src/widget/raw_event.rs index 0be7fb84b8..e5e68525f1 100644 --- a/xilem/src/widget/raw_event.rs +++ b/xilem/src/widget/raw_event.rs @@ -1,4 +1,7 @@ -use druid_shell::{kurbo::{Point, Vec2}, Modifiers, MouseButtons, MouseButton}; +use druid_shell::{ + kurbo::{Point, Vec2}, + Modifiers, MouseButton, MouseButtons, +}; // Copyright 2022 The Druid Authors. // @@ -36,6 +39,10 @@ pub struct MouseEvent { pub wheel_delta: Vec2, } +pub enum LifeCycle { + HotChanged(bool), +} + impl<'a> From<&'a druid_shell::MouseEvent> for MouseEvent { fn from(src: &druid_shell::MouseEvent) -> MouseEvent { let druid_shell::MouseEvent { diff --git a/xilem/src/widget/text.rs b/xilem/src/widget/text.rs index df93962ff2..9e685d5843 100644 --- a/xilem/src/widget/text.rs +++ b/xilem/src/widget/text.rs @@ -19,7 +19,8 @@ use druid_shell::{ use super::{ align::{FirstBaseline, LastBaseline, SingleAlignment, VertAlignment}, - AlignCx, EventCx, LayoutCx, PaintCx, UpdateCx, Widget, + contexts::LifeCycleCx, + AlignCx, EventCx, LayoutCx, LifeCycle, PaintCx, RawEvent, UpdateCx, Widget, }; pub struct TextWidget { @@ -46,7 +47,9 @@ impl TextWidget { } impl Widget for TextWidget { - fn event(&mut self, _cx: &mut EventCx, _event: &super::RawEvent) {} + fn event(&mut self, _cx: &mut EventCx, _event: &RawEvent) {} + + fn lifecycle(&mut self, _cx: &mut LifeCycleCx, _event: &LifeCycle) {} fn update(&mut self, cx: &mut UpdateCx) { // All changes potentially require layout. Note: we could be finer diff --git a/xilem/src/widget/vstack.rs b/xilem/src/widget/vstack.rs index 910a1099d2..3407fb7f89 100644 --- a/xilem/src/widget/vstack.rs +++ b/xilem/src/widget/vstack.rs @@ -16,7 +16,8 @@ use druid_shell::kurbo::{Point, Size}; use super::{ align::{Center, SingleAlignment}, - EventCx, LayoutCx, PaintCx, Pod, UpdateCx, Widget, + contexts::LifeCycleCx, + EventCx, LayoutCx, LifeCycle, PaintCx, Pod, RawEvent, UpdateCx, Widget, }; pub struct VStack { @@ -42,12 +43,18 @@ impl VStack { } impl Widget for VStack { - fn event(&mut self, cx: &mut EventCx, event: &super::RawEvent) { + fn event(&mut self, cx: &mut EventCx, event: &RawEvent) { for child in &mut self.children { child.event(cx, event); } } + fn lifecycle(&mut self, cx: &mut LifeCycleCx, event: &LifeCycle) { + for child in &mut self.children { + child.lifecycle(cx, event); + } + } + fn update(&mut self, cx: &mut UpdateCx) { for child in &mut self.children { child.update(cx); From a6fb0b873fb53a2da08c367ff6ea9219036cbf1e Mon Sep 17 00:00:00 2001 From: Raph Levien Date: Fri, 20 May 2022 10:55:09 -0700 Subject: [PATCH 28/28] Basic button functionality This commit implements the core functionality of button, including hot/active states, drawing of appearance, and reasonable layout. There are other changes in widget infrastructure to support this functionality. --- xilem/src/app.rs | 2 +- xilem/src/widget.rs | 6 +- xilem/src/widget/button.rs | 116 +++++++++++++++++++++++++++------- xilem/src/widget/contexts.rs | 57 ++++++++++++++--- xilem/src/widget/core.rs | 24 +++++-- xilem/src/widget/raw_event.rs | 1 + 6 files changed, 166 insertions(+), 40 deletions(-) diff --git a/xilem/src/app.rs b/xilem/src/app.rs index 0d34c3d03e..fba98eff87 100644 --- a/xilem/src/app.rs +++ b/xilem/src/app.rs @@ -102,7 +102,7 @@ where // becomes extreme. self.run_app_logic(); } else { - let mut paint_cx = PaintCx::new(&mut cx_state, piet); + let mut paint_cx = PaintCx::new(&mut cx_state, &mut self.root_state, piet); root_pod.paint(&mut paint_cx); break; } diff --git a/xilem/src/widget.rs b/xilem/src/widget.rs index f62a496043..c1d7b9f70b 100644 --- a/xilem/src/widget.rs +++ b/xilem/src/widget.rs @@ -22,7 +22,7 @@ pub mod text; pub mod vstack; use std::any::Any; -use std::ops::DerefMut; +use std::ops::{Deref, DerefMut}; use druid_shell::kurbo::Size; @@ -104,6 +104,10 @@ impl Widget for Box { self.deref_mut().layout(cx, proposed_size) } + fn align(&self, cx: &mut AlignCx, alignment: SingleAlignment) { + self.deref().align(cx, alignment); + } + fn paint(&mut self, cx: &mut PaintCx) { self.deref_mut().paint(cx); } diff --git a/xilem/src/widget/button.rs b/xilem/src/widget/button.rs index aa98c2808e..59d7de03c5 100644 --- a/xilem/src/widget/button.rs +++ b/xilem/src/widget/button.rs @@ -13,21 +13,25 @@ // limitations under the License. use druid_shell::{ - kurbo::{Point, Size}, - piet::{Color, RenderContext, Text, TextLayoutBuilder}, + kurbo::{Insets, Size}, + piet::{ + Color, LinearGradient, PietTextLayout, RenderContext, Text, TextLayout, TextLayoutBuilder, + UnitPoint, + }, }; -use crate::{event::Event, id::IdPath}; +use crate::{event::Event, id::IdPath, VertAlignment}; use super::{ - contexts::LifeCycleCx, EventCx, LayoutCx, LifeCycle, PaintCx, RawEvent, UpdateCx, Widget, + align::{FirstBaseline, LastBaseline, SingleAlignment}, + contexts::LifeCycleCx, + AlignCx, EventCx, LayoutCx, LifeCycle, PaintCx, RawEvent, UpdateCx, Widget, }; -#[derive(Default)] - pub struct Button { id_path: IdPath, label: String, + layout: Option, } impl Button { @@ -35,24 +39,37 @@ impl Button { Button { id_path: id_path.clone(), label, + layout: None, } } pub fn set_label(&mut self, label: String) { self.label = label; + self.layout = None; } } -const FIXED_SIZE: Size = Size::new(100., 20.); +// See druid's button for info. +const LABEL_INSETS: Insets = Insets::uniform_xy(8., 2.); impl Widget for Button { - fn update(&mut self, _cx: &mut UpdateCx) { - // TODO: probably want to request layout when string changes + fn update(&mut self, cx: &mut UpdateCx) { + cx.request_layout(); } fn event(&mut self, cx: &mut EventCx, event: &RawEvent) { match event { - RawEvent::MouseDown(_) => cx.add_event(Event::new(self.id_path.clone(), ())), + RawEvent::MouseDown(_) => { + cx.set_active(true); + // TODO: request paint + } + RawEvent::MouseUp(_) => { + if cx.is_hot() { + cx.add_event(Event::new(self.id_path.clone(), ())); + } + cx.set_active(false); + // TODO: request paint + } _ => (), }; } @@ -60,27 +77,78 @@ impl Widget for Button { fn lifecycle(&mut self, cx: &mut LifeCycleCx, event: &LifeCycle) { match event { LifeCycle::HotChanged(_) => cx.request_paint(), + _ => (), } } - fn prelayout(&mut self, _cx: &mut LayoutCx) -> (Size, Size) { - // TODO: do text layout here. - (FIXED_SIZE, FIXED_SIZE) + fn prelayout(&mut self, cx: &mut LayoutCx) -> (Size, Size) { + let padding = Size::new(LABEL_INSETS.x_value(), LABEL_INSETS.y_value()); + let min_height = 24.0; + let layout = cx + .text() + .new_text_layout(self.label.clone()) + .text_color(Color::rgb8(0xf0, 0xf0, 0xea)) + .build() + .unwrap(); + let size = Size::new( + layout.size().width + padding.width, + (layout.size().height + padding.height).max(min_height), + ); + self.layout = Some(layout); + (Size::new(10.0, min_height), size) } - fn layout(&mut self, _cx: &mut LayoutCx, _proposed_size: Size) -> Size { - FIXED_SIZE + fn layout(&mut self, cx: &mut LayoutCx, proposed_size: Size) -> Size { + let size = Size::new( + proposed_size + .width + .clamp(cx.min_size().width, cx.max_size().width), + cx.max_size().height, + ); + size } - // TODO: alignment + fn align(&self, cx: &mut AlignCx, alignment: SingleAlignment) { + if alignment.id() == FirstBaseline.id() || alignment.id() == LastBaseline.id() { + let layout = self.layout.as_ref().unwrap(); + if let Some(metric) = layout.line_metric(0) { + let value = 0.5 * (cx.size().height - layout.size().height) + metric.baseline; + cx.aggregate(alignment, value); + } + } + } - fn paint(&mut self, ctx: &mut PaintCx) { - let layout = ctx - .text() - .new_text_layout(self.label.clone()) - .text_color(Color::WHITE) - .build() - .unwrap(); - ctx.draw_text(&layout, Point::ZERO); + fn paint(&mut self, cx: &mut PaintCx) { + let is_hot = cx.is_hot(); + let is_active = cx.is_active(); + let button_border_width = 2.0; + let rounded_rect = cx + .size() + .to_rect() + .inset(-0.5 * button_border_width) + .to_rounded_rect(4.0); + let border_color = if is_hot { + Color::rgb8(0xa1, 0xa1, 0xa1) + } else { + Color::rgb8(0x3a, 0x3a, 0x3a) + }; + let bg_gradient = if is_active { + LinearGradient::new( + UnitPoint::TOP, + UnitPoint::BOTTOM, + (Color::rgb8(0x3a, 0x3a, 0x3a), Color::rgb8(0xa1, 0xa1, 0xa1)), + ) + } else { + LinearGradient::new( + UnitPoint::TOP, + UnitPoint::BOTTOM, + (Color::rgb8(0xa1, 0xa1, 0xa1), Color::rgb8(0x3a, 0x3a, 0x3a)), + ) + }; + cx.stroke(rounded_rect, &border_color, button_border_width); + cx.fill(rounded_rect, &bg_gradient); + let layout = self.layout.as_ref().unwrap(); + let offset = (cx.size().to_vec2() - layout.size().to_vec2()) * 0.5; + cx.draw_text(layout, offset.to_point()); } } diff --git a/xilem/src/widget/contexts.rs b/xilem/src/widget/contexts.rs index 5d361347dd..1abae68489 100644 --- a/xilem/src/widget/contexts.rs +++ b/xilem/src/widget/contexts.rs @@ -20,7 +20,7 @@ use std::ops::{Deref, DerefMut}; use druid_shell::{ - kurbo::Point, + kurbo::{Point, Size}, piet::{Piet, PietText, RenderContext}, WindowHandle, }; @@ -67,6 +67,7 @@ pub struct AlignCx<'a> { pub struct PaintCx<'a, 'b, 'c> { pub(crate) cx_state: &'a mut CxState<'b>, + pub(crate) widget_state: &'a WidgetState, pub(crate) piet: &'a mut Piet<'c>, } @@ -95,16 +96,17 @@ impl<'a, 'b> EventCx<'a, 'b> { pub fn add_event(&mut self, event: Event) { self.cx_state.events.push(event); } -} -impl<'a, 'b> LifeCycleCx<'a, 'b> { - pub(crate) fn new(cx_state: &'a mut CxState<'b>, root_state: &'a mut WidgetState) -> Self { - LifeCycleCx { - cx_state, - widget_state: root_state, - } + pub fn set_active(&mut self, is_active: bool) { + self.widget_state.flags.set(PodFlags::IS_ACTIVE, is_active); } + pub fn is_hot(&self) -> bool { + self.widget_state.flags.contains(PodFlags::IS_HOT) + } +} + +impl<'a, 'b> LifeCycleCx<'a, 'b> { pub fn request_paint(&mut self) { self.widget_state.flags |= PodFlags::REQUEST_PAINT; } @@ -138,6 +140,17 @@ impl<'a, 'b> LayoutCx<'a, 'b> { pub fn add_event(&mut self, event: Event) { self.cx_state.events.push(event); } + + /// Access to minimum intrinsic size. + /// + /// Note: this shouldn't be called from prelayout. + pub fn min_size(&self) -> Size { + self.widget_state.min_size + } + + pub fn max_size(&self) -> Size { + self.widget_state.max_size + } } impl<'a> AlignCx<'a> { @@ -148,11 +161,23 @@ impl<'a> AlignCx<'a> { }; self.align_result.aggregate(alignment, value + origin_value); } + + pub fn size(&self) -> Size { + self.widget_state.size + } } impl<'a, 'b, 'c> PaintCx<'a, 'b, 'c> { - pub fn new(cx_state: &'a mut CxState<'b>, piet: &'a mut Piet<'c>) -> Self { - PaintCx { cx_state, piet } + pub(crate) fn new( + cx_state: &'a mut CxState<'b>, + widget_state: &'a mut WidgetState, + piet: &'a mut Piet<'c>, + ) -> Self { + PaintCx { + cx_state, + widget_state, + piet, + } } pub fn with_save(&mut self, f: impl FnOnce(&mut PaintCx)) { @@ -160,6 +185,18 @@ impl<'a, 'b, 'c> PaintCx<'a, 'b, 'c> { f(self); self.piet.restore().unwrap(); } + + pub fn is_hot(&self) -> bool { + self.widget_state.flags.contains(PodFlags::IS_HOT) + } + + pub fn is_active(&self) -> bool { + self.widget_state.flags.contains(PodFlags::IS_ACTIVE) + } + + pub fn size(&self) -> Size { + self.widget_state.size + } } impl<'c> Deref for PaintCx<'_, '_, 'c> { diff --git a/xilem/src/widget/core.rs b/xilem/src/widget/core.rs index ff362701f0..8ae409b0c7 100644 --- a/xilem/src/widget/core.rs +++ b/xilem/src/widget/core.rs @@ -42,7 +42,8 @@ bitflags! { const REQUEST_PAINT = 4; const IS_HOT = 8; - const HAS_ACTIVE = 16; + const IS_ACTIVE = 16; + const HAS_ACTIVE = 32; const UPWARD_FLAGS = Self::REQUEST_LAYOUT.bits | Self::REQUEST_PAINT.bits | Self::HAS_ACTIVE.bits; const INIT_FLAGS = Self::REQUEST_UPDATE.bits | Self::REQUEST_LAYOUT.bits | Self::REQUEST_PAINT.bits; @@ -181,7 +182,7 @@ impl Pod { if had_active || self.state.flags.contains(PodFlags::IS_HOT) || hot_changed { let mut mouse_event = mouse_event.clone(); mouse_event.pos -= self.state.origin.to_vec2(); - modified_event = Some(RawEvent::MouseUp(mouse_event)); + modified_event = Some(RawEvent::MouseMove(mouse_event)); true } else { false @@ -206,8 +207,17 @@ impl Pod { } }; if recurse { + let mut inner_cx = EventCx { + cx_state: cx.cx_state, + widget_state: &mut self.state, + }; self.widget - .event(cx, modified_event.as_ref().unwrap_or(event)); + .event(&mut inner_cx, modified_event.as_ref().unwrap_or(event)); + self.state.flags.set( + PodFlags::HAS_ACTIVE, + self.state.flags.contains(PodFlags::IS_ACTIVE), + ); + cx.widget_state.merge_up(&mut self.state); } } @@ -221,6 +231,7 @@ impl Pod { }; if recurse { self.widget.lifecycle(&mut child_cx, event); + cx.widget_state.merge_up(&mut self.state); } } @@ -284,7 +295,12 @@ impl Pod { cx.with_save(|cx| { cx.piet .transform(Affine::translate(self.state.origin.to_vec2())); - self.widget.paint(cx); + let mut inner_cx = PaintCx { + cx_state: cx.cx_state, + widget_state: &mut self.state, + piet: cx.piet, + }; + self.widget.paint(&mut inner_cx); }); } diff --git a/xilem/src/widget/raw_event.rs b/xilem/src/widget/raw_event.rs index e5e68525f1..879729ca9d 100644 --- a/xilem/src/widget/raw_event.rs +++ b/xilem/src/widget/raw_event.rs @@ -39,6 +39,7 @@ pub struct MouseEvent { pub wheel_delta: Vec2, } +#[derive(Debug)] pub enum LifeCycle { HotChanged(bool), }