mirror of https://github.com/linebender/xilem
xilem_core: Refactor Memoization (add `Frozen` view, fix force-rebuild in `Arc`, and cleanup `Memoize`) (#451)
This adds a `force` which is basically a specialized `Memoize` without any bound data. This also cleans up `Memoize` a little bit (mostly code-aesthetic stuff, but also adds `State` and `Action` params to the view, so that it doesn't offer weird surprises when composing it with views like `Adapt`). The `Arc` view is also fixed, as it didn't support force rebuilding yet, which is relevant for e.g. async, e.g. the `MemoizedAwait` view would not work inside an `Arc<impl View>` currently. --------- Co-authored-by: Daniel McNab <36049421+DJMcNab@users.noreply.github.com>
This commit is contained in:
parent
759d746653
commit
210afb4048
|
@ -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<AppState> {
|
|||
}
|
||||
|
||||
fn reset_button() -> impl WidgetView<AppState> {
|
||||
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<AppState> {
|
||||
|
|
|
@ -24,5 +24,3 @@ pub use prose::*;
|
|||
|
||||
mod textbox;
|
||||
pub use textbox::*;
|
||||
|
||||
pub use xilem_core::memoize;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -180,6 +180,11 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
pub struct ArcState<ViewState> {
|
||||
view_state: ViewState,
|
||||
dirty: bool,
|
||||
}
|
||||
|
||||
/// An implementation of [`View`] which only runs rebuild if the states are different
|
||||
impl<State, Action, Context, Message, V> View<State, Action, Context, Message> for Arc<V>
|
||||
where
|
||||
|
@ -187,10 +192,17 @@ where
|
|||
V: View<State, Action, Context, Message> + ?Sized,
|
||||
{
|
||||
type Element = V::Element;
|
||||
type ViewState = V::ViewState;
|
||||
type ViewState = ArcState<V::ViewState>;
|
||||
|
||||
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<Action, Message> {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<D, F> {
|
||||
data: D,
|
||||
child_cb: F,
|
||||
pub struct Memoize<Data, InitView, State, Action> {
|
||||
data: Data,
|
||||
init_view: InitView,
|
||||
phantom: PhantomData<fn() -> (State, Action)>,
|
||||
}
|
||||
|
||||
pub struct MemoizeState<State, Action, Context, Message, V>
|
||||
where
|
||||
Context: ViewPathTracker,
|
||||
V: View<State, Action, Context, Message>,
|
||||
{
|
||||
view: V,
|
||||
view_state: V::ViewState,
|
||||
dirty: bool,
|
||||
}
|
||||
|
||||
impl<D, V, F> Memoize<D, F>
|
||||
where
|
||||
F: Fn(&D) -> V,
|
||||
{
|
||||
const ASSERT_CONTEXTLESS_FN: () = {
|
||||
assert!(
|
||||
core::mem::size_of::<F>() == 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<i32> {
|
||||
/// memoize(
|
||||
/// count, // if this changes, the closure below is reevaluated
|
||||
/// |count| button(format!("clicked {count} times"), |count| { count += 1; }),
|
||||
/// )
|
||||
/// }
|
||||
/// ```
|
||||
pub fn memoize<State, Action, Context, Message, Data, V, InitView>(
|
||||
data: Data,
|
||||
init_view: InitView,
|
||||
) -> Memoize<Data, InitView, State, Action>
|
||||
where
|
||||
Data: PartialEq + 'static,
|
||||
InitView: Fn(&Data) -> V + 'static,
|
||||
V: View<State, Action, Context, Message>,
|
||||
Context: ViewPathTracker,
|
||||
{
|
||||
const {
|
||||
assert!(size_of::<InitView>() == 0, "{}", NON_CAPTURING_CLOSURE);
|
||||
}
|
||||
Memoize {
|
||||
data,
|
||||
init_view,
|
||||
phantom: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
pub struct MemoizeState<V, VState> {
|
||||
view: V,
|
||||
view_state: VState,
|
||||
dirty: bool,
|
||||
}
|
||||
|
||||
impl<State, Action, Context, Data, V, ViewFn, Message> View<State, Action, Context, Message>
|
||||
for Memoize<Data, ViewFn>
|
||||
for Memoize<Data, ViewFn, State, Action>
|
||||
where
|
||||
State: 'static,
|
||||
Action: 'static,
|
||||
Context: ViewPathTracker,
|
||||
Data: PartialEq + 'static,
|
||||
V: View<State, Action, Context, Message>,
|
||||
ViewFn: Fn(&Data) -> V + 'static,
|
||||
{
|
||||
type ViewState = MemoizeState<State, Action, Context, Message, V>;
|
||||
type ViewState = MemoizeState<V, V::ViewState>;
|
||||
|
||||
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<State, Action, Context, Message, Data, V, ViewFn>(
|
||||
data: Data,
|
||||
view: ViewFn,
|
||||
) -> Memoize<Data, ViewFn>
|
||||
where
|
||||
Data: PartialEq + 'static,
|
||||
ViewFn: Fn(&Data) -> V + 'static,
|
||||
V: View<State, Action, Context, Message>,
|
||||
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<InitView, State, Action> {
|
||||
init_view: InitView,
|
||||
phantom: PhantomData<fn() -> (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<i32> {
|
||||
/// frozen(|| button("doesn't access any external state", |count| { count += 1; })),
|
||||
/// }
|
||||
/// ```
|
||||
pub fn frozen<State, Action, Context, Message, V, InitView>(
|
||||
init_view: InitView,
|
||||
) -> Frozen<InitView, State, Action>
|
||||
where
|
||||
State: 'static,
|
||||
Action: 'static,
|
||||
Context: ViewPathTracker,
|
||||
V: View<State, Action, Context, Message>,
|
||||
InitView: Fn() -> V,
|
||||
{
|
||||
const {
|
||||
assert!(size_of::<InitView>() == 0, "{}", NON_CAPTURING_CLOSURE);
|
||||
}
|
||||
Frozen {
|
||||
init_view,
|
||||
phantom: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
impl<State, Action, Context, Message, V, InitView> View<State, Action, Context, Message>
|
||||
for Frozen<InitView, State, Action>
|
||||
where
|
||||
State: 'static,
|
||||
Action: 'static,
|
||||
Context: ViewPathTracker,
|
||||
V: View<State, Action, Context, Message>,
|
||||
InitView: Fn() -> V + 'static,
|
||||
{
|
||||
type Element = V::Element;
|
||||
|
||||
type ViewState = MemoizeState<V, V::ViewState>;
|
||||
|
||||
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<Action, Message> {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue