Skip to content

Commit

Permalink
xilem: Add Memoization views (Memoize and Arc<impl View>) (#267)
Browse files Browse the repository at this point in the history
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::<F>() == 0`)

It also ports the `Arc<impl View>` and `Arc<dyn AnyMasonryView>` from
#164 including the example there to show how these two forms of
memoization can be used.
  • Loading branch information
Philipp-M authored May 20, 2024
1 parent 9c1eb3b commit c6f6c78
Show file tree
Hide file tree
Showing 10 changed files with 292 additions and 25 deletions.
70 changes: 70 additions & 0 deletions xilem/examples/memoization.rs
Original file line number Diff line number Diff line change
@@ -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<i32>,
}

#[derive(Default)]
struct MemoizedArcView<D> {
data: D,
// When TAITs are stabilized this can be a non-erased concrete type
view: Option<Arc<dyn AnyMasonryView<AppState>>>,
}

// The following is an example to do memoization with an Arc
fn increase_button(state: &mut AppState) -> Arc<dyn AnyMasonryView<AppState>> {
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<AppState> {
memoize(state.count, |count| {
button(
format!("decrease the count: {count}"),
|data: &mut AppState| data.count -= 1,
)
})
}

fn reset_button() -> impl MasonryView<AppState> {
button("reset", |data: &mut AppState| data.count = 0)
}

fn app_logic(state: &mut AppState) -> impl MasonryView<AppState> {
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();
}
74 changes: 56 additions & 18 deletions xilem/src/any_view.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand All @@ -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<T, A = ()> = Box<dyn AnyMasonryView<T, A>>;
pub type BoxedMasonryView<State, Action = ()> = Box<dyn AnyMasonryView<State, Action>>;

impl<T: 'static, A: 'static> MasonryView<T, A> for BoxedMasonryView<T, A> {
impl<State: 'static, Action: 'static> MasonryView<State, Action>
for BoxedMasonryView<State, Action>
{
type Element = DynWidget;
type ViewState = AnyViewState;

Expand All @@ -36,9 +38,9 @@ impl<T: 'static, A: 'static> MasonryView<T, A> for BoxedMasonryView<T, A> {
&self,
view_state: &mut Self::ViewState,
id_path: &[ViewId],
message: Box<dyn std::any::Any>,
app_state: &mut T,
) -> crate::MessageResult<A> {
message: Box<dyn Any>,
app_state: &mut State,
) -> crate::MessageResult<Action> {
self.deref()
.dyn_message(view_state, id_path, message, app_state)
}
Expand All @@ -60,34 +62,70 @@ pub struct AnyViewState {
generation: u64,
}

impl<State: 'static, Action: 'static> MasonryView<State, Action>
for Arc<dyn AnyMasonryView<State, Action>>
{
type ViewState = AnyViewState;

type Element = DynWidget;

fn build(&self, cx: &mut ViewCx) -> (masonry::WidgetPod<Self::Element>, Self::ViewState) {
self.deref().dyn_build(cx)
}

fn rebuild(
&self,
view_state: &mut Self::ViewState,
cx: &mut ViewCx,
prev: &Self,
element: WidgetMut<Self::Element>,
) {
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<dyn Any>,
app_state: &mut State,
) -> MessageResult<Action> {
self.deref()
.dyn_message(view_state, id_path, message, app_state)
}
}

/// A trait enabling type erasure of views.
pub trait AnyMasonryView<T, A = ()>: Send {
fn as_any(&self) -> &dyn std::any::Any;
pub trait AnyMasonryView<State, Action = ()>: Send + Sync {
fn as_any(&self) -> &dyn Any;

fn dyn_build(&self, cx: &mut ViewCx) -> (WidgetPod<DynWidget>, AnyViewState);

fn dyn_rebuild(
&self,
dyn_state: &mut AnyViewState,
cx: &mut ViewCx,
prev: &dyn AnyMasonryView<T, A>,
prev: &dyn AnyMasonryView<State, Action>,
element: WidgetMut<DynWidget>,
);

fn dyn_message(
&self,
dyn_state: &mut AnyViewState,
id_path: &[ViewId],
message: Box<dyn std::any::Any>,
app_state: &mut T,
) -> MessageResult<A>;
message: Box<dyn Any>,
app_state: &mut State,
) -> MessageResult<Action>;
}

impl<T, A, V: MasonryView<T, A> + 'static> AnyMasonryView<T, A> for V
impl<State, Action, V: MasonryView<State, Action> + 'static> AnyMasonryView<State, Action> for V
where
V::ViewState: Any,
{
fn as_any(&self) -> &dyn std::any::Any {
fn as_any(&self) -> &dyn Any {
self
}

Expand All @@ -110,7 +148,7 @@ where
&self,
dyn_state: &mut AnyViewState,
cx: &mut ViewCx,
prev: &dyn AnyMasonryView<T, A>,
prev: &dyn AnyMasonryView<State, Action>,
mut element: WidgetMut<DynWidget>,
) {
if let Some(prev) = prev.as_any().downcast_ref() {
Expand Down Expand Up @@ -149,9 +187,9 @@ where
&self,
dyn_state: &mut AnyViewState,
id_path: &[ViewId],
message: Box<dyn std::any::Any>,
app_state: &mut T,
) -> MessageResult<A> {
message: Box<dyn Any>,
app_state: &mut State,
) -> MessageResult<Action> {
let (start, rest) = id_path
.split_first()
.expect("Id path has elements for AnyView");
Expand Down
2 changes: 1 addition & 1 deletion xilem/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ where
event_loop_runner::run(window_attributes, self.root_widget, self.driver)
}
}
pub trait MasonryView<State, Action = ()>: Send + 'static {
pub trait MasonryView<State, Action = ()>: Send + Sync + 'static {
type Element: Widget;
type ViewState;

Expand Down
43 changes: 43 additions & 0 deletions xilem/src/view/arc.rs
Original file line number Diff line number Diff line change
@@ -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<State: 'static, Action: 'static, V: MasonryView<State, Action>> MasonryView<State, Action>
for Arc<V>
{
type ViewState = V::ViewState;

type Element = V::Element;

fn build(&self, cx: &mut ViewCx) -> (masonry::WidgetPod<Self::Element>, Self::ViewState) {
self.deref().build(cx)
}

fn rebuild(
&self,
view_state: &mut Self::ViewState,
cx: &mut ViewCx,
prev: &Self,
element: WidgetMut<Self::Element>,
) {
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<dyn Any>,
app_state: &mut State,
) -> MessageResult<Action> {
self.deref()
.message(view_state, id_path, message, app_state)
}
}
2 changes: 1 addition & 1 deletion xilem/src/view/button.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ pub struct Button<F> {

impl<F, State, Action> MasonryView<State, Action> for Button<F>
where
F: Fn(&mut State) -> Action + Send + 'static,
F: Fn(&mut State) -> Action + Send + Sync + 'static,
{
type Element = masonry::widget::Button;
type ViewState = ();
Expand Down
2 changes: 1 addition & 1 deletion xilem/src/view/checkbox.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ pub struct Checkbox<F> {

impl<F, State, Action> MasonryView<State, Action> for Checkbox<F>
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 = ();
Expand Down
2 changes: 1 addition & 1 deletion xilem/src/view/flex.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ impl<VT, Marker> Flex<VT, Marker> {
}
}

impl<State, Action, Marker: 'static, Seq> MasonryView<State, Action> for Flex<Seq, Marker>
impl<State, Action, Marker: 'static, Seq: Sync> MasonryView<State, Action> for Flex<Seq, Marker>
where
Seq: ViewSequence<State, Action, Marker>,
{
Expand Down
111 changes: 111 additions & 0 deletions xilem/src/view/memoize.rs
Original file line number Diff line number Diff line change
@@ -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<D, F> {
data: D,
child_cb: F,
}

pub struct MemoizeState<T, A, V: MasonryView<T, A>> {
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!(
std::mem::size_of::<F>() == 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<State, Action, D, V, F> MasonryView<State, Action> for Memoize<D, F>
where
D: PartialEq + Send + Sync + 'static,
V: MasonryView<State, Action>,
F: Fn(&D) -> V + Send + Sync + 'static,
{
type ViewState = MemoizeState<State, Action, V>;

type Element = V::Element;

fn build(&self, cx: &mut ViewCx) -> (WidgetPod<Self::Element>, 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<Self::Element>,
) {
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<dyn Any>,
app_state: &mut State,
) -> MessageResult<Action> {
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<V, F>(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<D, V, F>(data: D, view: F) -> Memoize<D, F>
where
F: Fn(&D) -> V + Send,
{
Memoize::new(data, view)
}
Loading

0 comments on commit c6f6c78

Please sign in to comment.