diff --git a/xilem/examples/memoization.rs b/xilem/examples/memoization.rs index 17e9dc81..17c650a7 100644 --- a/xilem/examples/memoization.rs +++ b/xilem/examples/memoization.rs @@ -2,7 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 use std::sync::Arc; -use xilem::view::{button, flex, memoize}; +use xilem::{ + core::{frozen, memoize}, + view::{button, flex}, +}; use xilem::{AnyWidgetView, EventLoop, WidgetView, Xilem}; // There are currently two ways to do memoization @@ -48,7 +51,9 @@ fn decrease_button(state: &AppState) -> impl WidgetView { } fn reset_button() -> impl WidgetView { - button("reset", |data: &mut AppState| data.count = 0) + // The contents of this view never changes, so we use `frozen` to avoid unnecessary rebuilds. + // This is a special case of memoization for when the view doesn't depend on any data. + frozen(|| button("reset", |data: &mut AppState| data.count = 0)) } fn app_logic(state: &mut AppState) -> impl WidgetView { diff --git a/xilem/src/view/mod.rs b/xilem/src/view/mod.rs index b9e8170d..0f73f167 100644 --- a/xilem/src/view/mod.rs +++ b/xilem/src/view/mod.rs @@ -24,5 +24,3 @@ pub use prose::*; mod textbox; pub use textbox::*; - -pub use xilem_core::memoize; diff --git a/xilem_core/src/lib.rs b/xilem_core/src/lib.rs index 821e51ed..ed069454 100644 --- a/xilem_core/src/lib.rs +++ b/xilem_core/src/lib.rs @@ -33,8 +33,8 @@ pub use view::{View, ViewId, ViewPathTracker}; mod views; pub use views::{ - adapt, fork, map_action, map_state, memoize, one_of, run_once, run_once_raw, Adapt, AdaptThunk, - Fork, MapAction, MapState, Memoize, OrphanView, RunOnce, + adapt, fork, frozen, map_action, map_state, memoize, one_of, run_once, run_once_raw, Adapt, + AdaptThunk, Fork, Frozen, MapAction, MapState, Memoize, OrphanView, RunOnce, }; mod message; diff --git a/xilem_core/src/view.rs b/xilem_core/src/view.rs index 7fcead85..dd822dfd 100644 --- a/xilem_core/src/view.rs +++ b/xilem_core/src/view.rs @@ -180,6 +180,11 @@ where } } +pub struct ArcState { + view_state: ViewState, + dirty: bool, +} + /// An implementation of [`View`] which only runs rebuild if the states are different impl View for Arc where @@ -187,10 +192,17 @@ where V: View + ?Sized, { type Element = V::Element; - type ViewState = V::ViewState; + type ViewState = ArcState; fn build(&self, ctx: &mut Context) -> (Self::Element, Self::ViewState) { - self.deref().build(ctx) + let (element, view_state) = self.deref().build(ctx); + ( + element, + ArcState { + view_state, + dirty: false, + }, + ) } fn rebuild<'el>( @@ -200,11 +212,12 @@ where ctx: &mut Context, element: Mut<'el, Self::Element>, ) -> Mut<'el, Self::Element> { - if Arc::ptr_eq(self, prev) { - // If this is the same value, there's no need to rebuild - element + if core::mem::take(&mut view_state.dirty) || !Arc::ptr_eq(self, prev) { + self.deref() + .rebuild(prev, &mut view_state.view_state, ctx, element) } else { - self.deref().rebuild(prev, view_state, ctx, element) + // If this is the same value, or no rebuild was forced, there's no need to rebuild + element } } @@ -214,7 +227,8 @@ where ctx: &mut Context, element: Mut<'_, Self::Element>, ) { - self.deref().teardown(view_state, ctx, element); + self.deref() + .teardown(&mut view_state.view_state, ctx, element); } fn message( @@ -224,7 +238,12 @@ where message: Message, app_state: &mut State, ) -> MessageResult { - self.deref() - .message(view_state, id_path, message, app_state) + let message_result = + self.deref() + .message(&mut view_state.view_state, id_path, message, app_state); + if matches!(message_result, MessageResult::RequestRebuild) { + view_state.dirty = true; + } + message_result } } diff --git a/xilem_core/src/views/memoize.rs b/xilem_core/src/views/memoize.rs index 7e65a3a8..e628f90a 100644 --- a/xilem_core/src/views/memoize.rs +++ b/xilem_core/src/views/memoize.rs @@ -1,65 +1,85 @@ // Copyright 2024 the Xilem Authors // SPDX-License-Identifier: Apache-2.0 +use core::marker::PhantomData; +use core::mem::size_of; + use crate::{MessageResult, Mut, View, ViewId, ViewPathTracker}; /// A view which supports Memoization. /// /// The story of Memoization in Xilem is still being worked out, /// so the details of this view might change. -pub struct Memoize { - data: D, - child_cb: F, +pub struct Memoize { + data: Data, + init_view: InitView, + phantom: PhantomData (State, Action)>, } -pub struct MemoizeState -where - Context: ViewPathTracker, - V: View, -{ - view: V, - view_state: V::ViewState, - dirty: bool, -} - -impl Memoize -where - F: Fn(&D) -> V, -{ - const ASSERT_CONTEXTLESS_FN: () = { - assert!( - core::mem::size_of::() == 0, - " +const NON_CAPTURING_CLOSURE: &str = " 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. -" - ); - }; +"; - /// Create a new `Memoize` view. - pub fn new(data: D, child_cb: F) -> Self { - let () = Self::ASSERT_CONTEXTLESS_FN; - Memoize { data, child_cb } +/// Memoize the view, until the `data` changes (in which case `view` is called again) +/// +/// # Examples +/// +/// (From the Xilem implementation) +/// +/// ```ignore +/// fn memoized_button(count: u32) -> impl WidgetView { +/// memoize( +/// count, // if this changes, the closure below is reevaluated +/// |count| button(format!("clicked {count} times"), |count| { count += 1; }), +/// ) +/// } +/// ``` +pub fn memoize( + data: Data, + init_view: InitView, +) -> Memoize +where + Data: PartialEq + 'static, + InitView: Fn(&Data) -> V + 'static, + V: View, + Context: ViewPathTracker, +{ + const { + assert!(size_of::() == 0, "{}", NON_CAPTURING_CLOSURE); + } + Memoize { + data, + init_view, + phantom: PhantomData, } } +pub struct MemoizeState { + view: V, + view_state: VState, + dirty: bool, +} + impl View - for Memoize + for Memoize where + State: 'static, + Action: 'static, Context: ViewPathTracker, Data: PartialEq + 'static, V: View, ViewFn: Fn(&Data) -> V + 'static, { - type ViewState = MemoizeState; + type ViewState = MemoizeState; type Element = V::Element; fn build(&self, ctx: &mut Context) -> (Self::Element, Self::ViewState) { - let view = (self.child_cb)(&self.data); + let view = (self.init_view)(&self.data); let (element, view_state) = view.build(ctx); let memoize_state = MemoizeState { view, @@ -77,7 +97,7 @@ where element: Mut<'el, Self::Element>, ) -> Mut<'el, Self::Element> { if core::mem::take(&mut view_state.dirty) || prev.data != self.data { - let view = (self.child_cb)(&self.data); + let view = (self.init_view)(&self.data); let el = view.rebuild(&view_state.view, &mut view_state.view_state, ctx, element); view_state.view = view; el @@ -115,16 +135,112 @@ where } } -/// Memoize the view, until the `data` changes (in which case `view` is called again) -pub fn memoize( - data: Data, - view: ViewFn, -) -> Memoize -where - Data: PartialEq + 'static, - ViewFn: Fn(&Data) -> V + 'static, - V: View, - Context: ViewPathTracker, -{ - Memoize::new(data, view) +/// This view can be used, when there's no access to the `State`, other than in event callbacks +pub struct Frozen { + init_view: InitView, + phantom: PhantomData (State, Action)>, +} + +/// This view can be used, when the view returned by `init_view` doesn't access the `State`, other than in event callbacks +/// It only evaluates the `init_view` once, when it's being created. +/// +/// # Examples +/// +/// (From the Xilem implementation) +/// +/// ```ignore +/// fn frozen_button() -> impl WidgetView { +/// frozen(|| button("doesn't access any external state", |count| { count += 1; })), +/// } +/// ``` +pub fn frozen( + init_view: InitView, +) -> Frozen +where + State: 'static, + Action: 'static, + Context: ViewPathTracker, + V: View, + InitView: Fn() -> V, +{ + const { + assert!(size_of::() == 0, "{}", NON_CAPTURING_CLOSURE); + } + Frozen { + init_view, + phantom: PhantomData, + } +} + +impl View + for Frozen +where + State: 'static, + Action: 'static, + Context: ViewPathTracker, + V: View, + InitView: Fn() -> V + 'static, +{ + type Element = V::Element; + + type ViewState = MemoizeState; + + fn build(&self, ctx: &mut Context) -> (Self::Element, Self::ViewState) { + let view = (self.init_view)(); + let (element, view_state) = view.build(ctx); + let memoize_state = MemoizeState { + view, + view_state, + dirty: false, + }; + (element, memoize_state) + } + + fn rebuild<'el>( + &self, + _prev: &Self, + view_state: &mut Self::ViewState, + ctx: &mut Context, + element: crate::Mut<'el, Self::Element>, + ) -> crate::Mut<'el, Self::Element> { + if core::mem::take(&mut view_state.dirty) { + let view = (self.init_view)(); + let element = + view_state + .view + .rebuild(&view_state.view, &mut view_state.view_state, ctx, element); + view_state.view = view; + element + } else { + element + } + } + + fn teardown( + &self, + view_state: &mut Self::ViewState, + ctx: &mut Context, + element: crate::Mut<'_, Self::Element>, + ) { + view_state + .view + .teardown(&mut view_state.view_state, ctx, element); + } + + fn message( + &self, + view_state: &mut Self::ViewState, + id_path: &[crate::ViewId], + message: Message, + app_state: &mut State, + ) -> crate::MessageResult { + let message_result = + view_state + .view + .message(&mut view_state.view_state, id_path, message, app_state); + if matches!(message_result, MessageResult::RequestRebuild) { + view_state.dirty = true; + } + message_result + } } diff --git a/xilem_core/src/views/mod.rs b/xilem_core/src/views/mod.rs index 69209cfa..c362cebb 100644 --- a/xilem_core/src/views/mod.rs +++ b/xilem_core/src/views/mod.rs @@ -17,7 +17,7 @@ mod fork; pub use fork::{fork, Fork}; mod memoize; -pub use memoize::{memoize, Memoize}; +pub use memoize::{frozen, memoize, Frozen, Memoize}; pub mod one_of; diff --git a/xilem_core/tests/arc.rs b/xilem_core/tests/arc.rs index 8cc2a35c..dc28451a 100644 --- a/xilem_core/tests/arc.rs +++ b/xilem_core/tests/arc.rs @@ -22,7 +22,7 @@ fn record_ops(id: u32) -> OperationView<0> { fn arc_no_path() { let view1 = Arc::new(record_ops(0)); let mut ctx = TestCtx::default(); - let (element, ()) = view1.build(&mut ctx); + let (element, _) = view1.build(&mut ctx); ctx.assert_empty(); assert!(element.view_path.is_empty()); }