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
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use xilem::view::{button, flex, memoize};
|
use xilem::{
|
||||||
|
core::{frozen, memoize},
|
||||||
|
view::{button, flex},
|
||||||
|
};
|
||||||
use xilem::{AnyWidgetView, EventLoop, WidgetView, Xilem};
|
use xilem::{AnyWidgetView, EventLoop, WidgetView, Xilem};
|
||||||
|
|
||||||
// There are currently two ways to do memoization
|
// 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> {
|
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> {
|
fn app_logic(state: &mut AppState) -> impl WidgetView<AppState> {
|
||||||
|
|
|
@ -24,5 +24,3 @@ pub use prose::*;
|
||||||
|
|
||||||
mod textbox;
|
mod textbox;
|
||||||
pub use textbox::*;
|
pub use textbox::*;
|
||||||
|
|
||||||
pub use xilem_core::memoize;
|
|
||||||
|
|
|
@ -33,8 +33,8 @@ pub use view::{View, ViewId, ViewPathTracker};
|
||||||
|
|
||||||
mod views;
|
mod views;
|
||||||
pub use views::{
|
pub use views::{
|
||||||
adapt, fork, map_action, map_state, memoize, one_of, run_once, run_once_raw, Adapt, AdaptThunk,
|
adapt, fork, frozen, map_action, map_state, memoize, one_of, run_once, run_once_raw, Adapt,
|
||||||
Fork, MapAction, MapState, Memoize, OrphanView, RunOnce,
|
AdaptThunk, Fork, Frozen, MapAction, MapState, Memoize, OrphanView, RunOnce,
|
||||||
};
|
};
|
||||||
|
|
||||||
mod message;
|
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
|
/// 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>
|
impl<State, Action, Context, Message, V> View<State, Action, Context, Message> for Arc<V>
|
||||||
where
|
where
|
||||||
|
@ -187,10 +192,17 @@ where
|
||||||
V: View<State, Action, Context, Message> + ?Sized,
|
V: View<State, Action, Context, Message> + ?Sized,
|
||||||
{
|
{
|
||||||
type Element = V::Element;
|
type Element = V::Element;
|
||||||
type ViewState = V::ViewState;
|
type ViewState = ArcState<V::ViewState>;
|
||||||
|
|
||||||
fn build(&self, ctx: &mut Context) -> (Self::Element, Self::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>(
|
fn rebuild<'el>(
|
||||||
|
@ -200,11 +212,12 @@ where
|
||||||
ctx: &mut Context,
|
ctx: &mut Context,
|
||||||
element: Mut<'el, Self::Element>,
|
element: Mut<'el, Self::Element>,
|
||||||
) -> Mut<'el, Self::Element> {
|
) -> Mut<'el, Self::Element> {
|
||||||
if Arc::ptr_eq(self, prev) {
|
if core::mem::take(&mut view_state.dirty) || !Arc::ptr_eq(self, prev) {
|
||||||
// If this is the same value, there's no need to rebuild
|
self.deref()
|
||||||
element
|
.rebuild(prev, &mut view_state.view_state, ctx, element)
|
||||||
} else {
|
} 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,
|
ctx: &mut Context,
|
||||||
element: Mut<'_, Self::Element>,
|
element: Mut<'_, Self::Element>,
|
||||||
) {
|
) {
|
||||||
self.deref().teardown(view_state, ctx, element);
|
self.deref()
|
||||||
|
.teardown(&mut view_state.view_state, ctx, element);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn message(
|
fn message(
|
||||||
|
@ -224,7 +238,12 @@ where
|
||||||
message: Message,
|
message: Message,
|
||||||
app_state: &mut State,
|
app_state: &mut State,
|
||||||
) -> MessageResult<Action, Message> {
|
) -> MessageResult<Action, Message> {
|
||||||
self.deref()
|
let message_result =
|
||||||
.message(view_state, id_path, message, app_state)
|
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
|
// Copyright 2024 the Xilem Authors
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
use core::marker::PhantomData;
|
||||||
|
use core::mem::size_of;
|
||||||
|
|
||||||
use crate::{MessageResult, Mut, View, ViewId, ViewPathTracker};
|
use crate::{MessageResult, Mut, View, ViewId, ViewPathTracker};
|
||||||
|
|
||||||
/// A view which supports Memoization.
|
/// A view which supports Memoization.
|
||||||
///
|
///
|
||||||
/// The story of Memoization in Xilem is still being worked out,
|
/// The story of Memoization in Xilem is still being worked out,
|
||||||
/// so the details of this view might change.
|
/// so the details of this view might change.
|
||||||
pub struct Memoize<D, F> {
|
pub struct Memoize<Data, InitView, State, Action> {
|
||||||
data: D,
|
data: Data,
|
||||||
child_cb: F,
|
init_view: InitView,
|
||||||
|
phantom: PhantomData<fn() -> (State, Action)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct MemoizeState<State, Action, Context, Message, V>
|
const NON_CAPTURING_CLOSURE: &str = "
|
||||||
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,
|
|
||||||
"
|
|
||||||
It's not possible to use function pointers or captured context in closures,
|
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.
|
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.
|
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.
|
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.
|
/// Memoize the view, until the `data` changes (in which case `view` is called again)
|
||||||
pub fn new(data: D, child_cb: F) -> Self {
|
///
|
||||||
let () = Self::ASSERT_CONTEXTLESS_FN;
|
/// # Examples
|
||||||
Memoize { data, child_cb }
|
///
|
||||||
|
/// (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>
|
impl<State, Action, Context, Data, V, ViewFn, Message> View<State, Action, Context, Message>
|
||||||
for Memoize<Data, ViewFn>
|
for Memoize<Data, ViewFn, State, Action>
|
||||||
where
|
where
|
||||||
|
State: 'static,
|
||||||
|
Action: 'static,
|
||||||
Context: ViewPathTracker,
|
Context: ViewPathTracker,
|
||||||
Data: PartialEq + 'static,
|
Data: PartialEq + 'static,
|
||||||
V: View<State, Action, Context, Message>,
|
V: View<State, Action, Context, Message>,
|
||||||
ViewFn: Fn(&Data) -> V + 'static,
|
ViewFn: Fn(&Data) -> V + 'static,
|
||||||
{
|
{
|
||||||
type ViewState = MemoizeState<State, Action, Context, Message, V>;
|
type ViewState = MemoizeState<V, V::ViewState>;
|
||||||
|
|
||||||
type Element = V::Element;
|
type Element = V::Element;
|
||||||
|
|
||||||
fn build(&self, ctx: &mut Context) -> (Self::Element, Self::ViewState) {
|
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 (element, view_state) = view.build(ctx);
|
||||||
let memoize_state = MemoizeState {
|
let memoize_state = MemoizeState {
|
||||||
view,
|
view,
|
||||||
|
@ -77,7 +97,7 @@ where
|
||||||
element: Mut<'el, Self::Element>,
|
element: Mut<'el, Self::Element>,
|
||||||
) -> Mut<'el, Self::Element> {
|
) -> Mut<'el, Self::Element> {
|
||||||
if core::mem::take(&mut view_state.dirty) || prev.data != self.data {
|
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);
|
let el = view.rebuild(&view_state.view, &mut view_state.view_state, ctx, element);
|
||||||
view_state.view = view;
|
view_state.view = view;
|
||||||
el
|
el
|
||||||
|
@ -115,16 +135,112 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Memoize the view, until the `data` changes (in which case `view` is called again)
|
/// This view can be used, when there's no access to the `State`, other than in event callbacks
|
||||||
pub fn memoize<State, Action, Context, Message, Data, V, ViewFn>(
|
pub struct Frozen<InitView, State, Action> {
|
||||||
data: Data,
|
init_view: InitView,
|
||||||
view: ViewFn,
|
phantom: PhantomData<fn() -> (State, Action)>,
|
||||||
) -> Memoize<Data, ViewFn>
|
}
|
||||||
where
|
|
||||||
Data: PartialEq + 'static,
|
/// This view can be used, when the view returned by `init_view` doesn't access the `State`, other than in event callbacks
|
||||||
ViewFn: Fn(&Data) -> V + 'static,
|
/// It only evaluates the `init_view` once, when it's being created.
|
||||||
V: View<State, Action, Context, Message>,
|
///
|
||||||
Context: ViewPathTracker,
|
/// # Examples
|
||||||
{
|
///
|
||||||
Memoize::new(data, view)
|
/// (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};
|
pub use fork::{fork, Fork};
|
||||||
|
|
||||||
mod memoize;
|
mod memoize;
|
||||||
pub use memoize::{memoize, Memoize};
|
pub use memoize::{frozen, memoize, Frozen, Memoize};
|
||||||
|
|
||||||
pub mod one_of;
|
pub mod one_of;
|
||||||
|
|
||||||
|
|
|
@ -22,7 +22,7 @@ fn record_ops(id: u32) -> OperationView<0> {
|
||||||
fn arc_no_path() {
|
fn arc_no_path() {
|
||||||
let view1 = Arc::new(record_ops(0));
|
let view1 = Arc::new(record_ops(0));
|
||||||
let mut ctx = TestCtx::default();
|
let mut ctx = TestCtx::default();
|
||||||
let (element, ()) = view1.build(&mut ctx);
|
let (element, _) = view1.build(&mut ctx);
|
||||||
ctx.assert_empty();
|
ctx.assert_empty();
|
||||||
assert!(element.view_path.is_empty());
|
assert!(element.view_path.is_empty());
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue