diff --git a/xilem/examples/calc.rs b/xilem/examples/calc.rs index 4b3d8c7c7..fcf68508f 100644 --- a/xilem/examples/calc.rs +++ b/xilem/examples/calc.rs @@ -5,7 +5,7 @@ use masonry::widget::{CrossAxisAlignment, MainAxisAlignment}; use winit::dpi::LogicalSize; use winit::error::EventLoopError; use winit::window::Window; -use xilem::view::Flex; +use xilem::view::{Flex, FlexSequence}; use xilem::EventLoopBuilder; use xilem::{ view::{button, flex, label, sized_box, Axis, FlexExt as _, FlexSpacer}, @@ -254,7 +254,7 @@ fn app_logic(data: &mut Calculator) -> impl WidgetView { } /// Creates a horizontal centered flex row designed for the display portion of the calculator. -pub fn centered_flex_row(sequence: Seq) -> Flex { +pub fn centered_flex_row>(sequence: Seq) -> Flex { flex(sequence) .direction(Axis::Horizontal) .cross_axis_alignment(CrossAxisAlignment::Center) @@ -263,7 +263,7 @@ pub fn centered_flex_row(sequence: Seq) -> Flex { } /// Creates a horizontal filled flex row designed to be used in a grid. -pub fn flex_row(sequence: Seq) -> Flex { +pub fn flex_row>(sequence: Seq) -> Flex { flex(sequence) .direction(Axis::Horizontal) .cross_axis_alignment(CrossAxisAlignment::Fill) diff --git a/xilem/examples/elm.rs b/xilem/examples/elm.rs index 19d017ec8..d38985561 100644 --- a/xilem/examples/elm.rs +++ b/xilem/examples/elm.rs @@ -26,7 +26,7 @@ enum CountMessage { // `map_action()` is basically how elm works, i.e. provide a message that the parent view has to handle to update the state. // In this case the parent adjusts the count that is given to this view according to the message -fn elm_counter(count: i32) -> impl WidgetView { +fn elm_counter(count: i32) -> impl WidgetView { flex(( label(format!("elm count: {count}")), button("+", |_| CountMessage::Increment), diff --git a/xilem/examples/mason.rs b/xilem/examples/mason.rs index 497c78955..efce18e71 100644 --- a/xilem/examples/mason.rs +++ b/xilem/examples/mason.rs @@ -109,16 +109,18 @@ fn app_logic(data: &mut AppData) -> impl WidgetView { fn toggleable(data: &mut AppData) -> impl WidgetView { if data.active { - flex(( - button("Deactivate", |data: &mut AppData| { - data.active = false; - }), - button("Unlimited Power", |data: &mut AppData| { - data.count = -1_000_000; - }), + fork( + flex(( + button("Deactivate", |data: &mut AppData| { + data.active = false; + }), + button("Unlimited Power", |data: &mut AppData| { + data.count = -1_000_000; + }), + )) + .direction(Axis::Horizontal), run_once(|| tracing::warn!("The pathway to unlimited power has been revealed")), - )) - .direction(Axis::Horizontal) + ) .boxed() } else { button("Activate", |data: &mut AppData| data.active = true).boxed() diff --git a/xilem/src/lib.rs b/xilem/src/lib.rs index 1ac4b2311..591b51ed3 100644 --- a/xilem/src/lib.rs +++ b/xilem/src/lib.rs @@ -19,6 +19,7 @@ use winit::{ }; use xilem_core::{ AsyncCtx, MessageResult, RawProxy, SuperElement, View, ViewElement, ViewId, ViewPathTracker, + ViewSequence, }; pub use masonry::{ @@ -209,6 +210,30 @@ where type Widget = W; } +/// An ordered sequence of widget views, it's used for `0..N` views. +/// See [`ViewSequence`] for more technical details. +/// +/// # Examples +/// +/// ``` +/// use xilem::{view::prose, WidgetViewSequence}; +/// +/// fn prose_sequence( +/// texts: impl Iterator, +/// ) -> impl WidgetViewSequence { +/// texts.map(prose).collect::>() +/// } +/// ``` +pub trait WidgetViewSequence: + ViewSequence> +{ +} + +impl WidgetViewSequence for Seq where + Seq: ViewSequence> +{ +} + type WidgetMap = HashMap>; pub struct ViewCtx { diff --git a/xilem/src/view/async_repeat.rs b/xilem/src/view/async_repeat.rs index 0098d4b83..77e820421 100644 --- a/xilem/src/view/async_repeat.rs +++ b/xilem/src/view/async_repeat.rs @@ -4,7 +4,9 @@ use std::{future::Future, marker::PhantomData, sync::Arc}; use tokio::task::JoinHandle; -use xilem_core::{DynMessage, Message, MessageProxy, NoElement, View, ViewId, ViewPathTracker}; +use xilem_core::{ + DynMessage, Message, MessageProxy, NoElement, View, ViewId, ViewMarker, ViewPathTracker, +}; use crate::ViewCtx; @@ -70,6 +72,7 @@ pub struct AsyncRepeat { message: PhantomData M>, } +impl ViewMarker for AsyncRepeat {} impl View for AsyncRepeat where F: Fn(MessageProxy) -> Fut + 'static, diff --git a/xilem/src/view/button.rs b/xilem/src/view/button.rs index f7371c420..20d43bd46 100644 --- a/xilem/src/view/button.rs +++ b/xilem/src/view/button.rs @@ -3,7 +3,7 @@ use crate::{core::View, Pod}; use masonry::{widget, ArcStr}; -use xilem_core::Mut; +use xilem_core::{Mut, ViewMarker}; pub use masonry::PointerButton; @@ -41,6 +41,7 @@ pub struct Button { callback: F, } +impl ViewMarker for Button {} impl View for Button where F: Fn(&mut State, PointerButton) -> MessageResult + Send + Sync + 'static, diff --git a/xilem/src/view/checkbox.rs b/xilem/src/view/checkbox.rs index 34d6ef55c..755bd1786 100644 --- a/xilem/src/view/checkbox.rs +++ b/xilem/src/view/checkbox.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use masonry::{widget, ArcStr}; -use xilem_core::Mut; +use xilem_core::{Mut, ViewMarker}; use crate::{MessageResult, Pod, View, ViewCtx, ViewId}; @@ -27,6 +27,7 @@ pub struct Checkbox { callback: F, } +impl ViewMarker for Checkbox {} impl View for Checkbox where F: Fn(&mut State, bool) -> Action + Send + Sync + 'static, diff --git a/xilem/src/view/flex.rs b/xilem/src/view/flex.rs index 0502db194..694c7097e 100644 --- a/xilem/src/view/flex.rs +++ b/xilem/src/view/flex.rs @@ -9,36 +9,38 @@ use masonry::{ }; use xilem_core::{ AppendVec, DynMessage, ElementSplice, MessageResult, Mut, SuperElement, View, ViewElement, - ViewId, ViewPathTracker, ViewSequence, + ViewId, ViewMarker, ViewPathTracker, ViewSequence, }; use crate::{AnyWidgetView, Pod, ViewCtx, WidgetView}; pub use masonry::widget::{Axis, CrossAxisAlignment, FlexParams, MainAxisAlignment}; -pub fn flex(sequence: Seq) -> Flex { +pub fn flex>( + sequence: Seq, +) -> Flex { Flex { - phantom: PhantomData, sequence, axis: Axis::Vertical, cross_axis_alignment: CrossAxisAlignment::Center, main_axis_alignment: MainAxisAlignment::Start, fill_major_axis: false, gap: None, + phantom: PhantomData, } } -pub struct Flex { +pub struct Flex { sequence: Seq, axis: Axis, cross_axis_alignment: CrossAxisAlignment, main_axis_alignment: MainAxisAlignment, fill_major_axis: bool, - phantom: PhantomData Marker>, gap: Option, + phantom: PhantomData (State, Action)>, } -impl Flex { +impl Flex { pub fn direction(mut self, axis: Axis) -> Self { self.axis = axis; self @@ -85,9 +87,12 @@ impl Flex { } } -impl View for Flex +impl ViewMarker for Flex {} +impl View for Flex where - Seq: ViewSequence, + State: 'static, + Action: 'static, + Seq: FlexSequence, { type Element = Pod; @@ -303,6 +308,31 @@ impl ElementSplice for FlexSplice<'_> { } } +/// An ordered sequence of views for a [`Flex`] view. +/// See [`ViewSequence`] for more technical details. +/// +/// # Examples +/// +/// ``` +/// use xilem::view::{label, FlexSequence, FlexExt as _}; +/// +/// fn label_sequence( +/// labels: impl Iterator, +/// flex: f64, +/// ) -> impl FlexSequence { +/// labels.map(|l| label(l).flex(flex)).collect::>() +/// } +/// ``` +pub trait FlexSequence: + ViewSequence +{ +} + +impl FlexSequence for Seq where + Seq: ViewSequence +{ +} + /// A trait which extends a [`WidgetView`] with methods to provide parameters for a flex item, or being able to use it interchangeably with a spacer pub trait FlexExt: WidgetView { /// Applies [`impl Into`](`FlexParams`) to this view, can be used as child of a [`Flex`] [`View`] @@ -407,6 +437,7 @@ where } } +impl ViewMarker for FlexItem {} impl View for FlexItem where State: 'static, @@ -485,6 +516,7 @@ impl From for AnyFlexChild { } } +impl ViewMarker for FlexSpacer {} // This impl doesn't require a view id, as it neither receives, nor sends any messages // If this should ever change, it's necessary to adjust the `AnyFlexChild` `View` impl impl View for FlexSpacer { @@ -593,6 +625,7 @@ pub struct AnyFlexChildState { generation: u64, } +impl ViewMarker for AnyFlexChild {} impl View for AnyFlexChild where State: 'static, diff --git a/xilem/src/view/label.rs b/xilem/src/view/label.rs index 6d929d243..a859c80c7 100644 --- a/xilem/src/view/label.rs +++ b/xilem/src/view/label.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use masonry::{text::TextBrush, widget, ArcStr}; -use xilem_core::Mut; +use xilem_core::{Mut, ViewMarker}; use crate::{Color, MessageResult, Pod, TextAlignment, View, ViewCtx, ViewId}; @@ -43,6 +43,7 @@ impl Label { } } +impl ViewMarker for Label {} impl View for Label { type Element = Pod; type ViewState = (); diff --git a/xilem/src/view/prose.rs b/xilem/src/view/prose.rs index 6cf831c7c..0bd66615a 100644 --- a/xilem/src/view/prose.rs +++ b/xilem/src/view/prose.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use masonry::{text::TextBrush, widget, ArcStr}; -use xilem_core::Mut; +use xilem_core::{Mut, ViewMarker}; use crate::{Color, MessageResult, Pod, TextAlignment, View, ViewCtx, ViewId}; @@ -44,6 +44,7 @@ impl Prose { } } +impl ViewMarker for Prose {} impl View for Prose { type Element = Pod; type ViewState = (); diff --git a/xilem/src/view/sized_box.rs b/xilem/src/view/sized_box.rs index fafa6f1db..254c7d458 100644 --- a/xilem/src/view/sized_box.rs +++ b/xilem/src/view/sized_box.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use masonry::widget; +use xilem_core::ViewMarker; use crate::{ core::{Mut, View, ViewId}, @@ -74,6 +75,7 @@ impl SizedBox { } } +impl ViewMarker for SizedBox {} impl View for SizedBox where V: WidgetView, diff --git a/xilem/src/view/textbox.rs b/xilem/src/view/textbox.rs index 32153108b..b029a60c4 100644 --- a/xilem/src/view/textbox.rs +++ b/xilem/src/view/textbox.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use masonry::{text::TextBrush, widget}; -use xilem_core::{Mut, View}; +use xilem_core::{Mut, View, ViewMarker}; use crate::{Color, MessageResult, Pod, TextAlignment, ViewCtx, ViewId}; @@ -63,6 +63,7 @@ impl Textbox { } } +impl ViewMarker for Textbox {} impl View for Textbox { type Element = Pod; type ViewState = (); diff --git a/xilem_core/examples/filesystem.rs b/xilem_core/examples/filesystem.rs index d48e28a24..67408c428 100644 --- a/xilem_core/examples/filesystem.rs +++ b/xilem_core/examples/filesystem.rs @@ -4,7 +4,7 @@ use std::{io::stdin, path::PathBuf}; use xilem_core::{ - AnyElement, AnyView, Mut, SuperElement, View, ViewElement, ViewId, ViewPathTracker, + AnyElement, AnyView, Mut, SuperElement, View, ViewElement, ViewId, ViewMarker, ViewPathTracker, }; #[derive(Debug)] @@ -145,6 +145,7 @@ impl ViewElement for FsPath { type Mut<'a> = &'a mut PathBuf; } +impl ViewMarker for File {} impl View for File { type Element = FsPath; type ViewState = (); diff --git a/xilem_core/examples/user_interface.rs b/xilem_core/examples/user_interface.rs index b7f151d59..41eb6b362 100644 --- a/xilem_core/examples/user_interface.rs +++ b/xilem_core/examples/user_interface.rs @@ -6,7 +6,8 @@ use core::any::Any; use xilem_core::{ - DynMessage, MessageResult, Mut, SuperElement, View, ViewElement, ViewId, ViewPathTracker, + DynMessage, MessageResult, Mut, SuperElement, View, ViewElement, ViewId, ViewMarker, + ViewPathTracker, }; pub fn app_logic(_: &mut u32) -> impl WidgetView { @@ -44,6 +45,7 @@ impl ViewElement for WidgetPod { type Mut<'a> = WidgetMut<'a, W>; } +impl ViewMarker for Button {} impl View for Button { type Element = WidgetPod; type ViewState = (); diff --git a/xilem_core/src/any_view.rs b/xilem_core/src/any_view.rs index 67303f985..97796240a 100644 --- a/xilem_core/src/any_view.rs +++ b/xilem_core/src/any_view.rs @@ -8,7 +8,8 @@ use core::any::Any; use alloc::boxed::Box; use crate::{ - AnyElement, DynMessage, MessageResult, Mut, View, ViewElement, ViewId, ViewPathTracker, + AnyElement, DynMessage, MessageResult, Mut, View, ViewElement, ViewId, ViewMarker, + ViewPathTracker, }; /// A view which can have any view type where the [`View::Element`] is compatible with @@ -171,6 +172,10 @@ pub struct AnyViewState { generation: u64, } +impl ViewMarker + for dyn AnyView +{ +} impl View for dyn AnyView where @@ -220,6 +225,11 @@ where } // TODO: IWBN if we could avoid this + +impl ViewMarker + for dyn AnyView + Send +{ +} impl View for dyn AnyView + Send where @@ -268,6 +278,10 @@ where } } +impl ViewMarker + for dyn AnyView + Send + Sync +{ +} impl View for dyn AnyView + Send + Sync where @@ -316,6 +330,10 @@ where } } +impl ViewMarker + for dyn AnyView + Sync +{ +} impl View for dyn AnyView + Sync where diff --git a/xilem_core/src/element.rs b/xilem_core/src/element.rs index 142ef3ddb..22d4da790 100644 --- a/xilem_core/src/element.rs +++ b/xilem_core/src/element.rs @@ -94,3 +94,16 @@ pub struct NoElement; impl ViewElement for NoElement { type Mut<'a> = (); } + +impl SuperElement for NoElement { + fn upcast(child: NoElement) -> Self { + child + } + + fn with_downcast_val( + this: Mut<'_, Self>, + f: impl FnOnce(Mut<'_, NoElement>) -> R, + ) -> (Self::Mut<'_>, R) { + ((), f(this)) + } +} diff --git a/xilem_core/src/lib.rs b/xilem_core/src/lib.rs index ed069454a..923d814b4 100644 --- a/xilem_core/src/lib.rs +++ b/xilem_core/src/lib.rs @@ -29,7 +29,7 @@ mod deferred; pub use deferred::{AsyncCtx, MessageProxy, PhantomView, ProxyError, RawProxy}; mod view; -pub use view::{View, ViewId, ViewPathTracker}; +pub use view::{View, ViewId, ViewMarker, ViewPathTracker}; mod views; pub use views::{ diff --git a/xilem_core/src/sequence.rs b/xilem_core/src/sequence.rs index ebabb0b3f..b14329857 100644 --- a/xilem_core/src/sequence.rs +++ b/xilem_core/src/sequence.rs @@ -9,8 +9,10 @@ use core::sync::atomic::Ordering; use alloc::vec::Drain; use alloc::vec::Vec; -use crate::element::NoElement; -use crate::{DynMessage, MessageResult, SuperElement, View, ViewElement, ViewId, ViewPathTracker}; +// use crate::element::NoElement; +use crate::{ + DynMessage, MessageResult, SuperElement, View, ViewElement, ViewId, ViewMarker, ViewPathTracker, +}; /// An append only `Vec`. /// @@ -74,11 +76,11 @@ impl Default for AppendVec { /// Note that this will have persistent allocation with size proportional /// to the *longest* `Vec` which is ever provided in the View tree, as this /// uses a generational indexing scheme. +/// - An [`array`] of `ViewSequence` values. /// - Tuples of `ViewSequences` with up to 15 elements. /// These can be nested if an ad-hoc sequence of more than 15 sequences is needed. /// -pub trait ViewSequence: - 'static +pub trait ViewSequence: 'static where Context: ViewPathTracker, Element: ViewElement, @@ -143,15 +145,11 @@ pub trait ElementSplice { fn delete(&mut self, f: impl FnOnce(Element::Mut<'_>) -> R) -> R; } -/// Marker type to workaround trait ambiguity. -#[doc(hidden)] -pub struct WasAView; - impl - ViewSequence for V + ViewSequence for V where Context: ViewPathTracker, - V: View, + V: View + ViewMarker, Element: SuperElement, V::Element: ViewElement, { @@ -218,10 +216,10 @@ pub struct OptionSeqState { /// /// Will mark messages which were sent to a `Some` value if a `None` has since /// occurred as stale. -impl - ViewSequence, Message> for Option +impl + ViewSequence for Option where - Seq: ViewSequence, + Seq: ViewSequence, Context: ViewPathTracker, Element: ViewElement, { @@ -338,57 +336,6 @@ where } } -/// A `View` with [no element](crate::NoElement) can be added to any [`ViewSequence`], because it does not use any -/// properties of the `Element` type. -impl - ViewSequence for NoElementView -where - NoElementView: View, - Element: ViewElement, - Context: ViewPathTracker, -{ - #[doc(hidden)] - type SeqState = NoElementView::ViewState; - - #[doc(hidden)] - fn seq_build(&self, ctx: &mut Context, _: &mut AppendVec) -> Self::SeqState { - let (NoElement, state) = self.build(ctx); - state - } - - #[doc(hidden)] - fn seq_rebuild( - &self, - prev: &Self, - seq_state: &mut Self::SeqState, - ctx: &mut Context, - _: &mut impl ElementSplice, - ) { - self.rebuild(prev, seq_state, ctx, ()); - } - - #[doc(hidden)] - fn seq_teardown( - &self, - seq_state: &mut Self::SeqState, - ctx: &mut Context, - _: &mut impl ElementSplice, - ) { - self.teardown(seq_state, ctx, ()); - } - - #[doc(hidden)] - fn seq_message( - &self, - seq_state: &mut Self::SeqState, - id_path: &[ViewId], - message: Message, - app_state: &mut State, - ) -> MessageResult { - self.message(seq_state, id_path, message, app_state) - } -} - /// The state used to implement `ViewSequence` for `Vec` /// /// We use a generation arena for vector types, with half of the `ViewId` dedicated @@ -428,10 +375,10 @@ fn view_id_to_index_generation(view_id: ViewId) -> (usize, u32) { /// /// Will mark messages which were sent to any index as stale if /// that index has been unused in the meantime. -impl - ViewSequence, Message> for Vec +impl + ViewSequence for Vec where - Seq: ViewSequence, + Seq: ViewSequence, Context: ViewPathTracker, Element: ViewElement, { @@ -582,10 +529,10 @@ where } } -impl - ViewSequence, Message> for [Seq; N] +impl + ViewSequence for [Seq; N] where - Seq: ViewSequence, + Seq: ViewSequence, Context: ViewPathTracker, Element: ViewElement, { @@ -661,7 +608,7 @@ where } impl - ViewSequence for () + ViewSequence for () where Context: ViewPathTracker, Element: ViewElement, @@ -699,10 +646,10 @@ where } } -impl - ViewSequence for (Seq,) +impl + ViewSequence for (Seq,) where - Seq: ViewSequence, + Seq: ViewSequence, Context: ViewPathTracker, Element: ViewElement, { @@ -753,12 +700,9 @@ macro_rules! impl_view_tuple { Action, Context: ViewPathTracker, Element: ViewElement, - $( - $marker, - $seq: ViewSequence, - )+ + $($seq: ViewSequence,)+ Message, - > ViewSequence for ($($seq,)+) + > ViewSequence for ($($seq,)+) { type SeqState = ($($seq::SeqState,)+); diff --git a/xilem_core/src/view.rs b/xilem_core/src/view.rs index dd822dfd7..8118190d3 100644 --- a/xilem_core/src/view.rs +++ b/xilem_core/src/view.rs @@ -9,6 +9,22 @@ use alloc::{boxed::Box, sync::Arc}; use crate::{message::MessageResult, DynMessage, Mut, ViewElement}; +/// A type which can be a [`View`]. Imposes no requirements on the underlying type. +/// Should be implemented alongside every `View` implementation: +/// ```ignore +/// impl<...> ViewMarker for Button<...> {} +/// impl<...> View<...> for Button<...> {...} +/// ``` +/// +/// ## Details +/// +/// Because `View` is generic, Rust [allows you](https://doc.rust-lang.org/reference/items/implementations.html#orphan-rules) to implement this trait for certain non-local types. +/// These non-local types can include `Vec<_>` and `Option<_>`. +/// If this trait were not present, those implementations of `View` would conflict with those types' implementations of `ViewSequence`. +/// This is because every `View` type also implementations `ViewSequence`. +/// Since `ViewMarker` is not generic, these non-local implementations are not permitted for this trait, which means that the conflicting implementation cannot happen. +pub trait ViewMarker {} + /// A lightweight, short-lived representation of the state of a retained /// structure, usually a user interface node. /// @@ -30,11 +46,21 @@ use crate::{message::MessageResult, DynMessage, Mut, ViewElement}; /// During message handling, mutable access to the app state is given to view nodes, /// which will in turn generally expose it to callbacks. /// +/// Due to restrictions of the [orphan rules](https://doc.rust-lang.org/reference/items/implementations.html#orphan-rules), +/// `ViewMarker` needs to be implemented for every type that implements `View`, see [`ViewMarker`] for more details. +/// For example: +/// ```ignore +/// impl<...> ViewMarker for Button<...> {} +/// impl<...> View<...> for Button<...> {...} +/// ``` +/// /// ## Alloc /// /// In order to support the default open-ended [`DynMessage`] type as `Message`, this trait requires an /// allocator to be available. -pub trait View: 'static { +pub trait View: + ViewMarker + '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. @@ -137,6 +163,7 @@ pub trait ViewPathTracker { } } +impl ViewMarker for Box {} impl View for Box where Context: ViewPathTracker, @@ -185,6 +212,7 @@ pub struct ArcState { dirty: bool, } +impl ViewMarker for Arc {} /// An implementation of [`View`] which only runs rebuild if the states are different impl View for Arc where diff --git a/xilem_core/src/views/adapt.rs b/xilem_core/src/views/adapt.rs index b7a422a0e..e88ca51ca 100644 --- a/xilem_core/src/views/adapt.rs +++ b/xilem_core/src/views/adapt.rs @@ -3,7 +3,7 @@ use core::marker::PhantomData; -use crate::{MessageResult, Mut, View, ViewId, ViewPathTracker}; +use crate::{MessageResult, Mut, View, ViewId, ViewMarker, ViewPathTracker}; /// A view that wraps a child view and modifies the state that callbacks have access to. pub struct Adapt< @@ -18,9 +18,7 @@ pub struct Adapt< &mut ParentState, AdaptThunk, ) -> MessageResult, -> where - Context: ViewPathTracker, -{ +> { proxy_fn: ProxyFn, child: ChildView, #[allow(clippy::type_complexity)] @@ -147,6 +145,10 @@ where } } +impl ViewMarker + for Adapt +{ +} impl View for Adapt diff --git a/xilem_core/src/views/fork.rs b/xilem_core/src/views/fork.rs index 873ef90ba..5b7136644 100644 --- a/xilem_core/src/views/fork.rs +++ b/xilem_core/src/views/fork.rs @@ -1,40 +1,37 @@ // Copyright 2024 the Xilem Authors // SPDX-License-Identifier: Apache-2.0 -use core::marker::PhantomData; - use crate::{ - AppendVec, ElementSplice, Mut, NoElement, View, ViewId, ViewPathTracker, ViewSequence, + AppendVec, ElementSplice, Mut, NoElement, View, ViewId, ViewMarker, ViewPathTracker, + ViewSequence, }; /// Create a view which acts as `active_view`, whilst also running `alongside_view`, without inserting it into the tree. /// /// `alongside_view` must be a `ViewSequence` with an element type of [`NoElement`]. -pub fn fork( +pub fn fork( active_view: Active, alongside_view: Alongside, -) -> Fork { +) -> Fork { Fork { active_view, alongside_view, - marker: PhantomData, } } /// The view for [`fork`]. -pub struct Fork { +pub struct Fork { active_view: Active, alongside_view: Alongside, - marker: PhantomData, } -impl - View for Fork +impl ViewMarker for Fork {} +impl View + for Fork where Active: View, - Alongside: ViewSequence, + Alongside: ViewSequence, Context: ViewPathTracker, - Marker: 'static, { type Element = Active::Element; @@ -111,12 +108,8 @@ where /// A stub `ElementSplice` implementation for `NoElement`. /// -/// We know that none of the methods will be called, because the `ViewSequence` -/// implementation for `NoElement` views does not use the provided `elements`. -/// /// It is technically possible for someone to create an implementation of `ViewSequence` -/// which uses a `NoElement` `ElementSplice`. But we don't think that sequence could be meaningful, -/// so we still panic in that case. +/// which uses a `NoElement` `ElementSplice`. But we don't think that sequence could be meaningful. struct NoElements; impl ElementSplice for NoElements { @@ -127,21 +120,15 @@ impl ElementSplice for NoElements { ret } - fn insert(&mut self, _: NoElement) { - unreachable!() - } + fn insert(&mut self, _: NoElement) {} - fn mutate(&mut self, _: impl FnOnce(::Mut<'_>) -> R) -> R { - unreachable!() + fn mutate(&mut self, f: impl FnOnce(::Mut<'_>) -> R) -> R { + f(()) } - fn skip(&mut self, n: usize) { - if n > 0 { - unreachable!() - } - } + fn skip(&mut self, _: usize) {} - fn delete(&mut self, _: impl FnOnce(::Mut<'_>) -> R) -> R { - unreachable!() + fn delete(&mut self, f: impl FnOnce(::Mut<'_>) -> R) -> R { + f(()) } } diff --git a/xilem_core/src/views/map_action.rs b/xilem_core/src/views/map_action.rs index 9fa580c00..af1f54807 100644 --- a/xilem_core/src/views/map_action.rs +++ b/xilem_core/src/views/map_action.rs @@ -3,7 +3,7 @@ use core::marker::PhantomData; -use crate::{Mut, View, ViewId, ViewPathTracker}; +use crate::{Mut, View, ViewId, ViewMarker, ViewPathTracker}; /// A view that maps a child [`View`] to [`View`] while providing mutable access to `State` in the map function. /// @@ -68,6 +68,10 @@ where } } +impl ViewMarker + for MapAction +{ +} impl View for MapAction diff --git a/xilem_core/src/views/map_state.rs b/xilem_core/src/views/map_state.rs index 450500745..e178dddaa 100644 --- a/xilem_core/src/views/map_state.rs +++ b/xilem_core/src/views/map_state.rs @@ -3,7 +3,7 @@ use core::marker::PhantomData; -use crate::{MessageResult, Mut, View, ViewId, ViewPathTracker}; +use crate::{MessageResult, Mut, View, ViewId, ViewMarker, ViewPathTracker}; /// A view that "extracts" state from a [`View`] to [`View`]. /// This allows modularization of views based on their state. @@ -56,6 +56,7 @@ where } } +impl ViewMarker for MapState {} impl View for MapState where diff --git a/xilem_core/src/views/memoize.rs b/xilem_core/src/views/memoize.rs index e628f90a9..694f366cb 100644 --- a/xilem_core/src/views/memoize.rs +++ b/xilem_core/src/views/memoize.rs @@ -4,7 +4,7 @@ use core::marker::PhantomData; use core::mem::size_of; -use crate::{MessageResult, Mut, View, ViewId, ViewPathTracker}; +use crate::{MessageResult, Mut, View, ViewId, ViewMarker, ViewPathTracker}; /// A view which supports Memoization. /// @@ -64,6 +64,7 @@ pub struct MemoizeState { dirty: bool, } +impl ViewMarker for Memoize {} impl View for Memoize where @@ -172,6 +173,7 @@ where } } +impl ViewMarker for Frozen {} impl View for Frozen where diff --git a/xilem_core/src/views/one_of.rs b/xilem_core/src/views/one_of.rs index de7b71616..0b38b53c6 100644 --- a/xilem_core/src/views/one_of.rs +++ b/xilem_core/src/views/one_of.rs @@ -3,7 +3,7 @@ //! Statically typed alternatives to the type-erased [`AnyView`](`crate::AnyView`). -use crate::{MessageResult, Mut, View, ViewElement, ViewId, ViewPathTracker}; +use crate::{MessageResult, Mut, View, ViewElement, ViewId, ViewMarker, ViewPathTracker}; use hidden::OneOfState; /// This trait allows, specifying a type as `ViewElement`, which should never be constructed or used, @@ -146,6 +146,7 @@ pub trait OneOfCtx< ); } +impl ViewMarker for OneOf {} /// The `OneOf` types and `Either` are [`View`]s if all of their possible types are themselves `View`s. impl View for OneOf @@ -517,13 +518,14 @@ where // to export it. Since this (`one_of`) module is public, we create a new module, allowing it to be pub but not exposed. #[doc(hidden)] mod hidden { - use crate::View; + use crate::{View, ViewMarker}; use super::PhantomElementCtx; #[allow(unreachable_pub)] pub enum Never {} + impl ViewMarker for Never {} impl View for Never { diff --git a/xilem_core/src/views/orphan.rs b/xilem_core/src/views/orphan.rs index d9ebab45d..005d6afff 100644 --- a/xilem_core/src/views/orphan.rs +++ b/xilem_core/src/views/orphan.rs @@ -1,7 +1,9 @@ // Copyright 2024 the Xilem Authors // SPDX-License-Identifier: Apache-2.0 -use crate::{DynMessage, MessageResult, Mut, View, ViewElement, ViewId, ViewPathTracker}; +use crate::{ + DynMessage, MessageResult, Mut, View, ViewElement, ViewId, ViewMarker, ViewPathTracker, +}; /// This trait provides a way to add [`View`] implementations for types that would be restricted otherwise by the orphan rules. /// Every type that can be supported with this trait, needs a concrete `View` implementation in `xilem_core`, possibly feature-gated. @@ -43,6 +45,8 @@ pub trait OrphanView: ViewPathTracker + macro_rules! impl_orphan_view_for { ($ty: ty) => { + impl ViewMarker for $ty {} + impl View for $ty where Context: OrphanView<$ty, State, Action, Message>, @@ -111,7 +115,7 @@ impl_orphan_view_for!(usize); /// These [`OrphanView`] implementations can e.g. be used in a vector graphics context, as for example seen in `xilem_web` within svg nodes mod kurbo { use super::OrphanView; - use crate::{MessageResult, Mut, View, ViewId}; + use crate::{MessageResult, Mut, View, ViewId, ViewMarker}; impl_orphan_view_for!(kurbo::PathSeg); impl_orphan_view_for!(kurbo::Arc); impl_orphan_view_for!(kurbo::BezPath); diff --git a/xilem_core/src/views/run_once.rs b/xilem_core/src/views/run_once.rs index f8fed9510..b2fa7efa2 100644 --- a/xilem_core/src/views/run_once.rs +++ b/xilem_core/src/views/run_once.rs @@ -3,7 +3,7 @@ use core::fmt::Debug; -use crate::{MessageResult, NoElement, View, ViewPathTracker}; +use crate::{MessageResult, NoElement, View, ViewMarker, ViewPathTracker}; /// A view which executes `once` exactly once. /// @@ -75,6 +75,7 @@ pub struct RunOnce { once: F, } +impl ViewMarker for RunOnce {} impl View for RunOnce where Context: ViewPathTracker, diff --git a/xilem_core/tests/common/mod.rs b/xilem_core/tests/common/mod.rs index 33f20f2ec..31afc3a78 100644 --- a/xilem_core/tests/common/mod.rs +++ b/xilem_core/tests/common/mod.rs @@ -4,8 +4,6 @@ #![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)] @@ -68,27 +66,22 @@ pub(super) struct Action { _priv: (), } -pub(super) struct SequenceView { +pub(super) struct SequenceView { id: u32, seq: Seq, - phantom: PhantomData, } -pub(super) fn sequence(id: u32, seq: Seq) -> SequenceView +pub(super) fn sequence(id: u32, seq: Seq) -> SequenceView where - Seq: ViewSequence<(), Action, TestCtx, TestElement, Marker>, + Seq: ViewSequence<(), Action, TestCtx, TestElement>, { - SequenceView { - id, - seq, - phantom: PhantomData, - } + SequenceView { id, seq } } -impl View<(), Action, TestCtx> for SequenceView +impl ViewMarker for SequenceView {} +impl View<(), Action, TestCtx> for SequenceView where - Seq: ViewSequence<(), Action, TestCtx, TestElement, Marker>, - Marker: 'static, + Seq: ViewSequence<(), Action, TestCtx, TestElement>, { type Element = TestElement; @@ -160,6 +153,7 @@ where } } +impl ViewMarker for OperationView {} impl View<(), Action, TestCtx> for OperationView { type Element = TestElement; diff --git a/xilem_web/src/attribute.rs b/xilem_web/src/attribute.rs index c131c3a06..33001d3f2 100644 --- a/xilem_web/src/attribute.rs +++ b/xilem_web/src/attribute.rs @@ -3,7 +3,7 @@ use std::marker::PhantomData; use wasm_bindgen::{JsCast, UnwrapThrowExt}; -use xilem_core::{MessageResult, Mut, View, ViewElement, ViewId}; +use xilem_core::{MessageResult, Mut, View, ViewElement, ViewId, ViewMarker}; use crate::{ vecmap::VecMap, AttributeValue, DomNode, DynMessage, ElementProps, Pod, PodMut, ViewCtx, @@ -249,7 +249,8 @@ impl Attr { } } -impl View for Attr +impl ViewMarker for Attr {} +impl View for Attr where T: 'static, A: 'static, diff --git a/xilem_web/src/class.rs b/xilem_web/src/class.rs index 47700f8d3..1ad403968 100644 --- a/xilem_web/src/class.rs +++ b/xilem_web/src/class.rs @@ -4,7 +4,7 @@ use std::marker::PhantomData; use wasm_bindgen::{JsCast, UnwrapThrowExt}; -use xilem_core::{MessageResult, Mut, View, ViewElement, ViewId}; +use xilem_core::{MessageResult, Mut, View, ViewElement, ViewId, ViewMarker}; use crate::{vecmap::VecMap, DomNode, DynMessage, ElementProps, Pod, PodMut, ViewCtx}; @@ -289,6 +289,7 @@ impl Class { } } +impl ViewMarker for Class {} impl View for Class where T: 'static, diff --git a/xilem_web/src/concurrent/memoized_await.rs b/xilem_web/src/concurrent/memoized_await.rs index 55153919c..06db90ecc 100644 --- a/xilem_web/src/concurrent/memoized_await.rs +++ b/xilem_web/src/concurrent/memoized_await.rs @@ -5,7 +5,7 @@ use std::{future::Future, marker::PhantomData}; use wasm_bindgen::{closure::Closure, JsCast, UnwrapThrowExt}; use wasm_bindgen_futures::spawn_local; -use xilem_core::{MessageResult, Mut, NoElement, View, ViewId, ViewPathTracker}; +use xilem_core::{MessageResult, Mut, NoElement, View, ViewId, ViewMarker, ViewPathTracker}; use crate::{DynMessage, OptionalAction, ViewCtx}; @@ -153,6 +153,10 @@ enum MemoizedAwaitMessage { ScheduleUpdate, } +impl ViewMarker + for MemoizedAwait +{ +} impl View for MemoizedAwait where diff --git a/xilem_web/src/elements.rs b/xilem_web/src/elements.rs index 4c9b12812..64b937e68 100644 --- a/xilem_web/src/elements.rs +++ b/xilem_web/src/elements.rs @@ -8,23 +8,17 @@ use std::{any::Any, rc::Rc}; use wasm_bindgen::{JsCast, UnwrapThrowExt}; use crate::{ - core::{AppendVec, ElementSplice, MessageResult, Mut, View, ViewId, ViewSequence}, + core::{AppendVec, ElementSplice, MessageResult, Mut, View, ViewId, ViewMarker}, document, element_props::ElementProps, vec_splice::VecSplice, - AnyPod, DomNode, DynMessage, Pod, ViewCtx, HTML_NS, + AnyPod, DomFragment, DomNode, DynMessage, Pod, ViewCtx, HTML_NS, }; -mod sealed { - pub trait Sealed {} -} - // sealed, because this should only cover `ViewSequences` with the blanket impl below /// This is basically a specialized dynamically dispatchable [`ViewSequence`], It's currently not able to change the underlying type unlike [`AnyDomView`](crate::AnyDomView), so it should not be used as `dyn DomViewSequence`. /// It's mostly a hack to avoid a completely static view tree, which unfortunately brings rustc (type-checking) down to its knees and results in long compile-times -pub(crate) trait DomViewSequence: - sealed::Sealed + 'static -{ +pub(crate) trait DomViewSequence: 'static { /// Get an [`Any`] reference to `self`. fn as_any(&self) -> &dyn Any; @@ -35,7 +29,7 @@ pub(crate) trait DomViewSequence: /// Update the associated widgets. fn dyn_seq_rebuild( &self, - prev: &dyn DomViewSequence, + prev: &dyn DomViewSequence, seq_state: &mut Box, ctx: &mut ViewCtx, elements: &mut DomChildrenSplice, @@ -62,21 +56,11 @@ pub(crate) trait DomViewSequence: ) -> MessageResult; } -impl sealed::Sealed for S -where - State: 'static, - SeqMarker: 'static, - Action: 'static, - S: ViewSequence, -{ -} - -impl DomViewSequence for S +impl DomViewSequence for S where State: 'static, - SeqMarker: 'static, Action: 'static, - S: ViewSequence, + S: DomFragment, { fn as_any(&self) -> &dyn Any { self @@ -88,7 +72,7 @@ where fn dyn_seq_rebuild( &self, - prev: &dyn DomViewSequence, + prev: &dyn DomViewSequence, seq_state: &mut Box, ctx: &mut ViewCtx, elements: &mut DomChildrenSplice, @@ -233,8 +217,8 @@ impl ElementState { // These (boilerplatey) functions are there to reduce the boilerplate created by the macro-expansion below. -pub(crate) fn build_element( - children: &dyn DomViewSequence, +pub(crate) fn build_element( + children: &dyn DomViewSequence, tag_name: &str, ns: &str, ctx: &mut ViewCtx, @@ -243,7 +227,6 @@ where State: 'static, Action: 'static, Element: 'static, - SeqMarker: 'static, Element: From>, { let mut elements = AppendVec::default(); @@ -254,9 +237,9 @@ where ) } -pub(crate) fn rebuild_element<'el, State, Action, Element, SeqMarker>( - children: &dyn DomViewSequence, - prev_children: &dyn DomViewSequence, +pub(crate) fn rebuild_element<'el, State, Action, Element>( + children: &dyn DomViewSequence, + prev_children: &dyn DomViewSequence, element: Mut<'el, Pod>, state: &mut ElementState, ctx: &mut ViewCtx, @@ -265,7 +248,6 @@ where State: 'static, Action: 'static, Element: 'static, - SeqMarker: 'static, Element: DomNode, { let mut dom_children_splice = DomChildrenSplice::new( @@ -285,8 +267,8 @@ where element } -pub(crate) fn teardown_element( - children: &dyn DomViewSequence, +pub(crate) fn teardown_element( + children: &dyn DomViewSequence, element: Mut<'_, Pod>, state: &mut ElementState, ctx: &mut ViewCtx, @@ -294,7 +276,6 @@ pub(crate) fn teardown_element( State: 'static, Action: 'static, Element: 'static, - SeqMarker: 'static, Element: DomNode, { let mut dom_children_splice = DomChildrenSplice::new( @@ -309,34 +290,31 @@ pub(crate) fn teardown_element( } /// An element that can change its tag, it's useful for autonomous custom elements (i.e. web components) -pub struct CustomElement { +pub struct CustomElement { name: Cow<'static, str>, - children: Box>, + children: Box>, } /// An element that can change its tag, it's useful for autonomous custom elements (i.e. web components) -pub fn custom_element( +pub fn custom_element( name: impl Into>, children: Children, -) -> CustomElement +) -> CustomElement where State: 'static, Action: 'static, - SeqMarker: 'static, - Children: ViewSequence, + Children: DomFragment, { CustomElement { name: name.into(), children: Box::new(children), } } - -impl View - for CustomElement +impl ViewMarker for CustomElement {} +impl View for CustomElement where State: 'static, Action: 'static, - SeqMarker: 'static, { type Element = Pod; @@ -403,32 +381,26 @@ macro_rules! define_element { define_element!($ns, ($ty_name, $name, $dom_interface, stringify!($name))); }; ($ns:expr, ($ty_name:ident, $name:ident, $dom_interface:ident, $tag_name:expr)) => { - pub struct $ty_name { - children: Box>, + pub struct $ty_name { + children: Box>, } /// Builder function for a #[doc = concat!("`", $tag_name, "`")] /// element view. - pub fn $name< - State: 'static, - Action: 'static, - SeqMarker: 'static, - Children: ViewSequence, - >( + pub fn $name>( children: Children, - ) -> $ty_name { + ) -> $ty_name { $ty_name { children: Box::new(children), } } - impl View - for $ty_name + impl ViewMarker for $ty_name {} + impl View for $ty_name where State: 'static, Action: 'static, - SeqMarker: 'static, { type Element = Pod; @@ -485,8 +457,8 @@ macro_rules! define_elements { ($ns:ident, $($element_def:tt,)*) => { use super::{build_element, rebuild_element, teardown_element, DomViewSequence, ElementState}; use crate::{ - core::{MessageResult, Mut, ViewId, ViewSequence}, - AnyPod, DynMessage, ElementProps, Pod, View, ViewCtx, + core::{MessageResult, Mut, ViewId, ViewMarker}, + DomFragment, DynMessage, ElementProps, Pod, View, ViewCtx, }; $(define_element!(crate::$ns, $element_def);)* }; diff --git a/xilem_web/src/events.rs b/xilem_web/src/events.rs index 8b05f69e3..b78dde9ed 100644 --- a/xilem_web/src/events.rs +++ b/xilem_web/src/events.rs @@ -4,7 +4,7 @@ use std::{borrow::Cow, marker::PhantomData}; use wasm_bindgen::{prelude::Closure, throw_str, JsCast, UnwrapThrowExt}; use web_sys::AddEventListenerOptions; -use xilem_core::{MessageResult, Mut, View, ViewId, ViewPathTracker}; +use xilem_core::{MessageResult, Mut, View, ViewId, ViewMarker, ViewPathTracker}; use crate::{DynMessage, ElementAsRef, OptionalAction, ViewCtx}; @@ -224,6 +224,7 @@ where } } +impl ViewMarker for OnEvent {} impl View for OnEvent where @@ -327,6 +328,7 @@ macro_rules! event_definitions { pub(crate) phantom_event_ty: PhantomData (State, Action)>, } + impl ViewMarker for $ty_name {} impl $ty_name { pub fn new(element: V, handler: Callback) -> Self { Self { diff --git a/xilem_web/src/lib.rs b/xilem_web/src/lib.rs index ad39eb1af..3004ca4eb 100644 --- a/xilem_web/src/lib.rs +++ b/xilem_web/src/lib.rs @@ -59,6 +59,7 @@ pub use optional_action::{Action, OptionalAction}; pub use pointer::{Pointer, PointerDetails, PointerMsg}; pub use style::{style, ElementWithStyle, IntoStyles, Style, Styles, WithStyle}; pub use xilem_core as core; +use xilem_core::ViewSequence; /// A trait used for type erasure of [`DomNode`]s /// It is e.g. used in [`AnyPod`] @@ -169,6 +170,26 @@ where type Props = P; } +/// An ordered sequence of views, or sometimes also called fragment, it's used for `0..N` [`DomView`]s. +/// See [`ViewSequence`] for more technical details. +/// +/// # Examples +/// +/// ``` +/// fn huzzah(clicks: i32) -> impl xilem_web::DomFragment { +/// (clicks >= 5).then_some("Huzzah, clicked at least 5 times") +/// } +/// ``` +pub trait DomFragment: + ViewSequence +{ +} + +impl DomFragment for V where + V: ViewSequence +{ +} + /// A container, which holds the actual DOM node, and associated props, such as attributes or classes. /// These attributes are not directly set on the DOM node to avoid mutating or reading from the DOM tree unnecessarily, and to have more control over the whole update flow. pub struct Pod { diff --git a/xilem_web/src/pointer.rs b/xilem_web/src/pointer.rs index ace47faf3..c5d3de9f5 100644 --- a/xilem_web/src/pointer.rs +++ b/xilem_web/src/pointer.rs @@ -8,7 +8,7 @@ use std::marker::PhantomData; use wasm_bindgen::{prelude::Closure, throw_str, JsCast, UnwrapThrowExt}; use web_sys::PointerEvent; -use xilem_core::{MessageResult, Mut, View, ViewId, ViewPathTracker}; +use xilem_core::{MessageResult, Mut, View, ViewId, ViewMarker, ViewPathTracker}; use crate::{interfaces::Element, DynMessage, ElementAsRef, ViewCtx}; @@ -69,6 +69,7 @@ pub fn pointer>( } } +impl ViewMarker for Pointer {} impl View for Pointer where diff --git a/xilem_web/src/style.rs b/xilem_web/src/style.rs index 7f792e413..63b89e0c8 100644 --- a/xilem_web/src/style.rs +++ b/xilem_web/src/style.rs @@ -6,7 +6,7 @@ use std::{ marker::PhantomData, }; use wasm_bindgen::{JsCast, UnwrapThrowExt}; -use xilem_core::{MessageResult, Mut, View, ViewElement, ViewId}; +use xilem_core::{MessageResult, Mut, View, ViewElement, ViewId, ViewMarker}; use crate::{vecmap::VecMap, DomNode, DynMessage, ElementProps, Pod, PodMut, ViewCtx}; @@ -299,6 +299,7 @@ impl Style { } } +impl ViewMarker for Style {} impl View for Style where T: 'static, diff --git a/xilem_web/src/svg/common_attrs.rs b/xilem_web/src/svg/common_attrs.rs index 67d6e580c..472446b8b 100644 --- a/xilem_web/src/svg/common_attrs.rs +++ b/xilem_web/src/svg/common_attrs.rs @@ -5,7 +5,7 @@ use std::borrow::Cow; use std::marker::PhantomData; use peniko::Brush; -use xilem_core::{MessageResult, Mut, View, ViewId}; +use xilem_core::{MessageResult, Mut, View, ViewId, ViewMarker}; use crate::{ attribute::{ElementWithAttributes, WithAttributes}, @@ -61,6 +61,7 @@ fn brush_to_string(brush: &Brush) -> String { } } +impl ViewMarker for Fill {} impl View for Fill where State: 'static, @@ -116,6 +117,7 @@ where } } +impl ViewMarker for Stroke {} impl View for Stroke where State: 'static, diff --git a/xilem_web/web_examples/counter/src/main.rs b/xilem_web/web_examples/counter/src/main.rs index a205923e5..a4cc0b19e 100644 --- a/xilem_web/web_examples/counter/src/main.rs +++ b/xilem_web/web_examples/counter/src/main.rs @@ -5,7 +5,7 @@ use xilem_web::{ document_body, elements::html as el, interfaces::{Element, HtmlButtonElement, HtmlDivElement}, - App, + App, DomFragment, }; #[derive(Default)] @@ -50,9 +50,15 @@ fn btn( el::button(label).on_click(click_fn) } +/// And functions that return a sequence of views. +fn huzzah(state: &mut AppState) -> impl DomFragment { + (state.clicks >= 5).then_some("Huzzah, clicked at least 5 times") +} + fn app_logic(state: &mut AppState) -> impl HtmlDivElement { el::div(( el::span(format!("clicked {} times", state.clicks)).class(state.class), + huzzah(state), el::br(()), btn("+1 click", |state, _| state.increment()), btn("-1 click", |state, _| state.decrement()),