Skip to content

Commit

Permalink
xilem_masonry: Add Memoization views (Memoize and Arc<impl View>)
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 linebender#164
including the example there to show how these two forms of memoization can be used.
  • Loading branch information
Philipp-M committed May 12, 2024
1 parent eef5a23 commit c723e9f
Show file tree
Hide file tree
Showing 10 changed files with 274 additions and 25 deletions.
65 changes: 65 additions & 0 deletions xilem/examples/memoization.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
use std::sync::Arc;
use xilem::view::{button, flex, memoize};
use xilem::{AnyMasonryView, MasonryView, Xilem};

// There are currently two ways to do memoization

fn app_logic(state: &mut AppState) -> impl MasonryView<AppState> {
// The following is an example to do memoization with an Arc
let increase_button = if let Some(view) = &state.count_view {
view.clone()
} else {
let view = state.make_increase_button();
state.count_view = Some(view.clone());
view
};

flex((
increase_button,
// This is the alternative with Memoize
// Note how this requires a closure that returns the memoized view, while Arc does not
memoize(state.count, |count| {
button(
format!("decrease the count: {count}"),
|data: &mut AppState| {
data.count_view = None;
data.count -= 1;
},
)
}),
button("reset", |data: &mut AppState| {
if data.count != 0 {
data.count_view = None;
}
data.count = 0;
}),
))
}

struct AppState {
count: i32,
// When TAITs are stabilized this can be a non-erased concrete type
count_view: Option<Arc<dyn AnyMasonryView<AppState>>>,
}

impl AppState {
fn make_increase_button(&self) -> Arc<dyn AnyMasonryView<AppState>> {
Arc::new(button(
format!("current count is {}", self.count),
|state: &mut AppState| {
state.count += 1;
state.count_view = None;
},
))
}
}

fn main() {
let data = AppState {
count: 0,
count_view: None,
};

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
40 changes: 40 additions & 0 deletions xilem/src/view/arc.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
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
101 changes: 101 additions & 0 deletions xilem/src/view/memoize.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
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,
{
pub fn new(data: D, child_cb: F) -> Self {
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) {
assert!(
std::mem::size_of::<F>() == 0,
"The callback is not allowed to be a function pointer or a closure capturing context"
);
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,
{
assert!(
std::mem::size_of::<F>() == 0,
"The callback is not allowed to be a function pointer or a closure capturing context"
);
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 c723e9f

Please sign in to comment.