From 9f06dda8693b473068d6fb5e48e6aa39c8a27cf1 Mon Sep 17 00:00:00 2001 From: Raph Levien Date: Mon, 11 Apr 2022 20:47:25 -0700 Subject: [PATCH 01/10] 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/10] 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/10] 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/10] 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/10] 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/10] 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 83b895ff0d6756df697421ad290d6f06d53d25ac Mon Sep 17 00:00:00 2001 From: Raph Levien Date: Thu, 14 Apr 2022 21:03:43 -0700 Subject: [PATCH 07/10] Continuing to tinker Can call into Python and downcast resulting object... --- idiopath/Cargo.toml | 4 + idiopath/src/lib.rs | 193 ++++++++++++++++++++++++++++++++++ idiopath/src/view/any_view.rs | 2 +- idiopath/src/view/button.rs | 4 +- 4 files changed, 200 insertions(+), 3 deletions(-) create mode 100644 idiopath/src/lib.rs diff --git a/idiopath/Cargo.toml b/idiopath/Cargo.toml index 148802e512..e54563c542 100644 --- a/idiopath/Cargo.toml +++ b/idiopath/Cargo.toml @@ -4,5 +4,9 @@ version = "0.1.0" authors = ["Raph Levien "] edition = "2021" +[lib] +crate-type = ["cdylib"] + [dependencies] +pyo3 = { version = "0.16.3", features = ["extension-module"] } "druid-shell" = { path = "../druid-shell" } diff --git a/idiopath/src/lib.rs b/idiopath/src/lib.rs new file mode 100644 index 0000000000..6feffab0bc --- /dev/null +++ b/idiopath/src/lib.rs @@ -0,0 +1,193 @@ +use pyo3::prelude::*; + +#[pyfunction] +fn ui(py_app: PyObject) -> PyResult { + /* + Python::with_gil(|py| { + let result = py_app.call(py, (), None).unwrap(); + let b: PyResult> = result.as_ref(py).extract(); + if let Ok(button) = b { + println!("button label is {}", button.label); + } + }); + */ + py_main(py_app); + Ok("hello".to_string()) +} + +#[pyclass] +struct Button { + label: String, +} + +#[pyclass] +struct PyView { + view: Box + Send>, +} + +#[pyfunction] +fn button(label: String) -> PyView { + let view = crate::view::button::Button::new(label, |_data| println!("clicked")); + PyView { + view: Box::new(view), + } +} + +#[pymethods] +impl Button { + #[new] + fn new(label: String) -> Self { + println!("label is {}", label); + Button { label } + } +} + +#[pymodule] +fn foo(_py: Python<'_>, m: &PyModule) -> PyResult<()> { + m.add_function(wrap_pyfunction!(ui, m)?)?; + m.add_function(wrap_pyfunction!(button, m)?)?; + m.add_class::