mirror of https://github.com/linebender/xilem
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:
parent
3726c24c6a
commit
86d9592a3e
|
@ -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`
|
||||
|
|
|
@ -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"
|
||||
|
|
11
Cargo.toml
11
Cargo.toml
|
@ -1,15 +1,17 @@
|
|||
[workspace]
|
||||
resolver = "2"
|
||||
members = [
|
||||
"xilem",
|
||||
"xilem_core",
|
||||
"masonry",
|
||||
|
||||
"xilem_web",
|
||||
"xilem_web/xilem_web_core",
|
||||
"xilem_web/web_examples/counter",
|
||||
"xilem_web/web_examples/counter_custom_element",
|
||||
"xilem_web/web_examples/todomvc",
|
||||
"xilem_web/web_examples/mathml_svg",
|
||||
"xilem_web/web_examples/svgtoy",
|
||||
"masonry",
|
||||
"xilem",
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
|
@ -26,15 +28,16 @@ clippy.assigning_clones = "allow"
|
|||
rust.unexpected_cfgs = { level = "warn", check-cfg = ['cfg(FALSE)', 'cfg(tarpaulin_include)'] }
|
||||
|
||||
[workspace.dependencies]
|
||||
xilem_core = { version = "0.1.0", path = "xilem_core" }
|
||||
xilem_web_core = { version = "0.1.0", path = "xilem_web/xilem_web_core" }
|
||||
masonry = { version = "0.2.0", path = "masonry" }
|
||||
xilem_core = { version = "0.1.0", path = "xilem_core" }
|
||||
vello = "0.1.0"
|
||||
wgpu = "0.19.4"
|
||||
kurbo = "0.11.0"
|
||||
parley = "0.1.0"
|
||||
peniko = "0.1.0"
|
||||
winit = "0.30.0"
|
||||
tracing = "0.1.40"
|
||||
tracing = {version = "0.1.40", default-features = false}
|
||||
smallvec = "1.13.2"
|
||||
dpi = "0.1.1"
|
||||
fnv = "1.0.7"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -28,6 +28,7 @@ crate-type = ["cdylib"]
|
|||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
xilem_core.workspace = true
|
||||
masonry.workspace = true
|
||||
winit.workspace = true
|
||||
tracing.workspace = true
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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| {
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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`?");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
218
xilem/src/lib.rs
218
xilem/src/lib.rs
|
@ -2,116 +2,52 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
#![allow(clippy::comparison_chain)]
|
||||
use std::{any::Any, collections::HashMap};
|
||||
use std::collections::HashMap;
|
||||
|
||||
use driver::MasonryDriver;
|
||||
use masonry::{
|
||||
app_driver::AppDriver,
|
||||
dpi::LogicalSize,
|
||||
event_loop_runner,
|
||||
widget::{RootWidget, WidgetMut},
|
||||
Widget, WidgetId, WidgetPod,
|
||||
};
|
||||
pub use masonry::{widget::Axis, Color, TextAlignment};
|
||||
use winit::{
|
||||
error::EventLoopError,
|
||||
window::{Window, WindowAttributes},
|
||||
};
|
||||
use xilem_core::{MessageResult, SuperElement, View, ViewElement, ViewId, ViewPathTracker};
|
||||
|
||||
pub use masonry::{
|
||||
dpi,
|
||||
event_loop_runner::{EventLoop, EventLoopBuilder},
|
||||
widget::Axis,
|
||||
Color, TextAlignment,
|
||||
};
|
||||
pub use xilem_core as core;
|
||||
|
||||
mod any_view;
|
||||
mod id;
|
||||
mod sequence;
|
||||
mod vec_splice;
|
||||
pub use any_view::{AnyMasonryView, BoxedMasonryView};
|
||||
pub use any_view::AnyWidgetView;
|
||||
mod driver;
|
||||
pub mod view;
|
||||
pub use id::ViewId;
|
||||
pub use sequence::{ElementSplice, ViewSequence};
|
||||
pub use vec_splice::VecSplice;
|
||||
|
||||
pub use masonry::dpi;
|
||||
pub use masonry::event_loop_runner::{EventLoop, EventLoopBuilder};
|
||||
|
||||
pub struct Xilem<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>),
|
||||
}
|
||||
|
|
|
@ -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);
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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!(
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
/examples/filesystem
|
|
@ -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 = []
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<'_>;
|
||||
}
|
|
@ -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};
|
||||
|
|
|
@ -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
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
// Copyright 2024 the Xilem Authors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
mod memoize;
|
||||
pub use memoize::{memoize, Memoize};
|
|
@ -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(_)));
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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(_)));
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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(_)));
|
||||
}
|
|
@ -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"
|
||||
|
|
|
@ -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`).
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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]
|
|
@ -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.
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
|
@ -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;
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
};
|
||||
}
|
Loading…
Reference in New Issue