From 86d9592a3e52e45d97625f2c5ac25ee9527bf30c Mon Sep 17 00:00:00 2001 From: Daniel McNab <36049421+DJMcNab@users.noreply.github.com> Date: Thu, 6 Jun 2024 16:16:36 +0100 Subject: [PATCH] Move `xilem` onto a new `xilem_core`, which uses a generic View trait (#310) This: 1) Renames the current/old `xilem_core` to `xilem_web_core` and moves it to the `xilem_web/xilem_web_core` folder 2) Creates a new `xilem_core`, which does not use (non-tuple) macros and instead contains a `View` trait which is generic over the `Context` type 3) Ports `xilem` to this `xilem_core`, but with some functionality missing (namely a few of the extra views; I expect these to straightforward to port) 4) Ports the `mason` and `mason_android` examples to this new `xilem`, with less functionality. This continues ideas first explored in #235 The advantages of this new View trait are: 1) Improved support for ad-hoc views, such as views with additional attributes. This will be very useful for layout algorithms, and will also enable native *good* multi-window (and potentially menus?) 2) A lack of macros, to better enable using go-to-definition and other IDE features on the traits Possible disadvantages: 1) There are a few more traits to enable the flexibility 2) It can be less clear what `Self::Element::Mut` is in the `rebuild` function, because of how the resolution works 3) When implementing `View`, you need to specify the context (i.e. `impl View for Button`. --------- Co-authored-by: Philipp Mildenberger --- ARCHITECTURE.md | 18 +- Cargo.lock | 10 +- Cargo.toml | 11 +- masonry/Cargo.toml | 2 +- masonry/src/action.rs | 2 +- xilem/Cargo.toml | 1 + xilem/examples/flex.rs | 4 +- xilem/examples/mason.rs | 16 +- xilem/examples/memoization.rs | 12 +- xilem/examples/to_do_mvc.rs | 4 +- xilem/src/any_view.rs | 208 +--- xilem/src/driver.rs | 67 ++ xilem/src/id.rs | 30 - xilem/src/lib.rs | 218 ++-- xilem/src/sequence.rs | 478 -------- xilem/src/vec_splice.rs | 111 -- xilem/src/view/arc.rs | 43 - xilem/src/view/button.rs | 46 +- xilem/src/view/checkbox.rs | 43 +- xilem/src/view/flex.rs | 223 ++-- xilem/src/view/label.rs | 40 +- xilem/src/view/memoize.rs | 111 -- xilem/src/view/mod.rs | 7 +- xilem/src/view/prose.rs | 42 +- xilem/src/view/textbox.rs | 53 +- xilem_core/.gitignore | 1 + xilem_core/Cargo.toml | 15 +- xilem_core/README.md | 77 ++ xilem_core/examples/filesystem.rs | 213 ++++ xilem_core/examples/user_interface.rs | 150 +++ xilem_core/src/any_view.rs | 463 ++++++-- xilem_core/src/element.rs | 84 ++ xilem_core/src/lib.rs | 63 +- xilem_core/src/message.rs | 203 +++- xilem_core/src/sequence.rs | 1006 +++++++++++------ xilem_core/src/view.rs | 228 ++++ xilem_core/src/views/memoize.rs | 130 +++ xilem_core/src/views/mod.rs | 5 + xilem_core/tests/any_view.rs | 105 ++ xilem_core/tests/arc.rs | 185 +++ xilem_core/tests/base_sequence.rs | 359 ++++++ xilem_core/tests/common/mod.rs | 293 +++++ xilem_core/tests/tuple_sequence.rs | 195 ++++ xilem_core/tests/vec_sequence.rs | 240 ++++ xilem_web/Cargo.toml | 4 +- xilem_web/README.md | 6 +- xilem_web/src/lib.rs | 2 + xilem_web/xilem_web_core/Cargo.toml | 21 + xilem_web/xilem_web_core/LICENSE | 202 ++++ xilem_web/xilem_web_core/src/any_view.rs | 137 +++ .../xilem_web_core}/src/id.rs | 0 xilem_web/xilem_web_core/src/lib.rs | 27 + xilem_web/xilem_web_core/src/message.rs | 74 ++ xilem_web/xilem_web_core/src/sequence.rs | 363 ++++++ .../xilem_web_core}/src/vec_splice.rs | 0 .../xilem_web_core}/src/view/adapt.rs | 0 .../xilem_web_core}/src/view/memoize.rs | 0 .../xilem_web_core}/src/view/mod.rs | 0 58 files changed, 4782 insertions(+), 1869 deletions(-) create mode 100644 xilem/src/driver.rs delete mode 100644 xilem/src/id.rs delete mode 100644 xilem/src/sequence.rs delete mode 100644 xilem/src/vec_splice.rs delete mode 100644 xilem/src/view/arc.rs delete mode 100644 xilem/src/view/memoize.rs create mode 100644 xilem_core/.gitignore create mode 100644 xilem_core/README.md create mode 100644 xilem_core/examples/filesystem.rs create mode 100644 xilem_core/examples/user_interface.rs create mode 100644 xilem_core/src/element.rs create mode 100644 xilem_core/src/view.rs create mode 100644 xilem_core/src/views/memoize.rs create mode 100644 xilem_core/src/views/mod.rs create mode 100644 xilem_core/tests/any_view.rs create mode 100644 xilem_core/tests/arc.rs create mode 100644 xilem_core/tests/base_sequence.rs create mode 100644 xilem_core/tests/common/mod.rs create mode 100644 xilem_core/tests/tuple_sequence.rs create mode 100644 xilem_core/tests/vec_sequence.rs create mode 100644 xilem_web/xilem_web_core/Cargo.toml create mode 100644 xilem_web/xilem_web_core/LICENSE create mode 100644 xilem_web/xilem_web_core/src/any_view.rs rename {xilem_core => xilem_web/xilem_web_core}/src/id.rs (100%) create mode 100644 xilem_web/xilem_web_core/src/lib.rs create mode 100644 xilem_web/xilem_web_core/src/message.rs create mode 100644 xilem_web/xilem_web_core/src/sequence.rs rename {xilem_core => xilem_web/xilem_web_core}/src/vec_splice.rs (100%) rename {xilem_core => xilem_web/xilem_web_core}/src/view/adapt.rs (100%) rename {xilem_core => xilem_web/xilem_web_core}/src/view/memoize.rs (100%) rename {xilem_core => xilem_web/xilem_web_core}/src/view/mod.rs (100%) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 9542cfba..0656dd23 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -45,7 +45,7 @@ Your main interaction with the framework is through the `app_logic()`. Like Elm, ```rust struct AppData { count: u32, -}f +} fn app_logic(data: &mut AppData) -> impl View { let count = data.count @@ -79,13 +79,15 @@ The associated Elements of the `View` trait are either DOM nodes for `xilem_web` ### Framework Layer (`masonry`) ## Code Organisation -### `crates/xilem_core` -Contains the `View` trait, `Adapt`, `Memoize`, and `AnyView` view implementations. Is also contains the `Message`, `MessageResult`, `Id` types and the tree-structrure tracking. +### `xilem_core` +Contains the `View` trait, and other general implementations. Is also contains the `Message`, `MessageResult`, `Id` types and the tree-structrure tracking. -### `crates/xilem_masonry/view` -Contains the view implementations for Xilem native. +### `xilem_web/ +An implementation of Xilem running on the DOM. -### `crates/xilem_web/ +### `xilem_web_core` +A historical version of `xilem_core` used in `xilem_web` -### `crates/masonry/ -See `ARCHITECTURE.md` file located under `crates/masonry/doc` \ No newline at end of file + +### `masonry/ +See `ARCHITECTURE.md` file located under `crates/masonry/doc` diff --git a/Cargo.lock b/Cargo.lock index 2af739e1..7746b8fa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3846,11 +3846,15 @@ dependencies = [ "tracing", "vello", "winit", + "xilem_core", ] [[package]] name = "xilem_core" version = "0.1.0" +dependencies = [ + "tracing", +] [[package]] name = "xilem_web" @@ -3863,9 +3867,13 @@ dependencies = [ "peniko", "wasm-bindgen", "web-sys", - "xilem_core", + "xilem_web_core", ] +[[package]] +name = "xilem_web_core" +version = "0.1.0" + [[package]] name = "xkbcommon-dl" version = "0.4.2" diff --git a/Cargo.toml b/Cargo.toml index cf6ab359..fda58d60 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,15 +1,17 @@ [workspace] resolver = "2" members = [ + "xilem", "xilem_core", + "masonry", + "xilem_web", + "xilem_web/xilem_web_core", "xilem_web/web_examples/counter", "xilem_web/web_examples/counter_custom_element", "xilem_web/web_examples/todomvc", "xilem_web/web_examples/mathml_svg", "xilem_web/web_examples/svgtoy", - "masonry", - "xilem", ] [workspace.package] @@ -26,15 +28,16 @@ clippy.assigning_clones = "allow" rust.unexpected_cfgs = { level = "warn", check-cfg = ['cfg(FALSE)', 'cfg(tarpaulin_include)'] } [workspace.dependencies] -xilem_core = { version = "0.1.0", path = "xilem_core" } +xilem_web_core = { version = "0.1.0", path = "xilem_web/xilem_web_core" } masonry = { version = "0.2.0", path = "masonry" } +xilem_core = { version = "0.1.0", path = "xilem_core" } vello = "0.1.0" wgpu = "0.19.4" kurbo = "0.11.0" parley = "0.1.0" peniko = "0.1.0" winit = "0.30.0" -tracing = "0.1.40" +tracing = {version = "0.1.40", default-features = false} smallvec = "1.13.2" dpi = "0.1.1" fnv = "1.0.7" diff --git a/masonry/Cargo.toml b/masonry/Cargo.toml index 9344e67a..ed5d59d0 100644 --- a/masonry/Cargo.toml +++ b/masonry/Cargo.toml @@ -25,7 +25,7 @@ kurbo.workspace = true parley.workspace = true winit.workspace = true smallvec.workspace = true -tracing.workspace = true +tracing = { workspace = true, features = ["default"] } fnv.workspace = true image.workspace = true once_cell = "1.19.0" diff --git a/masonry/src/action.rs b/masonry/src/action.rs index 5dca0bf5..1c1b9934 100644 --- a/masonry/src/action.rs +++ b/masonry/src/action.rs @@ -18,7 +18,7 @@ pub enum Action { TextEntered(String), CheckboxChecked(bool), // FIXME - This is a huge hack - Other(Arc), + Other(Arc), } impl PartialEq for Action { diff --git a/xilem/Cargo.toml b/xilem/Cargo.toml index 2c12a58f..9509de0d 100644 --- a/xilem/Cargo.toml +++ b/xilem/Cargo.toml @@ -28,6 +28,7 @@ crate-type = ["cdylib"] workspace = true [dependencies] +xilem_core.workspace = true masonry.workspace = true winit.workspace = true tracing.workspace = true diff --git a/xilem/examples/flex.rs b/xilem/examples/flex.rs index f16bee9a..bf22e9ba 100644 --- a/xilem/examples/flex.rs +++ b/xilem/examples/flex.rs @@ -5,10 +5,10 @@ use masonry::widget::{CrossAxisAlignment, MainAxisAlignment}; use winit::error::EventLoopError; use xilem::{ view::{button, flex, label}, - EventLoop, MasonryView, Xilem, + EventLoop, WidgetView, Xilem, }; -fn app_logic(data: &mut i32) -> impl MasonryView { +fn app_logic(data: &mut i32) -> impl WidgetView { flex(( button("-", |data| { *data -= 1; diff --git a/xilem/examples/mason.rs b/xilem/examples/mason.rs index 299d4c9c..2d72ee50 100644 --- a/xilem/examples/mason.rs +++ b/xilem/examples/mason.rs @@ -4,16 +4,15 @@ // On Windows platform, don't show a console when opening the app. #![windows_subsystem = "windows"] -use xilem::view::{button, checkbox, flex, label, prose, textbox}; use xilem::{ - Axis, BoxedMasonryView, Color, EventLoop, EventLoopBuilder, MasonryView, TextAlignment, Xilem, + view::{button, checkbox, flex, label, prose, textbox}, + AnyWidgetView, Axis, Color, EventLoop, EventLoopBuilder, TextAlignment, WidgetView, Xilem, }; - const LOREM: &str = r"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi cursus mi sed euismod euismod. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nullam placerat efficitur tellus at semper. Morbi ac risus magna. Donec ut cursus ex. Etiam quis posuere tellus. Mauris posuere dui et turpis mollis, vitae luctus tellus consectetur. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur eu facilisis nisl. Phasellus in viverra dolor, vitae facilisis est. Maecenas malesuada massa vel ultricies feugiat. Vivamus venenatis et nibh nec pharetra. Phasellus vestibulum elit enim, nec scelerisque orci faucibus id. Vivamus consequat purus sit amet orci egestas, non iaculis massa porttitor. Vestibulum ut eros leo. In fermentum convallis magna in finibus. Donec justo leo, maximus ac laoreet id, volutpat ut elit. Mauris sed leo non neque laoreet faucibus. Aliquam orci arcu, faucibus in molestie eget, ornare non dui. Donec volutpat nulla in fringilla elementum. Aliquam vitae ante egestas ligula tempus vestibulum sit amet sed ante. "; -fn app_logic(data: &mut AppData) -> impl MasonryView { +fn app_logic(data: &mut AppData) -> impl WidgetView { // here's some logic, deriving state for the view from our state let count = data.count; let button_label = if count == 1 { @@ -38,7 +37,8 @@ fn app_logic(data: &mut AppData) -> impl MasonryView { label("Label") .color(Color::REBECCA_PURPLE) .alignment(TextAlignment::Start), - label("Disabled label").disabled(), + // TODO masonry doesn't allow setting disabled manually anymore? + // label("Disabled label").disabled(), )) .direction(Axis::Horizontal), textbox( @@ -59,8 +59,8 @@ fn app_logic(data: &mut AppData) -> impl MasonryView { )) } -fn toggleable(data: &mut AppData) -> impl MasonryView { - let inner_view: BoxedMasonryView<_, _> = if data.active { +fn toggleable(data: &mut AppData) -> impl WidgetView { + let inner_view: Box> = if data.active { Box::new( flex(( button("Deactivate", |data: &mut AppData| { @@ -86,8 +86,8 @@ struct AppData { fn run(event_loop: EventLoopBuilder) { let data = AppData { - textbox_contents: "".into(), count: 0, + textbox_contents: "Not quite a placeholder".into(), active: false, }; diff --git a/xilem/examples/memoization.rs b/xilem/examples/memoization.rs index 06f87627..17e9dc81 100644 --- a/xilem/examples/memoization.rs +++ b/xilem/examples/memoization.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use xilem::view::{button, flex, memoize}; -use xilem::{AnyMasonryView, EventLoop, MasonryView, Xilem}; +use xilem::{AnyWidgetView, EventLoop, WidgetView, Xilem}; // There are currently two ways to do memoization @@ -16,11 +16,11 @@ struct AppState { struct MemoizedArcView { data: D, // When TAITs are stabilized this can be a non-erased concrete type - view: Option>>, + view: Option>>, } // The following is an example to do memoization with an Arc -fn increase_button(state: &mut AppState) -> Arc> { +fn increase_button(state: &mut AppState) -> Arc> { if state.count != state.increase_button.data || state.increase_button.view.is_none() { let view = Arc::new(button( format!("current count is {}", state.count), @@ -38,7 +38,7 @@ fn increase_button(state: &mut AppState) -> Arc> { // 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 { +fn decrease_button(state: &AppState) -> impl WidgetView { memoize(state.count, |count| { button( format!("decrease the count: {count}"), @@ -47,11 +47,11 @@ fn decrease_button(state: &AppState) -> impl MasonryView { }) } -fn reset_button() -> impl MasonryView { +fn reset_button() -> impl WidgetView { button("reset", |data: &mut AppState| data.count = 0) } -fn app_logic(state: &mut AppState) -> impl MasonryView { +fn app_logic(state: &mut AppState) -> impl WidgetView { flex(( increase_button(state), decrease_button(state), diff --git a/xilem/examples/to_do_mvc.rs b/xilem/examples/to_do_mvc.rs index b7e21001..5f384231 100644 --- a/xilem/examples/to_do_mvc.rs +++ b/xilem/examples/to_do_mvc.rs @@ -5,7 +5,7 @@ #![windows_subsystem = "windows"] use xilem::view::{button, checkbox, flex, textbox}; -use xilem::{Axis, EventLoop, MasonryView, Xilem}; +use xilem::{Axis, EventLoop, WidgetView, Xilem}; struct Task { description: String, @@ -29,7 +29,7 @@ impl TaskList { } } -fn app_logic(task_list: &mut TaskList) -> impl MasonryView { +fn app_logic(task_list: &mut TaskList) -> impl WidgetView { let input_box = textbox( task_list.next_task.clone(), |task_list: &mut TaskList, new_value| { diff --git a/xilem/src/any_view.rs b/xilem/src/any_view.rs index 93bdddff..8a6d2c15 100644 --- a/xilem/src/any_view.rs +++ b/xilem/src/any_view.rs @@ -1,8 +1,6 @@ // Copyright 2024 the Xilem Authors // SPDX-License-Identifier: Apache-2.0 -use std::{any::Any, ops::Deref, sync::Arc}; - use accesskit::Role; use masonry::widget::{WidgetMut, WidgetRef}; use masonry::{ @@ -11,8 +9,9 @@ use masonry::{ }; use smallvec::SmallVec; use vello::Scene; +use xilem_core::{AnyElement, AnyView, SuperElement}; -use crate::{MasonryView, MessageResult, ViewCx, ViewId}; +use crate::{Pod, ViewCtx}; /// A view which can have any underlying view type. /// @@ -22,186 +21,35 @@ 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 = Box>; +pub type AnyWidgetView = + dyn AnyView> + Send + Sync; -impl MasonryView - for BoxedMasonryView -{ - type Element = DynWidget; - type ViewState = AnyViewState; - - fn build(&self, cx: &mut ViewCx) -> (masonry::WidgetPod, Self::ViewState) { - self.deref().dyn_build(cx) +impl SuperElement> for Pod { + fn upcast(child: Pod) -> Self { + WidgetPod::new(DynWidget { + inner: child.inner.boxed(), + }) + .into() } - fn message( - &self, - view_state: &mut Self::ViewState, - id_path: &[ViewId], - message: Box, - app_state: &mut State, - ) -> crate::MessageResult { - self.deref() - .dyn_message(view_state, id_path, message, app_state) - } + fn with_downcast_val( + mut this: Self::Mut<'_>, + f: impl FnOnce( as xilem_core::ViewElement>::Mut<'_>) -> R, + ) -> (Self::Mut<'_>, R) { + let ret = { + let mut child = this.ctx.get_mut(&mut this.widget.inner); + let downcast = child.downcast(); + f(downcast) + }; - fn rebuild( - &self, - view_state: &mut Self::ViewState, - cx: &mut ViewCx, - prev: &Self, - element: masonry::widget::WidgetMut, - ) { - self.deref() - .dyn_rebuild(view_state, cx, prev.deref(), element); + (this, ret) } } -pub struct AnyViewState { - inner_state: Box, - generation: u64, -} - -impl MasonryView - for Arc> -{ - type ViewState = AnyViewState; - - type Element = DynWidget; - - fn build(&self, cx: &mut ViewCx) -> (masonry::WidgetPod, Self::ViewState) { - self.deref().dyn_build(cx) - } - - fn rebuild( - &self, - view_state: &mut Self::ViewState, - cx: &mut ViewCx, - prev: &Self, - element: WidgetMut, - ) { - 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, - app_state: &mut State, - ) -> MessageResult { - self.deref() - .dyn_message(view_state, id_path, message, app_state) - } -} - -/// A trait enabling type erasure of views. -pub trait AnyMasonryView: Send + Sync { - fn as_any(&self) -> &dyn Any; - - fn dyn_build(&self, cx: &mut ViewCx) -> (WidgetPod, AnyViewState); - - fn dyn_rebuild( - &self, - dyn_state: &mut AnyViewState, - cx: &mut ViewCx, - prev: &dyn AnyMasonryView, - element: WidgetMut, - ); - - fn dyn_message( - &self, - dyn_state: &mut AnyViewState, - id_path: &[ViewId], - message: Box, - app_state: &mut State, - ) -> MessageResult; -} - -impl + 'static> AnyMasonryView for V -where - V::ViewState: Any, -{ - fn as_any(&self) -> &dyn Any { - self - } - - fn dyn_build(&self, cx: &mut ViewCx) -> (masonry::WidgetPod, AnyViewState) { - let generation = 0; - let (element, view_state) = - cx.with_id(ViewId::for_type::(generation), |cx| self.build(cx)); - ( - WidgetPod::new(DynWidget { - inner: element.boxed(), - }), - AnyViewState { - inner_state: Box::new(view_state), - generation, - }, - ) - } - - fn dyn_rebuild( - &self, - dyn_state: &mut AnyViewState, - cx: &mut ViewCx, - prev: &dyn AnyMasonryView, - mut element: WidgetMut, - ) { - if let Some(prev) = prev.as_any().downcast_ref() { - // If we were previously of this type, then do a normal rebuild - DynWidget::downcast(&mut element, |element| { - if let Some(element) = element { - if let Some(state) = dyn_state.inner_state.downcast_mut() { - cx.with_id(ViewId::for_type::(dyn_state.generation), move |cx| { - self.rebuild(state, cx, prev, element); - }); - } else { - tracing::error!("Unexpected element state type"); - } - } else { - eprintln!("downcast of element failed in dyn_rebuild"); - } - }); - } else { - // Otherwise, replace the element. - - // Increase the generation, because the underlying widget has been swapped out. - // Overflow condition: Impossible to overflow, as u64 only ever incremented by 1 - // and starting at 0. - dyn_state.generation = dyn_state.generation.wrapping_add(1); - let (new_element, view_state) = cx - .with_id(ViewId::for_type::(dyn_state.generation), |cx| { - self.build(cx) - }); - dyn_state.inner_state = Box::new(view_state); - DynWidget::replace_inner(&mut element, new_element.boxed()); - cx.mark_changed(); - } - } - - fn dyn_message( - &self, - dyn_state: &mut AnyViewState, - id_path: &[ViewId], - message: Box, - app_state: &mut State, - ) -> MessageResult { - let (start, rest) = id_path - .split_first() - .expect("Id path has elements for AnyView"); - if start.routing_id() != dyn_state.generation { - return MessageResult::Stale(message); - } - if let Some(view_state) = dyn_state.inner_state.downcast_mut() { - self.message(view_state, rest, message, app_state) - } else { - // Possibly softer failure? - panic!("downcast error in dyn_message"); - } +impl AnyElement> for Pod { + fn replace_inner(mut this: Self::Mut<'_>, child: Pod) -> Self::Mut<'_> { + DynWidget::replace_inner(&mut this, child.inner.boxed()); + this } } @@ -220,14 +68,6 @@ impl DynWidget { this.widget.inner = widget; this.ctx.children_changed(); } - - pub(crate) fn downcast( - this: &mut WidgetMut<'_, Self>, - f: impl FnOnce(Option>) -> R, - ) -> R { - let mut get_mut = this.ctx.get_mut(&mut this.widget.inner); - f(get_mut.try_downcast()) - } } /// Forward all events to the child widget. diff --git a/xilem/src/driver.rs b/xilem/src/driver.rs new file mode 100644 index 00000000..4dc748ae --- /dev/null +++ b/xilem/src/driver.rs @@ -0,0 +1,67 @@ +// Copyright 2024 the Xilem Authors +// SPDX-License-Identifier: Apache-2.0 + +use masonry::{app_driver::AppDriver, widget::RootWidget}; +use xilem_core::MessageResult; + +use crate::{ViewCtx, WidgetView}; + +pub struct MasonryDriver { + pub(crate) state: State, + pub(crate) logic: Logic, + pub(crate) current_view: View, + pub(crate) view_cx: ViewCtx, + pub(crate) view_state: ViewState, +} + +impl AppDriver for MasonryDriver +where + Logic: FnMut(&mut State) -> View, + View: WidgetView, +{ + fn on_action( + &mut self, + ctx: &mut masonry::app_driver::DriverCtx<'_>, + widget_id: masonry::WidgetId, + action: masonry::Action, + ) { + if let Some(id_path) = self.view_cx.widget_map.get(&widget_id) { + let message_result = self.current_view.message( + &mut self.view_state, + id_path.as_slice(), + Box::new(action), + &mut self.state, + ); + let rebuild = match message_result { + MessageResult::Action(()) => { + // It's not entirely clear what to do here + true + } + MessageResult::RequestRebuild => true, + MessageResult::Nop => false, + MessageResult::Stale(_) => { + tracing::info!("Discarding message"); + false + } + }; + if rebuild { + let next_view = (self.logic)(&mut self.state); + let mut root = ctx.get_root::>(); + + self.view_cx.view_tree_changed = false; + next_view.rebuild( + &self.current_view, + &mut self.view_state, + &mut self.view_cx, + root.get_element(), + ); + if cfg!(debug_assertions) && !self.view_cx.view_tree_changed { + tracing::debug!("Nothing changed as result of action"); + } + self.current_view = next_view; + } + } else { + eprintln!("Got action {action:?} for unknown widget. Did you forget to use `with_action_widget`?"); + } + } +} diff --git a/xilem/src/id.rs b/xilem/src/id.rs deleted file mode 100644 index 8905c672..00000000 --- a/xilem/src/id.rs +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright 2024 the Xilem Authors -// SPDX-License-Identifier: Apache-2.0 - -use std::fmt::Debug; - -#[derive(Copy, Clone)] -pub struct ViewId { - // TODO: This used to be NonZeroU64, but that wasn't really being used - routing_id: u64, - debug: &'static str, -} - -impl ViewId { - pub fn for_type(raw: u64) -> Self { - Self { - debug: std::any::type_name::(), - routing_id: raw, - } - } - - pub fn routing_id(self) -> u64 { - self.routing_id - } -} - -impl Debug for ViewId { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}@[{}]", self.routing_id, self.debug) - } -} diff --git a/xilem/src/lib.rs b/xilem/src/lib.rs index 593fd227..1a5ee765 100644 --- a/xilem/src/lib.rs +++ b/xilem/src/lib.rs @@ -2,116 +2,52 @@ // SPDX-License-Identifier: Apache-2.0 #![allow(clippy::comparison_chain)] -use std::{any::Any, collections::HashMap}; +use std::collections::HashMap; +use driver::MasonryDriver; use masonry::{ - app_driver::AppDriver, dpi::LogicalSize, event_loop_runner, widget::{RootWidget, WidgetMut}, Widget, WidgetId, WidgetPod, }; -pub use masonry::{widget::Axis, Color, TextAlignment}; use winit::{ error::EventLoopError, window::{Window, WindowAttributes}, }; +use xilem_core::{MessageResult, SuperElement, View, ViewElement, ViewId, ViewPathTracker}; + +pub use masonry::{ + dpi, + event_loop_runner::{EventLoop, EventLoopBuilder}, + widget::Axis, + Color, TextAlignment, +}; +pub use xilem_core as core; mod any_view; -mod id; -mod sequence; -mod vec_splice; -pub use any_view::{AnyMasonryView, BoxedMasonryView}; +pub use any_view::AnyWidgetView; +mod driver; pub mod view; -pub use id::ViewId; -pub use sequence::{ElementSplice, ViewSequence}; -pub use vec_splice::VecSplice; - -pub use masonry::dpi; -pub use masonry::event_loop_runner::{EventLoop, EventLoopBuilder}; pub struct Xilem where - View: MasonryView, + View: WidgetView, { - root_widget: RootWidget, + root_widget: RootWidget, driver: MasonryDriver, } -pub struct MasonryDriver { - state: State, - logic: Logic, - current_view: View, - view_cx: ViewCx, - view_state: ViewState, -} - -impl AppDriver for MasonryDriver -where - Logic: FnMut(&mut State) -> View, - View: MasonryView, -{ - fn on_action( - &mut self, - ctx: &mut masonry::app_driver::DriverCtx<'_>, - widget_id: masonry::WidgetId, - action: masonry::Action, - ) { - if let Some(id_path) = self.view_cx.widget_map.get(&widget_id) { - let message_result = self.current_view.message( - &mut self.view_state, - id_path.as_slice(), - Box::new(action), - &mut self.state, - ); - let rebuild = match message_result { - MessageResult::Action(()) => { - // It's not entirely clear what to do here - true - } - MessageResult::RequestRebuild => true, - MessageResult::Nop => false, - MessageResult::Stale(_) => { - tracing::info!("Discarding message"); - false - } - }; - if rebuild { - let next_view = (self.logic)(&mut self.state); - let mut root = ctx.get_root::>(); - - self.view_cx.view_tree_changed = false; - next_view.rebuild( - &mut self.view_state, - &mut self.view_cx, - &self.current_view, - root.get_element(), - ); - if cfg!(debug_assertions) && !self.view_cx.view_tree_changed { - tracing::debug!("Nothing changed as result of action"); - } - self.current_view = next_view; - } - } else { - eprintln!("Got action {action:?} for unknown widget. Did you forget to use `with_action_widget`?"); - } - } -} - impl Xilem where Logic: FnMut(&mut State) -> View, - View: MasonryView, + View: WidgetView, { pub fn new(mut state: State, mut logic: Logic) -> Self { let first_view = logic(&mut state); - let mut view_cx = ViewCx { - id_path: vec![], - widget_map: HashMap::new(), - view_tree_changed: false, - }; + let mut view_cx = ViewCtx::default(); let (pod, view_state) = first_view.build(&mut view_cx); - let root_widget = RootWidget::from_pod(pod); + let root_widget = RootWidget::from_pod(pod.inner); Xilem { driver: MasonryDriver { current_view: first_view, @@ -159,30 +95,62 @@ where event_loop_runner::run(event_loop, window_attributes, self.root_widget, self.driver) } } -pub trait MasonryView: Send + Sync + 'static { - type Element: Widget; - type ViewState; - fn build(&self, cx: &mut ViewCx) -> (WidgetPod, Self::ViewState); - - fn rebuild( - &self, - view_state: &mut Self::ViewState, - cx: &mut ViewCx, - prev: &Self, - element: WidgetMut, - ); - - fn message( - &self, - view_state: &mut Self::ViewState, - id_path: &[ViewId], - message: Box, - app_state: &mut State, - ) -> MessageResult; +/// A container for a [Masonry](masonry) widget to be used with Xilem. +/// +/// Equivalent to [`WidgetPod`], but in the [`xilem`](crate) crate to work around the orphan rule. +pub struct Pod { + pub inner: WidgetPod, } -pub struct ViewCx { +impl Pod { + /// Create a new `Pod` for `inner`. + pub fn new(inner: W) -> Self { + Self::from(WidgetPod::new(inner)) + } +} + +impl From> for Pod { + fn from(inner: WidgetPod) -> Self { + Pod { inner } + } +} + +impl ViewElement for Pod { + type Mut<'a> = WidgetMut<'a, W>; +} + +impl SuperElement> for Pod> { + fn upcast(child: Pod) -> Self { + child.inner.boxed().into() + } + + fn with_downcast_val( + mut this: Self::Mut<'_>, + f: impl FnOnce( as xilem_core::ViewElement>::Mut<'_>) -> R, + ) -> (Self::Mut<'_>, R) { + let downcast = this.downcast(); + let ret = f(downcast); + (this, ret) + } +} + +pub trait WidgetView: + View> + Send + Sync +{ + type Widget: Widget; +} + +impl WidgetView for V +where + V: View> + Send + Sync, + W: Widget, +{ + type Widget = W; +} + +#[derive(Default)] +pub struct ViewCtx { /// The map from a widgets id to its position in the View tree. /// /// This includes only the widgets which might send actions @@ -192,7 +160,21 @@ pub struct ViewCx { view_tree_changed: bool, } -impl ViewCx { +impl ViewPathTracker for ViewCtx { + fn push_id(&mut self, id: xilem_core::ViewId) { + self.id_path.push(id); + } + + fn pop_id(&mut self) { + self.id_path.pop(); + } + + fn view_path(&mut self) -> &[xilem_core::ViewId] { + &self.id_path + } +} + +impl ViewCtx { pub fn mark_changed(&mut self) { if cfg!(debug_assertions) { self.view_tree_changed = true; @@ -201,36 +183,20 @@ impl ViewCx { pub fn with_leaf_action_widget( &mut self, - f: impl FnOnce(&mut Self) -> WidgetPod, - ) -> (WidgetPod, ()) { + f: impl FnOnce(&mut Self) -> Pod, + ) -> (Pod, ()) { (self.with_action_widget(f), ()) } - pub fn with_action_widget( - &mut self, - f: impl FnOnce(&mut Self) -> WidgetPod, - ) -> WidgetPod { + pub fn with_action_widget(&mut self, f: impl FnOnce(&mut Self) -> Pod) -> Pod { let value = f(self); - let id = value.id(); + let id = value.inner.id(); let path = self.id_path.clone(); self.widget_map.insert(id, path); value } - pub fn with_id(&mut self, id: ViewId, f: impl FnOnce(&mut Self) -> R) -> R { - self.id_path.push(id); - let res = f(self); - self.id_path.pop(); - res + pub fn teardown_leaf(&mut self, widget: WidgetMut) { + self.widget_map.remove(&widget.ctx.widget_id()); } } - -/// A result wrapper type for event handlers. -#[derive(Default)] -pub enum MessageResult { - Action(A), - RequestRebuild, - #[default] - Nop, - Stale(Box), -} diff --git a/xilem/src/sequence.rs b/xilem/src/sequence.rs deleted file mode 100644 index 2095852d..00000000 --- a/xilem/src/sequence.rs +++ /dev/null @@ -1,478 +0,0 @@ -// Copyright 2023 the Xilem Authors -// SPDX-License-Identifier: Apache-2.0 - -use masonry::{widget::WidgetMut, Widget, WidgetPod}; - -use crate::{MasonryView, MessageResult, ViewCx, ViewId}; - -#[allow(clippy::len_without_is_empty)] -pub trait ElementSplice { - /// Insert a new element at the current index in the resulting collection (and increment the index by 1) - fn push(&mut self, element: WidgetPod>); - /// Mutate the next existing element, and add it to the resulting collection (and increment the index by 1) - // TODO: This should actually return `WidgetMut`, but that isn't supported in Masonry itself yet - fn mutate(&mut self) -> WidgetMut>; - /// Delete the next n existing elements (this doesn't change the index) - fn delete(&mut self, n: usize); - /// Current length of the elements collection - // TODO: Is `len` needed? - fn len(&self) -> usize; -} - -/// This trait represents a (possibly empty) sequence of views. -/// -/// It is up to the parent view how to lay out and display them. -pub trait ViewSequence: Send + 'static { - type SeqState; - // TODO: Rename to not overlap with MasonryView? - /// Build the associated widgets and initialize all states. - /// - /// To be able to monitor changes (e.g. tree-structure tracking) rather than just adding elements, - /// this takes an element splice as well (when it could be just a `Vec` otherwise) - #[must_use] - fn build(&self, cx: &mut ViewCx, elements: &mut dyn ElementSplice) -> Self::SeqState; - - /// Update the associated widget. - /// - /// Returns `true` when anything has changed. - fn rebuild( - &self, - seq_state: &mut Self::SeqState, - cx: &mut ViewCx, - prev: &Self, - elements: &mut dyn ElementSplice, - ); - - /// Propagate a message. - /// - /// Handle a message, propagating to elements if needed. Here, `id_path` is a slice - /// of ids beginning at an element of this view_sequence. - fn message( - &self, - seq_state: &mut Self::SeqState, - id_path: &[ViewId], - message: Box, - app_state: &mut State, - ) -> MessageResult; - - /// Returns the current amount of widgets built by this sequence. - fn count(&self) -> usize; -} - -/// Workaround for trait ambiguity -/// -/// These need to be public for type inference -#[doc(hidden)] -pub struct WasAView; -#[doc(hidden)] -/// See [`WasAView`] -pub struct WasASequence; - -impl> ViewSequence - for View -{ - type SeqState = View::ViewState; - fn build(&self, cx: &mut ViewCx, elements: &mut dyn ElementSplice) -> Self::SeqState { - let (element, view_state) = self.build(cx); - elements.push(element.boxed()); - view_state - } - - fn rebuild( - &self, - seq_state: &mut Self::SeqState, - cx: &mut ViewCx, - prev: &Self, - elements: &mut dyn ElementSplice, - ) { - let mut element = elements.mutate(); - let downcast = element.try_downcast::(); - - if let Some(element) = downcast { - self.rebuild(seq_state, cx, prev, element); - } else { - unreachable!("Tree structure tracking got wrong element type") - } - } - - fn message( - &self, - seq_state: &mut Self::SeqState, - id_path: &[ViewId], - message: Box, - app_state: &mut State, - ) -> MessageResult { - self.message(seq_state, id_path, message, app_state) - } - - fn count(&self) -> usize { - 1 - } -} - -pub struct OptionSeqState { - inner: Option, - generation: u64, -} - -impl> - ViewSequence for Option -{ - type SeqState = OptionSeqState; - fn build(&self, cx: &mut ViewCx, elements: &mut dyn ElementSplice) -> Self::SeqState { - let generation = 0; - match self { - Some(this) => { - let inner = cx.with_id(ViewId::for_type::(generation), |cx| { - this.build(cx, elements) - }); - OptionSeqState { - inner: Some(inner), - generation, - } - } - None => OptionSeqState { - inner: None, - generation, - }, - } - } - - fn rebuild( - &self, - seq_state: &mut Self::SeqState, - cx: &mut ViewCx, - prev: &Self, - elements: &mut dyn ElementSplice, - ) { - // If `prev` was `Some`, we set `seq_state` in reacting to it (and building the inner view) - // This could only fail if some malicious parent view was messing with our internal state - // (i.e. mixing up the state from different instances) - debug_assert_eq!(prev.is_some(), seq_state.inner.is_some()); - match (self, prev.as_ref().zip(seq_state.inner.as_mut())) { - (Some(this), Some((prev, prev_state))) => { - cx.with_id(ViewId::for_type::(seq_state.generation), |cx| { - this.rebuild(prev_state, cx, prev, elements); - }); - } - (None, Some((prev, _))) => { - // Maybe replace with `prev.cleanup`? - let count = prev.count(); - elements.delete(count); - seq_state.inner = None; - cx.mark_changed(); - } - (Some(this), None) => { - seq_state.generation += 1; - let new_state = cx.with_id(ViewId::for_type::(seq_state.generation), |cx| { - Some(this.build(cx, elements)) - }); - seq_state.inner = new_state; - cx.mark_changed(); - } - (None, None) => (), - } - } - - fn message( - &self, - seq_state: &mut Self::SeqState, - id_path: &[ViewId], - message: Box, - app_state: &mut State, - ) -> MessageResult { - let (start, rest) = id_path - .split_first() - .expect("Id path has elements for Option"); - if start.routing_id() != seq_state.generation { - return MessageResult::Stale(message); - } - debug_assert_eq!(self.is_some(), seq_state.inner.is_some()); - if let Some((this, seq_state)) = self.as_ref().zip(seq_state.inner.as_mut()) { - this.message(seq_state, rest, message, app_state) - } else { - MessageResult::Stale(message) - } - } - - fn count(&self) -> usize { - match self { - Some(this) => this.count(), - None => 0, - } - } -} - -pub struct VecViewState { - inner_with_generations: Vec<(InnerState, u32)>, - global_generation: u32, -} - -// TODO: We use raw indexing for this value. What would make it invalid? -impl> ViewSequence - for Vec -{ - type SeqState = VecViewState; - fn build(&self, cx: &mut ViewCx, elements: &mut dyn ElementSplice) -> Self::SeqState { - let generation = 0; - let inner = self.iter().enumerate().map(|(i, child)| { - let id = create_vector_view_id(i, generation); - - cx.with_id(ViewId::for_type::(id), |cx| child.build(cx, elements)) - }); - let inner_with_generations = inner.map(|it| (it, generation)).collect(); - VecViewState { - global_generation: generation, - inner_with_generations, - } - } - - fn rebuild( - &self, - seq_state: &mut Self::SeqState, - cx: &mut ViewCx, - prev: &Self, - elements: &mut dyn ElementSplice, - ) { - for (i, ((child, child_prev), (child_state, child_generation))) in self - .iter() - .zip(prev) - .zip(&mut seq_state.inner_with_generations) - .enumerate() - { - let id = create_vector_view_id(i, *child_generation); - cx.with_id(ViewId::for_type::(id), |cx| { - child.rebuild(child_state, cx, child_prev, elements); - }); - } - let n = self.len(); - if n < prev.len() { - let n_delete = prev[n..].iter().map(ViewSequence::count).sum(); - seq_state.inner_with_generations.drain(n..); - elements.delete(n_delete); - cx.mark_changed(); - } else if n > prev.len() { - // Overflow condition: u32 incrementing by up to 1 per rebuild. Plausible if unlikely to overflow - seq_state.global_generation = match seq_state.global_generation.checked_add(1) { - Some(new_generation) => new_generation, - None => { - // TODO: Inform the error - tracing::error!( - sequence_type = std::any::type_name::(), - issue_url = "https://github.com/linebender/xilem/issues", - "Got overflowing generation in ViewSequence. Please open an issue if you see this situation. There are known solutions" - ); - // The known solution mentioned in the above message is to use a different ViewId for the index and the generation - // We believe this to be superfluous for the default use case, as even with 1000 rebuilds a second, each adding - // to the same array, this would take 50 days of the application running continuously. - // See also https://github.com/bevyengine/bevy/pull/9907, where they warn in their equivalent case - // Note that we have a slightly different strategy to Bevy, where we use a global generation - // This theoretically allows some of the memory in `seq_state` to be reclaimed, at the cost of making overflow - // more likely here. Note that we don't actually reclaim this memory at the moment. - - // We use 0 to wrap around. It would require extremely unfortunate timing to get an async event - // with the correct generation exactly u32::MAX generations late, so wrapping is the best option - 0 - } - }; - seq_state.inner_with_generations.reserve(n - prev.len()); - // This suggestion from clippy is kind of bad, because we use the absolute index in the id - #[allow(clippy::needless_range_loop)] - for ix in prev.len()..n { - let id = create_vector_view_id(ix, seq_state.global_generation); - let new_state = cx.with_id(ViewId::for_type::(id), |cx| { - self[ix].build(cx, elements) - }); - seq_state - .inner_with_generations - .push((new_state, seq_state.global_generation)); - } - cx.mark_changed(); - } - } - - fn message( - &self, - seq_state: &mut Self::SeqState, - id_path: &[ViewId], - message: Box, - app_state: &mut T, - ) -> MessageResult { - let (start, rest) = id_path - .split_first() - .expect("Id path has elements for vector"); - let (index, generation) = view_id_to_index_generation(start.routing_id()); - let (seq_state, stored_generation) = &mut seq_state.inner_with_generations[index]; - if *stored_generation != generation { - return MessageResult::Stale(message); - } - self[index].message(seq_state, rest, message, app_state) - } - - fn count(&self) -> usize { - self.iter().map(ViewSequence::count).sum() - } -} - -/// Turns an index and a generation into a packed id, suitable for use in -/// [`ViewId`]s -fn create_vector_view_id(index: usize, generation: u32) -> u64 { - let id_low: u32 = index.try_into().expect( - "Can't have more than 4294967295 (u32::MAX-1) views in a single vector backed sequence", - ); - let id_low: u64 = id_low.into(); - let id_high: u64 = u64::from(generation) << 32; - id_high | id_low -} - -/// Undoes [`create_vector_view_id`] -fn view_id_to_index_generation(view_id: u64) -> (usize, u32) { - let id_low_ix = view_id as u32; - let id_high_gen = (view_id >> 32) as u32; - (id_low_ix as usize, id_high_gen) -} - -impl ViewSequence for () { - type SeqState = (); - fn build(&self, _: &mut ViewCx, _: &mut dyn ElementSplice) {} - - fn rebuild( - &self, - _seq_state: &mut Self::SeqState, - _cx: &mut ViewCx, - _prev: &Self, - _elements: &mut dyn ElementSplice, - ) { - } - - fn message( - &self, - _seq_state: &mut Self::SeqState, - id_path: &[ViewId], - message: Box, - _app_state: &mut T, - ) -> MessageResult { - tracing::warn!(?id_path, "Dispatched message to empty tuple"); - MessageResult::Stale(message) - } - - fn count(&self) -> usize { - 0 - } -} - -impl> ViewSequence - for (Seq0,) -{ - type SeqState = Seq0::SeqState; - fn build(&self, cx: &mut ViewCx, elements: &mut dyn ElementSplice) -> Self::SeqState { - self.0.build(cx, elements) - } - - fn rebuild( - &self, - seq_state: &mut Self::SeqState, - cx: &mut ViewCx, - prev: &Self, - elements: &mut dyn ElementSplice, - ) { - self.0.rebuild(seq_state, cx, &prev.0, elements); - } - - fn message( - &self, - seq_state: &mut Self::SeqState, - id_path: &[ViewId], - message: Box, - app_state: &mut State, - ) -> MessageResult { - self.0.message(seq_state, id_path, message, app_state) - } - - fn count(&self) -> usize { - self.0.count() - } -} - -macro_rules! impl_view_tuple { - ( - // We could use the ${index} metavariable here once it's stable - // https://veykril.github.io/tlborm/decl-macros/minutiae/metavar-expr.html - $($marker: ident, $seq: ident, $idx: tt);+ - ) => { - impl< - State, - Action, - $( - $marker, - $seq: ViewSequence, - )+ - > ViewSequence for ($($seq,)+) - { - type SeqState = ($($seq::SeqState,)+); - fn build(&self, cx: &mut ViewCx, elements: &mut dyn ElementSplice) -> Self::SeqState { - ($( - cx.with_id(ViewId::for_type::<$seq>($idx), |cx| { - self.$idx.build(cx, elements) - }), - )+) - } - - fn rebuild( - &self, - seq_state: &mut Self::SeqState, - cx: &mut ViewCx, - prev: &Self, - elements: &mut dyn ElementSplice, - ) { - $( - cx.with_id(ViewId::for_type::<$seq>($idx), |cx| { - self.$idx.rebuild(&mut seq_state.$idx, cx, &prev.$idx, elements); - }); - )+ - } - - fn message( - &self, - seq_state: &mut Self::SeqState, - id_path: &[ViewId], - message: Box, - app_state: &mut State, - ) -> MessageResult { - let (start, rest) = id_path - .split_first() - .expect("Id path has elements for tuple"); - match start.routing_id() { - $( - $idx => self.$idx.message(&mut seq_state.$idx, rest, message, app_state), - )+ - // If we have received a message, our parent is (mostly) certain that we requested it - // The only time that wouldn't be the case is when a generational index has overflowed? - _ => unreachable!("Unexpected id path {start:?} in tuple (wants to be routed via {rest:?})"), - } - } - - fn count(&self) -> usize { - // Is there a way to do this which avoids the `+0`? - $(self.$idx.count()+)+ 0 - } - } - }; -} - -// We implement for tuples of length up to 15. 0 and 1 are special cased to be more efficient -impl_view_tuple!(M0, Seq0, 0; M1, Seq1, 1); -impl_view_tuple!(M0, Seq0, 0; M1, Seq1, 1; M2, Seq2, 2); -impl_view_tuple!(M0, Seq0, 0; M1, Seq1, 1; M2, Seq2, 2; M3, Seq3, 3); -impl_view_tuple!(M0, Seq0, 0; M1, Seq1, 1; M2, Seq2, 2; M3, Seq3, 3; M4, Seq4, 4); -impl_view_tuple!(M0, Seq0, 0; M1, Seq1, 1; M2, Seq2, 2; M3, Seq3, 3; M4, Seq4, 4; M5, Seq5, 5); -impl_view_tuple!(M0, Seq0, 0; M1, Seq1, 1; M2, Seq2, 2; M3, Seq3, 3; M4, Seq4, 4; M5, Seq5, 5; M6, Seq6, 6); -impl_view_tuple!(M0, Seq0, 0; M1, Seq1, 1; M2, Seq2, 2; M3, Seq3, 3; M4, Seq4, 4; M5, Seq5, 5; M6, Seq6, 6; M7, Seq7, 7); -impl_view_tuple!(M0, Seq0, 0; M1, Seq1, 1; M2, Seq2, 2; M3, Seq3, 3; M4, Seq4, 4; M5, Seq5, 5; M6, Seq6, 6; M7, Seq7, 7; M8, Seq8, 8); -impl_view_tuple!(M0, Seq0, 0; M1, Seq1, 1; M2, Seq2, 2; M3, Seq3, 3; M4, Seq4, 4; M5, Seq5, 5; M6, Seq6, 6; M7, Seq7, 7; M8, Seq8, 8; M9, Seq9, 9); -impl_view_tuple!(M0, Seq0, 0; M1, Seq1, 1; M2, Seq2, 2; M3, Seq3, 3; M4, Seq4, 4; M5, Seq5, 5; M6, Seq6, 6; M7, Seq7, 7; M8, Seq8, 8; M9, Seq9, 9; M10, Seq10, 10); -impl_view_tuple!(M0, Seq0, 0; M1, Seq1, 1; M2, Seq2, 2; M3, Seq3, 3; M4, Seq4, 4; M5, Seq5, 5; M6, Seq6, 6; M7, Seq7, 7; M8, Seq8, 8; M9, Seq9, 9; M10, Seq10, 10; M11, Seq11, 11); -impl_view_tuple!(M0, Seq0, 0; M1, Seq1, 1; M2, Seq2, 2; M3, Seq3, 3; M4, Seq4, 4; M5, Seq5, 5; M6, Seq6, 6; M7, Seq7, 7; M8, Seq8, 8; M9, Seq9, 9; M10, Seq10, 10; M11, Seq11, 11; M12, Seq12, 12); -impl_view_tuple!(M0, Seq0, 0; M1, Seq1, 1; M2, Seq2, 2; M3, Seq3, 3; M4, Seq4, 4; M5, Seq5, 5; M6, Seq6, 6; M7, Seq7, 7; M8, Seq8, 8; M9, Seq9, 9; M10, Seq10, 10; M11, Seq11, 11; M12, Seq12, 12; M13, Seq13, 13); -impl_view_tuple!(M0, Seq0, 0; M1, Seq1, 1; M2, Seq2, 2; M3, Seq3, 3; M4, Seq4, 4; M5, Seq5, 5; M6, Seq6, 6; M7, Seq7, 7; M8, Seq8, 8; M9, Seq9, 9; M10, Seq10, 10; M11, Seq11, 11; M12, Seq12, 12; M13, Seq13, 13; M14, Seq14, 14); -impl_view_tuple!(M0, Seq0, 0; M1, Seq1, 1; M2, Seq2, 2; M3, Seq3, 3; M4, Seq4, 4; M5, Seq5, 5; M6, Seq6, 6; M7, Seq7, 7; M8, Seq8, 8; M9, Seq9, 9; M10, Seq10, 10; M11, Seq11, 11; M12, Seq12, 12; M13, Seq13, 13; M14, Seq14, 14; M15, Seq15, 15); diff --git a/xilem/src/vec_splice.rs b/xilem/src/vec_splice.rs deleted file mode 100644 index e69b49b1..00000000 --- a/xilem/src/vec_splice.rs +++ /dev/null @@ -1,111 +0,0 @@ -// Copyright 2023 the Xilem Authors -// SPDX-License-Identifier: Apache-2.0 - -use masonry::widget::WidgetMut; - -use crate::ElementSplice; - -pub struct VecSplice<'a, 'b, T> { - v: &'a mut Vec, - scratch: &'b mut Vec, - ix: usize, -} - -impl<'a, 'b, T> VecSplice<'a, 'b, T> { - pub fn new(v: &'a mut Vec, scratch: &'b mut Vec) -> Self { - let ix = 0; - VecSplice { v, scratch, ix } - } - - pub fn skip(&mut self, n: usize) { - if self.v.len() < self.ix + n { - let l = self.scratch.len(); - self.v.extend(self.scratch.splice(l - n.., [])); - self.v[self.ix..].reverse(); - } - self.ix += n; - } - - pub fn delete(&mut self, n: usize) { - if self.v.len() < self.ix + n { - self.scratch.truncate(self.scratch.len() - n); - } else { - if self.v.len() > self.ix + n { - let removed = self.v.splice(self.ix + n.., []).rev(); - self.scratch.extend(removed); - } - self.v.truncate(self.ix); - } - } - - pub fn push(&mut self, value: T) { - self.clear_tail(); - self.v.push(value); - self.ix += 1; - } - - pub fn mutate(&mut self) -> &mut T { - if self.v.len() == self.ix { - self.v.push(self.scratch.pop().unwrap()); - } - let ix = self.ix; - self.ix += 1; - &mut self.v[ix] - } - - pub fn last_mutated(&self) -> Option<&T> { - if self.ix == 0 { - None - } else { - self.v.get(self.ix - 1) - } - } - - pub fn last_mutated_mut(&mut self) -> Option<&mut T> { - if self.ix == 0 { - None - } else { - self.v.get_mut(self.ix - 1) - } - } - - pub fn len(&self) -> usize { - self.ix - } - - pub fn is_empty(&self) -> bool { - self.len() == 0 - } - - pub fn as_vec) -> R>(&mut self, f: F) -> R { - self.clear_tail(); - let ret = f(self.v); - self.ix = self.v.len(); - ret - } - - fn clear_tail(&mut self) { - if self.v.len() > self.ix { - let removed = self.v.splice(self.ix.., []).rev(); - self.scratch.extend(removed); - } - } -} - -impl ElementSplice for VecSplice<'_, '_, masonry::WidgetPod>> { - fn push(&mut self, element: masonry::WidgetPod>) { - self.push(element); - } - - fn mutate(&mut self) -> WidgetMut> { - unreachable!("VecSplice can only be used for `build`, not rebuild") - } - - fn delete(&mut self, n: usize) { - self.delete(n); - } - - fn len(&self) -> usize { - self.len() - } -} diff --git a/xilem/src/view/arc.rs b/xilem/src/view/arc.rs deleted file mode 100644 index f7b8a30f..00000000 --- a/xilem/src/view/arc.rs +++ /dev/null @@ -1,43 +0,0 @@ -// 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> MasonryView - for Arc -{ - type ViewState = V::ViewState; - - type Element = V::Element; - - fn build(&self, cx: &mut ViewCx) -> (masonry::WidgetPod, Self::ViewState) { - self.deref().build(cx) - } - - fn rebuild( - &self, - view_state: &mut Self::ViewState, - cx: &mut ViewCx, - prev: &Self, - element: WidgetMut, - ) { - 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, - app_state: &mut State, - ) -> MessageResult { - self.deref() - .message(view_state, id_path, message, app_state) - } -} diff --git a/xilem/src/view/button.rs b/xilem/src/view/button.rs index 0b956a92..2e7c64fe 100644 --- a/xilem/src/view/button.rs +++ b/xilem/src/view/button.rs @@ -1,9 +1,11 @@ // Copyright 2024 the Xilem Authors // SPDX-License-Identifier: Apache-2.0 -use masonry::{widget::WidgetMut, ArcStr, WidgetPod}; +use crate::{core::View, Pod}; +use masonry::{widget, ArcStr}; +use xilem_core::Mut; -use crate::{MasonryView, MessageResult, ViewCx, ViewId}; +use crate::{MessageResult, ViewCtx, ViewId}; pub fn button(label: impl Into, callback: F) -> Button where @@ -20,39 +22,47 @@ pub struct Button { callback: F, } -impl MasonryView for Button +impl View for Button where F: Fn(&mut State) -> Action + Send + Sync + 'static, { - type Element = masonry::widget::Button; + type Element = Pod; type ViewState = (); - fn build(&self, cx: &mut ViewCx) -> (WidgetPod, Self::ViewState) { - cx.with_leaf_action_widget(|_| { - WidgetPod::new(masonry::widget::Button::new(self.label.clone())) - }) + fn build(&self, ctx: &mut ViewCtx) -> (Self::Element, Self::ViewState) { + ctx.with_leaf_action_widget(|_| Pod::new(widget::Button::new(self.label.clone()))) } - fn rebuild( + fn rebuild<'el>( &self, - _view_state: &mut Self::ViewState, - cx: &mut ViewCx, prev: &Self, - mut element: WidgetMut, - ) { + _: &mut Self::ViewState, + ctx: &mut ViewCtx, + mut element: Mut<'el, Self::Element>, + ) -> Mut<'el, Self::Element> { if prev.label != self.label { element.set_text(self.label.clone()); - cx.mark_changed(); + ctx.mark_changed(); } + element + } + + fn teardown( + &self, + _: &mut Self::ViewState, + ctx: &mut ViewCtx, + element: Mut<'_, Self::Element>, + ) { + ctx.teardown_leaf(element); } fn message( &self, - _view_state: &mut Self::ViewState, + _: &mut Self::ViewState, id_path: &[ViewId], - message: Box, + message: xilem_core::DynMessage, app_state: &mut State, - ) -> crate::MessageResult { + ) -> MessageResult { debug_assert!( id_path.is_empty(), "id path should be empty in Button::message" @@ -67,7 +77,7 @@ where } } Err(message) => { - tracing::error!("Wrong message type in Button::message"); + tracing::error!("Wrong message type in Button::message: {message:?}"); MessageResult::Stale(message) } } diff --git a/xilem/src/view/checkbox.rs b/xilem/src/view/checkbox.rs index 69aea988..34d6ef55 100644 --- a/xilem/src/view/checkbox.rs +++ b/xilem/src/view/checkbox.rs @@ -1,9 +1,10 @@ // Copyright 2024 the Xilem Authors // SPDX-License-Identifier: Apache-2.0 -use masonry::{widget::WidgetMut, ArcStr, WidgetPod}; +use masonry::{widget, ArcStr}; +use xilem_core::Mut; -use crate::{MasonryView, MessageResult, ViewCx, ViewId}; +use crate::{MessageResult, Pod, View, ViewCtx, ViewId}; pub fn checkbox( label: impl Into, @@ -26,44 +27,54 @@ pub struct Checkbox { callback: F, } -impl MasonryView for Checkbox +impl View for Checkbox where F: Fn(&mut State, bool) -> Action + Send + Sync + 'static, { - type Element = masonry::widget::Checkbox; + type Element = Pod; type ViewState = (); - fn build(&self, cx: &mut ViewCx) -> (WidgetPod, Self::ViewState) { - cx.with_leaf_action_widget(|_| { - WidgetPod::new(masonry::widget::Checkbox::new( + fn build(&self, ctx: &mut ViewCtx) -> (Self::Element, Self::ViewState) { + ctx.with_leaf_action_widget(|_| { + Pod::new(masonry::widget::Checkbox::new( self.checked, self.label.clone(), )) }) } - fn rebuild( + fn rebuild<'el>( &self, - _view_state: &mut Self::ViewState, - cx: &mut ViewCx, prev: &Self, - mut element: WidgetMut, - ) { + (): &mut Self::ViewState, + ctx: &mut ViewCtx, + mut element: Mut<'el, Self::Element>, + ) -> Mut<'el, Self::Element> { if prev.label != self.label { element.set_text(self.label.clone()); - cx.mark_changed(); + ctx.mark_changed(); } if prev.checked != self.checked { element.set_checked(self.checked); - cx.mark_changed(); + ctx.mark_changed(); } + element + } + + fn teardown( + &self, + (): &mut Self::ViewState, + ctx: &mut ViewCtx, + element: Mut<'_, Self::Element>, + ) { + ctx.teardown_leaf(element); } fn message( &self, - _view_state: &mut Self::ViewState, + (): &mut Self::ViewState, id_path: &[ViewId], - message: Box, + message: xilem_core::DynMessage, app_state: &mut State, ) -> MessageResult { debug_assert!( diff --git a/xilem/src/view/flex.rs b/xilem/src/view/flex.rs index 2149e0b9..9536360c 100644 --- a/xilem/src/view/flex.rs +++ b/xilem/src/view/flex.rs @@ -5,13 +5,14 @@ use std::marker::PhantomData; use masonry::{ widget::{self, Axis, CrossAxisAlignment, MainAxisAlignment, WidgetMut}, - Widget, WidgetPod, + Widget, }; +use xilem_core::{AppendVec, ElementSplice, Mut, View, ViewSequence}; -use crate::{ElementSplice, MasonryView, VecSplice, ViewSequence}; +use crate::{Pod, ViewCtx}; -// TODO: Allow configuring flex properties. I think this actually needs its own view trait? -pub fn flex(sequence: VT) -> Flex { +// TODO: Create a custom ViewSequence dynamic element for this +pub fn flex(sequence: Seq) -> Flex { Flex { phantom: PhantomData, sequence, @@ -22,8 +23,8 @@ pub fn flex(sequence: VT) -> Flex { } } -pub struct Flex { - sequence: VT, +pub struct Flex { + sequence: Seq, axis: Axis, cross_axis_alignment: CrossAxisAlignment, main_axis_alignment: MainAxisAlignment, @@ -31,7 +32,7 @@ pub struct Flex { phantom: PhantomData Marker>, } -impl Flex { +impl Flex { pub fn direction(mut self, axis: Axis) -> Self { self.axis = axis; self @@ -52,127 +53,147 @@ impl Flex { } } -impl MasonryView for Flex +impl View for Flex where - Seq: ViewSequence, + Seq: ViewSequence>, Marker>, { - type Element = widget::Flex; + type Element = Pod; + type ViewState = Seq::SeqState; - fn build( - &self, - cx: &mut crate::ViewCx, - ) -> (masonry::WidgetPod, Self::ViewState) { - let mut elements = Vec::new(); - let mut scratch = Vec::new(); - let mut splice = VecSplice::new(&mut elements, &mut scratch); - let seq_state = self.sequence.build(cx, &mut splice); - let mut view = widget::Flex::for_axis(self.axis) + fn build(&self, ctx: &mut ViewCtx) -> (Self::Element, Self::ViewState) { + let mut elements = AppendVec::default(); + let mut widget = widget::Flex::for_axis(self.axis) .cross_axis_alignment(self.cross_axis_alignment) + .with_default_spacer() .must_fill_main_axis(self.fill_major_axis) .main_axis_alignment(self.main_axis_alignment); - debug_assert!( - scratch.is_empty(), - // TODO: Not at all confident about this, but linear_layout makes this assumption - "ViewSequence shouldn't leave splice in strange state" - ); - for item in elements.drain(..) { - view = view.with_child_pod(item).with_default_spacer(); + let seq_state = self.sequence.seq_build(ctx, &mut elements); + for item in elements.into_inner() { + widget = widget.with_child_pod(item.inner).with_default_spacer(); } - (WidgetPod::new(view), seq_state) + (Pod::new(widget), seq_state) + } + + fn rebuild<'el>( + &self, + prev: &Self, + view_state: &mut Self::ViewState, + ctx: &mut ViewCtx, + mut element: Mut<'el, Self::Element>, + ) -> Mut<'el, Self::Element> { + if prev.axis != self.axis { + element.set_direction(self.axis); + ctx.mark_changed(); + } + if prev.cross_axis_alignment != self.cross_axis_alignment { + element.set_cross_axis_alignment(self.cross_axis_alignment); + ctx.mark_changed(); + } + if prev.main_axis_alignment != self.main_axis_alignment { + element.set_main_axis_alignment(self.main_axis_alignment); + ctx.mark_changed(); + } + if prev.fill_major_axis != self.fill_major_axis { + element.set_must_fill_main_axis(self.fill_major_axis); + ctx.mark_changed(); + } + // TODO: Re-use scratch space? + let mut splice = FlexSplice { + // Skip the initial spacer which is always present + ix: 1, + element: &mut element, + scratch: AppendVec::default(), + }; + self.sequence + .seq_rebuild(&prev.sequence, view_state, ctx, &mut splice); + debug_assert!(splice.scratch.into_inner().is_empty()); + element + } + + fn teardown( + &self, + view_state: &mut Self::ViewState, + ctx: &mut ViewCtx, + mut element: Mut<'_, Self::Element>, + ) { + let mut splice = FlexSplice { + // Skip the initial spacer which is always present + ix: 1, + element: &mut element, + scratch: AppendVec::default(), + }; + self.sequence.seq_teardown(view_state, ctx, &mut splice); + debug_assert!(splice.scratch.into_inner().is_empty()); } fn message( &self, view_state: &mut Self::ViewState, - id_path: &[crate::ViewId], - message: Box, + id_path: &[xilem_core::ViewId], + message: xilem_core::DynMessage, app_state: &mut State, - ) -> crate::MessageResult { + ) -> xilem_core::MessageResult { self.sequence - .message(view_state, id_path, message, app_state) - } - - fn rebuild( - &self, - view_state: &mut Self::ViewState, - cx: &mut crate::ViewCx, - prev: &Self, - mut element: widget::WidgetMut, - ) { - if prev.axis != self.axis { - element.set_direction(self.axis); - cx.mark_changed(); - } - if prev.cross_axis_alignment != self.cross_axis_alignment { - element.set_cross_axis_alignment(self.cross_axis_alignment); - cx.mark_changed(); - } - if prev.main_axis_alignment != self.main_axis_alignment { - element.set_main_axis_alignment(self.main_axis_alignment); - cx.mark_changed(); - } - if prev.fill_major_axis != self.fill_major_axis { - element.set_must_fill_main_axis(self.fill_major_axis); - cx.mark_changed(); - } - let mut splice = FlexSplice { ix: 0, element }; - self.sequence - .rebuild(view_state, cx, &prev.sequence, &mut splice); + .seq_message(view_state, id_path, message, app_state) } } -struct FlexSplice<'w> { +struct FlexSplice<'f, 'w> { ix: usize, - element: WidgetMut<'w, widget::Flex>, + element: &'f mut WidgetMut<'w, widget::Flex>, + scratch: AppendVec>>, } -impl ElementSplice for FlexSplice<'_> { - fn push(&mut self, element: WidgetPod>) { - self.element.insert_child_pod(self.ix, element); - self.element.insert_default_spacer(self.ix); +impl<'f> ElementSplice>> for FlexSplice<'f, '_> { + fn insert(&mut self, element: Pod>) { + self.element.insert_child_pod(self.ix, element.inner); + // Insert a spacer after the child + self.element.insert_default_spacer(self.ix + 1); self.ix += 2; } - - fn mutate(&mut self) -> WidgetMut> { - #[cfg(debug_assertions)] - let mut iterations = 0; - #[cfg(debug_assertions)] - let max = self.element.widget.len(); - loop { - #[cfg(debug_assertions)] - { - if iterations > max { - panic!("Got into infinite loop in FlexSplice::mutate"); - } - iterations += 1; - } - let child = self.element.child_mut(self.ix); - if child.is_some() { - break; - } - self.ix += 1; + fn with_scratch(&mut self, f: impl FnOnce(&mut AppendVec>>) -> R) -> R { + let ret = f(&mut self.scratch); + for element in self.scratch.drain() { + self.element.insert_child_pod(self.ix, element.inner); + self.element.insert_default_spacer(self.ix + 1); + self.ix += 2; } - let child = self.element.child_mut(self.ix).unwrap(); - self.ix += 1; - child + ret } - fn delete(&mut self, n: usize) { - let mut deleted_count = 0; - while deleted_count < n { - { - // TODO: use a drain/retain type method - let element = self.element.child_mut(self.ix); - if element.is_some() { - deleted_count += 1; - } - } - self.element.remove_child(self.ix); - } + fn mutate( + &mut self, + f: impl FnOnce(> as xilem_core::ViewElement>::Mut<'_>) -> R, + ) -> R { + let child = self + .element + .child_mut(self.ix) + .expect("ElementSplice::mutate won't overflow"); + let ret = f(child); + // Skip past the implicit spacer as well as this child + self.ix += 2; + ret } - fn len(&self) -> usize { - self.ix / 2 + fn delete( + &mut self, + f: impl FnOnce(> as xilem_core::ViewElement>::Mut<'_>) -> R, + ) -> R { + let child = self + .element + .child_mut(self.ix) + .expect("ElementSplice::mutate won't overflow"); + let ret = f(child); + self.element.remove_child(self.ix); + // Also remove the implicit spacer + // TODO: Make the spacers be explicit? + self.element.remove_child(self.ix); + + ret + } + + fn skip(&mut self, n: usize) { + self.ix += n * 2; } } diff --git a/xilem/src/view/label.rs b/xilem/src/view/label.rs index ea4ff0e3..d20934b9 100644 --- a/xilem/src/view/label.rs +++ b/xilem/src/view/label.rs @@ -1,9 +1,10 @@ // Copyright 2024 the Xilem Authors // SPDX-License-Identifier: Apache-2.0 -use masonry::{widget::WidgetMut, ArcStr, WidgetPod}; +use masonry::{widget, ArcStr}; +use xilem_core::Mut; -use crate::{Color, MasonryView, MessageResult, TextAlignment, ViewCx, ViewId}; +use crate::{Color, MessageResult, Pod, TextAlignment, View, ViewCtx, ViewId}; pub fn label(label: impl Into) -> Label { Label { @@ -39,49 +40,52 @@ impl Label { } } -impl MasonryView for Label { - type Element = masonry::widget::Label; +impl View for Label { + type Element = Pod; type ViewState = (); - fn build(&self, _cx: &mut ViewCx) -> (WidgetPod, Self::ViewState) { - let widget_pod = WidgetPod::new( - masonry::widget::Label::new(self.label.clone()) + fn build(&self, _ctx: &mut ViewCtx) -> (Self::Element, Self::ViewState) { + let widget_pod = Pod::new( + widget::Label::new(self.label.clone()) .with_text_brush(self.text_color) .with_text_alignment(self.alignment), ); (widget_pod, ()) } - fn rebuild( + fn rebuild<'el>( &self, - _view_state: &mut Self::ViewState, - cx: &mut ViewCx, prev: &Self, - mut element: WidgetMut, - ) { + (): &mut Self::ViewState, + ctx: &mut ViewCtx, + mut element: Mut<'el, Self::Element>, + ) -> Mut<'el, Self::Element> { if prev.label != self.label { element.set_text(self.label.clone()); - cx.mark_changed(); + ctx.mark_changed(); } // if prev.disabled != self.disabled { // element.set_disabled(self.disabled); - // cx.mark_changed(); + // ctx.mark_changed(); // } if prev.text_color != self.text_color { element.set_text_brush(self.text_color); - cx.mark_changed(); + ctx.mark_changed(); } if prev.alignment != self.alignment { element.set_alignment(self.alignment); - cx.mark_changed(); + ctx.mark_changed(); } + element } + fn teardown(&self, (): &mut Self::ViewState, _: &mut ViewCtx, _: Mut<'_, Self::Element>) {} + fn message( &self, - _view_state: &mut Self::ViewState, + (): &mut Self::ViewState, _id_path: &[ViewId], - message: Box, + message: xilem_core::DynMessage, _app_state: &mut State, ) -> crate::MessageResult { tracing::error!("Message arrived in Label::message, but Label doesn't consume any messages, this is a bug"); diff --git a/xilem/src/view/memoize.rs b/xilem/src/view/memoize.rs deleted file mode 100644 index ea8346ad..00000000 --- a/xilem/src/view/memoize.rs +++ /dev/null @@ -1,111 +0,0 @@ -// 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 { - data: D, - child_cb: F, -} - -pub struct MemoizeState> { - view: V, - view_state: V::ViewState, - dirty: bool, -} - -impl Memoize -where - F: Fn(&D) -> V, -{ - const ASSERT_CONTEXTLESS_FN: () = { - assert!( - std::mem::size_of::() == 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 MasonryView for Memoize -where - D: PartialEq + Send + Sync + 'static, - V: MasonryView, - F: Fn(&D) -> V + Send + Sync + 'static, -{ - type ViewState = MemoizeState; - - type Element = V::Element; - - fn build(&self, cx: &mut ViewCx) -> (WidgetPod, 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, - ) { - 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, - app_state: &mut State, - ) -> MessageResult { - 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(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(data: D, view: F) -> Memoize -where - F: Fn(&D) -> V + Send, -{ - Memoize::new(data, view) -} diff --git a/xilem/src/view/mod.rs b/xilem/src/view/mod.rs index 1e181c90..c7bda113 100644 --- a/xilem/src/view/mod.rs +++ b/xilem/src/view/mod.rs @@ -1,8 +1,6 @@ // Copyright 2024 the Xilem Authors // SPDX-License-Identifier: Apache-2.0 -mod arc; - mod button; pub use button::*; @@ -15,11 +13,10 @@ pub use flex::*; mod label; pub use label::*; -mod memoize; -pub use memoize::*; - mod prose; pub use prose::*; mod textbox; pub use textbox::*; + +pub use xilem_core::memoize; diff --git a/xilem/src/view/prose.rs b/xilem/src/view/prose.rs index 68571fbc..546a7cde 100644 --- a/xilem/src/view/prose.rs +++ b/xilem/src/view/prose.rs @@ -1,9 +1,10 @@ // Copyright 2024 the Xilem Authors // SPDX-License-Identifier: Apache-2.0 -use masonry::{text2::TextBrush, widget::WidgetMut, ArcStr, WidgetPod}; +use masonry::{text2::TextBrush, widget, ArcStr}; +use xilem_core::Mut; -use crate::{Color, MasonryView, MessageResult, TextAlignment, ViewCx, ViewId}; +use crate::{Color, MessageResult, Pod, TextAlignment, View, ViewCtx, ViewId}; pub fn prose(label: impl Into) -> Prose { Prose { @@ -40,52 +41,51 @@ impl Prose { } } -impl MasonryView for Prose { - type Element = masonry::widget::Prose; +impl View for Prose { + type Element = Pod; type ViewState = (); - fn build(&self, _cx: &mut ViewCx) -> (WidgetPod, Self::ViewState) { - let widget_pod = WidgetPod::new( - masonry::widget::Prose::new(self.label.clone()) + fn build(&self, _ctx: &mut ViewCtx) -> (Self::Element, Self::ViewState) { + let widget_pod = Pod::new( + widget::Prose::new(self.label.clone()) .with_text_brush(self.text_brush.clone()) .with_text_alignment(self.alignment), ); (widget_pod, ()) } - fn rebuild( + fn rebuild<'el>( &self, - _view_state: &mut Self::ViewState, - cx: &mut ViewCx, prev: &Self, - mut element: WidgetMut, - ) { + (): &mut Self::ViewState, + ctx: &mut ViewCtx, + mut element: Mut<'el, Self::Element>, + ) -> Mut<'el, Self::Element> { if prev.label != self.label { element.set_text(self.label.clone()); - cx.mark_changed(); + ctx.mark_changed(); } - // if prev.disabled != self.disabled { - // element.set_disabled(self.disabled); - // cx.mark_changed(); - // } if prev.text_brush != self.text_brush { element.set_text_brush(self.text_brush.clone()); - cx.mark_changed(); + ctx.mark_changed(); } if prev.alignment != self.alignment { element.set_alignment(self.alignment); - cx.mark_changed(); + ctx.mark_changed(); } + element } + fn teardown(&self, (): &mut Self::ViewState, _: &mut ViewCtx, _: Mut<'_, Self::Element>) {} + fn message( &self, _view_state: &mut Self::ViewState, _id_path: &[ViewId], - message: Box, + message: xilem_core::DynMessage, _app_state: &mut State, ) -> crate::MessageResult { - tracing::error!("Message arrived in Label::message, but Label doesn't consume any messages, this is a bug"); + tracing::error!("Message arrived in Prose::message, but Prose doesn't consume any messages, this is a bug"); MessageResult::Stale(message) } } diff --git a/xilem/src/view/textbox.rs b/xilem/src/view/textbox.rs index e0bf57ff..fd2b046b 100644 --- a/xilem/src/view/textbox.rs +++ b/xilem/src/view/textbox.rs @@ -1,9 +1,10 @@ // Copyright 2024 the Xilem Authors // SPDX-License-Identifier: Apache-2.0 -use masonry::{text2::TextBrush, widget::WidgetMut, WidgetPod}; +use masonry::{text2::TextBrush, widget}; +use xilem_core::{Mut, View}; -use crate::{Color, MasonryView, MessageResult, TextAlignment, ViewCx, ViewId}; +use crate::{Color, MessageResult, Pod, TextAlignment, ViewCtx, ViewId}; // FIXME - A major problem of the current approach (always setting the textbox contents) // is that if the user forgets to hook up the modify the state's contents in the callback, @@ -62,13 +63,13 @@ impl Textbox { } } -impl MasonryView for Textbox { - type Element = masonry::widget::Textbox; +impl View for Textbox { + type Element = Pod; type ViewState = (); - fn build(&self, cx: &mut ViewCx) -> (WidgetPod, Self::ViewState) { - cx.with_leaf_action_widget(|_| { - WidgetPod::new( + fn build(&self, ctx: &mut ViewCtx) -> (Self::Element, Self::ViewState) { + ctx.with_leaf_action_widget(|_| { + Pod::new( masonry::widget::Textbox::new(self.contents.clone()) .with_text_brush(self.text_brush.clone()) .with_text_alignment(self.alignment), @@ -76,44 +77,52 @@ impl MasonryView for Textbox( &self, - _view_state: &mut Self::ViewState, - cx: &mut ViewCx, prev: &Self, - mut element: WidgetMut, - ) { + _: &mut Self::ViewState, + ctx: &mut ViewCtx, + mut element: Mut<'el, Self::Element>, + ) -> Mut<'el, Self::Element> { // Unlike the other properties, we don't compare to the previous value; // instead, we compare directly to the element's text. This is to handle // cases like "Previous data says contents is 'fooba', user presses 'r', // now data and contents are both 'foobar' but previous data is 'fooba'" // without calling `set_text`. + + // This is probably not the right behaviour, but determining what is the right behaviour is hard if self.contents != element.text() { element.reset_text(self.contents.clone()); - cx.mark_changed(); + ctx.mark_changed(); } - // if prev.disabled != self.disabled { - // element.set_disabled(self.disabled); - // cx.mark_changed(); - // } if prev.text_brush != self.text_brush { element.set_text_brush(self.text_brush.clone()); - cx.mark_changed(); + ctx.mark_changed(); } if prev.alignment != self.alignment { element.set_alignment(self.alignment); - cx.mark_changed(); + ctx.mark_changed(); } + element + } + + fn teardown( + &self, + _: &mut Self::ViewState, + ctx: &mut ViewCtx, + element: Mut<'_, Self::Element>, + ) { + ctx.teardown_leaf(element); } fn message( &self, - _view_state: &mut Self::ViewState, + _: &mut Self::ViewState, id_path: &[ViewId], - message: Box, + message: xilem_core::DynMessage, app_state: &mut State, - ) -> crate::MessageResult { + ) -> MessageResult { debug_assert!( id_path.is_empty(), "id path should be empty in Textbox::message" diff --git a/xilem_core/.gitignore b/xilem_core/.gitignore new file mode 100644 index 00000000..13990ec3 --- /dev/null +++ b/xilem_core/.gitignore @@ -0,0 +1 @@ +/examples/filesystem diff --git a/xilem_core/Cargo.toml b/xilem_core/Cargo.toml index 00314009..061cfad3 100644 --- a/xilem_core/Cargo.toml +++ b/xilem_core/Cargo.toml @@ -4,18 +4,19 @@ version = "0.1.0" description = "Common core of the Xilem Rust UI framework." keywords = ["xilem", "ui", "reactive", "performance"] categories = ["gui"] -publish = false # Until it's ready edition.workspace = true license.workspace = true repository.workspace = true -homepage.workspace = true -[package.metadata.docs.rs] -all-features = true -# rustdoc-scrape-examples tracking issue https://github.com/rust-lang/rust/issues/88791 -cargo-args = ["-Zunstable-options", "-Zrustdoc-scrape-examples"] +publish = false # We'll publish this alongside Xilem 0.2 + +[dependencies] +tracing.workspace = true [lints] workspace = true -[dependencies] +[package.metadata.docs.rs] +default-target = "x86_64-unknown-linux-gnu" +# xilem_core is entirely platform-agnostic, so only display docs for one platform +targets = [] diff --git a/xilem_core/README.md b/xilem_core/README.md new file mode 100644 index 00000000..a2728378 --- /dev/null +++ b/xilem_core/README.md @@ -0,0 +1,77 @@ + +
+ +# Xilem Core + +
+ + + + + +
+ +**Reactivity primitives for Rust** + +[![Latest published version.](https://img.shields.io/crates/v/xilem_core.svg)](https://crates.io/crates/xilem_core) +[![Documentation build status.](https://img.shields.io/docsrs/xilem_core.svg)](https://docs.rs/xilem_core) +[![Apache 2.0 license.](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](#license) + +[![Linebender Zulip chat.](https://img.shields.io/badge/Linebender-%23xilem-blue?logo=Zulip)](https://xi.zulipchat.com/#narrow/stream/354396-xilem) +[![GitHub Actions CI status.](https://img.shields.io/github/actions/workflow/status/linebender/xilem/ci.yml?logo=github&label=CI)](https://github.com/linebender/xilem/actions) +[![Dependency staleness status.](https://deps.rs/crate/xilem_core/latest/status.svg)](https://deps.rs/crate/xilem_core) + +
+ +Xilem Core provides primitives which are used by [Xilem][] (a cross-platform GUI toolkit). +If you are using Xilem, [its documentation][xilem docs] will probably be more helpful for you. + +Xilem apps will interact with some of the functions from this crate, in particular [`memoize`][]. +Xilem apps which use custom widgets (and therefore must implement custom views), will implement the [`View`][] trait. + +If you wish to implement the Xilem pattern in a different domain (such as for a terminal user interface), this crate can be used to do so. + +## Hot reloading + +Xilem Core does not currently include infrastructure to enable hot reloading, but this is planned. +The current proposal would split the application into two processes: + + - The app process, which contains the app state and create the views, which would be extremely lightweight and can be recompiled and restarted quickly. + - The display process, which contains the widgets and would be long-lived, updating to match the new state of the view tree provided by the app process. + +## Quickstart + +## no_std support + +Xilem Core supports running with `#![no_std]`, but does require an allocator to be available. + +It is plausible that this reactivity pattern could be used without allocation being required, but that is not provided by this package. +If you wish to use Xilem Core in environments where an allocator is not available, feel free to bring this up on [Zulip](#community). + + + + +
+ +## Community + +Discussion of Xilem Core development happens in the [Linebender Zulip](https://xi.zulipchat.com/), specifically in +[#xilem](https://xi.zulipchat.com/#narrow/stream/354396-xilem). +All public content can be read without logging in. + +Contributions are welcome by pull request. The [Rust code of conduct][] applies. + +## License + +- Licensed under the Apache License, Version 2.0 + ([LICENSE] or ) + +
+ +[rust code of conduct]: https://www.rust-lang.org/policies/code-of-conduct + +[LICENSE]: LICENSE +[Xilem]: https://crates.io/crates/xilem +[xilem docs]: https://docs.rs/xilem/latest/xilem/ +[`memoize`]: https://docs.rs/xilem_core/latest/xilem_core/views/memoize/fn.memoize.html +[`View`]: https://docs.rs/xilem_core/latest/xilem_core/view/trait.View.html diff --git a/xilem_core/examples/filesystem.rs b/xilem_core/examples/filesystem.rs new file mode 100644 index 00000000..a87a4743 --- /dev/null +++ b/xilem_core/examples/filesystem.rs @@ -0,0 +1,213 @@ +// Copyright 2024 the Xilem Authors +// SPDX-License-Identifier: Apache-2.0 + +use std::{io::stdin, path::PathBuf}; + +use xilem_core::{ + AnyElement, AnyView, Mut, SuperElement, View, ViewElement, ViewId, ViewPathTracker, +}; + +#[derive(Debug)] +enum State { + Setup, + Empty, + Complex(String), +} + +fn complex_state(value: &str) -> impl FileView { + File { + name: value.to_string(), + contents: value.to_string(), + } +} + +fn app_logic(state: &mut State) -> impl FileView { + let res: DynFileView = match state { + State::Setup => Box::new(File { + name: "file1.txt".into(), + contents: "Test file contents".into(), + }), + State::Empty => + /* Box::new(Folder { + name: "nothing".into(), + seq: (), + }) */ + { + todo!() + } + State::Complex(value) => Box::new(complex_state(value.as_str())), + }; + res +} + +fn main() { + let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + path.push("examples/filesystem"); + if path.exists() { + std::fs::remove_dir_all(&path).expect("Could create directory"); + } + std::fs::create_dir(&path).expect("Could tidy up directory"); + let mut state = State::Setup; + + let mut previous = app_logic(&mut state); + let mut input_buf = String::new(); + let mut root_ctx = ViewCtx { + current_folder_path: path.clone(), + view_path: Vec::new(), + }; + let (mut element, mut initial_state) = previous.build(&mut root_ctx); + loop { + input_buf.clear(); + let read_count = stdin() + .read_line(&mut input_buf) + .expect("Could read from stdin"); + if read_count == 0 { + // Reached EOF, i.e. user has finished + break; + } + input_buf.make_ascii_lowercase(); + let input = input_buf.trim(); + match input { + "begin" => { + state = State::Setup; + } + "clear" => { + state = State::Empty; + } + complex if complex.starts_with("complex ") => { + state = State::Complex(complex.strip_prefix("complex ").unwrap().into()); + } + other => { + eprint!("Unknown command {other:?}. Please try again:"); + continue; + } + }; + let new_view = app_logic(&mut state); + root_ctx.current_folder_path.clone_from(&path); + new_view.rebuild(&previous, &mut initial_state, &mut root_ctx, &mut element.0); + previous = new_view; + } +} + +trait FileView: View {} + +impl FileView for V where + V: View +{ +} + +type DynFileView = Box>; + +impl SuperElement for FsPath { + fn upcast(child: FsPath) -> Self { + child + } + + fn with_downcast_val( + this: Self::Mut<'_>, + f: impl FnOnce(Mut<'_, FsPath>) -> R, + ) -> (Self::Mut<'_>, R) { + let ret = f(this); + (this, ret) + } +} + +impl AnyElement for FsPath { + fn replace_inner(this: Self::Mut<'_>, child: FsPath) -> Self::Mut<'_> { + *this = child.0; + this + } +} + +// Folder is meant to showcase ViewSequence, but isn't currently wired up +// struct Folder> { +// name: String, +// seq: Seq, +// phantom: PhantomData Marker>, +// } + +#[derive(Clone)] +struct File { + name: String, + contents: String, +} + +struct FsPath(PathBuf); + +impl From for FsPath { + fn from(value: PathBuf) -> Self { + Self(value) + } +} + +impl ViewElement for FsPath { + // TODO: This data is pretty redundant + type Mut<'a> = &'a mut PathBuf; +} + +impl View for File { + type Element = FsPath; + type ViewState = (); + + fn build(&self, ctx: &mut ViewCtx) -> (Self::Element, Self::ViewState) { + let path = ctx.current_folder_path.join(&*self.name); + + // TODO: How to handle errors here? + let _ = std::fs::write(&path, self.contents.as_bytes()); + (path.into(), ()) + } + + fn rebuild<'el>( + &self, + prev: &Self, + _view_state: &mut Self::ViewState, + ctx: &mut ViewCtx, + element: Mut<'el, Self::Element>, + ) -> Mut<'el, Self::Element> { + if prev.name != self.name { + let new_path = ctx.current_folder_path.join(&*self.name); + let _ = std::fs::rename(&element, &new_path); + *element = new_path; + } + if self.contents != prev.contents { + let _ = std::fs::write(&element, self.contents.as_bytes()); + } + element + } + + fn teardown( + &self, + _view_state: &mut Self::ViewState, + _ctx: &mut ViewCtx, + element: Mut<'_, Self::Element>, + ) { + let _ = std::fs::remove_file(element); + } + + fn message( + &self, + _view_state: &mut Self::ViewState, + _id_path: &[ViewId], + _message: xilem_core::DynMessage, + _app_state: &mut State, + ) -> xilem_core::MessageResult { + unreachable!() + } +} + +struct ViewCtx { + view_path: Vec, + current_folder_path: PathBuf, +} + +impl ViewPathTracker for ViewCtx { + fn push_id(&mut self, id: ViewId) { + self.view_path.push(id); + } + fn pop_id(&mut self) { + self.view_path.pop(); + } + fn view_path(&mut self) -> &[ViewId] { + &self.view_path + } +} diff --git a/xilem_core/examples/user_interface.rs b/xilem_core/examples/user_interface.rs new file mode 100644 index 00000000..b7f151d5 --- /dev/null +++ b/xilem_core/examples/user_interface.rs @@ -0,0 +1,150 @@ +// Copyright 2024 the Xilem Authors +// SPDX-License-Identifier: Apache-2.0 + +//! Model version of Masonry for exploration + +use core::any::Any; + +use xilem_core::{ + DynMessage, MessageResult, Mut, SuperElement, View, ViewElement, ViewId, ViewPathTracker, +}; + +pub fn app_logic(_: &mut u32) -> impl WidgetView { + Button {} +} + +pub fn main() { + let view = app_logic(&mut 10); + let mut ctx = ViewCtx { path: vec![] }; + let (_widget_tree, _state) = view.build(&mut ctx); + // TODO: dbg!(widget_tree); +} + +// Toy version of Masonry +pub trait Widget: 'static + Any { + fn as_mut_any(&mut self) -> &mut dyn Any; +} +pub struct WidgetPod { + widget: W, +} +pub struct WidgetMut<'a, W: Widget> { + value: &'a mut W, +} +impl Widget for Box { + fn as_mut_any(&mut self) -> &mut dyn Any { + self + } +} + +// Model version of xilem_masonry (`xilem`) + +// Hmm, this implementation can't exist in `xilem` if `xilem_core` and/or `masonry` are a different crate +// due to the orphan rules... +impl ViewElement for WidgetPod { + type Mut<'a> = WidgetMut<'a, W>; +} + +impl View for Button { + type Element = WidgetPod; + type ViewState = (); + + fn build(&self, _ctx: &mut ViewCtx) -> (Self::Element, Self::ViewState) { + ( + WidgetPod { + widget: ButtonWidget {}, + }, + (), + ) + } + + fn rebuild<'el>( + &self, + _prev: &Self, + _view_state: &mut Self::ViewState, + _ctx: &mut ViewCtx, + element: Mut<'el, Self::Element>, + ) -> Mut<'el, Self::Element> { + // Nothing to do + element + } + + fn teardown( + &self, + _view_state: &mut Self::ViewState, + _ctx: &mut ViewCtx, + _element: Mut<'_, Self::Element>, + ) { + // Nothing to do + } + + fn message( + &self, + _view_state: &mut Self::ViewState, + _id_path: &[ViewId], + _message: DynMessage, + _app_state: &mut State, + ) -> MessageResult { + MessageResult::Nop + } +} + +pub struct Button {} + +pub struct ButtonWidget {} +impl Widget for ButtonWidget { + fn as_mut_any(&mut self) -> &mut dyn Any { + self + } +} + +impl SuperElement> for WidgetPod> { + fn upcast(child: WidgetPod) -> Self { + WidgetPod { + widget: Box::new(child.widget), + } + } + fn with_downcast_val( + this: Self::Mut<'_>, + f: impl FnOnce( as ViewElement>::Mut<'_>) -> R, + ) -> (Self::Mut<'_>, R) { + let value = WidgetMut { + value: this.value.as_mut_any().downcast_mut().expect( + "this widget should have been created from a child widget of type `W` in `Self::upcast`", + ), + }; + let ret = f(value); + (this, ret) + } +} + +pub struct ViewCtx { + path: Vec, +} + +impl ViewPathTracker for ViewCtx { + fn push_id(&mut self, id: ViewId) { + self.path.push(id); + } + + fn pop_id(&mut self) { + self.path.pop(); + } + + fn view_path(&mut self) -> &[ViewId] { + &self.path + } +} + +pub trait WidgetView: + View> + Send + Sync +{ + type Widget: Widget + Send + Sync; +} + +impl WidgetView for V +where + V: View> + Send + Sync, + W: Widget + Send + Sync, +{ + type Widget = W; +} diff --git a/xilem_core/src/any_view.rs b/xilem_core/src/any_view.rs index 172f1aa4..559e7b50 100644 --- a/xilem_core/src/any_view.rs +++ b/xilem_core/src/any_view.rs @@ -1,137 +1,350 @@ // Copyright 2023 the Xilem Authors // SPDX-License-Identifier: Apache-2.0 -#[macro_export] -macro_rules! generate_anyview_trait { - ($anyview:ident, $viewtrait:ident, $viewmarker:ty, $cx:ty, $changeflags:ty, $anywidget:ident, $boxedview:ident; $($ss:tt)*) => { - /// A trait enabling type erasure of views. - pub trait $anyview { - fn as_any(&self) -> &dyn std::any::Any; +//! Support for a type erased [`View`]. - fn dyn_build( - &self, - cx: &mut $cx, - ) -> ($crate::Id, Box, Box); +use core::any::Any; - fn dyn_rebuild( - &self, - cx: &mut $cx, - prev: &dyn $anyview, - id: &mut $crate::Id, - state: &mut Box, - element: &mut Box, - ) -> $changeflags; +use alloc::boxed::Box; - fn dyn_message( - &self, - id_path: &[$crate::Id], - state: &mut dyn std::any::Any, - message: Box, - app_state: &mut T, - ) -> $crate::MessageResult
; - } +use crate::{AnyElement, DynMessage, MessageResult, Mut, View, ViewId, ViewPathTracker}; - impl + 'static> $anyview for V - where - V::State: 'static, - V::Element: $anywidget + 'static, - { - fn as_any(&self) -> &dyn std::any::Any { - self - } +/// A view which can have any view type where the [`View::Element`] is compatible with +/// `Element`. +/// +/// This is primarily used for type erasure of views, and is not expected to be implemented +/// by end-users. Instead a blanket implementation exists for all applicable [`View`]s. +/// +/// This is useful for a view which can be any of several view types, by using +/// `Box>`, which implements [`View`]. +// TODO: Mention `Either` when we have implemented that? +/// +/// This is also useful for memoization, by storing an `Option>>`, +/// then [inserting](Option::get_or_insert_with) into that option at view tree construction time. +/// +/// Libraries using `xilem_core` are expected to have a type alias for their own `AnyView`, which specifies +/// the `Context` and `Element` types. +pub trait AnyView { + /// Get an [`Any`] reference to `self`. + fn as_any(&self) -> &dyn Any; - fn dyn_build( - &self, - cx: &mut $cx, - ) -> ($crate::Id, Box, Box) { - let (id, state, element) = self.build(cx); - (id, Box::new(state), Box::new(element)) - } + /// Type erased [`View::build`]. + fn dyn_build(&self, ctx: &mut Context) -> (Element, AnyViewState); - fn dyn_rebuild( - &self, - cx: &mut $cx, - prev: &dyn $anyview, - id: &mut $crate::Id, - state: &mut Box, - element: &mut Box, - ) -> ChangeFlags { - use std::ops::DerefMut; - if let Some(prev) = prev.as_any().downcast_ref() { - if let Some(state) = state.downcast_mut() { - if let Some(element) = element.deref_mut().as_any_mut().downcast_mut() { - self.rebuild(cx, prev, id, state, element) - } else { - eprintln!("downcast of element failed in dyn_rebuild"); - <$changeflags>::default() - } - } else { - eprintln!("downcast of state failed in dyn_rebuild"); - <$changeflags>::default() - } - } else { - let (new_id, new_state, new_element) = self.build(cx); - *id = new_id; - *state = Box::new(new_state); - *element = Box::new(new_element); - <$changeflags>::tree_structure() - } - } + /// Type erased [`View::rebuild`]. + fn dyn_rebuild<'el>( + &self, + dyn_state: &mut AnyViewState, + ctx: &mut Context, + prev: &dyn AnyView, + element: Element::Mut<'el>, + ) -> Element::Mut<'el>; - fn dyn_message( - &self, - id_path: &[$crate::Id], - state: &mut dyn std::any::Any, - message: Box, - app_state: &mut T, - ) -> $crate::MessageResult { - if let Some(state) = state.downcast_mut() { - self.message(id_path, state, message, app_state) - } else { - // Possibly softer failure? - panic!("downcast error in dyn_event"); - } - } - } + /// Type erased [`View::teardown`]. + /// + /// Returns `Element::Mut<'el>` so that the element's inner value can be replaced in `dyn_rebuild`. + fn dyn_teardown<'el>( + &self, + dyn_state: &mut AnyViewState, + ctx: &mut Context, + element: Element::Mut<'el>, + ) -> Element::Mut<'el>; - pub type $boxedview = Box $( $ss )* >; - - impl $viewmarker for $boxedview {} - - impl $viewtrait for $boxedview { - type State = Box; - - type Element = Box; - - fn build(&self, cx: &mut $cx) -> ($crate::Id, Self::State, Self::Element) { - use std::ops::Deref; - self.deref().dyn_build(cx) - } - - fn rebuild( - &self, - cx: &mut $cx, - prev: &Self, - id: &mut $crate::Id, - state: &mut Self::State, - element: &mut Self::Element, - ) -> $changeflags { - use std::ops::Deref; - self.deref() - .dyn_rebuild(cx, prev.deref(), id, state, element) - } - - fn message( - &self, - id_path: &[$crate::Id], - state: &mut Self::State, - message: Box, - app_state: &mut T, - ) -> $crate::MessageResult { - use std::ops::{Deref, DerefMut}; - self.deref() - .dyn_message(id_path, state.deref_mut(), message, app_state) - } - } - }; + /// Type erased [`View::message`]. + fn dyn_message( + &self, + dyn_state: &mut AnyViewState, + id_path: &[ViewId], + message: DynMessage, + app_state: &mut State, + ) -> MessageResult; +} + +impl AnyView + for V +where + DynamicElement: AnyElement, + Context: ViewPathTracker, + V: View + 'static, + V::ViewState: 'static, +{ + fn as_any(&self) -> &dyn Any { + self + } + + fn dyn_build(&self, ctx: &mut Context) -> (DynamicElement, AnyViewState) { + let generation = 0; + let (element, view_state) = ctx.with_id(ViewId::new(generation), |ctx| self.build(ctx)); + ( + DynamicElement::upcast(element), + AnyViewState { + inner_state: Box::new(view_state), + generation, + }, + ) + } + + fn dyn_rebuild<'el>( + &self, + dyn_state: &mut AnyViewState, + ctx: &mut Context, + prev: &dyn AnyView, + mut element: DynamicElement::Mut<'el>, + ) -> DynamicElement::Mut<'el> { + if let Some(prev) = prev.as_any().downcast_ref() { + // If we were previously of this type, then do a normal rebuild + DynamicElement::with_downcast(element, |element| { + let state = dyn_state + .inner_state + .downcast_mut() + .expect("build or rebuild always set the correct corresponding state type"); + + ctx.with_id(ViewId::new(dyn_state.generation), move |ctx| { + self.rebuild(prev, state, ctx, element); + }); + }) + } else { + // Otherwise, teardown the old element, then replace the value + // Note that we need to use `dyn_teardown` here, because `prev` + // is of a different type. + element = prev.dyn_teardown(dyn_state, ctx, element); + + // Increase the generation, because the underlying widget has been swapped out. + // Overflow condition: Impossible to overflow, as u64 only ever incremented by 1 + // and starting at 0. + dyn_state.generation = dyn_state.generation.wrapping_add(1); + let (new_element, view_state) = + ctx.with_id(ViewId::new(dyn_state.generation), |ctx| self.build(ctx)); + dyn_state.inner_state = Box::new(view_state); + DynamicElement::replace_inner(element, new_element) + } + } + fn dyn_teardown<'el>( + &self, + dyn_state: &mut AnyViewState, + ctx: &mut Context, + element: DynamicElement::Mut<'el>, + ) -> DynamicElement::Mut<'el> { + let state = dyn_state + .inner_state + .downcast_mut() + .expect("build or rebuild always set the correct corresponding state type"); + + // We only need to teardown the inner value - there's no other state to cleanup in this widget + DynamicElement::with_downcast(element, |element| { + ctx.with_id(ViewId::new(dyn_state.generation), |ctx| { + self.teardown(state, ctx, element); + }); + }) + } + + fn dyn_message( + &self, + dyn_state: &mut AnyViewState, + id_path: &[ViewId], + message: DynMessage, + app_state: &mut State, + ) -> MessageResult { + let state = dyn_state + .inner_state + .downcast_mut() + .expect("build or rebuild always set the correct corresponding state type"); + let Some((first, remainder)) = id_path.split_first() else { + unreachable!("Parent view of `AnyView` sent outdated and/or incorrect empty view path"); + }; + if first.routing_id() != dyn_state.generation { + // Do we want to log something here? + return MessageResult::Stale(message); + } + self.message(state, remainder, message, app_state) + } +} + +/// The state used by [`AnyView`]. +#[doc(hidden)] +pub struct AnyViewState { + inner_state: Box, + /// The generation is the value which is shown + generation: u64, +} + +impl View + for dyn AnyView +where + // Element must be `static` so it can be downcasted + Element: crate::ViewElement + 'static, + Context: crate::ViewPathTracker + 'static, +{ + type Element = Element; + + type ViewState = AnyViewState; + + fn build(&self, ctx: &mut Context) -> (Self::Element, Self::ViewState) { + self.dyn_build(ctx) + } + + fn rebuild<'el>( + &self, + prev: &Self, + view_state: &mut Self::ViewState, + ctx: &mut Context, + element: Mut<'el, Self::Element>, + ) -> Mut<'el, Self::Element> { + self.dyn_rebuild(view_state, ctx, prev, element) + } + + fn teardown( + &self, + view_state: &mut Self::ViewState, + ctx: &mut Context, + element: Mut<'_, Self::Element>, + ) { + self.dyn_teardown(view_state, ctx, element); + } + + fn message( + &self, + view_state: &mut Self::ViewState, + id_path: &[crate::ViewId], + message: crate::DynMessage, + app_state: &mut State, + ) -> crate::MessageResult { + self.dyn_message(view_state, id_path, message, app_state) + } +} + +// TODO: IWBN if we could avoid this +impl View + for dyn AnyView + Send +where + // Element must be `static` so it can be downcasted + Element: crate::ViewElement + 'static, + Context: crate::ViewPathTracker + 'static, +{ + type Element = Element; + + type ViewState = AnyViewState; + + fn build(&self, ctx: &mut Context) -> (Self::Element, Self::ViewState) { + self.dyn_build(ctx) + } + + fn rebuild<'el>( + &self, + prev: &Self, + view_state: &mut Self::ViewState, + ctx: &mut Context, + element: Mut<'el, Self::Element>, + ) -> Mut<'el, Self::Element> { + self.dyn_rebuild(view_state, ctx, prev, element) + } + + fn teardown( + &self, + view_state: &mut Self::ViewState, + ctx: &mut Context, + element: Mut<'_, Self::Element>, + ) { + self.dyn_teardown(view_state, ctx, element); + } + + fn message( + &self, + view_state: &mut Self::ViewState, + id_path: &[crate::ViewId], + message: crate::DynMessage, + app_state: &mut State, + ) -> crate::MessageResult { + self.dyn_message(view_state, id_path, message, app_state) + } +} + +impl View + for dyn AnyView + Send + Sync +where + // Element must be `static` so it can be downcasted + Element: crate::ViewElement + 'static, + Context: crate::ViewPathTracker + 'static, +{ + type Element = Element; + + type ViewState = AnyViewState; + + fn build(&self, ctx: &mut Context) -> (Self::Element, Self::ViewState) { + self.dyn_build(ctx) + } + + fn rebuild<'el>( + &self, + prev: &Self, + view_state: &mut Self::ViewState, + ctx: &mut Context, + element: Mut<'el, Self::Element>, + ) -> Mut<'el, Self::Element> { + self.dyn_rebuild(view_state, ctx, prev, element) + } + + fn teardown( + &self, + view_state: &mut Self::ViewState, + ctx: &mut Context, + element: Mut<'_, Self::Element>, + ) { + self.dyn_teardown(view_state, ctx, element); + } + + fn message( + &self, + view_state: &mut Self::ViewState, + id_path: &[crate::ViewId], + message: crate::DynMessage, + app_state: &mut State, + ) -> crate::MessageResult { + self.dyn_message(view_state, id_path, message, app_state) + } +} + +impl View + for dyn AnyView + Sync +where + // Element must be `static` so it can be downcasted + Element: crate::ViewElement + 'static, + Context: crate::ViewPathTracker + 'static, +{ + type Element = Element; + + type ViewState = AnyViewState; + + fn build(&self, ctx: &mut Context) -> (Self::Element, Self::ViewState) { + self.dyn_build(ctx) + } + + fn rebuild<'el>( + &self, + prev: &Self, + view_state: &mut Self::ViewState, + ctx: &mut Context, + element: Mut<'el, Self::Element>, + ) -> Mut<'el, Self::Element> { + self.dyn_rebuild(view_state, ctx, prev, element) + } + + fn teardown( + &self, + view_state: &mut Self::ViewState, + ctx: &mut Context, + element: Mut<'_, Self::Element>, + ) { + self.dyn_teardown(view_state, ctx, element); + } + + fn message( + &self, + view_state: &mut Self::ViewState, + id_path: &[crate::ViewId], + message: crate::DynMessage, + app_state: &mut State, + ) -> crate::MessageResult { + self.dyn_message(view_state, id_path, message, app_state) + } } diff --git a/xilem_core/src/element.rs b/xilem_core/src/element.rs new file mode 100644 index 00000000..fe5cbd5c --- /dev/null +++ b/xilem_core/src/element.rs @@ -0,0 +1,84 @@ +// Copyright 2024 the Xilem Authors +// SPDX-License-Identifier: Apache-2.0 + +//! The types which can be used as elements in a [`View`](crate::View) + +/// A type which can be used as the `Element` associated type for a [`View`](crate::View). +/// +/// It is expected that most libraries using `xilem_core` will have a generic +/// implementation of this trait for their widget type. +/// Additionally, this may also be implemented for other types, depending on the +/// needs of the specific parent view. +/// In Xilem (the user interface library), this is also used for types containing the +/// flex properties of their child views, and window properties. +/// +/// In most cases, there will be a corresponding implementation of [`SuperElement`] for +/// some other type. +/// This will be the generic form of this type, which is used for the implementation of [`AnyView`]. +/// +/// [`AnyView`]: crate::AnyView +/// +// TODO: Rename so that it doesn't conflict with the type parameter names +pub trait ViewElement { + /// The reference form of this `Element` for editing. + /// + /// This is provided to [`View::rebuild`](crate::View::rebuild) and + /// [`View::teardown`](crate::View::teardown). + /// This enables greater flexibility in the use of the traits, such as + /// for reference types which contain access to parent state. + type Mut<'a>; +} + +/// This alias is syntax sugar to avoid the elaborate expansion of +/// `::Mut<'el>` in the View trait when implementing it (e.g. via rust-analyzer) +pub type Mut<'el, E> = ::Mut<'el>; + +/// This element type is a superset of `Child`. +/// +/// There are two primary use cases for this type: +/// 1) The dynamic form of the element type, used for [`AnyView`] and [`ViewSequence`]s. +/// 2) Additional, optional, information which can be added to an element type. +/// This will primarily be used in [`ViewSequence`] implementations. +/// +/// [`AnyView`]: crate::AnyView +/// [`ViewSequence`]: crate::ViewSequence +pub trait SuperElement: ViewElement +where + Child: ViewElement, +{ + /// Convert from the child to this element type. + fn upcast(child: Child) -> Self; + + /// Perform a reborrowing downcast to the child reference type. + /// + /// This may panic if `this` is not the reference form of a value created by + /// `Self::upcast`. + /// For example, this may perform a downcasting operation, which would fail + /// if the value is not of the expected type. + /// You can safely use this methods in contexts where it is known that the + /// + /// If you need to return a value, see [`with_downcast_val`](SuperElement::with_downcast_val). + fn with_downcast(this: Self::Mut<'_>, f: impl FnOnce(Child::Mut<'_>)) -> Self::Mut<'_> { + let (this, ()) = Self::with_downcast_val(this, f); + this + } + /// Perform a reborrowing downcast. + /// + /// This may panic if `this` is not the reference form of a value created by + /// `Self::upcast`. + /// + /// If you don't need to return a value, see [`with_downcast`](SuperElement::with_downcast). + fn with_downcast_val( + this: Self::Mut<'_>, + f: impl FnOnce(Child::Mut<'_>) -> R, + ) -> (Self::Mut<'_>, R); +} + +/// An element which can be used for an [`AnyView`](crate::AnyView) containing `Child`. +pub trait AnyElement: SuperElement +where + Child: ViewElement, +{ + /// Replace the inner value of this reference entirely + fn replace_inner(this: Self::Mut<'_>, child: Child) -> Self::Mut<'_>; +} diff --git a/xilem_core/src/lib.rs b/xilem_core/src/lib.rs index afe57a0d..52f0a255 100644 --- a/xilem_core/src/lib.rs +++ b/xilem_core/src/lib.rs @@ -1,27 +1,48 @@ // Copyright 2022 the Xilem Authors // SPDX-License-Identifier: Apache-2.0 -//! Generic implementation of Xilem view traits. -//! -//! This crate has a few basic types needed to support views, and also -//! a set of macros used to instantiate the main view traits. The client -//! will need to supply a bound on elements, a "pod" type which -//! supports dynamic dispatching and marking of change flags, and a -//! context. -//! -//! All this is still experimental. This crate is where more of the core -//! Xilem architecture will land (some of which was implemented in the -//! original prototype but not yet ported): adapt, memoize, use_state, -//! and possibly some async logic. Likely most of env will also land -//! here, but that also requires coordination with the context. +#![cfg_attr(not(test), no_std)] +#![forbid(unsafe_code)] +#![warn(missing_docs, unreachable_pub)] +// TODO: Point at documentation for this pattern of README include. +// It has some neat advantages but is quite esoteric +#![doc = concat!( +" + +[LICENSE]: https://github.com/linebender/xilem/blob/main/xilem_core/LICENSE + + + +[`alloc`]: alloc +[`View`]: crate::View +[`memoize`]: memoize + + + + +
+", + include_str!("../README.md"), +)] + +extern crate alloc; + +mod view; +pub use view::{View, ViewId, ViewPathTracker}; + +mod views; +pub use views::{memoize, Memoize}; + +mod message; +pub use message::{DynMessage, Message, MessageResult}; + +mod element; +pub use element::{AnyElement, Mut, SuperElement, ViewElement}; mod any_view; -mod id; -mod message; -mod sequence; -mod vec_splice; -mod view; +pub use any_view::AnyView; -pub use id::{Id, IdPath}; -pub use message::{AsyncWake, MessageResult}; -pub use vec_splice::VecSplice; +mod sequence; +pub use sequence::{AppendVec, ElementSplice, ViewSequence}; diff --git a/xilem_core/src/message.rs b/xilem_core/src/message.rs index ac6bdb5a..af2ee7a6 100644 --- a/xilem_core/src/message.rs +++ b/xilem_core/src/message.rs @@ -1,74 +1,157 @@ -// Copyright 2022 the Xilem Authors +// Copyright 2024 the Xilem Authors // SPDX-License-Identifier: Apache-2.0 -use std::any::Any; +//! Message routing and type erasure primitives. -#[macro_export] -macro_rules! message { - ($($bounds:tt)*) => { - pub struct Message { - pub id_path: xilem_core::IdPath, - pub body: Box, - } +use core::{any::Any, fmt::Debug, ops::Deref}; - impl Message { - pub fn new(id_path: xilem_core::IdPath, event: impl std::any::Any + $($bounds)*) -> Message { - Message { - id_path, - body: Box::new(event), - } - } - } - }; -} +use alloc::boxed::Box; -/// A result wrapper type for event handlers. +/// The possible outcomes from a [`View::message`] +/// +/// [`View::message`]: crate::View::message #[derive(Default)] -pub enum MessageResult { - /// The event handler was invoked and returned an action. +pub enum MessageResult { + /// An action for a parent message handler to use /// - /// Use this return type if your widgets should respond to events by passing - /// a value up the tree, rather than changing their internal state. - Action(A), - /// The event handler received a change request that requests a rebuild. - /// - /// Note: A rebuild will always occur if there was a state change. This return - /// type can be used to indicate that a full rebuild is necessary even if the - /// state remained the same. It is expected that this type won't be used very - /// often. - #[allow(unused)] + /// This allows for sub-sections of your app to use an elm-like architecture + Action(Action), + // TODO: What does this mean? + /// This message's handler needs a rebuild to happen. + /// The exact semantics of this method haven't been determined. RequestRebuild, - /// The event handler discarded the event. - /// - /// This is the variant that you **almost always want** when you're not returning - /// an action. - #[allow(unused)] #[default] + /// This event had no impact on the app state, or the impact it did have + /// does not require the element tree to be recreated. Nop, - /// The event was addressed to an id path no longer in the tree. + /// The view this message was being routed to no longer exists. + Stale(DynMessage), +} + +/// A dynamically typed message for the [`View`] trait. +/// +/// Mostly equivalent to `Box`, but with support for debug printing. +// We can't use intra-doc links here because of +/// The primary interface for this type is [`dyn Message::downcast`](trait.Message.html#method.downcast). +/// +/// These messages must also be [`Send`]. +/// This makes using this message type in a multithreaded context easier. +/// If this requirement is causing you issues, feel free to open an issue +/// to discuss. +/// We are aware of potential backwards-compatible workarounds, but +/// are not aware of any tangible need for this. +/// +/// [`View`]: crate::View +pub type DynMessage = Box; +/// Types which can be contained in a [`DynMessage`]. +// The `View` trait could have been made generic over the message type, +// primarily to enable flexibility around Send/Sync and avoid the need +// for allocation. +pub trait Message: 'static + Send { + /// Convert `self` into a [`Box`]. + fn into_any(self: Box) -> Box; + /// Convert `self` into a [`Box`]. + fn as_any(&self) -> &(dyn Any + Send); + /// Gets the debug representation of this message. + fn dyn_debug(&self) -> &dyn Debug; +} + +impl Message for T +where + T: Any + Debug + Send, +{ + fn into_any(self: Box) -> Box { + self + } + fn as_any(&self) -> &(dyn Any + Send) { + self + } + fn dyn_debug(&self) -> &dyn Debug { + self + } +} + +impl dyn Message { + /// Access the actual type of this [`DynMessage`]. /// - /// This is a normal outcome for async operation when the tree is changing - /// dynamically, but otherwise indicates a logic error. - Stale(Box), -} - -// TODO: does this belong in core? -pub struct AsyncWake; - -impl MessageResult { - pub fn map(self, f: impl FnOnce(A) -> B) -> MessageResult { - match self { - MessageResult::Action(a) => MessageResult::Action(f(a)), - MessageResult::RequestRebuild => MessageResult::RequestRebuild, - MessageResult::Stale(event) => MessageResult::Stale(event), - MessageResult::Nop => MessageResult::Nop, - } - } - - pub fn or(self, f: impl FnOnce(Box) -> Self) -> Self { - match self { - MessageResult::Stale(event) => f(event), - _ => self, + /// In most cases, this will be unwrapped, as each [`View`](crate::View) will + /// coordinate with their runner and/or element type to only receive messages + /// of a single, expected, underlying type. + /// + /// ## Errors + /// + /// If the message contained within `self` is not of type `T`, returns `self` + /// (so that e.g. a different type can be used) + pub fn downcast(self: Box) -> Result, Box> { + // The panic is unreachable + #![allow(clippy::missing_panics_doc)] + if self.deref().as_any().is::() { + Ok(self + .into_any() + .downcast::() + .expect("`as_any` should correspond with `into_any`")) + } else { + Err(self) } } } + +impl Debug for dyn Message { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + let inner = self.dyn_debug(); + f.debug_tuple("Message").field(&inner).finish() + } +} + +/* /// Types which can route a message to a child [`View`]. +// TODO: This trait needs to exist for desktop hot reloading +// This would be a supertrait of View +pub trait ViewMessage { + type ViewState; +} +*/ + +#[cfg(test)] +mod tests { + use core::fmt::Debug; + + use alloc::boxed::Box; + + use crate::DynMessage; + + struct MyMessage(String); + + impl Debug for MyMessage { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.write_str("A present message") + } + } + + #[derive(Debug)] + struct NotMyMessage; + + #[test] + /// Downcasting a message to the correct type should work + fn message_downcast() { + let message: DynMessage = Box::new(MyMessage("test".to_string())); + let result: Box = message.downcast().unwrap(); + assert_eq!(&result.0, "test"); + } + #[test] + /// Downcasting a message to the wrong type shouldn't panic + fn message_downcast_wrong_type() { + let message: DynMessage = Box::new(MyMessage("test".to_string())); + let message = message.downcast::().unwrap_err(); + let result: Box = message.downcast().unwrap(); + assert_eq!(&result.0, "test"); + } + + #[test] + /// DynMessage's debug should pass through the debug implementation of + fn message_debug() { + let message: DynMessage = Box::new(MyMessage("".to_string())); + let debug_result = format!("{message:?}"); + // Note that we + assert!(debug_result.contains("A present message")); + } +} diff --git a/xilem_core/src/sequence.rs b/xilem_core/src/sequence.rs index 5773fcca..cd0dedef 100644 --- a/xilem_core/src/sequence.rs +++ b/xilem_core/src/sequence.rs @@ -1,363 +1,701 @@ // Copyright 2023 the Xilem Authors // SPDX-License-Identifier: Apache-2.0 -#[doc(hidden)] -#[macro_export] -macro_rules! impl_view_tuple { - ( $viewseq:ident, $elements_splice: ident, $pod:ty, $cx:ty, $changeflags:ty, $( $t:ident),* ; $( $i:tt ),* ) => { - impl ),* > $viewseq for ( $( $t, )* ) { - type State = ( $( $t::State, )*); +//! Support for sequences of views with a shared element type. - fn build(&self, cx: &mut $cx, elements: &mut dyn $elements_splice) -> Self::State { - let b = ( $( self.$i.build(cx, elements), )* ); - let state = ( $( b.$i, )*); - state - } +use core::sync::atomic::AtomicBool; +use core::sync::atomic::Ordering; - fn rebuild( - &self, - cx: &mut $cx, - prev: &Self, - state: &mut Self::State, - els: &mut dyn $elements_splice, - ) -> ChangeFlags { - let mut changed = <$changeflags>::default(); - $( - let el_changed = self.$i.rebuild(cx, &prev.$i, &mut state.$i, els); - changed |= el_changed; - )* - changed - } +use alloc::vec::Drain; +use alloc::vec::Vec; - fn message( - &self, - id_path: &[$crate::Id], - state: &mut Self::State, - message: Box, - app_state: &mut T, - ) -> $crate::MessageResult { - $crate::MessageResult::Stale(message) - $( - .or(|message|{ - self.$i.message(id_path, &mut state.$i, message, app_state) - }) - )* - } +use crate::{DynMessage, MessageResult, SuperElement, View, ViewElement, ViewId, ViewPathTracker}; - fn count(&self, state: &Self::State) -> usize { - 0 - $( - + self.$i.count(&state.$i) - )* - } +/// An append only `Vec`. +/// +/// This will be passed to [`ViewSequence::seq_build`] to +/// build the list of initial elements whilst materializing the sequence. +#[derive(Debug)] +pub struct AppendVec { + inner: Vec, +} + +impl AppendVec { + /// Convert `self` into the underlying `Vec` + #[must_use] + pub fn into_inner(self) -> Vec { + self.inner + } + /// Add an item to the end of the vector. + pub fn push(&mut self, item: T) { + self.inner.push(item); + } + /// [Drain](Vec::drain) all items from this `AppendVec`. + pub fn drain(&mut self) -> Drain<'_, T> { + self.inner.drain(..) + } +} + +impl From> for AppendVec { + fn from(inner: Vec) -> Self { + Self { inner } + } +} + +impl Default for AppendVec { + fn default() -> Self { + Self { + inner: Vec::default(), } } } -#[macro_export] -macro_rules! generate_viewsequence_trait { - ($viewseq:ident, $view:ident, $viewmarker: ident, $elements_splice: ident, $bound:ident, $cx:ty, $changeflags:ty, $pod:ty; $( $ss:tt )* ) => { - /// A temporary "splice" to add, update, delete and monitor elements in a sequence of elements. - /// It is mainly intended for view sequences - /// - /// Usually it's backed by a collection (e.g. `Vec`) that holds all the (existing) elements. - /// It sweeps over the element collection and does updates in place. - /// Internally it works by having a pointer/index to the current/old element (0 at the beginning), - /// and the pointer is incremented by basically all methods that mutate that sequence. - pub trait $elements_splice { - /// Insert a new element at the current index in the resulting collection (and increment the index by 1) - fn push(&mut self, element: $pod, cx: &mut $cx); - /// Mutate the next existing element, and add it to the resulting collection (and increment the index by 1) - fn mutate(&mut self, cx: &mut $cx) -> &mut $pod; - // TODO(#160) this could also track view id changes (old_id, new_id) - /// Mark any changes done by `mutate` on the current element (this doesn't change the index) - fn mark(&mut self, changeflags: $changeflags, cx: &mut $cx) -> $changeflags; - /// Delete the next n existing elements (this doesn't change the index) - fn delete(&mut self, n: usize, cx: &mut $cx); - /// Current length of the elements collection - fn len(&self) -> usize; - // TODO(#160) add a skip method when it is necessary (e.g. relevant for immutable ViewSequences like ropes) +/// Views for ordered sequences of elements. +/// +/// Generally, a container view will internally contain a `ViewSequence`. +/// The child elements of the container will be updated by the `ViewSequence`. +/// +/// This is implemented for: +/// - Any single [`View`], where the view's element type +/// is [compatible](SuperElement) with the sequence's element type. +/// This is the root implementation, by which the sequence actually +/// updates the relevant element. +/// - An `Option` of a `ViewSequence` value. +/// The elements of the inner sequence will be inserted into the +/// sequence if the value is `Some`, and removed once the value is `None`. +/// - A [`Vec`] of `ViewSequence` values. +/// Note that this will have persistent allocation with size proportional +/// to the *longest* `Vec` which is ever provided in the View tree, as this +/// uses a generational indexing scheme. +/// - Tuples of `ViewSequences` with up to 15 elements. +/// These can be nested if an ad-hoc sequence of more than 15 sequences is needed. +/// +pub trait ViewSequence: + 'static +{ + /// The associated state of this sequence. The main purposes of this are: + /// - To store generations and other data needed to avoiding routing stale messages + /// to incorrect views. + /// - To pass on the state of child sequences, or a child View's [`ViewState`]. + /// + /// [`ViewState`]: View::ViewState + type SeqState; + + /// Build the associated widgets into `elements` and initialize all states. + #[must_use] + fn seq_build(&self, ctx: &mut Context, elements: &mut AppendVec) -> Self::SeqState; + + /// Update the associated widgets. + fn seq_rebuild( + &self, + prev: &Self, + seq_state: &mut Self::SeqState, + ctx: &mut Context, + elements: &mut impl ElementSplice, + ); + + /// Update the associated widgets. + fn seq_teardown( + &self, + seq_state: &mut Self::SeqState, + ctx: &mut Context, + elements: &mut impl ElementSplice, + ); + + /// Propagate a message. + /// + /// Handle a message, propagating to elements if needed. Here, `id_path` is a slice + /// of ids, where the first item identifiers a child element of this sequence, if necessary. + fn seq_message( + &self, + seq_state: &mut Self::SeqState, + id_path: &[ViewId], + message: DynMessage, + app_state: &mut State, + ) -> MessageResult; +} + +/// A temporary "splice" to add, update and delete in an (ordered) sequence of elements. +/// It is mainly intended for view sequences. +pub trait ElementSplice { + /// Run a function with access to the associated [`AppendVec`]. + /// + /// Each element [pushed](AppendVec::push) to the provided vector will be logically + /// [inserted](ElementSplice::insert) into `self`. + fn with_scratch(&mut self, f: impl FnOnce(&mut AppendVec) -> R) -> R; + /// Insert a new element at the current index in the resulting collection. + fn insert(&mut self, element: Element); + /// Mutate the next existing element. + fn mutate(&mut self, f: impl FnOnce(Element::Mut<'_>) -> R) -> R; + /// Don't make any changes to the next n existing elements. + fn skip(&mut self, n: usize); + /// Delete the next existing element, after running a function on it. + fn delete(&mut self, f: impl FnOnce(Element::Mut<'_>) -> R) -> R; +} + +/// Marker type to workaround trait ambiguity. +#[doc(hidden)] +pub struct WasAView; + +impl ViewSequence + for V +where + Context: ViewPathTracker, + V: View, + Element: SuperElement, + V::Element: ViewElement, +{ + type SeqState = V::ViewState; + + fn seq_build(&self, ctx: &mut Context, elements: &mut AppendVec) -> Self::SeqState { + let (element, view_state) = self.build(ctx); + elements.push(Element::upcast(element)); + view_state + } + fn seq_rebuild( + &self, + prev: &Self, + seq_state: &mut Self::SeqState, + ctx: &mut Context, + elements: &mut impl ElementSplice, + ) { + // Mutate the item we added in `seq_build` + elements.mutate(|this_element| { + Element::with_downcast(this_element, |element| { + self.rebuild(prev, seq_state, ctx, element); + }); + }); + } + fn seq_teardown( + &self, + seq_state: &mut Self::SeqState, + ctx: &mut Context, + elements: &mut impl ElementSplice, + ) { + elements.delete(|this_element| { + Element::with_downcast(this_element, |element| { + self.teardown(seq_state, ctx, element); + }); + }); + } + + fn seq_message( + &self, + seq_state: &mut Self::SeqState, + id_path: &[ViewId], + message: DynMessage, + app_state: &mut State, + ) -> MessageResult { + self.message(seq_state, id_path, message, app_state) + } +} + +/// The state used to implement `ViewSequence` for `Option` +#[doc(hidden)] // Implementation detail, public because of trait visibility rules +pub struct OptionSeqState { + /// The current state. + /// + /// Will be `None` if the previous value was `None`. + inner: Option, + /// The generation this option is at. + /// + /// If the inner sequence was Some, then None, then Some, the sequence + /// is treated as a new sequence, as e.g. build has been called again. + generation: u64, +} + +/// The implementation for an `Option` of a `ViewSequence`. +/// +/// Will mark messages which were sent to a `Some` value if a `None` has since +/// occurred as stale. +impl + ViewSequence> for Option +where + Seq: ViewSequence, + Context: ViewPathTracker, + Element: ViewElement, +{ + // We hide all the items in these implementation so that the top-level + // comment is always shown. This lets us explain the caveats. + #[doc(hidden)] + type SeqState = OptionSeqState; + + #[doc(hidden)] + fn seq_build(&self, ctx: &mut Context, elements: &mut AppendVec) -> Self::SeqState { + let generation = 0; + match self { + Some(seq) => { + let inner = + ctx.with_id(ViewId::new(generation), |ctx| seq.seq_build(ctx, elements)); + OptionSeqState { + inner: Some(inner), + generation, + } + } + None => OptionSeqState { + inner: None, + generation, + }, } + } - impl<'a, 'b> $elements_splice for $crate::VecSplice<'a, 'b, $pod> { - fn push(&mut self, element: $pod, _cx: &mut $cx) { - self.push(element); + #[doc(hidden)] + fn seq_rebuild( + &self, + prev: &Self, + seq_state: &mut Self::SeqState, + ctx: &mut Context, + elements: &mut impl ElementSplice, + ) { + // If `prev` was `Some`, we set `seq_state` in reacting to it (and building the inner view) + // This could only fail if some malicious parent view was messing with our internal state + // (i.e. mixing up the state from different instances) + assert_eq!(prev.is_some(), seq_state.inner.is_some()); + match (self, prev.as_ref().zip(seq_state.inner.as_mut())) { + (None, None) => { + // Nothing to do, there is no corresponding element } - - fn mutate(&mut self, _cx: &mut $cx) -> &mut $pod - { - self.mutate() - } - - fn mark(&mut self, changeflags: $changeflags, _cx: &mut $cx) -> $changeflags - { - self.last_mutated_mut().map(|pod| pod.mark(changeflags)).unwrap_or_default() - } - - fn delete(&mut self, n: usize, _cx: &mut $cx) { - self.delete(n) - } - - fn len(&self) -> usize { - self.len() - } - } - - /// This trait represents a (possibly empty) sequence of views. - /// - /// It is up to the parent view how to lay out and display them. - pub trait $viewseq $( $ss )* { - /// Associated states for the views. - type State $( $ss )*; - - /// Build the associated widgets and initialize all states. - /// - /// To be able to monitor changes (e.g. tree-structure tracking) rather than just adding elements, - /// this takes an element splice as well (when it could be just a `Vec` otherwise) - fn build(&self, cx: &mut $cx, elements: &mut dyn $elements_splice) -> Self::State; - - /// Update the associated widget. - /// - /// Returns `true` when anything has changed. - fn rebuild( - &self, - cx: &mut $cx, - prev: &Self, - state: &mut Self::State, - elements: &mut dyn $elements_splice, - ) -> $changeflags; - - /// Propagate a message. - /// - /// Handle a message, propagating to elements if needed. Here, `id_path` is a slice - /// of ids beginning at an element of this view_sequence. - fn message( - &self, - id_path: &[$crate::Id], - state: &mut Self::State, - message: Box, - app_state: &mut T, - ) -> $crate::MessageResult; - - /// Returns the current amount of widgets built by this sequence. - fn count(&self, state: &Self::State) -> usize; - } - - impl + $viewmarker> $viewseq for V - where - V::Element: $bound + 'static, - { - type State = (>::State, $crate::Id); - - fn build(&self, cx: &mut $cx, elements: &mut dyn $elements_splice) -> Self::State { - let (id, state, pod) = cx.with_new_pod(|cx| >::build(self, cx)); - elements.push(pod, cx); - (state, id) - } - - fn rebuild( - &self, - cx: &mut $cx, - prev: &Self, - state: &mut Self::State, - elements: &mut dyn $elements_splice, - ) -> $changeflags { - let pod = elements.mutate(cx); - let flags = cx.with_pod(pod, |el, cx| { - >::rebuild( - self, - cx, - prev, - &mut state.1, - &mut state.0, - el, - ) + (Some(seq), Some((prev, inner_state))) => { + // Perform a normal rebuild + ctx.with_id(ViewId::new(seq_state.generation), |ctx| { + seq.seq_rebuild(prev, inner_state, ctx, elements); }); - elements.mark(flags, cx) } + (Some(seq), None) => { + // The sequence is newly re-added, build the inner sequence + // We don't increment the generation here, as that was already done in the below case + let inner_state = ctx.with_id(ViewId::new(seq_state.generation), |ctx| { + elements.with_scratch(|elements| seq.seq_build(ctx, elements)) + }); + seq_state.inner = Some(inner_state); + } + (None, Some((prev, inner_state))) => { + // Run teardown with the old path + ctx.with_id(ViewId::new(seq_state.generation), |ctx| { + prev.seq_teardown(inner_state, ctx, elements); + }); + // The sequence has just been destroyed, teardown the old view + // We increment the generation only on the falling edge by convention + // This choice has no impact on functionality + seq_state.inner = None; - fn message( - &self, - id_path: &[$crate::Id], - state: &mut Self::State, - message: Box, - app_state: &mut T, - ) -> $crate::MessageResult { - if let Some((first, rest_path)) = id_path.split_first() { - if first == &state.1 { - return >::message( - self, - rest_path, - &mut state.0, - message, - app_state, + // Overflow handling: u64 starts at 0, incremented by 1 always. + // Can never realistically overflow, scale is too large. + // If would overflow, wrap to zero. Would need async message sent + // to view *exactly* `u64::MAX` versions of the view ago, which is implausible + seq_state.generation = seq_state.generation.wrapping_add(1); + } + } + } + + #[doc(hidden)] + fn seq_teardown( + &self, + seq_state: &mut Self::SeqState, + ctx: &mut Context, + elements: &mut impl ElementSplice, + ) { + assert_eq!(self.is_some(), seq_state.inner.is_some()); + if let Some((seq, inner_state)) = self.as_ref().zip(seq_state.inner.as_mut()) { + ctx.with_id(ViewId::new(seq_state.generation), |ctx| { + seq.seq_teardown(inner_state, ctx, elements); + }); + } + } + + #[doc(hidden)] + fn seq_message( + &self, + seq_state: &mut Self::SeqState, + id_path: &[ViewId], + message: DynMessage, + app_state: &mut State, + ) -> MessageResult { + let (start, rest) = id_path + .split_first() + .expect("Id path has elements for Option"); + if start.routing_id() != seq_state.generation { + // The message was sent to a previous edition of the inner value + return MessageResult::Stale(message); + } + assert_eq!(self.is_some(), seq_state.inner.is_some()); + if let Some((seq, inner_state)) = self.as_ref().zip(seq_state.inner.as_mut()) { + seq.seq_message(inner_state, rest, message, app_state) + } else { + // TODO: this should be unreachable as the generation was increased on the falling edge + MessageResult::Stale(message) + } + } +} + +/// The state used to implement `ViewSequence` for `Vec` +/// +/// We use a generation arena for vector types, with half of the `ViewId` dedicated +/// to the index, and the other half used for the generation. +/// +// This is managed in [`create_vector_view_id`] and [`view_id_to_index_generation`] +#[doc(hidden)] // Implementation detail, public because of trait visibility rules +pub struct VecViewState { + inner_states: Vec, + + generations: Vec, +} + +/// Turns an index and a generation into a packed id, suitable for use in +/// [`ViewId`]s +fn create_generational_view_id(index: usize, generation: u32) -> ViewId { + let id_low: u32 = index + .try_into() + // If you're seeing this panic, you can use a nested `Vec>`, where each individual `Vec` + // has fewer than u32::MAX-1 elements. + .expect("Views in a vector backed sequence must be indexable by u32"); + let id_low: u64 = id_low.into(); + let id_high: u64 = u64::from(generation) << 32; + ViewId::new(id_high | id_low) +} + +/// Undoes [`create_vector_view_id`] +fn view_id_to_index_generation(view_id: ViewId) -> (usize, u32) { + #![allow(clippy::cast_possible_truncation)] + let view_id = view_id.routing_id(); + let id_low_ix = view_id as u32; + let id_high_gen = (view_id >> 32) as u32; + (id_low_ix as usize, id_high_gen) +} + +/// The implementation for an `Vec` of a `ViewSequence`. +/// +/// Will mark messages which were sent to any index as stale if +/// that index has been unused in the meantime. +impl + ViewSequence> for Vec +where + Seq: ViewSequence, + Context: ViewPathTracker, + Element: ViewElement, +{ + // We hide all the items in these implementation so that the top-level + // comment is always shown. This lets us explain the caveats. + #[doc(hidden)] + type SeqState = VecViewState; + + #[doc(hidden)] + fn seq_build(&self, ctx: &mut Context, elements: &mut AppendVec) -> Self::SeqState { + let generations = alloc::vec![0; self.len()]; + let inner_states = self + .iter() + .enumerate() + .zip(&generations) + .map(|((index, seq), generation)| { + let id = create_generational_view_id(index, *generation); + ctx.with_id(id, |ctx| seq.seq_build(ctx, elements)) + }) + .collect(); + VecViewState { + generations, + inner_states, + } + } + + #[doc(hidden)] + fn seq_rebuild( + &self, + prev: &Self, + seq_state: &mut Self::SeqState, + ctx: &mut Context, + elements: &mut impl ElementSplice, + ) { + for (i, (((child, child_prev), child_state), child_generation)) in self + .iter() + .zip(prev) + .zip(&mut seq_state.inner_states) + .zip(&seq_state.generations) + .enumerate() + { + // Rebuild the items which are common to both vectors + let id = create_generational_view_id(i, *child_generation); + ctx.with_id(id, |ctx| { + child.seq_rebuild(child_prev, child_state, ctx, elements); + }); + } + let n = self.len(); + let prev_n = prev.len(); + #[allow(clippy::comparison_chain)] + if n < prev_n { + let to_teardown = prev[n..].iter(); + // Keep the generations + let generations = seq_state.generations[n..].iter_mut(); + // But remove the old states + let states = seq_state.inner_states.drain(n..); + for (index, ((old_seq, generation), mut inner_state)) in + to_teardown.zip(generations).zip(states).enumerate() + { + let id = create_generational_view_id(index + n, *generation); + ctx.with_id(id, |ctx| { + old_seq.seq_teardown(&mut inner_state, ctx, elements); + }); + // We increment the generation on the "falling edge" by convention + *generation = generation.checked_add(1).unwrap_or_else(|| { + static SHOULD_WARN: AtomicBool = AtomicBool::new(true); + // We only want to warn about this once + // because e.g. if every item in a vector hits + // this at the same time, we don't want to repeat it too many times + if SHOULD_WARN.swap(false, Ordering::Relaxed) { + tracing::warn!( + inner_type = core::any::type_name::(), + issue_url = "https://github.com/linebender/xilem/issues", + "Got overflowing generation in ViewSequence from `Vec`.\ + This can possibly cause incorrect routing of async messages in extreme cases.\ + Please open an issue if you see this. There are known solutions" ); } - } - $crate::MessageResult::Stale(message) + // The known solution mentioned in the above message is to use a different ViewId for the index and the generation + // We believe this to be superfluous for the default use case, as even with 1000 rebuilds a second, each adding + // to the same array, this would take 50 days of the application running continuously. + // See also https://github.com/bevyengine/bevy/pull/9907, where they warn in their equivalent case + // Note that we have a slightly different strategy to Bevy, where we use a global generation + // This theoretically allows some of the memory in `seq_state` to be reclaimed, at the cost of making overflow + // more likely here. Note that we don't actually reclaim this memory at the moment. + + // We use 0 to wrap around. It would require extremely unfortunate timing to get an async event + // with the correct generation exactly u32::MAX generations late, so wrapping is the best option + 0 + }); } - - fn count(&self, _state: &Self::State) -> usize { - 1 - } - } - - impl> $viewseq for Option { - type State = Option; - - fn build(&self, cx: &mut $cx, elements: &mut dyn $elements_splice) -> Self::State { - match self { - None => None, - Some(vt) => { - let state = vt.build(cx, elements); - Some(state) - } - } - } - - fn rebuild( - &self, - cx: &mut $cx, - prev: &Self, - state: &mut Self::State, - elements: &mut dyn $elements_splice, - ) -> $changeflags { - match (self, &mut *state, prev) { - (Some(this), Some(state), Some(prev)) => this.rebuild(cx, prev, state, elements), - (None, Some(seq_state), Some(prev)) => { - let count = prev.count(&seq_state); - elements.delete(count, cx); - *state = None; - - <$changeflags>::tree_structure() - } - (Some(this), None, None) => { - *state = Some(this.build(cx, elements)); - - <$changeflags>::tree_structure() - } - (None, None, None) => <$changeflags>::empty(), - _ => panic!("non matching state and prev value"), - } - } - - fn message( - &self, - id_path: &[$crate::Id], - state: &mut Self::State, - message: Box, - app_state: &mut T, - ) -> $crate::MessageResult { - match (self, state) { - (Some(vt), Some(state)) => vt.message(id_path, state, message, app_state), - (None, None) => $crate::MessageResult::Stale(message), - _ => panic!("non matching state and prev value"), - } - } - - fn count(&self, state: &Self::State) -> usize { - match (self, state) { - (Some(vt), Some(state)) => vt.count(state), - (None, None) => 0, - _ => panic!("non matching state and prev value"), - } - } - } - - impl> $viewseq for Vec { - type State = Vec; - - fn build(&self, cx: &mut $cx, elements: &mut dyn $elements_splice) -> Self::State { - self.iter().map(|child| child.build(cx, elements)).collect() - } - - fn rebuild( - &self, - cx: &mut $cx, - prev: &Self, - state: &mut Self::State, - elements: &mut dyn $elements_splice, - ) -> $changeflags { - let mut changed = <$changeflags>::default(); - for ((child, child_prev), child_state) in self.iter().zip(prev).zip(state.iter_mut()) { - let el_changed = child.rebuild(cx, child_prev, child_state, elements); - changed |= el_changed; - } - let n = self.len(); - if n < prev.len() { - let n_delete = state - .splice(n.., []) + } else if n > prev_n { + // If needed, create new generations + seq_state.generations.resize(n, 0); + elements.with_scratch(|elements| { + seq_state.inner_states.extend( + self[prev_n..] + .iter() + .zip(&seq_state.generations[prev_n..]) .enumerate() - .map(|(i, state)| prev[n + i].count(&state)) - .sum(); - elements.delete(n_delete, cx); - changed |= <$changeflags>::tree_structure(); - } else if n > prev.len() { - for i in prev.len()..n { - state.push(self[i].build(cx, elements)); - } - changed |= <$changeflags>::tree_structure(); - } - changed - } + .map(|(index, (seq, generation))| { + let id = create_generational_view_id(index + prev_n, *generation); + ctx.with_id(id, |ctx| seq.seq_build(ctx, elements)) + }), + ); + }); + } + } - fn count(&self, state: &Self::State) -> usize { - self.iter().zip(state).map(|(child, child_state)| - child.count(child_state)) - .sum() - } + #[doc(hidden)] + fn seq_teardown( + &self, + seq_state: &mut Self::SeqState, + ctx: &mut Context, + elements: &mut impl ElementSplice, + ) { + for (index, ((seq, state), generation)) in self + .iter() + .zip(&mut seq_state.inner_states) + .zip(&seq_state.generations) + .enumerate() + { + let id = create_generational_view_id(index, *generation); + ctx.with_id(id, |ctx| seq.seq_teardown(state, ctx, elements)); + } + } - fn message( + #[doc(hidden)] + fn seq_message( + &self, + seq_state: &mut Self::SeqState, + id_path: &[ViewId], + message: DynMessage, + app_state: &mut State, + ) -> MessageResult { + let (start, rest) = id_path + .split_first() + .expect("Id path has elements for Option"); + let (index, generation) = view_id_to_index_generation(*start); + let stored_generation = &seq_state.generations[index]; + if *stored_generation != generation { + // The value in the sequence i + return MessageResult::Stale(message); + } + // Panics if index is out of bounds, but we know it isn't because this is the same generation + let inner_state = &mut seq_state.inner_states[index]; + self[index].seq_message(inner_state, rest, message, app_state) + } +} + +impl ViewSequence for () +where + Context: ViewPathTracker, + Element: ViewElement, +{ + type SeqState = (); + + fn seq_build(&self, _: &mut Context, _: &mut AppendVec) -> Self::SeqState {} + + fn seq_rebuild( + &self, + _: &Self, + _: &mut Self::SeqState, + _: &mut Context, + _: &mut impl ElementSplice, + ) { + } + + fn seq_teardown( + &self, + _seq_state: &mut Self::SeqState, + _ctx: &mut Context, + _elements: &mut impl ElementSplice, + ) { + } + + fn seq_message( + &self, + _: &mut Self::SeqState, + _: &[ViewId], + message: DynMessage, + _: &mut State, + ) -> MessageResult { + unreachable!("Messages should never be dispatched to an empty tuple, got {message:?}"); + } +} + +impl + ViewSequence for (Seq,) +where + Seq: ViewSequence, + Context: ViewPathTracker, + Element: ViewElement, +{ + type SeqState = Seq::SeqState; + + fn seq_build(&self, ctx: &mut Context, elements: &mut AppendVec) -> Self::SeqState { + self.0.seq_build(ctx, elements) + } + + fn seq_rebuild( + &self, + prev: &Self, + seq_state: &mut Self::SeqState, + ctx: &mut Context, + elements: &mut impl ElementSplice, + ) { + self.0.seq_rebuild(&prev.0, seq_state, ctx, elements); + } + + fn seq_teardown( + &self, + seq_state: &mut Self::SeqState, + ctx: &mut Context, + elements: &mut impl ElementSplice, + ) { + self.0.seq_teardown(seq_state, ctx, elements); + } + + fn seq_message( + &self, + seq_state: &mut Self::SeqState, + id_path: &[ViewId], + message: DynMessage, + app_state: &mut State, + ) -> MessageResult { + self.0.seq_message(seq_state, id_path, message, app_state) + } +} + +macro_rules! impl_view_tuple { + ( + // We could use the ${index} metavariable here once it's stable + // https://veykril.github.io/tlborm/decl-macros/minutiae/metavar-expr.html + $($marker: ident, $seq: ident, $idx: tt);+ + ) => { + impl< + State, + Action, + Context: ViewPathTracker, + Element: ViewElement, + $( + $marker, + $seq: ViewSequence, + )+ + > ViewSequence for ($($seq,)+) + + { + type SeqState = ($($seq::SeqState,)+); + + fn seq_build( &self, - id_path: &[$crate::Id], - state: &mut Self::State, - message: Box, - app_state: &mut T, - ) -> $crate::MessageResult { - let mut result = $crate::MessageResult::Stale(message); - for (child, child_state) in self.iter().zip(state) { - if let $crate::MessageResult::Stale(message) = result { - result = child.message(id_path, child_state, message, app_state); - } else { - break; - } + ctx: &mut Context, + elements: &mut AppendVec, + ) -> Self::SeqState { + ($( + ctx.with_id(ViewId::new($idx), |ctx| { + self.$idx.seq_build(ctx, elements) + }), + )+) + } + + fn seq_rebuild( + &self, + prev: &Self, + seq_state: &mut Self::SeqState, + ctx: &mut Context, + elements: &mut impl ElementSplice, + ) { + $( + ctx.with_id(ViewId::new($idx), |ctx| { + self.$idx.seq_rebuild(&prev.$idx, &mut seq_state.$idx, ctx, elements); + }); + )+ + } + + fn seq_teardown( + &self, + seq_state: &mut Self::SeqState, + ctx: &mut Context, + elements: &mut impl ElementSplice, + ) { + $( + ctx.with_id(ViewId::new($idx), |ctx| { + self.$idx.seq_teardown(&mut seq_state.$idx, ctx, elements) + }); + )+ + } + + fn seq_message( + &self, + seq_state: &mut Self::SeqState, + id_path: &[ViewId], + message: DynMessage, + app_state: &mut State, + ) -> MessageResult { + let (start, rest) = id_path + .split_first() + .expect("Id path has elements for tuple"); + match start.routing_id() { + $( + $idx => self.$idx.seq_message(&mut seq_state.$idx, rest, message, app_state), + )+ + // If we have received a message, our parent is (mostly) certain that we requested it + // The only time that wouldn't be the case is when a generational index has overflowed? + _ => unreachable!("Unexpected id path {start:?} in tuple (wants to be routed via {rest:?})"), } - result } } - - /// This trait marks a type a - #[doc = concat!(stringify!($view), ".")] - /// - /// This trait is a workaround for Rust's orphan rules. It serves as a switch between - /// default and custom - #[doc = concat!("`", stringify!($viewseq), "`")] - /// implementations. You can't implement - #[doc = concat!("`", stringify!($viewseq), "`")] - /// for types which also implement - #[doc = concat!("`", stringify!($viewmarker), "`.")] - pub trait $viewmarker {} - - $crate::impl_view_tuple!($viewseq, $elements_splice, $pod, $cx, $changeflags, ;); - $crate::impl_view_tuple!($viewseq, $elements_splice, $pod, $cx, $changeflags, - V0; 0); - $crate::impl_view_tuple!($viewseq, $elements_splice, $pod, $cx, $changeflags, - V0, V1; 0, 1); - $crate::impl_view_tuple!($viewseq, $elements_splice, $pod, $cx, $changeflags, - V0, V1, V2; 0, 1, 2); - $crate::impl_view_tuple!($viewseq, $elements_splice, $pod, $cx, $changeflags, - V0, V1, V2, V3; 0, 1, 2, 3); - $crate::impl_view_tuple!($viewseq, $elements_splice, $pod, $cx, $changeflags, - V0, V1, V2, V3, V4; 0, 1, 2, 3, 4); - $crate::impl_view_tuple!($viewseq, $elements_splice, $pod, $cx, $changeflags, - V0, V1, V2, V3, V4, V5; 0, 1, 2, 3, 4, 5); - $crate::impl_view_tuple!($viewseq, $elements_splice, $pod, $cx, $changeflags, - V0, V1, V2, V3, V4, V5, V6; 0, 1, 2, 3, 4, 5, 6); - $crate::impl_view_tuple!($viewseq, $elements_splice, $pod, $cx, $changeflags, - V0, V1, V2, V3, V4, V5, V6, V7; 0, 1, 2, 3, 4, 5, 6, 7); - $crate::impl_view_tuple!($viewseq, $elements_splice, $pod, $cx, $changeflags, - V0, V1, V2, V3, V4, V5, V6, V7, V8; 0, 1, 2, 3, 4, 5, 6, 7, 8); - $crate::impl_view_tuple!($viewseq, $elements_splice, $pod, $cx, $changeflags, - V0, V1, V2, V3, V4, V5, V6, V7, V8, V9; 0, 1, 2, 3, 4, 5, 6, 7, 8, 9); }; } + +// We implement for tuples of length up to 15. 0 and 1 are special cased to be more efficient +impl_view_tuple!(M0, Seq0, 0; M1, Seq1, 1); +impl_view_tuple!(M0, Seq0, 0; M1, Seq1, 1; M2, Seq2, 2); +impl_view_tuple!(M0, Seq0, 0; M1, Seq1, 1; M2, Seq2, 2; M3, Seq3, 3); +impl_view_tuple!(M0, Seq0, 0; M1, Seq1, 1; M2, Seq2, 2; M3, Seq3, 3; M4, Seq4, 4); +impl_view_tuple!(M0, Seq0, 0; M1, Seq1, 1; M2, Seq2, 2; M3, Seq3, 3; M4, Seq4, 4; M5, Seq5, 5); +impl_view_tuple!(M0, Seq0, 0; M1, Seq1, 1; M2, Seq2, 2; M3, Seq3, 3; M4, Seq4, 4; M5, Seq5, 5; M6, Seq6, 6); +impl_view_tuple!(M0, Seq0, 0; M1, Seq1, 1; M2, Seq2, 2; M3, Seq3, 3; M4, Seq4, 4; M5, Seq5, 5; M6, Seq6, 6; M7, Seq7, 7); +impl_view_tuple!(M0, Seq0, 0; M1, Seq1, 1; M2, Seq2, 2; M3, Seq3, 3; M4, Seq4, 4; M5, Seq5, 5; M6, Seq6, 6; M7, Seq7, 7; M8, Seq8, 8); +impl_view_tuple!(M0, Seq0, 0; M1, Seq1, 1; M2, Seq2, 2; M3, Seq3, 3; M4, Seq4, 4; M5, Seq5, 5; M6, Seq6, 6; M7, Seq7, 7; M8, Seq8, 8; M9, Seq9, 9); +impl_view_tuple!(M0, Seq0, 0; M1, Seq1, 1; M2, Seq2, 2; M3, Seq3, 3; M4, Seq4, 4; M5, Seq5, 5; M6, Seq6, 6; M7, Seq7, 7; M8, Seq8, 8; M9, Seq9, 9; M10, Seq10, 10); +impl_view_tuple!(M0, Seq0, 0; M1, Seq1, 1; M2, Seq2, 2; M3, Seq3, 3; M4, Seq4, 4; M5, Seq5, 5; M6, Seq6, 6; M7, Seq7, 7; M8, Seq8, 8; M9, Seq9, 9; M10, Seq10, 10; M11, Seq11, 11); +impl_view_tuple!(M0, Seq0, 0; M1, Seq1, 1; M2, Seq2, 2; M3, Seq3, 3; M4, Seq4, 4; M5, Seq5, 5; M6, Seq6, 6; M7, Seq7, 7; M8, Seq8, 8; M9, Seq9, 9; M10, Seq10, 10; M11, Seq11, 11; M12, Seq12, 12); +impl_view_tuple!(M0, Seq0, 0; M1, Seq1, 1; M2, Seq2, 2; M3, Seq3, 3; M4, Seq4, 4; M5, Seq5, 5; M6, Seq6, 6; M7, Seq7, 7; M8, Seq8, 8; M9, Seq9, 9; M10, Seq10, 10; M11, Seq11, 11; M12, Seq12, 12; M13, Seq13, 13); +impl_view_tuple!(M0, Seq0, 0; M1, Seq1, 1; M2, Seq2, 2; M3, Seq3, 3; M4, Seq4, 4; M5, Seq5, 5; M6, Seq6, 6; M7, Seq7, 7; M8, Seq8, 8; M9, Seq9, 9; M10, Seq10, 10; M11, Seq11, 11; M12, Seq12, 12; M13, Seq13, 13; M14, Seq14, 14); +impl_view_tuple!(M0, Seq0, 0; M1, Seq1, 1; M2, Seq2, 2; M3, Seq3, 3; M4, Seq4, 4; M5, Seq5, 5; M6, Seq6, 6; M7, Seq7, 7; M8, Seq8, 8; M9, Seq9, 9; M10, Seq10, 10; M11, Seq11, 11; M12, Seq12, 12; M13, Seq13, 13; M14, Seq14, 14; M15, Seq15, 15); diff --git a/xilem_core/src/view.rs b/xilem_core/src/view.rs new file mode 100644 index 00000000..f42df74b --- /dev/null +++ b/xilem_core/src/view.rs @@ -0,0 +1,228 @@ +// Copyright 2024 the Xilem Authors +// SPDX-License-Identifier: Apache-2.0 + +//! The primary view trait and associated trivial implementations. + +use core::ops::Deref; + +use alloc::{boxed::Box, sync::Arc}; + +use crate::{message::MessageResult, DynMessage, Mut, ViewElement}; + +/// A lightweight, short-lived representation of the state of a retained +/// structure, usually a user interface node. +/// +/// This is the central reactivity primitive in Xilem. +/// An app will generate a tree of these objects (the view tree) to represent +/// the state it wants to show in its element tree. +/// The framework will then run methods on these views to create the associated +/// element tree, or to perform incremental updates to the element tree. +/// Once this process is complete, the element tree will reflect the view tree. +/// The view tree is also used to dispatch messages, such as those sent when a +/// user presses a button. +/// +/// The view tree is transitory and is retained only long enough to dispatch +/// messages and then serve as a reference for diffing for the next view tree. +/// +/// The `View` trait is parameterized by `State`, which is known as the "app state", +/// and also a type for actions which are passed up the tree in message +/// propagation. +/// During message handling, mutable access to the app state is given to view nodes, +/// which will in turn generally expose it to callbacks. +/// +/// ## Alloc +/// +/// In order to support the open-ended [`DynMessage`] type, this trait requires an +/// allocator to be available. +/// It is possible (hopefully in a backwards compatible way) to add a generic +/// defaulted parameter for the message type in future. +pub trait View: 'static { + /// The element type which this view operates on. + type Element: ViewElement; + /// State that is used over the lifetime of the retained representation of the view. + /// + /// This often means routing information for messages to child views or view sequences, + /// to avoid sending outdated views. + /// This is also used in [`memoize`](crate::memoize) to store the previously constructed view. + type ViewState; + + /// Create the corresponding Element value. + fn build(&self, ctx: &mut Context) -> (Self::Element, Self::ViewState); + + /// Update `element` based on the difference between `self` and `prev`. + /// + /// This returns `element`, to allow parent views to modify the element after this `rebuild` has + /// completed. This returning is needed as some reference types do not allow reborrowing, + /// without unwieldy boilerplate. + fn rebuild<'el>( + &self, + prev: &Self, + view_state: &mut Self::ViewState, + ctx: &mut Context, + element: Mut<'el, Self::Element>, + ) -> Mut<'el, Self::Element>; + + /// Handle `element` being removed from the tree. + /// + /// The main use-cases of this method are to: + /// - Cancel any async tasks + /// - Clean up any book-keeping set-up in `build` and `rebuild` + // TODO: Should this take ownership of the `ViewState` + // We have chosen not to because it makes swapping versions more awkward + fn teardown( + &self, + view_state: &mut Self::ViewState, + ctx: &mut Context, + element: Mut<'_, Self::Element>, + ); + + /// Route `message` to `id_path`, if that is still a valid path. + fn message( + &self, + view_state: &mut Self::ViewState, + id_path: &[ViewId], + message: DynMessage, + app_state: &mut State, + ) -> MessageResult; + + // fn debug_name? +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +/// An identifier used to differentiation between the direct children of a [View]. +/// +/// These are [u64] backed identifiers, which will be added to the "view path" in +/// [`View::build`] and [`View::rebuild`] (and their [`ViewSequence`](crate::ViewSequence) counterparts), +/// and removed from the start of the path if necessary in [`View::message`]. +/// The value of `ViewId`s are only meaningful for the `View` or `ViewSequence` added them +/// to the path, and can be used to store indices and/or generations. +// TODO: maybe also provide debugging information to give e.g. a useful stack trace? +// TODO: Rethink name, as 'Id' suggests global uniqueness +pub struct ViewId(u64); + +impl ViewId { + /// Create a new `ViewId` with the given value. + #[must_use] + pub fn new(raw: u64) -> Self { + Self(raw) + } + + /// Access the raw value of this id. + #[must_use] + pub fn routing_id(self) -> u64 { + self.0 + } +} + +/// A tracker for view paths, used in [`View::build`] and [`View::rebuild`]. +/// These paths are used for routing messages in [`View::message`]. +/// +/// Each `View` is expected to be implemented for one logical context type, +/// and this context may be used to store auxiliary data. +/// For example, this context could be used to store a mapping from the +/// id of widget to view path, to enable event routing. +pub trait ViewPathTracker { + /// Add `id` to the end of current view path + fn push_id(&mut self, id: ViewId); + /// Remove the most recently `push`ed id from the current view path + fn pop_id(&mut self); + + /// The path to the current view in the view tree + fn view_path(&mut self) -> &[ViewId]; + + /// Run `f` in a context with `id` pushed to the current view path + fn with_id(&mut self, id: ViewId, f: impl FnOnce(&mut Self) -> R) -> R { + self.push_id(id); + let res = f(self); + self.pop_id(); + res + } +} + +impl + ?Sized> + View for Box +{ + type Element = V::Element; + type ViewState = V::ViewState; + + fn build(&self, ctx: &mut Context) -> (Self::Element, Self::ViewState) { + self.deref().build(ctx) + } + + fn rebuild<'el>( + &self, + prev: &Self, + view_state: &mut Self::ViewState, + ctx: &mut Context, + element: Mut<'el, Self::Element>, + ) -> Mut<'el, Self::Element> { + self.deref().rebuild(prev, view_state, ctx, element) + } + + fn teardown( + &self, + view_state: &mut Self::ViewState, + ctx: &mut Context, + element: Mut<'_, Self::Element>, + ) { + self.deref().teardown(view_state, ctx, element); + } + + fn message( + &self, + view_state: &mut Self::ViewState, + id_path: &[ViewId], + message: DynMessage, + app_state: &mut State, + ) -> MessageResult { + self.deref() + .message(view_state, id_path, message, app_state) + } +} + +/// An implementation of [`View`] which only runs rebuild if the states are different +impl + ?Sized> + View for Arc +{ + type Element = V::Element; + type ViewState = V::ViewState; + + fn build(&self, ctx: &mut Context) -> (Self::Element, Self::ViewState) { + self.deref().build(ctx) + } + + fn rebuild<'el>( + &self, + prev: &Self, + view_state: &mut Self::ViewState, + 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 + } else { + self.deref().rebuild(prev, view_state, ctx, element) + } + } + + fn teardown( + &self, + view_state: &mut Self::ViewState, + ctx: &mut Context, + element: Mut<'_, Self::Element>, + ) { + self.deref().teardown(view_state, ctx, element); + } + + fn message( + &self, + view_state: &mut Self::ViewState, + id_path: &[ViewId], + message: DynMessage, + app_state: &mut State, + ) -> MessageResult { + self.deref() + .message(view_state, id_path, message, app_state) + } +} diff --git a/xilem_core/src/views/memoize.rs b/xilem_core/src/views/memoize.rs new file mode 100644 index 00000000..ab7e4cf7 --- /dev/null +++ b/xilem_core/src/views/memoize.rs @@ -0,0 +1,130 @@ +// Copyright 2024 the Xilem Authors +// SPDX-License-Identifier: Apache-2.0 + +use crate::{DynMessage, 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 { + data: D, + child_cb: F, +} + +pub struct MemoizeState +where + Context: ViewPathTracker, + V: View, +{ + view: V, + view_state: V::ViewState, + dirty: bool, +} + +impl Memoize +where + F: Fn(&D) -> V, +{ + const ASSERT_CONTEXTLESS_FN: () = { + assert!( + core::mem::size_of::() == 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. +" + ); + }; + + /// Create a new `Memoize` view. + pub fn new(data: D, child_cb: F) -> Self { + #[allow(clippy::let_unit_value)] + let _ = Self::ASSERT_CONTEXTLESS_FN; + Memoize { data, child_cb } + } +} + +impl View for Memoize +where + Context: ViewPathTracker, + Data: PartialEq + 'static, + V: View, + ViewFn: Fn(&Data) -> V + 'static, +{ + type ViewState = MemoizeState; + + type Element = V::Element; + + fn build(&self, cx: &mut Context) -> (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<'el>( + &self, + prev: &Self, + view_state: &mut Self::ViewState, + cx: &mut Context, + 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 el = view.rebuild(&view_state.view, &mut view_state.view_state, cx, element); + view_state.view = view; + el + } else { + element + } + } + + fn message( + &self, + view_state: &mut Self::ViewState, + id_path: &[ViewId], + message: DynMessage, + app_state: &mut State, + ) -> MessageResult { + 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 + } + + fn teardown( + &self, + view_state: &mut Self::ViewState, + ctx: &mut Context, + element: Mut<'_, Self::Element>, + ) { + view_state + .view + .teardown(&mut view_state.view_state, ctx, element); + } +} + +/// Memoize the view, until the `data` changes (in which case `view` is called again) +pub fn memoize( + data: Data, + view: ViewFn, +) -> Memoize +where + Data: PartialEq + 'static, + ViewFn: Fn(&Data) -> V + 'static, + V: View, + Context: ViewPathTracker, +{ + Memoize::new(data, view) +} diff --git a/xilem_core/src/views/mod.rs b/xilem_core/src/views/mod.rs new file mode 100644 index 00000000..f278b76c --- /dev/null +++ b/xilem_core/src/views/mod.rs @@ -0,0 +1,5 @@ +// Copyright 2024 the Xilem Authors +// SPDX-License-Identifier: Apache-2.0 + +mod memoize; +pub use memoize::{memoize, Memoize}; diff --git a/xilem_core/tests/any_view.rs b/xilem_core/tests/any_view.rs new file mode 100644 index 00000000..95aa4824 --- /dev/null +++ b/xilem_core/tests/any_view.rs @@ -0,0 +1,105 @@ +// Copyright 2024 the Xilem Authors +// SPDX-License-Identifier: Apache-2.0 + +//! Tests that [`AnyView`] has the correct routing behaviour + +use xilem_core::{AnyView, MessageResult, View}; + +mod common; +use common::*; + +type AnyNoopView = dyn AnyView<(), Action, TestCx, TestElement>; + +#[test] +fn messages_to_inner_view() { + let view: Box = Box::new(OperationView::<0>(0)); + let mut ctx = TestCx::default(); + let (element, mut state) = view.build(&mut ctx); + ctx.assert_empty(); + assert_eq!(element.operations, &[Operation::Build(0)]); + + let result = view.message(&mut state, &element.view_path, Box::new(()), &mut ()); + assert_action(result, 0); +} + +#[test] +fn message_after_rebuild() { + let view: Box = Box::new(OperationView::<0>(0)); + let mut ctx = TestCx::default(); + let (mut element, mut state) = view.build(&mut ctx); + ctx.assert_empty(); + let path = element.view_path.clone(); + + let view2: Box = Box::new(OperationView::<0>(1)); + view2.rebuild(&view, &mut state, &mut ctx, &mut element); + ctx.assert_empty(); + assert_eq!( + element.operations, + &[Operation::Build(0), Operation::Rebuild { from: 0, to: 1 }] + ); + + let result = view2.message(&mut state, &path, Box::new(()), &mut ()); + assert_action(result, 1); +} + +#[test] +fn no_message_after_stale() { + let view: Box = Box::new(OperationView::<0>(0)); + let mut ctx = TestCx::default(); + let (mut element, mut state) = view.build(&mut ctx); + ctx.assert_empty(); + let path = element.view_path.clone(); + + let view2: Box = Box::new(OperationView::<1>(1)); + view2.rebuild(&view, &mut state, &mut ctx, &mut element); + ctx.assert_empty(); + assert_eq!( + element.operations, + &[ + Operation::Build(0), + Operation::Teardown(0), + Operation::Replace(1) + ] + ); + + let result = view2.message(&mut state, &path, Box::new(()), &mut ()); + assert!(matches!(result, MessageResult::Stale(_))); +} + +#[test] +fn no_message_after_stale_then_same_type() { + let view: Box = Box::new(OperationView::<0>(0)); + let mut ctx = TestCx::default(); + let (mut element, mut state) = view.build(&mut ctx); + ctx.assert_empty(); + let path = element.view_path.clone(); + + let view2: Box = Box::new(OperationView::<1>(1)); + view2.rebuild(&view, &mut state, &mut ctx, &mut element); + ctx.assert_empty(); + assert_eq!( + element.operations, + &[ + Operation::Build(0), + Operation::Teardown(0), + Operation::Replace(1) + ] + ); + + let view3: Box = Box::new(OperationView::<0>(2)); + view3.rebuild(&view2, &mut state, &mut ctx, &mut element); + ctx.assert_empty(); + assert_eq!( + element.operations, + &[ + Operation::Build(0), + Operation::Teardown(0), + Operation::Replace(1), + Operation::Teardown(1), + Operation::Replace(2) + ] + ); + + let result = view3.message(&mut state, &path, Box::new(()), &mut ()); + assert!(matches!(result, MessageResult::Stale(_))); +} diff --git a/xilem_core/tests/arc.rs b/xilem_core/tests/arc.rs new file mode 100644 index 00000000..64bce4ba --- /dev/null +++ b/xilem_core/tests/arc.rs @@ -0,0 +1,185 @@ +// Copyright 2024 the Xilem Authors +// SPDX-License-Identifier: Apache-2.0 + +//! Tests for the behaviour of [`Arc`] where `V` is a view. +//! +//! Also has some tests for [`Box`], for which there is no special behaviour +//! +//! This is an integration test so that it can use the infrastructure in [`common`]. + +use std::sync::Arc; +use xilem_core::{MessageResult, View}; + +mod common; +use common::*; + +fn record_ops(id: u32) -> OperationView<0> { + OperationView(id) +} + +#[test] +/// The Arc view shouldn't impact the view path +fn arc_no_path() { + let view1 = Arc::new(record_ops(0)); + let mut ctx = TestCx::default(); + let (element, ()) = view1.build(&mut ctx); + ctx.assert_empty(); + assert!(element.view_path.is_empty()); +} + +#[test] +fn same_arc_skip_rebuild() { + let view1 = Arc::new(record_ops(0)); + let mut ctx = TestCx::default(); + let (mut element, mut state) = view1.build(&mut ctx); + ctx.assert_empty(); + assert_eq!(element.operations, &[Operation::Build(0)]); + + let view2 = Arc::clone(&view1); + view2.rebuild(&view1, &mut state, &mut ctx, &mut element); + ctx.assert_empty(); + assert_eq!(element.operations, &[Operation::Build(0)]); +} + +#[test] +/// If use a different Arc, a rebuild should happen +fn new_arc_rebuild() { + let view1 = Arc::new(record_ops(0)); + let mut ctx = TestCx::default(); + let (mut element, mut state) = view1.build(&mut ctx); + ctx.assert_empty(); + assert_eq!(element.operations, &[Operation::Build(0)]); + + let view2 = Arc::new(record_ops(1)); + view2.rebuild(&view1, &mut state, &mut ctx, &mut element); + ctx.assert_empty(); + assert_eq!( + element.operations, + &[Operation::Build(0), Operation::Rebuild { from: 0, to: 1 }] + ); +} + +#[test] +/// If use a different Arc, a rebuild should happen +fn new_arc_rebuild_same_value() { + let view1 = Arc::new(record_ops(0)); + let mut ctx = TestCx::default(); + let (mut element, mut state) = view1.build(&mut ctx); + ctx.assert_empty(); + assert_eq!(element.operations, &[Operation::Build(0)]); + + let view2 = Arc::new(record_ops(0)); + view2.rebuild(&view1, &mut state, &mut ctx, &mut element); + ctx.assert_empty(); + assert_eq!( + element.operations, + &[Operation::Build(0), Operation::Rebuild { from: 0, to: 0 }] + ); +} + +#[test] +/// Arc should successfully allow the child to teardown +fn arc_passthrough_teardown() { + let view1 = Arc::new(record_ops(0)); + let mut ctx = TestCx::default(); + let (mut element, mut state) = view1.build(&mut ctx); + ctx.assert_empty(); + assert_eq!(element.operations, &[Operation::Build(0)]); + + view1.teardown(&mut state, &mut ctx, &mut element); + ctx.assert_empty(); + assert_eq!( + element.operations, + &[Operation::Build(0), Operation::Teardown(0)] + ); +} + +#[test] +fn arc_passthrough_message() { + let view1 = Arc::new(record_ops(0)); + let mut ctx = TestCx::default(); + let (element, mut state) = view1.build(&mut ctx); + ctx.assert_empty(); + assert_eq!(element.operations, &[Operation::Build(0)]); + + let result = view1.message(&mut state, &element.view_path, Box::new(()), &mut ()); + assert_action(result, 0); +} + +/// --- MARK: Box tests --- +#[test] +/// The Box view shouldn't impact the view path +fn box_no_path() { + let view1 = Box::new(record_ops(0)); + let mut ctx = TestCx::default(); + let (element, ()) = view1.build(&mut ctx); + ctx.assert_empty(); + assert!(element.view_path.is_empty()); +} + +#[test] +/// The Box view should always rebuild +fn box_passthrough_rebuild() { + let view1 = Box::new(record_ops(0)); + let mut ctx = TestCx::default(); + let (mut element, mut state) = view1.build(&mut ctx); + ctx.assert_empty(); + assert_eq!(element.operations, &[Operation::Build(0)]); + + let view2 = Box::new(record_ops(1)); + view2.rebuild(&view1, &mut state, &mut ctx, &mut element); + ctx.assert_empty(); + assert_eq!( + element.operations, + &[Operation::Build(0), Operation::Rebuild { from: 0, to: 1 }] + ); +} + +#[test] +/// The Box view should always rebuild +fn box_passthrough_rebuild_same_value() { + let view1 = Box::new(record_ops(0)); + let mut ctx = TestCx::default(); + let (mut element, mut state) = view1.build(&mut ctx); + ctx.assert_empty(); + assert_eq!(element.operations, &[Operation::Build(0)]); + + let view2 = Box::new(record_ops(0)); + view2.rebuild(&view1, &mut state, &mut ctx, &mut element); + ctx.assert_empty(); + assert_eq!( + element.operations, + &[Operation::Build(0), Operation::Rebuild { from: 0, to: 0 }] + ); +} + +#[test] +fn box_passthrough_teardown() { + let view1 = Box::new(record_ops(0)); + let mut ctx = TestCx::default(); + let (mut element, mut state) = view1.build(&mut ctx); + ctx.assert_empty(); + assert_eq!(element.operations, &[Operation::Build(0)]); + + view1.teardown(&mut state, &mut ctx, &mut element); + ctx.assert_empty(); + assert_eq!( + element.operations, + &[Operation::Build(0), Operation::Teardown(0)] + ); +} + +#[test] +fn box_passthrough_message() { + let view1 = Box::new(record_ops(0)); + let mut ctx = TestCx::default(); + let (element, mut state) = view1.build(&mut ctx); + ctx.assert_empty(); + assert_eq!(element.operations, &[Operation::Build(0)]); + + let result = view1.message(&mut state, &element.view_path, Box::new(()), &mut ()); + let MessageResult::Action(inner) = result else { + panic!() + }; + assert_eq!(inner.id, 0); +} diff --git a/xilem_core/tests/base_sequence.rs b/xilem_core/tests/base_sequence.rs new file mode 100644 index 00000000..75edced0 --- /dev/null +++ b/xilem_core/tests/base_sequence.rs @@ -0,0 +1,359 @@ +// Copyright 2024 the Xilem Authors +// SPDX-License-Identifier: Apache-2.0 + +//! Tests of the primary [`ViewSequence`] implementations +//! +//! [`ViewSequence`]: xilem_core::ViewSequence + +mod common; +use common::*; +use xilem_core::{MessageResult, View}; + +fn record_ops(id: u32) -> OperationView<0> { + OperationView(id) +} + +/// The implicit sequence of a single View should forward all operations +#[test] +fn one_element_sequence_passthrough() { + let view = sequence(1, record_ops(0)); + let mut ctx = TestCx::default(); + let (mut element, mut state) = view.build(&mut ctx); + ctx.assert_empty(); + assert_eq!(element.operations, &[Operation::Build(1)]); + assert_eq!(element.view_path, &[]); + + let seq_children = element.children.as_ref().unwrap(); + assert!(seq_children.deleted.is_empty()); + assert_eq!(seq_children.active.len(), 1); + let child = seq_children.active.first().unwrap(); + assert_eq!(child.operations, &[Operation::Build(0)]); + assert_eq!( + child.view_path, + &[], + "The single `View` ViewSequence shouldn't add to the view path" + ); + + let view2 = sequence(3, record_ops(2)); + view2.rebuild(&view, &mut state, &mut ctx, &mut element); + ctx.assert_empty(); + let seq_children = element.children.as_ref().unwrap(); + assert_eq!( + element.operations, + &[Operation::Build(1), Operation::Rebuild { from: 1, to: 3 }] + ); + + assert_eq!(seq_children.active.len(), 1); + assert!(seq_children.deleted.is_empty()); + let child = seq_children.active.first().unwrap(); + assert_eq!( + child.operations, + &[Operation::Build(0), Operation::Rebuild { from: 0, to: 2 }] + ); + + let result = view2.message(&mut state, &[], Box::new(()), &mut ()); + // The message should have been routed to the only child + assert_action(result, 2); + + view2.teardown(&mut state, &mut ctx, &mut element); + assert_eq!( + element.operations, + &[ + Operation::Build(1), + Operation::Rebuild { from: 1, to: 3 }, + Operation::Teardown(3) + ] + ); + + let seq_children = element.children.as_ref().unwrap(); + // It has been removed from the parent sequence when tearing down + assert_eq!(seq_children.active.len(), 0); + assert_eq!(seq_children.deleted.len(), 1); + let (child_idx, child) = seq_children.deleted.first().unwrap(); + assert_eq!(*child_idx, 0); + assert_eq!( + child.operations, + &[ + Operation::Build(0), + Operation::Rebuild { from: 0, to: 2 }, + Operation::Teardown(2) + ] + ); +} + +#[test] +fn option_none_none() { + let view = sequence(0, None::>); + let mut ctx = TestCx::default(); + let (mut element, mut state) = view.build(&mut ctx); + ctx.assert_empty(); + assert_eq!(element.operations, &[Operation::Build(0)]); + assert_eq!(element.view_path, &[]); + + let seq_children = element.children.as_ref().unwrap(); + assert!(seq_children.deleted.is_empty()); + assert!(seq_children.active.is_empty()); + + let view2 = sequence(1, None); + view2.rebuild(&view, &mut state, &mut ctx, &mut element); + ctx.assert_empty(); + assert_eq!( + element.operations, + &[Operation::Build(0), Operation::Rebuild { from: 0, to: 1 }] + ); + + let seq_children = element.children.as_ref().unwrap(); + assert!(seq_children.deleted.is_empty()); + assert!(seq_children.active.is_empty()); + + view2.teardown(&mut state, &mut ctx, &mut element); + ctx.assert_empty(); + assert_eq!( + element.operations, + &[ + Operation::Build(0), + Operation::Rebuild { from: 0, to: 1 }, + Operation::Teardown(1) + ] + ); + + let seq_children = element.children.as_ref().unwrap(); + assert!(seq_children.deleted.is_empty()); + assert!(seq_children.active.is_empty()); +} + +#[test] +fn option_some_some() { + let view = sequence(1, Some(record_ops(0))); + let mut ctx = TestCx::default(); + let (mut element, mut state) = view.build(&mut ctx); + ctx.assert_empty(); + assert_eq!(element.operations, &[Operation::Build(1)]); + assert_eq!(element.view_path, &[]); + + let seq_children = element.children.as_ref().unwrap(); + assert!(seq_children.deleted.is_empty()); + assert_eq!(seq_children.active.len(), 1); + let child = seq_children.active.first().unwrap(); + assert_eq!(child.operations, &[Operation::Build(0)]); + // Option is allowed (and expected) to add to the view path + assert_eq!(child.view_path.len(), 1); + + let view2 = sequence(3, Some(record_ops(2))); + view2.rebuild(&view, &mut state, &mut ctx, &mut element); + ctx.assert_empty(); + assert_eq!( + element.operations, + &[Operation::Build(1), Operation::Rebuild { from: 1, to: 3 }] + ); + + let seq_children = element.children.as_ref().unwrap(); + assert!(seq_children.deleted.is_empty()); + assert_eq!(seq_children.active.len(), 1); + let child = seq_children.active.first().unwrap(); + assert_eq!( + child.operations, + &[Operation::Build(0), Operation::Rebuild { from: 0, to: 2 }] + ); + + view2.teardown(&mut state, &mut ctx, &mut element); + ctx.assert_empty(); + assert_eq!( + element.operations, + &[ + Operation::Build(1), + Operation::Rebuild { from: 1, to: 3 }, + Operation::Teardown(3) + ] + ); + + let seq_children = element.children.as_ref().unwrap(); + assert_eq!(seq_children.deleted.len(), 1); + assert!(seq_children.active.is_empty()); + let (child_idx, child) = seq_children.deleted.first().unwrap(); + assert_eq!(*child_idx, 0); + assert_eq!( + child.operations, + &[ + Operation::Build(0), + Operation::Rebuild { from: 0, to: 2 }, + Operation::Teardown(2) + ] + ); +} + +#[test] +fn option_none_some() { + let view = sequence(0, None); + let mut ctx = TestCx::default(); + let (mut element, mut state) = view.build(&mut ctx); + ctx.assert_empty(); + assert_eq!(element.operations, &[Operation::Build(0)]); + assert_eq!(element.view_path, &[]); + + let seq_children = element.children.as_ref().unwrap(); + assert!(seq_children.deleted.is_empty()); + assert!(seq_children.active.is_empty()); + + let view2 = sequence(2, Some(record_ops(1))); + view2.rebuild(&view, &mut state, &mut ctx, &mut element); + ctx.assert_empty(); + assert_eq!( + element.operations, + &[Operation::Build(0), Operation::Rebuild { from: 0, to: 2 }] + ); + + let seq_children = element.children.as_ref().unwrap(); + assert!(seq_children.deleted.is_empty()); + assert_eq!(seq_children.active.len(), 1); + let child = seq_children.active.first().unwrap(); + assert_eq!(child.operations, &[Operation::Build(1)]); + + view2.teardown(&mut state, &mut ctx, &mut element); + ctx.assert_empty(); + assert_eq!( + element.operations, + &[ + Operation::Build(0), + Operation::Rebuild { from: 0, to: 2 }, + Operation::Teardown(2) + ] + ); + + let seq_children = element.children.as_ref().unwrap(); + assert_eq!(seq_children.deleted.len(), 1); + assert!(seq_children.active.is_empty()); + let (child_idx, child) = seq_children.deleted.first().unwrap(); + assert_eq!(*child_idx, 0); + assert_eq!( + child.operations, + &[Operation::Build(1), Operation::Teardown(1)] + ); +} + +#[test] +fn option_some_none() { + let view = sequence(1, Some(record_ops(0))); + let mut ctx = TestCx::default(); + let (mut element, mut state) = view.build(&mut ctx); + ctx.assert_empty(); + assert_eq!(element.operations, &[Operation::Build(1)]); + assert_eq!(element.view_path, &[]); + + let seq_children = element.children.as_ref().unwrap(); + assert!(seq_children.deleted.is_empty()); + assert_eq!(seq_children.active.len(), 1); + let child = seq_children.active.first().unwrap(); + assert_eq!(child.operations, &[Operation::Build(0)]); + // Option is allowed (and expected) to add to the view path + assert_eq!(child.view_path.len(), 1); + + let view2 = sequence(2, None); + view2.rebuild(&view, &mut state, &mut ctx, &mut element); + ctx.assert_empty(); + assert_eq!( + element.operations, + &[Operation::Build(1), Operation::Rebuild { from: 1, to: 2 }] + ); + + let seq_children = element.children.as_ref().unwrap(); + assert_eq!(seq_children.deleted.len(), 1); + assert!(seq_children.active.is_empty()); + let (child_idx, child) = seq_children.deleted.first().unwrap(); + assert_eq!(*child_idx, 0); + assert_eq!( + child.operations, + &[Operation::Build(0), Operation::Teardown(0)] + ); + + view2.teardown(&mut state, &mut ctx, &mut element); + ctx.assert_empty(); + assert_eq!( + element.operations, + &[ + Operation::Build(1), + Operation::Rebuild { from: 1, to: 2 }, + Operation::Teardown(2) + ] + ); + + let seq_children = element.children.as_ref().unwrap(); + assert_eq!(seq_children.deleted.len(), 1); + assert!(seq_children.active.is_empty()); +} + +#[test] +fn option_message_some() { + let view = sequence(1, Some(record_ops(0))); + let mut ctx = TestCx::default(); + let (element, mut state) = view.build(&mut ctx); + ctx.assert_empty(); + + let seq_children = element.children.as_ref().unwrap(); + assert_eq!(seq_children.active.len(), 1); + let child = seq_children.active.first().unwrap(); + let path = child.view_path.to_vec(); + + let result = view.message(&mut state, &path, Box::new(()), &mut ()); + assert_action(result, 0); +} + +#[test] +fn option_message_some_some() { + let view = sequence(0, Some(record_ops(0))); + let mut ctx = TestCx::default(); + let (mut element, mut state) = view.build(&mut ctx); + ctx.assert_empty(); + + let seq_children = element.children.as_ref().unwrap(); + assert_eq!(seq_children.active.len(), 1); + let child = seq_children.active.first().unwrap(); + let path = child.view_path.to_vec(); + + let view2 = sequence(0, Some(record_ops(1))); + view2.rebuild(&view, &mut state, &mut ctx, &mut element); + + let result = view2.message(&mut state, &path, Box::new(()), &mut ()); + assert_action(result, 1); +} + +#[test] +fn option_message_some_none_stale() { + let view = sequence(0, Some(record_ops(0))); + let mut ctx = TestCx::default(); + let (mut element, mut state) = view.build(&mut ctx); + ctx.assert_empty(); + + let seq_children = element.children.as_ref().unwrap(); + assert_eq!(seq_children.active.len(), 1); + let child = seq_children.active.first().unwrap(); + let path = child.view_path.to_vec(); + + let view2 = sequence(0, None); + view2.rebuild(&view, &mut state, &mut ctx, &mut element); + + let result = view2.message(&mut state, &path, Box::new(()), &mut ()); + assert!(matches!(result, MessageResult::Stale(_))); +} + +#[test] +fn option_message_some_none_some_stale() { + let view = sequence(0, Some(record_ops(0))); + let mut ctx = TestCx::default(); + let (mut element, mut state) = view.build(&mut ctx); + ctx.assert_empty(); + + let seq_children = element.children.as_ref().unwrap(); + assert_eq!(seq_children.active.len(), 1); + let child = seq_children.active.first().unwrap(); + let path = child.view_path.to_vec(); + + let view2 = sequence(0, None); + view2.rebuild(&view, &mut state, &mut ctx, &mut element); + + let view3 = sequence(0, Some(record_ops(1))); + view3.rebuild(&view2, &mut state, &mut ctx, &mut element); + + let result = view2.message(&mut state, &path, Box::new(()), &mut ()); + assert!(matches!(result, MessageResult::Stale(_))); +} diff --git a/xilem_core/tests/common/mod.rs b/xilem_core/tests/common/mod.rs new file mode 100644 index 00000000..71cfe5bf --- /dev/null +++ b/xilem_core/tests/common/mod.rs @@ -0,0 +1,293 @@ +// Copyright 2024 the Xilem Authors +// SPDX-License-Identifier: Apache-2.0 + +#![allow(dead_code)] // This is a utility module, which means that some exposed items aren't +#![deny(unreachable_pub)] + +use std::marker::PhantomData; + +use xilem_core::*; + +#[derive(Default)] +pub(super) struct TestCx(Vec); + +impl ViewPathTracker for TestCx { + fn push_id(&mut self, id: ViewId) { + self.0.push(id); + } + fn pop_id(&mut self) { + self.0 + .pop() + .expect("Each pop_id should have a matching push_id"); + } + fn view_path(&mut self) -> &[ViewId] { + &self.0 + } +} + +impl TestCx { + pub(super) fn assert_empty(&self) { + assert!( + self.0.is_empty(), + "Views should always match push_ids and pop_ids" + ); + } +} + +#[derive(PartialEq, Eq, Debug, Clone, Copy)] +pub(super) enum Operation { + Build(u32), + Rebuild { from: u32, to: u32 }, + Teardown(u32), + Replace(u32), +} + +#[derive(Clone)] +pub(super) struct TestElement { + pub(super) operations: Vec, + pub(super) view_path: Vec, + /// The child sequence, if applicable + /// + /// This avoids having to create more element types + pub(super) children: Option, +} +impl ViewElement for TestElement { + type Mut<'a> = &'a mut Self; +} + +/// A view which records all operations which happen on it into the element +/// +/// The const generic parameter is used for testing `AnyView` +pub(super) struct OperationView(pub(super) u32); + +#[allow(clippy::manual_non_exhaustive)] +// non_exhaustive is crate level, but this is to "protect" against +// the parent tests from constructing this +pub(super) struct Action { + pub(super) id: u32, + _priv: (), +} + +pub(super) struct SequenceView { + id: u32, + seq: Seq, + phantom: PhantomData, +} + +pub(super) fn sequence(id: u32, seq: Seq) -> SequenceView +where + Seq: ViewSequence<(), Action, TestCx, TestElement, Marker>, +{ + SequenceView { + id, + seq, + phantom: PhantomData, + } +} + +impl View<(), Action, TestCx> for SequenceView +where + Seq: ViewSequence<(), Action, TestCx, TestElement, Marker>, + Marker: 'static, +{ + type Element = TestElement; + + type ViewState = (Seq::SeqState, AppendVec); + + fn build(&self, ctx: &mut TestCx) -> (Self::Element, Self::ViewState) { + let mut elements = AppendVec::default(); + let state = self.seq.seq_build(ctx, &mut elements); + ( + TestElement { + operations: vec![Operation::Build(self.id)], + children: Some(SeqChildren { + active: elements.into_inner(), + deleted: vec![], + }), + view_path: ctx.view_path().to_vec(), + }, + (state, AppendVec::default()), + ) + } + + fn rebuild<'el>( + &self, + prev: &Self, + view_state: &mut Self::ViewState, + ctx: &mut TestCx, + element: Mut<'el, Self::Element>, + ) -> Mut<'el, Self::Element> { + assert_eq!(&*element.view_path, ctx.view_path()); + element.operations.push(Operation::Rebuild { + from: prev.id, + to: self.id, + }); + let mut elements = SeqTracker { + inner: element.children.as_mut().unwrap(), + ix: 0, + scratch: &mut view_state.1, + }; + self.seq + .seq_rebuild(&prev.seq, &mut view_state.0, ctx, &mut elements); + element + } + + fn teardown( + &self, + view_state: &mut Self::ViewState, + ctx: &mut TestCx, + element: Mut<'_, Self::Element>, + ) { + assert_eq!(&*element.view_path, ctx.view_path()); + element.operations.push(Operation::Teardown(self.id)); + let mut elements = SeqTracker { + inner: element.children.as_mut().unwrap(), + ix: 0, + scratch: &mut view_state.1, + }; + self.seq.seq_teardown(&mut view_state.0, ctx, &mut elements); + } + + fn message( + &self, + view_state: &mut Self::ViewState, + id_path: &[ViewId], + message: DynMessage, + app_state: &mut (), + ) -> MessageResult { + self.seq + .seq_message(&mut view_state.0, id_path, message, app_state) + } +} + +impl View<(), Action, TestCx> for OperationView { + type Element = TestElement; + + type ViewState = (); + + fn build(&self, ctx: &mut TestCx) -> (Self::Element, Self::ViewState) { + ( + TestElement { + operations: vec![Operation::Build(self.0)], + view_path: ctx.view_path().to_vec(), + children: None, + }, + (), + ) + } + + fn rebuild<'el>( + &self, + prev: &Self, + _: &mut Self::ViewState, + ctx: &mut TestCx, + element: Mut<'el, Self::Element>, + ) -> Mut<'el, Self::Element> { + assert_eq!(&*element.view_path, ctx.view_path()); + element.operations.push(Operation::Rebuild { + from: prev.0, + to: self.0, + }); + element + } + + fn teardown(&self, _: &mut Self::ViewState, ctx: &mut TestCx, element: Mut<'_, Self::Element>) { + assert_eq!(&*element.view_path, ctx.view_path()); + element.operations.push(Operation::Teardown(self.0)); + } + + fn message( + &self, + _: &mut Self::ViewState, + _: &[ViewId], + _: DynMessage, + _: &mut (), + ) -> MessageResult { + // If we get an `Action` value, we know it came from here + MessageResult::Action(Action { + _priv: (), + id: self.0, + }) + } +} + +impl SuperElement for TestElement { + fn upcast(child: TestElement) -> Self { + child + } + + fn with_downcast_val( + this: Self::Mut<'_>, + f: impl FnOnce(Mut<'_, TestElement>) -> R, + ) -> (Self::Mut<'_>, R) { + let ret = f(this); + (this, ret) + } +} + +impl AnyElement for TestElement { + fn replace_inner(this: Self::Mut<'_>, child: TestElement) -> Self::Mut<'_> { + assert_eq!(child.operations.len(), 1); + let Operation::Build(child_id) = child.operations.first().unwrap() else { + panic!() + }; + assert_ne!(child.view_path, this.view_path); + this.operations.push(Operation::Replace(*child_id)); + this.view_path = child.view_path; + if let Some((mut new_seq, old_seq)) = child.children.zip(this.children.as_mut()) { + new_seq.deleted.extend(old_seq.deleted.iter().cloned()); + new_seq + .deleted + .extend(old_seq.active.iter().cloned().enumerate()); + *old_seq = new_seq; + } + this + } +} + +#[derive(Clone)] +pub(super) struct SeqChildren { + pub(super) active: Vec, + pub(super) deleted: Vec<(usize, TestElement)>, +} + +pub(super) struct SeqTracker<'a> { + scratch: &'a mut AppendVec, + ix: usize, + inner: &'a mut SeqChildren, +} + +#[track_caller] +pub(super) fn assert_action(result: MessageResult, id: u32) { + let MessageResult::Action(inner) = result else { + panic!() + }; + assert_eq!(inner.id, id); +} + +impl<'a> ElementSplice for SeqTracker<'a> { + fn with_scratch(&mut self, f: impl FnOnce(&mut AppendVec) -> R) -> R { + let ret = f(self.scratch); + for element in self.scratch.drain() { + self.inner.active.push(element); + } + ret + } + fn insert(&mut self, element: TestElement) { + self.inner.active.push(element); + } + fn mutate(&mut self, f: impl FnOnce(Mut<'_, TestElement>) -> R) -> R { + let ix = self.ix; + self.ix += 1; + f(&mut self.inner.active[ix]) + } + fn skip(&mut self, n: usize) { + self.ix += n; + } + fn delete(&mut self, f: impl FnOnce(Mut<'_, TestElement>) -> R) -> R { + let ret = f(&mut self.inner.active[self.ix]); + let val = self.inner.active.remove(self.ix); + self.inner.deleted.push((self.ix, val)); + ret + } +} diff --git a/xilem_core/tests/tuple_sequence.rs b/xilem_core/tests/tuple_sequence.rs new file mode 100644 index 00000000..eab2ea85 --- /dev/null +++ b/xilem_core/tests/tuple_sequence.rs @@ -0,0 +1,195 @@ +// Copyright 2024 the Xilem Authors +// SPDX-License-Identifier: Apache-2.0 + +mod common; +use common::*; +use xilem_core::View; + +fn record_ops(id: u32) -> OperationView<0> { + OperationView(id) +} + +#[test] +fn unit_no_elements() { + let view = sequence(0, ()); + let mut ctx = TestCx::default(); + let (element, _state) = view.build(&mut ctx); + ctx.assert_empty(); + assert!(element.children.unwrap().active.is_empty()); +} + +/// The sequence (item,) should pass through all methods to the child +#[test] +fn one_element_passthrough() { + let view = sequence(1, (record_ops(0),)); + let mut ctx = TestCx::default(); + let (mut element, mut state) = view.build(&mut ctx); + ctx.assert_empty(); + assert_eq!(element.operations, &[Operation::Build(1)]); + assert_eq!(element.view_path, &[]); + + let seq_children = element.children.as_ref().unwrap(); + assert!(seq_children.deleted.is_empty()); + assert_eq!(seq_children.active.len(), 1); + let child = seq_children.active.first().unwrap(); + assert_eq!(child.operations, &[Operation::Build(0)]); + assert_eq!( + child.view_path, + &[], + "The single item tuple ViewSequence shouldn't add to the view path" + ); + + let view2 = sequence(3, (record_ops(2),)); + view2.rebuild(&view, &mut state, &mut ctx, &mut element); + ctx.assert_empty(); + let seq_children = element.children.as_ref().unwrap(); + assert_eq!( + element.operations, + &[Operation::Build(1), Operation::Rebuild { from: 1, to: 3 }] + ); + + assert_eq!(seq_children.active.len(), 1); + assert!(seq_children.deleted.is_empty()); + let child = seq_children.active.first().unwrap(); + assert_eq!( + child.operations, + &[Operation::Build(0), Operation::Rebuild { from: 0, to: 2 }] + ); + + let result = view2.message(&mut state, &[], Box::new(()), &mut ()); + // The message should have been routed to the only child + assert_action(result, 2); + + view2.teardown(&mut state, &mut ctx, &mut element); + assert_eq!( + element.operations, + &[ + Operation::Build(1), + Operation::Rebuild { from: 1, to: 3 }, + Operation::Teardown(3) + ] + ); + + let seq_children = element.children.as_ref().unwrap(); + // It has been removed from the parent sequence when tearing down + assert_eq!(seq_children.active.len(), 0); + assert_eq!(seq_children.deleted.len(), 1); + let (child_idx, child) = seq_children.deleted.first().unwrap(); + assert_eq!(*child_idx, 0); + assert_eq!( + child.operations, + &[ + Operation::Build(0), + Operation::Rebuild { from: 0, to: 2 }, + Operation::Teardown(2) + ] + ); +} + +/// The sequence (item, item) should pass through all methods to the children +#[test] +fn two_element_passthrough() { + let view = sequence(2, (record_ops(0), record_ops(1))); + let mut ctx = TestCx::default(); + let (mut element, mut state) = view.build(&mut ctx); + ctx.assert_empty(); + assert_eq!(element.operations, &[Operation::Build(2)]); + assert_eq!(element.view_path, &[]); + + let seq_children = element.children.as_ref().unwrap(); + assert!(seq_children.deleted.is_empty()); + assert_eq!(seq_children.active.len(), 2); + let first_child = &seq_children.active[0]; + assert_eq!(first_child.operations, &[Operation::Build(0)]); + assert_eq!(first_child.view_path.len(), 1); + let second_child = &seq_children.active[1]; + assert_eq!(second_child.operations, &[Operation::Build(1)]); + assert_eq!(second_child.view_path.len(), 1); + + let view2 = sequence(5, (record_ops(3), record_ops(4))); + view2.rebuild(&view, &mut state, &mut ctx, &mut element); + ctx.assert_empty(); + assert_eq!( + element.operations, + &[Operation::Build(2), Operation::Rebuild { from: 2, to: 5 }] + ); + + let seq_children = element.children.as_ref().unwrap(); + assert!(seq_children.deleted.is_empty()); + assert_eq!(seq_children.active.len(), 2); + let first_child = &seq_children.active[0]; + assert_eq!( + first_child.operations, + &[Operation::Build(0), Operation::Rebuild { from: 0, to: 3 }] + ); + let second_child = &seq_children.active[1]; + assert_eq!( + second_child.operations, + &[Operation::Build(1), Operation::Rebuild { from: 1, to: 4 }] + ); + + view2.teardown(&mut state, &mut ctx, &mut element); + assert_eq!( + element.operations, + &[ + Operation::Build(2), + Operation::Rebuild { from: 2, to: 5 }, + Operation::Teardown(5) + ] + ); + + let seq_children = element.children.as_ref().unwrap(); + // It was removed from the parent sequence when tearing down + assert_eq!(seq_children.active.len(), 0); + assert_eq!(seq_children.deleted.len(), 2); + let (first_child_idx, first_child) = &seq_children.deleted[0]; + assert_eq!(*first_child_idx, 0); + assert_eq!( + first_child.operations, + &[ + Operation::Build(0), + Operation::Rebuild { from: 0, to: 3 }, + Operation::Teardown(3) + ] + ); + let (second_child_idx, second_child) = &seq_children.deleted[1]; + // At the time of being deleted, this was effectively the item at index 0 + assert_eq!(*second_child_idx, 0); + assert_eq!( + second_child.operations, + &[ + Operation::Build(1), + Operation::Rebuild { from: 1, to: 4 }, + Operation::Teardown(4) + ] + ); +} + +/// The sequence (item, item) should pass through all methods to the children +#[test] +fn two_element_message() { + let view = sequence(2, (record_ops(0), record_ops(1))); + let mut ctx = TestCx::default(); + let (element, mut state) = view.build(&mut ctx); + ctx.assert_empty(); + assert_eq!(element.operations, &[Operation::Build(2)]); + assert_eq!(element.view_path, &[]); + + let seq_children = element.children.as_ref().unwrap(); + assert!(seq_children.deleted.is_empty()); + assert_eq!(seq_children.active.len(), 2); + let first_child = &seq_children.active[0]; + assert_eq!(first_child.operations, &[Operation::Build(0)]); + let first_path = first_child.view_path.to_vec(); + let second_child = &seq_children.active[1]; + assert_eq!(second_child.operations, &[Operation::Build(1)]); + let second_path = second_child.view_path.to_vec(); + + let result = view.message(&mut state, &first_path, Box::new(()), &mut ()); + assert_action(result, 0); + + let result = view.message(&mut state, &second_path, Box::new(()), &mut ()); + assert_action(result, 1); +} + +// We don't test higher tuples, because these all use the same implementation diff --git a/xilem_core/tests/vec_sequence.rs b/xilem_core/tests/vec_sequence.rs new file mode 100644 index 00000000..9f867ace --- /dev/null +++ b/xilem_core/tests/vec_sequence.rs @@ -0,0 +1,240 @@ +// Copyright 2024 the Xilem Authors +// SPDX-License-Identifier: Apache-2.0 + +mod common; +use common::*; +use xilem_core::{MessageResult, View}; + +fn record_ops(id: u32) -> OperationView<0> { + OperationView(id) +} + +#[test] +fn zero_zero() { + let view = sequence(0, Vec::>::new()); + let mut ctx = TestCx::default(); + let (mut element, mut state) = view.build(&mut ctx); + ctx.assert_empty(); + assert_eq!(element.operations, &[Operation::Build(0)]); + assert_eq!(element.view_path, &[]); + + let seq_children = element.children.as_ref().unwrap(); + assert!(seq_children.deleted.is_empty()); + assert!(seq_children.active.is_empty()); + + let view2 = sequence(1, vec![]); + view2.rebuild(&view, &mut state, &mut ctx, &mut element); + ctx.assert_empty(); + assert_eq!( + element.operations, + &[Operation::Build(0), Operation::Rebuild { from: 0, to: 1 }] + ); + + let seq_children = element.children.as_ref().unwrap(); + assert!(seq_children.deleted.is_empty()); + assert!(seq_children.active.is_empty()); + + view2.teardown(&mut state, &mut ctx, &mut element); + ctx.assert_empty(); + assert_eq!( + element.operations, + &[ + Operation::Build(0), + Operation::Rebuild { from: 0, to: 1 }, + Operation::Teardown(1) + ] + ); + + let seq_children = element.children.as_ref().unwrap(); + assert!(seq_children.deleted.is_empty()); + assert!(seq_children.active.is_empty()); +} + +#[test] +fn one_zero() { + let view = sequence(1, vec![record_ops(0)]); + let mut ctx = TestCx::default(); + let (mut element, mut state) = view.build(&mut ctx); + ctx.assert_empty(); + assert_eq!(element.operations, &[Operation::Build(1)]); + assert_eq!(element.view_path, &[]); + + let seq_children = element.children.as_ref().unwrap(); + assert!(seq_children.deleted.is_empty()); + assert_eq!(seq_children.active.len(), 1); + let child = seq_children.active.first().unwrap(); + assert_eq!(child.operations, &[Operation::Build(0)]); + assert_eq!(child.view_path.len(), 1); + + let view2 = sequence(2, vec![]); + view2.rebuild(&view, &mut state, &mut ctx, &mut element); + ctx.assert_empty(); + assert_eq!( + element.operations, + &[Operation::Build(1), Operation::Rebuild { from: 1, to: 2 }] + ); + + let seq_children = element.children.as_ref().unwrap(); + assert!(seq_children.active.is_empty()); + assert_eq!(seq_children.deleted.len(), 1); + let (child_idx, child) = seq_children.deleted.first().unwrap(); + assert_eq!(*child_idx, 0); + assert_eq!( + child.operations, + &[Operation::Build(0), Operation::Teardown(0)] + ); + + view2.teardown(&mut state, &mut ctx, &mut element); + ctx.assert_empty(); + assert_eq!( + element.operations, + &[ + Operation::Build(1), + Operation::Rebuild { from: 1, to: 2 }, + Operation::Teardown(2) + ] + ); + + let seq_children = element.children.as_ref().unwrap(); + assert!(seq_children.active.is_empty()); + assert_eq!(seq_children.deleted.len(), 1); + let (child_idx, child) = seq_children.deleted.first().unwrap(); + assert_eq!(*child_idx, 0); + assert_eq!( + child.operations, + &[Operation::Build(0), Operation::Teardown(0)] + ); +} + +#[test] +fn one_two() { + let view = sequence(1, vec![record_ops(0)]); + let mut ctx = TestCx::default(); + let (mut element, mut state) = view.build(&mut ctx); + ctx.assert_empty(); + assert_eq!(element.operations, &[Operation::Build(1)]); + assert_eq!(element.view_path, &[]); + + let seq_children = element.children.as_ref().unwrap(); + assert!(seq_children.deleted.is_empty()); + assert_eq!(seq_children.active.len(), 1); + let child = seq_children.active.first().unwrap(); + assert_eq!(child.operations, &[Operation::Build(0)]); + assert_eq!(child.view_path.len(), 1); + + let view2 = sequence(4, vec![record_ops(2), record_ops(3)]); + view2.rebuild(&view, &mut state, &mut ctx, &mut element); + ctx.assert_empty(); + assert_eq!( + element.operations, + &[Operation::Build(1), Operation::Rebuild { from: 1, to: 4 }] + ); + + let seq_children = element.children.as_ref().unwrap(); + assert!(seq_children.deleted.is_empty()); + assert_eq!(seq_children.active.len(), 2); + let first_child = &seq_children.active[0]; + assert_eq!( + first_child.operations, + &[Operation::Build(0), Operation::Rebuild { from: 0, to: 2 }] + ); + assert_eq!(first_child.view_path.len(), 1); + let second_child = &seq_children.active[1]; + assert_eq!(second_child.operations, &[Operation::Build(3)]); + assert_eq!(second_child.view_path.len(), 1); + + view2.teardown(&mut state, &mut ctx, &mut element); + ctx.assert_empty(); + assert_eq!( + element.operations, + &[ + Operation::Build(1), + Operation::Rebuild { from: 1, to: 4 }, + Operation::Teardown(4) + ] + ); + + let seq_children = element.children.as_ref().unwrap(); + assert!(seq_children.active.is_empty()); + assert_eq!(seq_children.deleted.len(), 2); + let (first_child_idx, first_child) = &seq_children.deleted[0]; + assert_eq!(*first_child_idx, 0); + assert_eq!( + first_child.operations, + &[ + Operation::Build(0), + Operation::Rebuild { from: 0, to: 2 }, + Operation::Teardown(2) + ] + ); + let (second_child_idx, second_child) = &seq_children.deleted[1]; + assert_eq!(*second_child_idx, 0); + assert_eq!( + second_child.operations, + &[Operation::Build(3), Operation::Teardown(3)] + ); +} + +#[test] +fn normal_messages() { + let view = sequence(0, vec![record_ops(0), record_ops(1)]); + let mut ctx = TestCx::default(); + let (mut element, mut state) = view.build(&mut ctx); + ctx.assert_empty(); + assert_eq!(element.view_path, &[]); + + let seq_children = element.children.as_ref().unwrap(); + assert!(seq_children.deleted.is_empty()); + assert_eq!(seq_children.active.len(), 2); + let first_child = &seq_children.active[0]; + let first_path = first_child.view_path.to_vec(); + + let second_child = &seq_children.active[1]; + let second_path = second_child.view_path.to_vec(); + + let result = view.message(&mut state, &first_path, Box::new(()), &mut ()); + assert_action(result, 0); + let result = view.message(&mut state, &second_path, Box::new(()), &mut ()); + assert_action(result, 1); + + let view2 = sequence(0, vec![record_ops(2), record_ops(3)]); + view2.rebuild(&view, &mut state, &mut ctx, &mut element); + ctx.assert_empty(); + + let result = view2.message(&mut state, &first_path, Box::new(()), &mut ()); + assert_action(result, 2); + let result = view2.message(&mut state, &second_path, Box::new(()), &mut ()); + assert_action(result, 3); +} + +#[test] +fn stale_messages() { + let view = sequence(0, vec![record_ops(0)]); + let mut ctx = TestCx::default(); + let (mut element, mut state) = view.build(&mut ctx); + ctx.assert_empty(); + assert_eq!(element.view_path, &[]); + + let seq_children = element.children.as_ref().unwrap(); + assert!(seq_children.deleted.is_empty()); + assert_eq!(seq_children.active.len(), 1); + let first_child = seq_children.active.first().unwrap(); + let first_path = first_child.view_path.to_vec(); + + let result = view.message(&mut state, &first_path, Box::new(()), &mut ()); + assert_action(result, 0); + + let view2 = sequence(0, vec![]); + view2.rebuild(&view, &mut state, &mut ctx, &mut element); + ctx.assert_empty(); + + let result = view2.message(&mut state, &first_path, Box::new(()), &mut ()); + assert!(matches!(result, MessageResult::Stale(_))); + + let view3 = sequence(0, vec![record_ops(1)]); + view3.rebuild(&view2, &mut state, &mut ctx, &mut element); + ctx.assert_empty(); + + let result = view3.message(&mut state, &first_path, Box::new(()), &mut ()); + assert!(matches!(result, MessageResult::Stale(_))); +} diff --git a/xilem_web/Cargo.toml b/xilem_web/Cargo.toml index 40fac40d..dccb3292 100644 --- a/xilem_web/Cargo.toml +++ b/xilem_web/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" description = "HTML DOM frontend for the Xilem Rust UI framework." keywords = ["xilem", "html", "dom", "web", "ui"] categories = ["gui", "web-programming"] -publish = false # Until it's ready +publish = false # Until it's ready edition.workspace = true license.workspace = true repository.workspace = true @@ -19,7 +19,7 @@ cargo-args = ["-Zunstable-options", "-Zrustdoc-scrape-examples"] workspace = true [dependencies] -xilem_core.workspace = true +xilem_web_core = { workspace = true } peniko.workspace = true bitflags.workspace = true wasm-bindgen = "0.2.92" diff --git a/xilem_web/README.md b/xilem_web/README.md index 9c20492b..cd5ce9be 100644 --- a/xilem_web/README.md +++ b/xilem_web/README.md @@ -1,8 +1,8 @@ # `xilem_web` prototype -This is an early prototype of a potential implementation of the Xilem architecture using DOM elements -as Xilem elements (unfortunately the two concepts have the same name). This uses xilem_core under the hood, -and offers a proof that it can be used outside of `xilem` proper. +This is an early prototype of a potential implementation of the Xilem architecture using DOM elements +as Xilem elements (unfortunately the two concepts have the same name). This uses xilem_web_core under the hood, +which is a legacy version of xilem_core. The easiest way to run it is to use [Trunk]. Run `trunk serve`, then navigate the browser to the link provided (usually `http://localhost:8080`). diff --git a/xilem_web/src/lib.rs b/xilem_web/src/lib.rs index 0600378b..bd923bec 100644 --- a/xilem_web/src/lib.rs +++ b/xilem_web/src/lib.rs @@ -25,6 +25,8 @@ mod vecmap; mod view; mod view_ext; +extern crate xilem_web_core as xilem_core; + pub use xilem_core::MessageResult; pub use app::App; diff --git a/xilem_web/xilem_web_core/Cargo.toml b/xilem_web/xilem_web_core/Cargo.toml new file mode 100644 index 00000000..12c09d22 --- /dev/null +++ b/xilem_web/xilem_web_core/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "xilem_web_core" +version = "0.1.0" +description = "Common core of the Xilem Rust UI framework." +keywords = ["xilem", "ui", "reactive", "performance"] +categories = ["gui"] +publish = false # Until it's ready +edition.workspace = true +license.workspace = true +repository.workspace = true +homepage.workspace = true + +[package.metadata.docs.rs] +all-features = true +# rustdoc-scrape-examples tracking issue https://github.com/rust-lang/rust/issues/88791 +cargo-args = ["-Zunstable-options", "-Zrustdoc-scrape-examples"] + +[lints] +workspace = true + +[dependencies] diff --git a/xilem_web/xilem_web_core/LICENSE b/xilem_web/xilem_web_core/LICENSE new file mode 100644 index 00000000..d6456956 --- /dev/null +++ b/xilem_web/xilem_web_core/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/xilem_web/xilem_web_core/src/any_view.rs b/xilem_web/xilem_web_core/src/any_view.rs new file mode 100644 index 00000000..172f1aa4 --- /dev/null +++ b/xilem_web/xilem_web_core/src/any_view.rs @@ -0,0 +1,137 @@ +// Copyright 2023 the Xilem Authors +// SPDX-License-Identifier: Apache-2.0 + +#[macro_export] +macro_rules! generate_anyview_trait { + ($anyview:ident, $viewtrait:ident, $viewmarker:ty, $cx:ty, $changeflags:ty, $anywidget:ident, $boxedview:ident; $($ss:tt)*) => { + /// A trait enabling type erasure of views. + pub trait $anyview { + fn as_any(&self) -> &dyn std::any::Any; + + fn dyn_build( + &self, + cx: &mut $cx, + ) -> ($crate::Id, Box, Box); + + fn dyn_rebuild( + &self, + cx: &mut $cx, + prev: &dyn $anyview, + id: &mut $crate::Id, + state: &mut Box, + element: &mut Box, + ) -> $changeflags; + + fn dyn_message( + &self, + id_path: &[$crate::Id], + state: &mut dyn std::any::Any, + message: Box, + app_state: &mut T, + ) -> $crate::MessageResult; + } + + impl + 'static> $anyview for V + where + V::State: 'static, + V::Element: $anywidget + 'static, + { + fn as_any(&self) -> &dyn std::any::Any { + self + } + + fn dyn_build( + &self, + cx: &mut $cx, + ) -> ($crate::Id, Box, Box) { + let (id, state, element) = self.build(cx); + (id, Box::new(state), Box::new(element)) + } + + fn dyn_rebuild( + &self, + cx: &mut $cx, + prev: &dyn $anyview, + id: &mut $crate::Id, + state: &mut Box, + element: &mut Box, + ) -> ChangeFlags { + use std::ops::DerefMut; + if let Some(prev) = prev.as_any().downcast_ref() { + if let Some(state) = state.downcast_mut() { + if let Some(element) = element.deref_mut().as_any_mut().downcast_mut() { + self.rebuild(cx, prev, id, state, element) + } else { + eprintln!("downcast of element failed in dyn_rebuild"); + <$changeflags>::default() + } + } else { + eprintln!("downcast of state failed in dyn_rebuild"); + <$changeflags>::default() + } + } else { + let (new_id, new_state, new_element) = self.build(cx); + *id = new_id; + *state = Box::new(new_state); + *element = Box::new(new_element); + <$changeflags>::tree_structure() + } + } + + fn dyn_message( + &self, + id_path: &[$crate::Id], + state: &mut dyn std::any::Any, + message: Box, + app_state: &mut T, + ) -> $crate::MessageResult { + if let Some(state) = state.downcast_mut() { + self.message(id_path, state, message, app_state) + } else { + // Possibly softer failure? + panic!("downcast error in dyn_event"); + } + } + } + + pub type $boxedview = Box $( $ss )* >; + + impl $viewmarker for $boxedview {} + + impl $viewtrait for $boxedview { + type State = Box; + + type Element = Box; + + fn build(&self, cx: &mut $cx) -> ($crate::Id, Self::State, Self::Element) { + use std::ops::Deref; + self.deref().dyn_build(cx) + } + + fn rebuild( + &self, + cx: &mut $cx, + prev: &Self, + id: &mut $crate::Id, + state: &mut Self::State, + element: &mut Self::Element, + ) -> $changeflags { + use std::ops::Deref; + self.deref() + .dyn_rebuild(cx, prev.deref(), id, state, element) + } + + fn message( + &self, + id_path: &[$crate::Id], + state: &mut Self::State, + message: Box, + app_state: &mut T, + ) -> $crate::MessageResult { + use std::ops::{Deref, DerefMut}; + self.deref() + .dyn_message(id_path, state.deref_mut(), message, app_state) + } + } + }; +} diff --git a/xilem_core/src/id.rs b/xilem_web/xilem_web_core/src/id.rs similarity index 100% rename from xilem_core/src/id.rs rename to xilem_web/xilem_web_core/src/id.rs diff --git a/xilem_web/xilem_web_core/src/lib.rs b/xilem_web/xilem_web_core/src/lib.rs new file mode 100644 index 00000000..afe57a0d --- /dev/null +++ b/xilem_web/xilem_web_core/src/lib.rs @@ -0,0 +1,27 @@ +// Copyright 2022 the Xilem Authors +// SPDX-License-Identifier: Apache-2.0 + +//! Generic implementation of Xilem view traits. +//! +//! This crate has a few basic types needed to support views, and also +//! a set of macros used to instantiate the main view traits. The client +//! will need to supply a bound on elements, a "pod" type which +//! supports dynamic dispatching and marking of change flags, and a +//! context. +//! +//! All this is still experimental. This crate is where more of the core +//! Xilem architecture will land (some of which was implemented in the +//! original prototype but not yet ported): adapt, memoize, use_state, +//! and possibly some async logic. Likely most of env will also land +//! here, but that also requires coordination with the context. + +mod any_view; +mod id; +mod message; +mod sequence; +mod vec_splice; +mod view; + +pub use id::{Id, IdPath}; +pub use message::{AsyncWake, MessageResult}; +pub use vec_splice::VecSplice; diff --git a/xilem_web/xilem_web_core/src/message.rs b/xilem_web/xilem_web_core/src/message.rs new file mode 100644 index 00000000..ac6bdb5a --- /dev/null +++ b/xilem_web/xilem_web_core/src/message.rs @@ -0,0 +1,74 @@ +// Copyright 2022 the Xilem Authors +// SPDX-License-Identifier: Apache-2.0 + +use std::any::Any; + +#[macro_export] +macro_rules! message { + ($($bounds:tt)*) => { + pub struct Message { + pub id_path: xilem_core::IdPath, + pub body: Box, + } + + impl Message { + pub fn new(id_path: xilem_core::IdPath, event: impl std::any::Any + $($bounds)*) -> Message { + Message { + id_path, + body: Box::new(event), + } + } + } + }; +} + +/// A result wrapper type for event handlers. +#[derive(Default)] +pub enum MessageResult { + /// The event handler was invoked and returned an action. + /// + /// Use this return type if your widgets should respond to events by passing + /// a value up the tree, rather than changing their internal state. + Action(A), + /// The event handler received a change request that requests a rebuild. + /// + /// Note: A rebuild will always occur if there was a state change. This return + /// type can be used to indicate that a full rebuild is necessary even if the + /// state remained the same. It is expected that this type won't be used very + /// often. + #[allow(unused)] + RequestRebuild, + /// The event handler discarded the event. + /// + /// This is the variant that you **almost always want** when you're not returning + /// an action. + #[allow(unused)] + #[default] + Nop, + /// The event was addressed to an id path no longer in the tree. + /// + /// This is a normal outcome for async operation when the tree is changing + /// dynamically, but otherwise indicates a logic error. + Stale(Box), +} + +// TODO: does this belong in core? +pub struct AsyncWake; + +impl MessageResult { + pub fn map(self, f: impl FnOnce(A) -> B) -> MessageResult { + match self { + MessageResult::Action(a) => MessageResult::Action(f(a)), + MessageResult::RequestRebuild => MessageResult::RequestRebuild, + MessageResult::Stale(event) => MessageResult::Stale(event), + MessageResult::Nop => MessageResult::Nop, + } + } + + pub fn or(self, f: impl FnOnce(Box) -> Self) -> Self { + match self { + MessageResult::Stale(event) => f(event), + _ => self, + } + } +} diff --git a/xilem_web/xilem_web_core/src/sequence.rs b/xilem_web/xilem_web_core/src/sequence.rs new file mode 100644 index 00000000..5773fcca --- /dev/null +++ b/xilem_web/xilem_web_core/src/sequence.rs @@ -0,0 +1,363 @@ +// Copyright 2023 the Xilem Authors +// SPDX-License-Identifier: Apache-2.0 + +#[doc(hidden)] +#[macro_export] +macro_rules! impl_view_tuple { + ( $viewseq:ident, $elements_splice: ident, $pod:ty, $cx:ty, $changeflags:ty, $( $t:ident),* ; $( $i:tt ),* ) => { + impl ),* > $viewseq for ( $( $t, )* ) { + type State = ( $( $t::State, )*); + + fn build(&self, cx: &mut $cx, elements: &mut dyn $elements_splice) -> Self::State { + let b = ( $( self.$i.build(cx, elements), )* ); + let state = ( $( b.$i, )*); + state + } + + fn rebuild( + &self, + cx: &mut $cx, + prev: &Self, + state: &mut Self::State, + els: &mut dyn $elements_splice, + ) -> ChangeFlags { + let mut changed = <$changeflags>::default(); + $( + let el_changed = self.$i.rebuild(cx, &prev.$i, &mut state.$i, els); + changed |= el_changed; + )* + changed + } + + fn message( + &self, + id_path: &[$crate::Id], + state: &mut Self::State, + message: Box, + app_state: &mut T, + ) -> $crate::MessageResult { + $crate::MessageResult::Stale(message) + $( + .or(|message|{ + self.$i.message(id_path, &mut state.$i, message, app_state) + }) + )* + } + + fn count(&self, state: &Self::State) -> usize { + 0 + $( + + self.$i.count(&state.$i) + )* + } + } + } +} +#[macro_export] +macro_rules! generate_viewsequence_trait { + ($viewseq:ident, $view:ident, $viewmarker: ident, $elements_splice: ident, $bound:ident, $cx:ty, $changeflags:ty, $pod:ty; $( $ss:tt )* ) => { + + /// A temporary "splice" to add, update, delete and monitor elements in a sequence of elements. + /// It is mainly intended for view sequences + /// + /// Usually it's backed by a collection (e.g. `Vec`) that holds all the (existing) elements. + /// It sweeps over the element collection and does updates in place. + /// Internally it works by having a pointer/index to the current/old element (0 at the beginning), + /// and the pointer is incremented by basically all methods that mutate that sequence. + pub trait $elements_splice { + /// Insert a new element at the current index in the resulting collection (and increment the index by 1) + fn push(&mut self, element: $pod, cx: &mut $cx); + /// Mutate the next existing element, and add it to the resulting collection (and increment the index by 1) + fn mutate(&mut self, cx: &mut $cx) -> &mut $pod; + // TODO(#160) this could also track view id changes (old_id, new_id) + /// Mark any changes done by `mutate` on the current element (this doesn't change the index) + fn mark(&mut self, changeflags: $changeflags, cx: &mut $cx) -> $changeflags; + /// Delete the next n existing elements (this doesn't change the index) + fn delete(&mut self, n: usize, cx: &mut $cx); + /// Current length of the elements collection + fn len(&self) -> usize; + // TODO(#160) add a skip method when it is necessary (e.g. relevant for immutable ViewSequences like ropes) + } + + impl<'a, 'b> $elements_splice for $crate::VecSplice<'a, 'b, $pod> { + fn push(&mut self, element: $pod, _cx: &mut $cx) { + self.push(element); + } + + fn mutate(&mut self, _cx: &mut $cx) -> &mut $pod + { + self.mutate() + } + + fn mark(&mut self, changeflags: $changeflags, _cx: &mut $cx) -> $changeflags + { + self.last_mutated_mut().map(|pod| pod.mark(changeflags)).unwrap_or_default() + } + + fn delete(&mut self, n: usize, _cx: &mut $cx) { + self.delete(n) + } + + fn len(&self) -> usize { + self.len() + } + } + + /// This trait represents a (possibly empty) sequence of views. + /// + /// It is up to the parent view how to lay out and display them. + pub trait $viewseq $( $ss )* { + /// Associated states for the views. + type State $( $ss )*; + + /// Build the associated widgets and initialize all states. + /// + /// To be able to monitor changes (e.g. tree-structure tracking) rather than just adding elements, + /// this takes an element splice as well (when it could be just a `Vec` otherwise) + fn build(&self, cx: &mut $cx, elements: &mut dyn $elements_splice) -> Self::State; + + /// Update the associated widget. + /// + /// Returns `true` when anything has changed. + fn rebuild( + &self, + cx: &mut $cx, + prev: &Self, + state: &mut Self::State, + elements: &mut dyn $elements_splice, + ) -> $changeflags; + + /// Propagate a message. + /// + /// Handle a message, propagating to elements if needed. Here, `id_path` is a slice + /// of ids beginning at an element of this view_sequence. + fn message( + &self, + id_path: &[$crate::Id], + state: &mut Self::State, + message: Box, + app_state: &mut T, + ) -> $crate::MessageResult; + + /// Returns the current amount of widgets built by this sequence. + fn count(&self, state: &Self::State) -> usize; + } + + impl + $viewmarker> $viewseq for V + where + V::Element: $bound + 'static, + { + type State = (>::State, $crate::Id); + + fn build(&self, cx: &mut $cx, elements: &mut dyn $elements_splice) -> Self::State { + let (id, state, pod) = cx.with_new_pod(|cx| >::build(self, cx)); + elements.push(pod, cx); + (state, id) + } + + fn rebuild( + &self, + cx: &mut $cx, + prev: &Self, + state: &mut Self::State, + elements: &mut dyn $elements_splice, + ) -> $changeflags { + let pod = elements.mutate(cx); + let flags = cx.with_pod(pod, |el, cx| { + >::rebuild( + self, + cx, + prev, + &mut state.1, + &mut state.0, + el, + ) + }); + elements.mark(flags, cx) + } + + fn message( + &self, + id_path: &[$crate::Id], + state: &mut Self::State, + message: Box, + app_state: &mut T, + ) -> $crate::MessageResult { + if let Some((first, rest_path)) = id_path.split_first() { + if first == &state.1 { + return >::message( + self, + rest_path, + &mut state.0, + message, + app_state, + ); + } + } + $crate::MessageResult::Stale(message) + } + + fn count(&self, _state: &Self::State) -> usize { + 1 + } + } + + impl> $viewseq for Option { + type State = Option; + + fn build(&self, cx: &mut $cx, elements: &mut dyn $elements_splice) -> Self::State { + match self { + None => None, + Some(vt) => { + let state = vt.build(cx, elements); + Some(state) + } + } + } + + fn rebuild( + &self, + cx: &mut $cx, + prev: &Self, + state: &mut Self::State, + elements: &mut dyn $elements_splice, + ) -> $changeflags { + match (self, &mut *state, prev) { + (Some(this), Some(state), Some(prev)) => this.rebuild(cx, prev, state, elements), + (None, Some(seq_state), Some(prev)) => { + let count = prev.count(&seq_state); + elements.delete(count, cx); + *state = None; + + <$changeflags>::tree_structure() + } + (Some(this), None, None) => { + *state = Some(this.build(cx, elements)); + + <$changeflags>::tree_structure() + } + (None, None, None) => <$changeflags>::empty(), + _ => panic!("non matching state and prev value"), + } + } + + fn message( + &self, + id_path: &[$crate::Id], + state: &mut Self::State, + message: Box, + app_state: &mut T, + ) -> $crate::MessageResult { + match (self, state) { + (Some(vt), Some(state)) => vt.message(id_path, state, message, app_state), + (None, None) => $crate::MessageResult::Stale(message), + _ => panic!("non matching state and prev value"), + } + } + + fn count(&self, state: &Self::State) -> usize { + match (self, state) { + (Some(vt), Some(state)) => vt.count(state), + (None, None) => 0, + _ => panic!("non matching state and prev value"), + } + } + } + + impl> $viewseq for Vec { + type State = Vec; + + fn build(&self, cx: &mut $cx, elements: &mut dyn $elements_splice) -> Self::State { + self.iter().map(|child| child.build(cx, elements)).collect() + } + + fn rebuild( + &self, + cx: &mut $cx, + prev: &Self, + state: &mut Self::State, + elements: &mut dyn $elements_splice, + ) -> $changeflags { + let mut changed = <$changeflags>::default(); + for ((child, child_prev), child_state) in self.iter().zip(prev).zip(state.iter_mut()) { + let el_changed = child.rebuild(cx, child_prev, child_state, elements); + changed |= el_changed; + } + let n = self.len(); + if n < prev.len() { + let n_delete = state + .splice(n.., []) + .enumerate() + .map(|(i, state)| prev[n + i].count(&state)) + .sum(); + elements.delete(n_delete, cx); + changed |= <$changeflags>::tree_structure(); + } else if n > prev.len() { + for i in prev.len()..n { + state.push(self[i].build(cx, elements)); + } + changed |= <$changeflags>::tree_structure(); + } + changed + } + + fn count(&self, state: &Self::State) -> usize { + self.iter().zip(state).map(|(child, child_state)| + child.count(child_state)) + .sum() + } + + fn message( + &self, + id_path: &[$crate::Id], + state: &mut Self::State, + message: Box, + app_state: &mut T, + ) -> $crate::MessageResult { + let mut result = $crate::MessageResult::Stale(message); + for (child, child_state) in self.iter().zip(state) { + if let $crate::MessageResult::Stale(message) = result { + result = child.message(id_path, child_state, message, app_state); + } else { + break; + } + } + result + } + } + + /// This trait marks a type a + #[doc = concat!(stringify!($view), ".")] + /// + /// This trait is a workaround for Rust's orphan rules. It serves as a switch between + /// default and custom + #[doc = concat!("`", stringify!($viewseq), "`")] + /// implementations. You can't implement + #[doc = concat!("`", stringify!($viewseq), "`")] + /// for types which also implement + #[doc = concat!("`", stringify!($viewmarker), "`.")] + pub trait $viewmarker {} + + $crate::impl_view_tuple!($viewseq, $elements_splice, $pod, $cx, $changeflags, ;); + $crate::impl_view_tuple!($viewseq, $elements_splice, $pod, $cx, $changeflags, + V0; 0); + $crate::impl_view_tuple!($viewseq, $elements_splice, $pod, $cx, $changeflags, + V0, V1; 0, 1); + $crate::impl_view_tuple!($viewseq, $elements_splice, $pod, $cx, $changeflags, + V0, V1, V2; 0, 1, 2); + $crate::impl_view_tuple!($viewseq, $elements_splice, $pod, $cx, $changeflags, + V0, V1, V2, V3; 0, 1, 2, 3); + $crate::impl_view_tuple!($viewseq, $elements_splice, $pod, $cx, $changeflags, + V0, V1, V2, V3, V4; 0, 1, 2, 3, 4); + $crate::impl_view_tuple!($viewseq, $elements_splice, $pod, $cx, $changeflags, + V0, V1, V2, V3, V4, V5; 0, 1, 2, 3, 4, 5); + $crate::impl_view_tuple!($viewseq, $elements_splice, $pod, $cx, $changeflags, + V0, V1, V2, V3, V4, V5, V6; 0, 1, 2, 3, 4, 5, 6); + $crate::impl_view_tuple!($viewseq, $elements_splice, $pod, $cx, $changeflags, + V0, V1, V2, V3, V4, V5, V6, V7; 0, 1, 2, 3, 4, 5, 6, 7); + $crate::impl_view_tuple!($viewseq, $elements_splice, $pod, $cx, $changeflags, + V0, V1, V2, V3, V4, V5, V6, V7, V8; 0, 1, 2, 3, 4, 5, 6, 7, 8); + $crate::impl_view_tuple!($viewseq, $elements_splice, $pod, $cx, $changeflags, + V0, V1, V2, V3, V4, V5, V6, V7, V8, V9; 0, 1, 2, 3, 4, 5, 6, 7, 8, 9); + }; +} diff --git a/xilem_core/src/vec_splice.rs b/xilem_web/xilem_web_core/src/vec_splice.rs similarity index 100% rename from xilem_core/src/vec_splice.rs rename to xilem_web/xilem_web_core/src/vec_splice.rs diff --git a/xilem_core/src/view/adapt.rs b/xilem_web/xilem_web_core/src/view/adapt.rs similarity index 100% rename from xilem_core/src/view/adapt.rs rename to xilem_web/xilem_web_core/src/view/adapt.rs diff --git a/xilem_core/src/view/memoize.rs b/xilem_web/xilem_web_core/src/view/memoize.rs similarity index 100% rename from xilem_core/src/view/memoize.rs rename to xilem_web/xilem_web_core/src/view/memoize.rs diff --git a/xilem_core/src/view/mod.rs b/xilem_web/xilem_web_core/src/view/mod.rs similarity index 100% rename from xilem_core/src/view/mod.rs rename to xilem_web/xilem_web_core/src/view/mod.rs