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:
Philipp Mildenberger 2024-08-01 13:21:56 +02:00 committed by GitHub
parent 759d746653
commit 210afb4048
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 199 additions and 61 deletions

View File

@ -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> {

View File

@ -24,5 +24,3 @@ pub use prose::*;
mod textbox;
pub use textbox::*;
pub use xilem_core::memoize;

View File

@ -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;

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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;

View File

@ -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());
}