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<State, Action> View<State, Action, [new] ViewCtx> for
Button<State, Action>`.

---------

Co-authored-by: Philipp Mildenberger <philipp@mildenberger.me>
This commit is contained in:
Daniel McNab 2024-06-06 16:16:36 +01:00 committed by GitHub
parent 3726c24c6a
commit 86d9592a3e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
58 changed files with 4782 additions and 1869 deletions

View File

@ -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<AppData, (), Element = impl Widget> {
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`
### `masonry/
See `ARCHITECTURE.md` file located under `crates/masonry/doc`

10
Cargo.lock generated
View File

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

View File

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

View File

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

View File

@ -18,7 +18,7 @@ pub enum Action {
TextEntered(String),
CheckboxChecked(bool),
// FIXME - This is a huge hack
Other(Arc<dyn Any>),
Other(Arc<dyn Any + Send + Sync>),
}
impl PartialEq for Action {

View File

@ -28,6 +28,7 @@ crate-type = ["cdylib"]
workspace = true
[dependencies]
xilem_core.workspace = true
masonry.workspace = true
winit.workspace = true
tracing.workspace = true

View File

@ -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<i32> {
fn app_logic(data: &mut i32) -> impl WidgetView<i32> {
flex((
button("-", |data| {
*data -= 1;

View File

@ -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<AppData> {
fn app_logic(data: &mut AppData) -> impl WidgetView<AppData> {
// 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<AppData> {
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<AppData> {
))
}
fn toggleable(data: &mut AppData) -> impl MasonryView<AppData> {
let inner_view: BoxedMasonryView<_, _> = if data.active {
fn toggleable(data: &mut AppData) -> impl WidgetView<AppData> {
let inner_view: Box<AnyWidgetView<_>> = 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,
};

View File

@ -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<D> {
data: D,
// When TAITs are stabilized this can be a non-erased concrete type
view: Option<Arc<dyn AnyMasonryView<AppState>>>,
view: Option<Arc<AnyWidgetView<AppState>>>,
}
// The following is an example to do memoization with an Arc
fn increase_button(state: &mut AppState) -> Arc<dyn AnyMasonryView<AppState>> {
fn increase_button(state: &mut AppState) -> Arc<AnyWidgetView<AppState>> {
if state.count != state.increase_button.data || state.increase_button.view.is_none() {
let view = Arc::new(button(
format!("current count is {}", state.count),
@ -38,7 +38,7 @@ fn increase_button(state: &mut AppState) -> Arc<dyn AnyMasonryView<AppState>> {
// This is the alternative with Memoize
// Note how this requires a closure that returns the memoized view, while Arc does not
fn decrease_button(state: &AppState) -> impl MasonryView<AppState> {
fn decrease_button(state: &AppState) -> impl WidgetView<AppState> {
memoize(state.count, |count| {
button(
format!("decrease the count: {count}"),
@ -47,11 +47,11 @@ fn decrease_button(state: &AppState) -> impl MasonryView<AppState> {
})
}
fn reset_button() -> impl MasonryView<AppState> {
fn reset_button() -> impl WidgetView<AppState> {
button("reset", |data: &mut AppState| data.count = 0)
}
fn app_logic(state: &mut AppState) -> impl MasonryView<AppState> {
fn app_logic(state: &mut AppState) -> impl WidgetView<AppState> {
flex((
increase_button(state),
decrease_button(state),

View File

@ -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<TaskList> {
fn app_logic(task_list: &mut TaskList) -> impl WidgetView<TaskList> {
let input_box = textbox(
task_list.next_task.clone(),
|task_list: &mut TaskList, new_value| {

View File

@ -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<State, Action = ()> = Box<dyn AnyMasonryView<State, Action>>;
pub type AnyWidgetView<State, Action = ()> =
dyn AnyView<State, Action, ViewCtx, Pod<DynWidget>> + Send + Sync;
impl<State: 'static, Action: 'static> MasonryView<State, Action>
for BoxedMasonryView<State, Action>
{
type Element = DynWidget;
type ViewState = AnyViewState;
fn build(&self, cx: &mut ViewCx) -> (masonry::WidgetPod<Self::Element>, Self::ViewState) {
self.deref().dyn_build(cx)
impl<W: Widget> SuperElement<Pod<W>> for Pod<DynWidget> {
fn upcast(child: Pod<W>) -> Self {
WidgetPod::new(DynWidget {
inner: child.inner.boxed(),
})
.into()
}
fn message(
&self,
view_state: &mut Self::ViewState,
id_path: &[ViewId],
message: Box<dyn Any>,
app_state: &mut State,
) -> crate::MessageResult<Action> {
self.deref()
.dyn_message(view_state, id_path, message, app_state)
}
fn with_downcast_val<R>(
mut this: Self::Mut<'_>,
f: impl FnOnce(<Pod<W> 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::Element>,
) {
self.deref()
.dyn_rebuild(view_state, cx, prev.deref(), element);
(this, ret)
}
}
pub struct AnyViewState {
inner_state: Box<dyn Any>,
generation: u64,
}
impl<State: 'static, Action: 'static> MasonryView<State, Action>
for Arc<dyn AnyMasonryView<State, Action>>
{
type ViewState = AnyViewState;
type Element = DynWidget;
fn build(&self, cx: &mut ViewCx) -> (masonry::WidgetPod<Self::Element>, Self::ViewState) {
self.deref().dyn_build(cx)
}
fn rebuild(
&self,
view_state: &mut Self::ViewState,
cx: &mut ViewCx,
prev: &Self,
element: WidgetMut<Self::Element>,
) {
if !Arc::ptr_eq(self, prev) {
self.deref()
.dyn_rebuild(view_state, cx, prev.deref(), element);
}
}
fn message(
&self,
view_state: &mut Self::ViewState,
id_path: &[ViewId],
message: Box<dyn Any>,
app_state: &mut State,
) -> MessageResult<Action> {
self.deref()
.dyn_message(view_state, id_path, message, app_state)
}
}
/// A trait enabling type erasure of views.
pub trait AnyMasonryView<State, Action = ()>: Send + Sync {
fn as_any(&self) -> &dyn Any;
fn dyn_build(&self, cx: &mut ViewCx) -> (WidgetPod<DynWidget>, AnyViewState);
fn dyn_rebuild(
&self,
dyn_state: &mut AnyViewState,
cx: &mut ViewCx,
prev: &dyn AnyMasonryView<State, Action>,
element: WidgetMut<DynWidget>,
);
fn dyn_message(
&self,
dyn_state: &mut AnyViewState,
id_path: &[ViewId],
message: Box<dyn Any>,
app_state: &mut State,
) -> MessageResult<Action>;
}
impl<State, Action, V: MasonryView<State, Action> + 'static> AnyMasonryView<State, Action> for V
where
V::ViewState: Any,
{
fn as_any(&self) -> &dyn Any {
self
}
fn dyn_build(&self, cx: &mut ViewCx) -> (masonry::WidgetPod<DynWidget>, AnyViewState) {
let generation = 0;
let (element, view_state) =
cx.with_id(ViewId::for_type::<V>(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<State, Action>,
mut element: WidgetMut<DynWidget>,
) {
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::<V>(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::<V>(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<dyn Any>,
app_state: &mut State,
) -> MessageResult<Action> {
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<W: Widget> AnyElement<Pod<W>> for Pod<DynWidget> {
fn replace_inner(mut this: Self::Mut<'_>, child: Pod<W>) -> 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<W: Widget, R>(
this: &mut WidgetMut<'_, Self>,
f: impl FnOnce(Option<WidgetMut<'_, W>>) -> 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.

67
xilem/src/driver.rs Normal file
View File

@ -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<State, Logic, View, ViewState> {
pub(crate) state: State,
pub(crate) logic: Logic,
pub(crate) current_view: View,
pub(crate) view_cx: ViewCtx,
pub(crate) view_state: ViewState,
}
impl<State, Logic, View> AppDriver for MasonryDriver<State, Logic, View, View::ViewState>
where
Logic: FnMut(&mut State) -> View,
View: WidgetView<State>,
{
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::<RootWidget<View::Widget>>();
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`?");
}
}
}

View File

@ -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<T: 'static>(raw: u64) -> Self {
Self {
debug: std::any::type_name::<T>(),
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)
}
}

View File

@ -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<State, Logic, View>
where
View: MasonryView<State>,
View: WidgetView<State>,
{
root_widget: RootWidget<View::Element>,
root_widget: RootWidget<View::Widget>,
driver: MasonryDriver<State, Logic, View, View::ViewState>,
}
pub struct MasonryDriver<State, Logic, View, ViewState> {
state: State,
logic: Logic,
current_view: View,
view_cx: ViewCx,
view_state: ViewState,
}
impl<State, Logic, View> AppDriver for MasonryDriver<State, Logic, View, View::ViewState>
where
Logic: FnMut(&mut State) -> View,
View: MasonryView<State>,
{
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::<RootWidget<View::Element>>();
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<State, Logic, View> Xilem<State, Logic, View>
where
Logic: FnMut(&mut State) -> View,
View: MasonryView<State>,
View: WidgetView<State>,
{
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<State, Action = ()>: Send + Sync + 'static {
type Element: Widget;
type ViewState;
fn build(&self, cx: &mut ViewCx) -> (WidgetPod<Self::Element>, Self::ViewState);
fn rebuild(
&self,
view_state: &mut Self::ViewState,
cx: &mut ViewCx,
prev: &Self,
element: WidgetMut<Self::Element>,
);
fn message(
&self,
view_state: &mut Self::ViewState,
id_path: &[ViewId],
message: Box<dyn Any>,
app_state: &mut State,
) -> MessageResult<Action>;
/// A container for a [Masonry](masonry) widget to be used with Xilem.
///
/// Equivalent to [`WidgetPod<W>`], but in the [`xilem`](crate) crate to work around the orphan rule.
pub struct Pod<W: Widget> {
pub inner: WidgetPod<W>,
}
pub struct ViewCx {
impl<W: Widget> Pod<W> {
/// Create a new `Pod` for `inner`.
pub fn new(inner: W) -> Self {
Self::from(WidgetPod::new(inner))
}
}
impl<W: Widget> From<WidgetPod<W>> for Pod<W> {
fn from(inner: WidgetPod<W>) -> Self {
Pod { inner }
}
}
impl<W: Widget> ViewElement for Pod<W> {
type Mut<'a> = WidgetMut<'a, W>;
}
impl<W: Widget> SuperElement<Pod<W>> for Pod<Box<dyn Widget>> {
fn upcast(child: Pod<W>) -> Self {
child.inner.boxed().into()
}
fn with_downcast_val<R>(
mut this: Self::Mut<'_>,
f: impl FnOnce(<Pod<W> as xilem_core::ViewElement>::Mut<'_>) -> R,
) -> (Self::Mut<'_>, R) {
let downcast = this.downcast();
let ret = f(downcast);
(this, ret)
}
}
pub trait WidgetView<State, Action = ()>:
View<State, Action, ViewCtx, Element = Pod<Self::Widget>> + Send + Sync
{
type Widget: Widget;
}
impl<V, State, Action, W> WidgetView<State, Action> for V
where
V: View<State, Action, ViewCtx, Element = Pod<W>> + 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<E: Widget>(
&mut self,
f: impl FnOnce(&mut Self) -> WidgetPod<E>,
) -> (WidgetPod<E>, ()) {
f: impl FnOnce(&mut Self) -> Pod<E>,
) -> (Pod<E>, ()) {
(self.with_action_widget(f), ())
}
pub fn with_action_widget<E: Widget>(
&mut self,
f: impl FnOnce(&mut Self) -> WidgetPod<E>,
) -> WidgetPod<E> {
pub fn with_action_widget<E: Widget>(&mut self, f: impl FnOnce(&mut Self) -> Pod<E>) -> Pod<E> {
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<R>(&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<E: Widget>(&mut self, widget: WidgetMut<E>) {
self.widget_map.remove(&widget.ctx.widget_id());
}
}
/// A result wrapper type for event handlers.
#[derive(Default)]
pub enum MessageResult<A> {
Action(A),
RequestRebuild,
#[default]
Nop,
Stale(Box<dyn Any>),
}

View File

@ -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<Box<dyn Widget>>);
/// Mutate the next existing element, and add it to the resulting collection (and increment the index by 1)
// TODO: This should actually return `WidgetMut<dyn Widget>`, but that isn't supported in Masonry itself yet
fn mutate(&mut self) -> WidgetMut<Box<dyn Widget>>;
/// 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<State, Action, Marker>: 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<dyn std::any::Any>,
app_state: &mut State,
) -> MessageResult<Action>;
/// 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<State, Action, View: MasonryView<State, Action>> ViewSequence<State, Action, WasAView>
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::<View::Element>();
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<dyn std::any::Any>,
app_state: &mut State,
) -> MessageResult<Action> {
self.message(seq_state, id_path, message, app_state)
}
fn count(&self) -> usize {
1
}
}
pub struct OptionSeqState<InnerState> {
inner: Option<InnerState>,
generation: u64,
}
impl<State, Action, Marker, VT: ViewSequence<State, Action, Marker>>
ViewSequence<State, Action, (WasASequence, Marker)> for Option<VT>
{
type SeqState = OptionSeqState<VT::SeqState>;
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::<VT>(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::<VT>(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::<VT>(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<dyn std::any::Any>,
app_state: &mut State,
) -> MessageResult<Action> {
let (start, rest) = id_path
.split_first()
.expect("Id path has elements for Option<ViewSequence>");
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<InnerState> {
inner_with_generations: Vec<(InnerState, u32)>,
global_generation: u32,
}
// TODO: We use raw indexing for this value. What would make it invalid?
impl<T, A, Marker, VT: ViewSequence<T, A, Marker>> ViewSequence<T, A, (WasASequence, Marker)>
for Vec<VT>
{
type SeqState = VecViewState<VT::SeqState>;
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::<VT>(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::<VT>(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::<VT>(),
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::<VT>(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<dyn std::any::Any>,
app_state: &mut T,
) -> MessageResult<A> {
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<T, A> ViewSequence<T, A, ()> 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<dyn std::any::Any>,
_app_state: &mut T,
) -> MessageResult<A> {
tracing::warn!(?id_path, "Dispatched message to empty tuple");
MessageResult::Stale(message)
}
fn count(&self) -> usize {
0
}
}
impl<State, Action, M0, Seq0: ViewSequence<State, Action, M0>> ViewSequence<State, Action, (M0,)>
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<dyn std::any::Any>,
app_state: &mut State,
) -> MessageResult<Action> {
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<State, Action, $marker>,
)+
> ViewSequence<State, Action, ($($marker,)+)> 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<dyn std::any::Any>,
app_state: &mut State,
) -> MessageResult<Action> {
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);

View File

@ -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<T>,
scratch: &'b mut Vec<T>,
ix: usize,
}
impl<'a, 'b, T> VecSplice<'a, 'b, T> {
pub fn new(v: &'a mut Vec<T>, scratch: &'b mut Vec<T>) -> 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, F: FnOnce(&mut Vec<T>) -> 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<Box<dyn masonry::Widget>>> {
fn push(&mut self, element: masonry::WidgetPod<Box<dyn masonry::Widget>>) {
self.push(element);
}
fn mutate(&mut self) -> WidgetMut<Box<dyn masonry::Widget>> {
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()
}
}

View File

@ -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<State: 'static, Action: 'static, V: MasonryView<State, Action>> MasonryView<State, Action>
for Arc<V>
{
type ViewState = V::ViewState;
type Element = V::Element;
fn build(&self, cx: &mut ViewCx) -> (masonry::WidgetPod<Self::Element>, Self::ViewState) {
self.deref().build(cx)
}
fn rebuild(
&self,
view_state: &mut Self::ViewState,
cx: &mut ViewCx,
prev: &Self,
element: WidgetMut<Self::Element>,
) {
if !Arc::ptr_eq(self, prev) {
self.deref().rebuild(view_state, cx, prev.deref(), element);
}
}
fn message(
&self,
view_state: &mut Self::ViewState,
id_path: &[ViewId],
message: Box<dyn Any>,
app_state: &mut State,
) -> MessageResult<Action> {
self.deref()
.message(view_state, id_path, message, app_state)
}
}

View File

@ -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<F, State, Action>(label: impl Into<ArcStr>, callback: F) -> Button<F>
where
@ -20,39 +22,47 @@ pub struct Button<F> {
callback: F,
}
impl<F, State, Action> MasonryView<State, Action> for Button<F>
impl<F, State, Action> View<State, Action, ViewCtx> for Button<F>
where
F: Fn(&mut State) -> Action + Send + Sync + 'static,
{
type Element = masonry::widget::Button;
type Element = Pod<widget::Button>;
type ViewState = ();
fn build(&self, cx: &mut ViewCx) -> (WidgetPod<Self::Element>, 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<Self::Element>,
) {
_: &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<dyn std::any::Any>,
message: xilem_core::DynMessage,
app_state: &mut State,
) -> crate::MessageResult<Action> {
) -> MessageResult<Action> {
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)
}
}

View File

@ -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<F, State, Action>(
label: impl Into<ArcStr>,
@ -26,44 +27,54 @@ pub struct Checkbox<F> {
callback: F,
}
impl<F, State, Action> MasonryView<State, Action> for Checkbox<F>
impl<F, State, Action> View<State, Action, ViewCtx> for Checkbox<F>
where
F: Fn(&mut State, bool) -> Action + Send + Sync + 'static,
{
type Element = masonry::widget::Checkbox;
type Element = Pod<widget::Checkbox>;
type ViewState = ();
fn build(&self, cx: &mut ViewCx) -> (WidgetPod<Self::Element>, 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<Self::Element>,
) {
(): &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<dyn std::any::Any>,
message: xilem_core::DynMessage,
app_state: &mut State,
) -> MessageResult<Action> {
debug_assert!(

View File

@ -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<VT, Marker>(sequence: VT) -> Flex<VT, Marker> {
// TODO: Create a custom ViewSequence dynamic element for this
pub fn flex<Seq, Marker>(sequence: Seq) -> Flex<Seq, Marker> {
Flex {
phantom: PhantomData,
sequence,
@ -22,8 +23,8 @@ pub fn flex<VT, Marker>(sequence: VT) -> Flex<VT, Marker> {
}
}
pub struct Flex<VT, Marker> {
sequence: VT,
pub struct Flex<Seq, Marker> {
sequence: Seq,
axis: Axis,
cross_axis_alignment: CrossAxisAlignment,
main_axis_alignment: MainAxisAlignment,
@ -31,7 +32,7 @@ pub struct Flex<VT, Marker> {
phantom: PhantomData<fn() -> Marker>,
}
impl<VT, Marker> Flex<VT, Marker> {
impl<Seq, Marker> Flex<Seq, Marker> {
pub fn direction(mut self, axis: Axis) -> Self {
self.axis = axis;
self
@ -52,127 +53,147 @@ impl<VT, Marker> Flex<VT, Marker> {
}
}
impl<State, Action, Marker: 'static, Seq: Sync> MasonryView<State, Action> for Flex<Seq, Marker>
impl<State, Action, Seq, Marker: 'static> View<State, Action, ViewCtx> for Flex<Seq, Marker>
where
Seq: ViewSequence<State, Action, Marker>,
Seq: ViewSequence<State, Action, ViewCtx, Pod<Box<dyn Widget>>, Marker>,
{
type Element = widget::Flex;
type Element = Pod<widget::Flex>;
type ViewState = Seq::SeqState;
fn build(
&self,
cx: &mut crate::ViewCx,
) -> (masonry::WidgetPod<Self::Element>, 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<dyn std::any::Any>,
id_path: &[xilem_core::ViewId],
message: xilem_core::DynMessage,
app_state: &mut State,
) -> crate::MessageResult<Action> {
) -> xilem_core::MessageResult<Action> {
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<Self::Element>,
) {
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<Pod<Box<dyn Widget>>>,
}
impl ElementSplice for FlexSplice<'_> {
fn push(&mut self, element: WidgetPod<Box<dyn masonry::Widget>>) {
self.element.insert_child_pod(self.ix, element);
self.element.insert_default_spacer(self.ix);
impl<'f> ElementSplice<Pod<Box<dyn Widget>>> for FlexSplice<'f, '_> {
fn insert(&mut self, element: Pod<Box<dyn masonry::Widget>>) {
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<Box<dyn Widget>> {
#[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<R>(&mut self, f: impl FnOnce(&mut AppendVec<Pod<Box<dyn Widget>>>) -> 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<R>(
&mut self,
f: impl FnOnce(<Pod<Box<dyn Widget>> 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<R>(
&mut self,
f: impl FnOnce(<Pod<Box<dyn Widget>> 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;
}
}

View File

@ -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<ArcStr>) -> Label {
Label {
@ -39,49 +40,52 @@ impl Label {
}
}
impl<State, Action> MasonryView<State, Action> for Label {
type Element = masonry::widget::Label;
impl<State, Action> View<State, Action, ViewCtx> for Label {
type Element = Pod<widget::Label>;
type ViewState = ();
fn build(&self, _cx: &mut ViewCx) -> (WidgetPod<Self::Element>, 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<Self::Element>,
) {
(): &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<dyn std::any::Any>,
message: xilem_core::DynMessage,
_app_state: &mut State,
) -> crate::MessageResult<Action> {
tracing::error!("Message arrived in Label::message, but Label doesn't consume any messages, this is a bug");

View File

@ -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<D, F> {
data: D,
child_cb: F,
}
pub struct MemoizeState<T, A, V: MasonryView<T, A>> {
view: V,
view_state: V::ViewState,
dirty: bool,
}
impl<D, V, F> Memoize<D, F>
where
F: Fn(&D) -> V,
{
const ASSERT_CONTEXTLESS_FN: () = {
assert!(
std::mem::size_of::<F>() == 0,
"
It's not possible to use function pointers or captured context in closures,
as this potentially messes up the logic of memoize or produces unwanted effects.
For example a different kind of view could be instantiated with a different callback, while the old one is still memoized, but it's not updated then.
It's not possible in Rust currently to check whether the (content of the) callback has changed with the `Fn` trait, which would make this otherwise possible.
"
);
};
pub fn new(data: D, child_cb: F) -> Self {
#[allow(clippy::let_unit_value)]
let _ = Self::ASSERT_CONTEXTLESS_FN;
Memoize { data, child_cb }
}
}
impl<State, Action, D, V, F> MasonryView<State, Action> for Memoize<D, F>
where
D: PartialEq + Send + Sync + 'static,
V: MasonryView<State, Action>,
F: Fn(&D) -> V + Send + Sync + 'static,
{
type ViewState = MemoizeState<State, Action, V>;
type Element = V::Element;
fn build(&self, cx: &mut ViewCx) -> (WidgetPod<Self::Element>, Self::ViewState) {
let view = (self.child_cb)(&self.data);
let (element, view_state) = view.build(cx);
let memoize_state = MemoizeState {
view,
view_state,
dirty: false,
};
(element, memoize_state)
}
fn rebuild(
&self,
view_state: &mut Self::ViewState,
cx: &mut ViewCx,
prev: &Self,
element: WidgetMut<Self::Element>,
) {
if std::mem::take(&mut view_state.dirty) || prev.data != self.data {
let view = (self.child_cb)(&self.data);
view.rebuild(&mut view_state.view_state, cx, &view_state.view, element);
view_state.view = view;
}
}
fn message(
&self,
view_state: &mut Self::ViewState,
id_path: &[ViewId],
message: Box<dyn Any>,
app_state: &mut State,
) -> MessageResult<Action> {
let r = view_state
.view
.message(&mut view_state.view_state, id_path, message, app_state);
if matches!(r, MessageResult::RequestRebuild) {
view_state.dirty = true;
}
r
}
}
/// A static view, all of the content of the `view` should be constant, as this function is only run once
pub fn static_view<V, F>(view: F) -> Memoize<(), impl Fn(&()) -> V>
where
F: Fn() -> V + Send + 'static,
{
Memoize::new((), move |_: &()| view())
}
/// Memoize the view, until the `data` changes (in which case `view` is called again)
pub fn memoize<D, V, F>(data: D, view: F) -> Memoize<D, F>
where
F: Fn(&D) -> V + Send,
{
Memoize::new(data, view)
}

View File

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

View File

@ -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<ArcStr>) -> Prose {
Prose {
@ -40,52 +41,51 @@ impl Prose {
}
}
impl<State, Action> MasonryView<State, Action> for Prose {
type Element = masonry::widget::Prose;
impl<State, Action> View<State, Action, ViewCtx> for Prose {
type Element = Pod<widget::Prose>;
type ViewState = ();
fn build(&self, _cx: &mut ViewCx) -> (WidgetPod<Self::Element>, 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<Self::Element>,
) {
(): &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<dyn std::any::Any>,
message: xilem_core::DynMessage,
_app_state: &mut State,
) -> crate::MessageResult<Action> {
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)
}
}

View File

@ -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<State, Action> Textbox<State, Action> {
}
}
impl<State: 'static, Action: 'static> MasonryView<State, Action> for Textbox<State, Action> {
type Element = masonry::widget::Textbox;
impl<State: 'static, Action: 'static> View<State, Action, ViewCtx> for Textbox<State, Action> {
type Element = Pod<widget::Textbox>;
type ViewState = ();
fn build(&self, cx: &mut ViewCx) -> (WidgetPod<Self::Element>, 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<State: 'static, Action: 'static> MasonryView<State, Action> for Textbox<Sta
})
}
fn rebuild(
fn rebuild<'el>(
&self,
_view_state: &mut Self::ViewState,
cx: &mut ViewCx,
prev: &Self,
mut element: WidgetMut<Self::Element>,
) {
_: &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<dyn std::any::Any>,
message: xilem_core::DynMessage,
app_state: &mut State,
) -> crate::MessageResult<Action> {
) -> MessageResult<Action> {
debug_assert!(
id_path.is_empty(),
"id path should be empty in Textbox::message"

1
xilem_core/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/examples/filesystem

View File

@ -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 = []

77
xilem_core/README.md Normal file
View File

@ -0,0 +1,77 @@
<div align="center">
# Xilem Core
</div>
<!-- Close the <div> opened in lib.rs for rustdoc, which hides the above title -->
</div>
<div align="center">
**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)
</div>
Xilem Core provides primitives which are used by [Xilem][] (a cross-platform GUI toolkit). <!-- and Xilem Web (a web frontend framework) -->
If you are using Xilem, [its documentation][xilem docs] will probably be more helpful for you. <!-- TODO: In the long-term, we probably also need a book? -->
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).
<!-- MSRV will go here once we settle on that for this repository -->
<!-- We hide these elements when viewing in Rustdoc, because they're not expected to be present in crate level docs -->
<div class="rustdoc-hidden">
## 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 <http://www.apache.org/licenses/LICENSE-2.0>)
</div>
[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

View File

@ -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<State> {
File {
name: value.to_string(),
contents: value.to_string(),
}
}
fn app_logic(state: &mut State) -> impl FileView<State> {
let res: DynFileView<State> = 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<State, Action = ()>: View<State, Action, ViewCtx, Element = FsPath> {}
impl<V, State, Action> FileView<State, Action> for V where
V: View<State, Action, ViewCtx, Element = FsPath>
{
}
type DynFileView<State, Action = ()> = Box<dyn AnyView<State, Action, ViewCtx, FsPath>>;
impl SuperElement<FsPath> for FsPath {
fn upcast(child: FsPath) -> Self {
child
}
fn with_downcast_val<R>(
this: Self::Mut<'_>,
f: impl FnOnce(Mut<'_, FsPath>) -> R,
) -> (Self::Mut<'_>, R) {
let ret = f(this);
(this, ret)
}
}
impl AnyElement<FsPath> 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<Marker, Seq: ViewSequence<(), (), ViewCtx, FsPath, Marker>> {
// name: String,
// seq: Seq,
// phantom: PhantomData<fn() -> Marker>,
// }
#[derive(Clone)]
struct File {
name: String,
contents: String,
}
struct FsPath(PathBuf);
impl From<PathBuf> 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<State, Action> View<State, Action, ViewCtx> 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<Action> {
unreachable!()
}
}
struct ViewCtx {
view_path: Vec<ViewId>,
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
}
}

View File

@ -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<u32> {
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<W: Widget> {
widget: W,
}
pub struct WidgetMut<'a, W: Widget> {
value: &'a mut W,
}
impl Widget for Box<dyn Widget> {
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<W: Widget> ViewElement for WidgetPod<W> {
type Mut<'a> = WidgetMut<'a, W>;
}
impl<State, Action> View<State, Action, ViewCtx> for Button {
type Element = WidgetPod<ButtonWidget>;
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<Action> {
MessageResult::Nop
}
}
pub struct Button {}
pub struct ButtonWidget {}
impl Widget for ButtonWidget {
fn as_mut_any(&mut self) -> &mut dyn Any {
self
}
}
impl<W: Widget> SuperElement<WidgetPod<W>> for WidgetPod<Box<dyn Widget>> {
fn upcast(child: WidgetPod<W>) -> Self {
WidgetPod {
widget: Box::new(child.widget),
}
}
fn with_downcast_val<R>(
this: Self::Mut<'_>,
f: impl FnOnce(<WidgetPod<W> 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<ViewId>,
}
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<State, Action = ()>:
View<State, Action, ViewCtx, Element = WidgetPod<Self::Widget>> + Send + Sync
{
type Widget: Widget + Send + Sync;
}
impl<V, State, Action, W> WidgetView<State, Action> for V
where
V: View<State, Action, ViewCtx, Element = WidgetPod<W>> + Send + Sync,
W: Widget + Send + Sync,
{
type Widget = W;
}

View File

@ -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<T, A = ()> {
fn as_any(&self) -> &dyn std::any::Any;
//! Support for a type erased [`View`].
fn dyn_build(
&self,
cx: &mut $cx,
) -> ($crate::Id, Box<dyn std::any::Any $( $ss )* >, Box<dyn $anywidget>);
use core::any::Any;
fn dyn_rebuild(
&self,
cx: &mut $cx,
prev: &dyn $anyview<T, A>,
id: &mut $crate::Id,
state: &mut Box<dyn std::any::Any $( $ss )* >,
element: &mut Box<dyn $anywidget>,
) -> $changeflags;
use alloc::boxed::Box;
fn dyn_message(
&self,
id_path: &[$crate::Id],
state: &mut dyn std::any::Any,
message: Box<dyn std::any::Any>,
app_state: &mut T,
) -> $crate::MessageResult<A>;
}
use crate::{AnyElement, DynMessage, MessageResult, Mut, View, ViewId, ViewPathTracker};
impl<T, A, V: $viewtrait<T, A> + 'static> $anyview<T, A> 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<dyn AnyView<...>>`, which implements [`View`].
// TODO: Mention `Either` when we have implemented that?
///
/// This is also useful for memoization, by storing an `Option<Arc<dyn AnyView<...>>>`,
/// 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<State, Action, Context, Element: crate::ViewElement> {
/// Get an [`Any`] reference to `self`.
fn as_any(&self) -> &dyn Any;
fn dyn_build(
&self,
cx: &mut $cx,
) -> ($crate::Id, Box<dyn std::any::Any $( $ss )* >, Box<dyn $anywidget>) {
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<T, A>,
id: &mut $crate::Id,
state: &mut Box<dyn std::any::Any $( $ss )* >,
element: &mut Box<dyn $anywidget>,
) -> 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<State, Action, Context, Element>,
element: Element::Mut<'el>,
) -> Element::Mut<'el>;
fn dyn_message(
&self,
id_path: &[$crate::Id],
state: &mut dyn std::any::Any,
message: Box<dyn std::any::Any>,
app_state: &mut T,
) -> $crate::MessageResult<A> {
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<T, A = ()> = Box<dyn $anyview<T, A> $( $ss )* >;
impl<T, A> $viewmarker for $boxedview<T, A> {}
impl<T, A> $viewtrait<T, A> for $boxedview<T, A> {
type State = Box<dyn std::any::Any $( $ss )* >;
type Element = Box<dyn $anywidget>;
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<dyn std::any::Any>,
app_state: &mut T,
) -> $crate::MessageResult<A> {
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<Action>;
}
impl<State, Action, Context, DynamicElement, V> AnyView<State, Action, Context, DynamicElement>
for V
where
DynamicElement: AnyElement<V::Element>,
Context: ViewPathTracker,
V: View<State, Action, Context> + '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<State, Action, Context, DynamicElement>,
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<Action> {
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<dyn Any>,
/// The generation is the value which is shown
generation: u64,
}
impl<State: 'static, Action: 'static, Context, Element> View<State, Action, Context>
for dyn AnyView<State, Action, Context, Element>
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<Action> {
self.dyn_message(view_state, id_path, message, app_state)
}
}
// TODO: IWBN if we could avoid this
impl<State: 'static, Action: 'static, Context, Element> View<State, Action, Context>
for dyn AnyView<State, Action, Context, Element> + 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<Action> {
self.dyn_message(view_state, id_path, message, app_state)
}
}
impl<State: 'static, Action: 'static, Context, Element> View<State, Action, Context>
for dyn AnyView<State, Action, Context, Element> + 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<Action> {
self.dyn_message(view_state, id_path, message, app_state)
}
}
impl<State: 'static, Action: 'static, Context, Element> View<State, Action, Context>
for dyn AnyView<State, Action, Context, Element> + 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<Action> {
self.dyn_message(view_state, id_path, message, app_state)
}
}

84
xilem_core/src/element.rs Normal file
View File

@ -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<Self>`] 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
/// `<Self::Element as ViewElement>::Mut<'el>` in the View trait when implementing it (e.g. via rust-analyzer)
pub type Mut<'el, E> = <E as ViewElement>::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<Child>: 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<R>(
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<Child>: SuperElement<Child>
where
Child: ViewElement,
{
/// Replace the inner value of this reference entirely
fn replace_inner(this: Self::Mut<'_>, child: Child) -> Self::Mut<'_>;
}

View File

@ -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!(
"
<!-- This license link is in a .rustdoc-hidden section, but we may as well give the correct link -->
[LICENSE]: https://github.com/linebender/xilem/blob/main/xilem_core/LICENSE
<!-- intra-doc-links go here -->
<!-- TODO: If the alloc feature is disabled, this link doesn't resolve -->
[`alloc`]: alloc
[`View`]: crate::View
[`memoize`]: memoize
<style>
.rustdoc-hidden { display: none; }
</style>
<!-- Hide the header section of the README when using rustdoc -->
<div style=\"display:none\">
",
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};

View File

@ -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<dyn std::any::Any + $($bounds)*>,
}
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<A> {
/// The event handler was invoked and returned an action.
pub enum MessageResult<Action> {
/// 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<dyn Any>`, 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<dyn Message>;
/// 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<dyn Any>`].
fn into_any(self: Box<Self>) -> Box<dyn Any + Send>;
/// Convert `self` into a [`Box<dyn Any>`].
fn as_any(&self) -> &(dyn Any + Send);
/// Gets the debug representation of this message.
fn dyn_debug(&self) -> &dyn Debug;
}
impl<T> Message for T
where
T: Any + Debug + Send,
{
fn into_any(self: Box<Self>) -> Box<dyn Any + Send> {
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<dyn Any>),
}
// TODO: does this belong in core?
pub struct AsyncWake;
impl<A> MessageResult<A> {
pub fn map<B>(self, f: impl FnOnce(A) -> B) -> MessageResult<B> {
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<dyn Any>) -> 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<T: Message>(self: Box<Self>) -> Result<Box<T>, Box<Self>> {
// The panic is unreachable
#![allow(clippy::missing_panics_doc)]
if self.deref().as_any().is::<T>() {
Ok(self
.into_any()
.downcast::<T>()
.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<State, Action> {
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<MyMessage> = 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::<NotMyMessage>().unwrap_err();
let result: Box<MyMessage> = 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"));
}
}

File diff suppressed because it is too large Load Diff

228
xilem_core/src/view.rs Normal file
View File

@ -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<State, Action, Context: ViewPathTracker>: '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<Action>;
// 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<R>(&mut self, id: ViewId, f: impl FnOnce(&mut Self) -> R) -> R {
self.push_id(id);
let res = f(self);
self.pop_id();
res
}
}
impl<State, Action, Context: ViewPathTracker, V: View<State, Action, Context> + ?Sized>
View<State, Action, Context> for Box<V>
{
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<Action> {
self.deref()
.message(view_state, id_path, message, app_state)
}
}
/// An implementation of [`View`] which only runs rebuild if the states are different
impl<State, Action, Context: ViewPathTracker, V: View<State, Action, Context> + ?Sized>
View<State, Action, Context> for Arc<V>
{
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<Action> {
self.deref()
.message(view_state, id_path, message, app_state)
}
}

View File

@ -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<D, F> {
data: D,
child_cb: F,
}
pub struct MemoizeState<State, Action, Context, V>
where
Context: ViewPathTracker,
V: View<State, Action, Context>,
{
view: V,
view_state: V::ViewState,
dirty: bool,
}
impl<D, V, F> Memoize<D, F>
where
F: Fn(&D) -> V,
{
const ASSERT_CONTEXTLESS_FN: () = {
assert!(
core::mem::size_of::<F>() == 0,
"
It's not possible to use function pointers or captured context in closures,
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<State, Action, Context, Data, V, ViewFn> View<State, Action, Context> for Memoize<Data, ViewFn>
where
Context: ViewPathTracker,
Data: PartialEq + 'static,
V: View<State, Action, Context>,
ViewFn: Fn(&Data) -> V + 'static,
{
type ViewState = MemoizeState<State, Action, Context, V>;
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<Action> {
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<State, Action, Context, Data, V, ViewFn>(
data: Data,
view: ViewFn,
) -> Memoize<Data, ViewFn>
where
Data: PartialEq + 'static,
ViewFn: Fn(&Data) -> V + 'static,
V: View<State, Action, Context>,
Context: ViewPathTracker,
{
Memoize::new(data, view)
}

View File

@ -0,0 +1,5 @@
// Copyright 2024 the Xilem Authors
// SPDX-License-Identifier: Apache-2.0
mod memoize;
pub use memoize::{memoize, Memoize};

View File

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

185
xilem_core/tests/arc.rs Normal file
View File

@ -0,0 +1,185 @@
// Copyright 2024 the Xilem Authors
// SPDX-License-Identifier: Apache-2.0
//! Tests for the behaviour of [`Arc<V>`] where `V` is a view.
//!
//! Also has some tests for [`Box<V>`], 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);
}

View File

@ -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::<OperationView<0>>);
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(_)));
}

View File

@ -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<ViewId>);
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<Operation>,
pub(super) view_path: Vec<ViewId>,
/// The child sequence, if applicable
///
/// This avoids having to create more element types
pub(super) children: Option<SeqChildren>,
}
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<const N: u32>(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<Seq, Marker> {
id: u32,
seq: Seq,
phantom: PhantomData<Marker>,
}
pub(super) fn sequence<Seq, Marker>(id: u32, seq: Seq) -> SequenceView<Seq, Marker>
where
Seq: ViewSequence<(), Action, TestCx, TestElement, Marker>,
{
SequenceView {
id,
seq,
phantom: PhantomData,
}
}
impl<Seq, Marker> View<(), Action, TestCx> for SequenceView<Seq, Marker>
where
Seq: ViewSequence<(), Action, TestCx, TestElement, Marker>,
Marker: 'static,
{
type Element = TestElement;
type ViewState = (Seq::SeqState, AppendVec<TestElement>);
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<Action> {
self.seq
.seq_message(&mut view_state.0, id_path, message, app_state)
}
}
impl<const N: u32> View<(), Action, TestCx> for OperationView<N> {
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<Action> {
// If we get an `Action` value, we know it came from here
MessageResult::Action(Action {
_priv: (),
id: self.0,
})
}
}
impl SuperElement<TestElement> for TestElement {
fn upcast(child: TestElement) -> Self {
child
}
fn with_downcast_val<R>(
this: Self::Mut<'_>,
f: impl FnOnce(Mut<'_, TestElement>) -> R,
) -> (Self::Mut<'_>, R) {
let ret = f(this);
(this, ret)
}
}
impl AnyElement<TestElement> 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<TestElement>,
pub(super) deleted: Vec<(usize, TestElement)>,
}
pub(super) struct SeqTracker<'a> {
scratch: &'a mut AppendVec<TestElement>,
ix: usize,
inner: &'a mut SeqChildren,
}
#[track_caller]
pub(super) fn assert_action(result: MessageResult<Action>, id: u32) {
let MessageResult::Action(inner) = result else {
panic!()
};
assert_eq!(inner.id, id);
}
impl<'a> ElementSplice<TestElement> for SeqTracker<'a> {
fn with_scratch<R>(&mut self, f: impl FnOnce(&mut AppendVec<TestElement>) -> 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<R>(&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<R>(&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
}
}

View File

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

View File

@ -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::<OperationView<0>>::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(_)));
}

View File

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

View File

@ -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`).

View File

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

View File

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

View File

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

View File

@ -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<T, A = ()> {
fn as_any(&self) -> &dyn std::any::Any;
fn dyn_build(
&self,
cx: &mut $cx,
) -> ($crate::Id, Box<dyn std::any::Any $( $ss )* >, Box<dyn $anywidget>);
fn dyn_rebuild(
&self,
cx: &mut $cx,
prev: &dyn $anyview<T, A>,
id: &mut $crate::Id,
state: &mut Box<dyn std::any::Any $( $ss )* >,
element: &mut Box<dyn $anywidget>,
) -> $changeflags;
fn dyn_message(
&self,
id_path: &[$crate::Id],
state: &mut dyn std::any::Any,
message: Box<dyn std::any::Any>,
app_state: &mut T,
) -> $crate::MessageResult<A>;
}
impl<T, A, V: $viewtrait<T, A> + 'static> $anyview<T, A> 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<dyn std::any::Any $( $ss )* >, Box<dyn $anywidget>) {
let (id, state, element) = self.build(cx);
(id, Box::new(state), Box::new(element))
}
fn dyn_rebuild(
&self,
cx: &mut $cx,
prev: &dyn $anyview<T, A>,
id: &mut $crate::Id,
state: &mut Box<dyn std::any::Any $( $ss )* >,
element: &mut Box<dyn $anywidget>,
) -> 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<dyn std::any::Any>,
app_state: &mut T,
) -> $crate::MessageResult<A> {
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<T, A = ()> = Box<dyn $anyview<T, A> $( $ss )* >;
impl<T, A> $viewmarker for $boxedview<T, A> {}
impl<T, A> $viewtrait<T, A> for $boxedview<T, A> {
type State = Box<dyn std::any::Any $( $ss )* >;
type Element = Box<dyn $anywidget>;
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<dyn std::any::Any>,
app_state: &mut T,
) -> $crate::MessageResult<A> {
use std::ops::{Deref, DerefMut};
self.deref()
.dyn_message(id_path, state.deref_mut(), message, app_state)
}
}
};
}

View File

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

View File

@ -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<dyn std::any::Any + $($bounds)*>,
}
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<A> {
/// 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<dyn Any>),
}
// TODO: does this belong in core?
pub struct AsyncWake;
impl<A> MessageResult<A> {
pub fn map<B>(self, f: impl FnOnce(A) -> B) -> MessageResult<B> {
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<dyn Any>) -> Self) -> Self {
match self {
MessageResult::Stale(event) => f(event),
_ => self,
}
}
}

View File

@ -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<T, A, $( $t: $viewseq<T, A> ),* > $viewseq<T, A> 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<dyn std::any::Any>,
app_state: &mut T,
) -> $crate::MessageResult<A> {
$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<T, A = ()> $( $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<dyn std::any::Any>,
app_state: &mut T,
) -> $crate::MessageResult<A>;
/// Returns the current amount of widgets built by this sequence.
fn count(&self, state: &Self::State) -> usize;
}
impl<T, A, V: $view<T, A> + $viewmarker> $viewseq<T, A> for V
where
V::Element: $bound + 'static,
{
type State = (<V as $view<T, A>>::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| <V as $view<T, A>>::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| {
<V as $view<T, A>>::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<dyn std::any::Any>,
app_state: &mut T,
) -> $crate::MessageResult<A> {
if let Some((first, rest_path)) = id_path.split_first() {
if first == &state.1 {
return <V as $view<T, A>>::message(
self,
rest_path,
&mut state.0,
message,
app_state,
);
}
}
$crate::MessageResult::Stale(message)
}
fn count(&self, _state: &Self::State) -> usize {
1
}
}
impl<T, A, VT: $viewseq<T, A>> $viewseq<T, A> for Option<VT> {
type State = Option<VT::State>;
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<dyn std::any::Any>,
app_state: &mut T,
) -> $crate::MessageResult<A> {
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<T, A, VT: $viewseq<T, A>> $viewseq<T, A> for Vec<VT> {
type State = Vec<VT::State>;
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<dyn std::any::Any>,
app_state: &mut T,
) -> $crate::MessageResult<A> {
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);
};
}