From c6f6c78f52e7025b6fe33a4198ba400e163b0cb1 Mon Sep 17 00:00:00 2001 From: Philipp Mildenberger Date: Mon, 20 May 2024 17:51:58 +0200 Subject: [PATCH] xilem: Add Memoization views (`Memoize` and `Arc`) (#267) This ports the `Memoize` view from old xilem, slightly enhances it, by checking whether the given view callback is a non-capturing closure and not a function pointer (by asserting `std::mem::size_of::() == 0`) It also ports the `Arc` and `Arc` from #164 including the example there to show how these two forms of memoization can be used. --- xilem/examples/memoization.rs | 70 +++++++++++++++++++++ xilem/src/any_view.rs | 74 +++++++++++++++++------ xilem/src/lib.rs | 2 +- xilem/src/view/arc.rs | 43 +++++++++++++ xilem/src/view/button.rs | 2 +- xilem/src/view/checkbox.rs | 2 +- xilem/src/view/flex.rs | 2 +- xilem/src/view/memoize.rs | 111 ++++++++++++++++++++++++++++++++++ xilem/src/view/mod.rs | 5 ++ xilem/src/view/textbox.rs | 6 +- 10 files changed, 292 insertions(+), 25 deletions(-) create mode 100644 xilem/examples/memoization.rs create mode 100644 xilem/src/view/arc.rs create mode 100644 xilem/src/view/memoize.rs diff --git a/xilem/examples/memoization.rs b/xilem/examples/memoization.rs new file mode 100644 index 000000000..c29096271 --- /dev/null +++ b/xilem/examples/memoization.rs @@ -0,0 +1,70 @@ +// Copyright 2024 the Xilem Authors +// SPDX-License-Identifier: Apache-2.0 + +use std::sync::Arc; +use xilem::view::{button, flex, memoize}; +use xilem::{AnyMasonryView, MasonryView, Xilem}; + +// There are currently two ways to do memoization + +struct AppState { + count: i32, + increase_button: MemoizedArcView, +} + +#[derive(Default)] +struct MemoizedArcView { + data: D, + // When TAITs are stabilized this can be a non-erased concrete type + view: Option>>, +} + +// The following is an example to do memoization with an Arc +fn increase_button(state: &mut AppState) -> Arc> { + if state.count != state.increase_button.data || state.increase_button.view.is_none() { + let view = Arc::new(button( + format!("current count is {}", state.count), + |state: &mut AppState| { + state.count += 1; + }, + )); + state.increase_button.data = state.count; + state.increase_button.view = Some(view.clone()); + view + } else { + state.increase_button.view.as_ref().unwrap().clone() + } +} + +// This is the alternative with Memoize +// Note how this requires a closure that returns the memoized view, while Arc does not +fn decrease_button(state: &AppState) -> impl MasonryView { + memoize(state.count, |count| { + button( + format!("decrease the count: {count}"), + |data: &mut AppState| data.count -= 1, + ) + }) +} + +fn reset_button() -> impl MasonryView { + button("reset", |data: &mut AppState| data.count = 0) +} + +fn app_logic(state: &mut AppState) -> impl MasonryView { + flex(( + increase_button(state), + decrease_button(state), + reset_button(), + )) +} + +fn main() { + let data = AppState { + count: 0, + increase_button: Default::default(), + }; + + let app = Xilem::new(data, app_logic); + app.run_windowed("Memoization".into()).unwrap(); +} diff --git a/xilem/src/any_view.rs b/xilem/src/any_view.rs index 6e43c2253..93bdddffd 100644 --- a/xilem/src/any_view.rs +++ b/xilem/src/any_view.rs @@ -1,7 +1,7 @@ // Copyright 2024 the Xilem Authors // SPDX-License-Identifier: Apache-2.0 -use std::{any::Any, ops::Deref}; +use std::{any::Any, ops::Deref, sync::Arc}; use accesskit::Role; use masonry::widget::{WidgetMut, WidgetRef}; @@ -22,9 +22,11 @@ use crate::{MasonryView, MessageResult, ViewCx, ViewId}; /// Note that `Option` can also be used for conditionally displaying /// views in a [`ViewSequence`](crate::ViewSequence). // TODO: Mention `Either` when we have implemented that? -pub type BoxedMasonryView = Box>; +pub type BoxedMasonryView = Box>; -impl MasonryView for BoxedMasonryView { +impl MasonryView + for BoxedMasonryView +{ type Element = DynWidget; type ViewState = AnyViewState; @@ -36,9 +38,9 @@ impl MasonryView for BoxedMasonryView { &self, view_state: &mut Self::ViewState, id_path: &[ViewId], - message: Box, - app_state: &mut T, - ) -> crate::MessageResult { + message: Box, + app_state: &mut State, + ) -> crate::MessageResult { self.deref() .dyn_message(view_state, id_path, message, app_state) } @@ -60,9 +62,45 @@ pub struct AnyViewState { generation: u64, } +impl MasonryView + for Arc> +{ + type ViewState = AnyViewState; + + type Element = DynWidget; + + fn build(&self, cx: &mut ViewCx) -> (masonry::WidgetPod, Self::ViewState) { + self.deref().dyn_build(cx) + } + + fn rebuild( + &self, + view_state: &mut Self::ViewState, + cx: &mut ViewCx, + prev: &Self, + element: WidgetMut, + ) { + if !Arc::ptr_eq(self, prev) { + self.deref() + .dyn_rebuild(view_state, cx, prev.deref(), element); + } + } + + fn message( + &self, + view_state: &mut Self::ViewState, + id_path: &[ViewId], + message: Box, + app_state: &mut State, + ) -> MessageResult { + self.deref() + .dyn_message(view_state, id_path, message, app_state) + } +} + /// A trait enabling type erasure of views. -pub trait AnyMasonryView: Send { - fn as_any(&self) -> &dyn std::any::Any; +pub trait AnyMasonryView: Send + Sync { + fn as_any(&self) -> &dyn Any; fn dyn_build(&self, cx: &mut ViewCx) -> (WidgetPod, AnyViewState); @@ -70,7 +108,7 @@ pub trait AnyMasonryView: Send { &self, dyn_state: &mut AnyViewState, cx: &mut ViewCx, - prev: &dyn AnyMasonryView, + prev: &dyn AnyMasonryView, element: WidgetMut, ); @@ -78,16 +116,16 @@ pub trait AnyMasonryView: Send { &self, dyn_state: &mut AnyViewState, id_path: &[ViewId], - message: Box, - app_state: &mut T, - ) -> MessageResult; + message: Box, + app_state: &mut State, + ) -> MessageResult; } -impl + 'static> AnyMasonryView for V +impl + 'static> AnyMasonryView for V where V::ViewState: Any, { - fn as_any(&self) -> &dyn std::any::Any { + fn as_any(&self) -> &dyn Any { self } @@ -110,7 +148,7 @@ where &self, dyn_state: &mut AnyViewState, cx: &mut ViewCx, - prev: &dyn AnyMasonryView, + prev: &dyn AnyMasonryView, mut element: WidgetMut, ) { if let Some(prev) = prev.as_any().downcast_ref() { @@ -149,9 +187,9 @@ where &self, dyn_state: &mut AnyViewState, id_path: &[ViewId], - message: Box, - app_state: &mut T, - ) -> MessageResult { + message: Box, + app_state: &mut State, + ) -> MessageResult { let (start, rest) = id_path .split_first() .expect("Id path has elements for AnyView"); diff --git a/xilem/src/lib.rs b/xilem/src/lib.rs index 08cd23b7e..d38dd7c19 100644 --- a/xilem/src/lib.rs +++ b/xilem/src/lib.rs @@ -146,7 +146,7 @@ where event_loop_runner::run(window_attributes, self.root_widget, self.driver) } } -pub trait MasonryView: Send + 'static { +pub trait MasonryView: Send + Sync + 'static { type Element: Widget; type ViewState; diff --git a/xilem/src/view/arc.rs b/xilem/src/view/arc.rs new file mode 100644 index 000000000..f7b8a30fe --- /dev/null +++ b/xilem/src/view/arc.rs @@ -0,0 +1,43 @@ +// Copyright 2024 the Xilem Authors +// SPDX-License-Identifier: Apache-2.0 + +use std::{any::Any, ops::Deref, sync::Arc}; + +use masonry::widget::WidgetMut; + +use crate::{MasonryView, MessageResult, ViewCx, ViewId}; + +impl> MasonryView + for Arc +{ + type ViewState = V::ViewState; + + type Element = V::Element; + + fn build(&self, cx: &mut ViewCx) -> (masonry::WidgetPod, Self::ViewState) { + self.deref().build(cx) + } + + fn rebuild( + &self, + view_state: &mut Self::ViewState, + cx: &mut ViewCx, + prev: &Self, + element: WidgetMut, + ) { + if !Arc::ptr_eq(self, prev) { + self.deref().rebuild(view_state, cx, prev.deref(), element); + } + } + + fn message( + &self, + view_state: &mut Self::ViewState, + id_path: &[ViewId], + message: Box, + app_state: &mut State, + ) -> MessageResult { + self.deref() + .message(view_state, id_path, message, app_state) + } +} diff --git a/xilem/src/view/button.rs b/xilem/src/view/button.rs index 338a48f02..0b956a92e 100644 --- a/xilem/src/view/button.rs +++ b/xilem/src/view/button.rs @@ -22,7 +22,7 @@ pub struct Button { impl MasonryView for Button where - F: Fn(&mut State) -> Action + Send + 'static, + F: Fn(&mut State) -> Action + Send + Sync + 'static, { type Element = masonry::widget::Button; type ViewState = (); diff --git a/xilem/src/view/checkbox.rs b/xilem/src/view/checkbox.rs index 15da509ce..69aea9885 100644 --- a/xilem/src/view/checkbox.rs +++ b/xilem/src/view/checkbox.rs @@ -28,7 +28,7 @@ pub struct Checkbox { impl MasonryView for Checkbox where - F: Fn(&mut State, bool) -> Action + Send + 'static, + F: Fn(&mut State, bool) -> Action + Send + Sync + 'static, { type Element = masonry::widget::Checkbox; type ViewState = (); diff --git a/xilem/src/view/flex.rs b/xilem/src/view/flex.rs index 58dbb72a0..2149e0b98 100644 --- a/xilem/src/view/flex.rs +++ b/xilem/src/view/flex.rs @@ -52,7 +52,7 @@ impl Flex { } } -impl MasonryView for Flex +impl MasonryView for Flex where Seq: ViewSequence, { diff --git a/xilem/src/view/memoize.rs b/xilem/src/view/memoize.rs new file mode 100644 index 000000000..ea8346adb --- /dev/null +++ b/xilem/src/view/memoize.rs @@ -0,0 +1,111 @@ +// Copyright 2024 the Xilem Authors +// SPDX-License-Identifier: Apache-2.0 + +use std::any::Any; + +use masonry::{widget::WidgetMut, WidgetPod}; + +use crate::{MasonryView, MessageResult, ViewCx, ViewId}; + +pub struct Memoize { + data: D, + child_cb: F, +} + +pub struct MemoizeState> { + view: V, + view_state: V::ViewState, + dirty: bool, +} + +impl Memoize +where + F: Fn(&D) -> V, +{ + const ASSERT_CONTEXTLESS_FN: () = { + assert!( + std::mem::size_of::() == 0, + " +It's not possible to use function pointers or captured context in closures, +as this potentially messes up the logic of memoize or produces unwanted effects. + +For example a different kind of view could be instantiated with a different callback, while the old one is still memoized, but it's not updated then. +It's not possible in Rust currently to check whether the (content of the) callback has changed with the `Fn` trait, which would make this otherwise possible. +" + ); + }; + + pub fn new(data: D, child_cb: F) -> Self { + #[allow(clippy::let_unit_value)] + let _ = Self::ASSERT_CONTEXTLESS_FN; + Memoize { data, child_cb } + } +} + +impl MasonryView for Memoize +where + D: PartialEq + Send + Sync + 'static, + V: MasonryView, + F: Fn(&D) -> V + Send + Sync + 'static, +{ + type ViewState = MemoizeState; + + type Element = V::Element; + + fn build(&self, cx: &mut ViewCx) -> (WidgetPod, Self::ViewState) { + let view = (self.child_cb)(&self.data); + let (element, view_state) = view.build(cx); + let memoize_state = MemoizeState { + view, + view_state, + dirty: false, + }; + (element, memoize_state) + } + + fn rebuild( + &self, + view_state: &mut Self::ViewState, + cx: &mut ViewCx, + prev: &Self, + element: WidgetMut, + ) { + if std::mem::take(&mut view_state.dirty) || prev.data != self.data { + let view = (self.child_cb)(&self.data); + view.rebuild(&mut view_state.view_state, cx, &view_state.view, element); + view_state.view = view; + } + } + + fn message( + &self, + view_state: &mut Self::ViewState, + id_path: &[ViewId], + message: Box, + app_state: &mut State, + ) -> MessageResult { + let r = view_state + .view + .message(&mut view_state.view_state, id_path, message, app_state); + if matches!(r, MessageResult::RequestRebuild) { + view_state.dirty = true; + } + r + } +} + +/// A static view, all of the content of the `view` should be constant, as this function is only run once +pub fn static_view(view: F) -> Memoize<(), impl Fn(&()) -> V> +where + F: Fn() -> V + Send + 'static, +{ + Memoize::new((), move |_: &()| view()) +} + +/// Memoize the view, until the `data` changes (in which case `view` is called again) +pub fn memoize(data: D, view: F) -> Memoize +where + F: Fn(&D) -> V + Send, +{ + Memoize::new(data, view) +} diff --git a/xilem/src/view/mod.rs b/xilem/src/view/mod.rs index 68d2378f5..1e181c904 100644 --- a/xilem/src/view/mod.rs +++ b/xilem/src/view/mod.rs @@ -1,6 +1,8 @@ // Copyright 2024 the Xilem Authors // SPDX-License-Identifier: Apache-2.0 +mod arc; + mod button; pub use button::*; @@ -13,6 +15,9 @@ pub use flex::*; mod label; pub use label::*; +mod memoize; +pub use memoize::*; + mod prose; pub use prose::*; diff --git a/xilem/src/view/textbox.rs b/xilem/src/view/textbox.rs index 5e0a50678..e0bf57ff4 100644 --- a/xilem/src/view/textbox.rs +++ b/xilem/src/view/textbox.rs @@ -9,11 +9,11 @@ use crate::{Color, MasonryView, MessageResult, TextAlignment, ViewCx, ViewId}; // is that if the user forgets to hook up the modify the state's contents in the callback, // the textbox will always be reset to the initial state. This will be very annoying for the user. -type Callback = Box Action + Send + 'static>; +type Callback = Box Action + Send + Sync + 'static>; pub fn textbox(contents: String, on_changed: F) -> Textbox where - F: Fn(&mut State, String) -> Action + Send + 'static, + F: Fn(&mut State, String) -> Action + Send + Sync + 'static, { // TODO: Allow setting a placeholder Textbox { @@ -55,7 +55,7 @@ impl Textbox { pub fn on_enter(mut self, on_enter: F) -> Self where - F: Fn(&mut State, String) -> Action + Send + 'static, + F: Fn(&mut State, String) -> Action + Send + Sync + 'static, { self.on_enter = Some(Box::new(on_enter)); self