From 53a5354c2df938e4122608abf6778499bc81f518 Mon Sep 17 00:00:00 2001 From: Philipp Mildenberger Date: Wed, 23 Oct 2024 20:24:43 +0200 Subject: [PATCH] xilem_web: Rewrite modifiers (`Attributes`, `Classes` and `Styles`), and cleanup/extend docs (#699) Previously the modifier systems had design issues i.e. bugs (non-deleted styles/classes/attributes), and were unnecessary complex. This aims to solve this (partly) by not using separate traits, but concrete types and a different mechanism that is closer to how `ElementSplice` works. There's a few fundamental properties that composable type-based modifiers need to support to avoid surprising/buggy behavior: * Minimize actual changes to the underlying element, as DOM traffic is expensive. * Be compatible to memoization: e.g. a `Rotate` view should still be applicable to possibly memoized transform values of the underlying element. * Recreation when the underlying element has changed (e.g. with a change of variants of a `OneOf`). To support all this, the modifier system needs to retain modifiers for each modifier-view, and track its changes of the corresponding view. Previously all elements were directly written and separated with markers into a `Vec` to limit the boundaries of such views, but this had issues, when e.g. all modifiers were deleted (e.g. clearing a `Vec` of classes), by not reacting to this (I noticed that issue in the todomvc example with the footer). With this PR, the count of modifiers of a modifier-view are directly stored either (hardcoded) in the view impl or its view state, which cleans up the concrete modifier elements (such as `AttributeModifier`, not including a separate `Marker` variant), and makes it less prone for errors (and is slightly less memory-intensive). The API to use these modifiers in modifier-views was also redesigned to hopefully be more straight-forward/idiomatic. But as mentioned above there's still challenges, which introduce complexity (which I'd like to hide at least for simpler cases than these modifiers, likely in a future PR). All of this should now be documented in the new `modifier` module, where now the modifiers `Attributes`, `Classes` and `Styles` reside. Other views (like events) may also end up there... One interesting aspect compared to the previous system is the use of a new trait `With` for modifiers. Instead of (roughly) `Element: WithStyle`, it works with `Element: With`. This prevents all kinds of reimplementations of something like `WithStyle` for elements. This gets especially visible in the `one_of` module, which now can be covered by a single blanket implementation. Further the cargo-feature "hydration" was deleted, as it causes more headaches to maintain than it really brings benefits (minimally less binary size), depending on the future, it may or may not make sense to reintroduce this. --- xilem_core/src/views/one_of.rs | 27 + xilem_web/Cargo.toml | 3 +- xilem_web/src/app.rs | 1 - xilem_web/src/attribute.rs | 396 ---------- xilem_web/src/class.rs | 384 --------- xilem_web/src/context.rs | 99 ++- xilem_web/src/diff.rs | 63 ++ xilem_web/src/element_props.rs | 150 +--- xilem_web/src/elements.rs | 71 +- xilem_web/src/interfaces.rs | 37 +- xilem_web/src/lib.rs | 24 +- xilem_web/src/modifiers/attribute.rs | 362 +++++++++ xilem_web/src/modifiers/class.rs | 393 ++++++++++ xilem_web/src/modifiers/mod.rs | 87 +++ xilem_web/src/modifiers/style.rs | 730 ++++++++++++++++++ xilem_web/src/one_of.rs | 350 ++------- xilem_web/src/style.rs | 696 ----------------- xilem_web/src/svg/common_attrs.rs | 193 ++--- xilem_web/src/svg/kurbo_shape.rs | 156 ++-- xilem_web/src/templated.rs | 8 +- xilem_web/src/text.rs | 11 +- xilem_web/src/vecmap.rs | 96 ++- xilem_web/web_examples/mathml_svg/src/main.rs | 2 +- xilem_web/web_examples/svgtoy/src/main.rs | 2 +- xilem_web/web_examples/todomvc/src/main.rs | 3 +- 25 files changed, 2173 insertions(+), 2171 deletions(-) delete mode 100644 xilem_web/src/attribute.rs delete mode 100644 xilem_web/src/class.rs create mode 100644 xilem_web/src/diff.rs create mode 100644 xilem_web/src/modifiers/attribute.rs create mode 100644 xilem_web/src/modifiers/class.rs create mode 100644 xilem_web/src/modifiers/mod.rs create mode 100644 xilem_web/src/modifiers/style.rs delete mode 100644 xilem_web/src/style.rs diff --git a/xilem_core/src/views/one_of.rs b/xilem_core/src/views/one_of.rs index cdfc95aa9..d123b9941 100644 --- a/xilem_core/src/views/one_of.rs +++ b/xilem_core/src/views/one_of.rs @@ -84,6 +84,33 @@ where } } +impl AsMut for OneOf +where + A: AsMut, + B: AsMut, + C: AsMut, + D: AsMut, + E: AsMut, + F: AsMut, + G: AsMut, + H: AsMut, + I: AsMut, +{ + fn as_mut(&mut self) -> &mut T { + match self { + OneOf::A(e) => >::as_mut(e), + OneOf::B(e) => >::as_mut(e), + OneOf::C(e) => >::as_mut(e), + OneOf::D(e) => >::as_mut(e), + OneOf::E(e) => >::as_mut(e), + OneOf::F(e) => >::as_mut(e), + OneOf::G(e) => >::as_mut(e), + OneOf::H(e) => >::as_mut(e), + OneOf::I(e) => >::as_mut(e), + } + } +} + /// A context type which can support [`OneOf9`] and [related views](super::one_of). /// /// This should be implemented by users of Xilem Core. diff --git a/xilem_web/Cargo.toml b/xilem_web/Cargo.toml index 063461082..08f56504b 100644 --- a/xilem_web/Cargo.toml +++ b/xilem_web/Cargo.toml @@ -177,7 +177,6 @@ features = [ ] [features] -default = ["hydration"] -hydration = [] +default = [] # This interns some often used strings, such as element tags ("div" etc.), which slightly improves performance when creating elements at the cost of a bigger wasm binary intern_strings = ["wasm-bindgen/enable-interning"] diff --git a/xilem_web/src/app.rs b/xilem_web/src/app.rs index c561bba8d..e007b0fda 100644 --- a/xilem_web/src/app.rs +++ b/xilem_web/src/app.rs @@ -141,7 +141,6 @@ where &inner.root, inner.ctx.fragment.clone(), false, - #[cfg(feature = "hydration")] false, ); new_fragment.seq_rebuild( diff --git a/xilem_web/src/attribute.rs b/xilem_web/src/attribute.rs deleted file mode 100644 index 507c6ed3d..000000000 --- a/xilem_web/src/attribute.rs +++ /dev/null @@ -1,396 +0,0 @@ -// Copyright 2024 the Xilem Authors -// SPDX-License-Identifier: Apache-2.0 - -use crate::{ - core::{MessageResult, Mut, View, ViewElement, ViewId, ViewMarker}, - vecmap::VecMap, - AttributeValue, DomNode, DomView, DynMessage, ElementProps, Pod, PodMut, ViewCtx, -}; -use std::marker::PhantomData; -use wasm_bindgen::{JsCast, UnwrapThrowExt}; - -type CowStr = std::borrow::Cow<'static, str>; - -/// This trait allows (modifying) HTML/SVG/MathML attributes on DOM [`Element`](`crate::interfaces::Element`)s. -/// -/// Modifications have to be done on the up-traversal of [`View::rebuild`], i.e. after [`View::rebuild`] was invoked for descendent views. -/// See [`Attr::build`] and [`Attr::rebuild`], how to use this for [`ViewElement`]s that implement this trait. -/// When these methods are used, they have to be used in every reconciliation pass (i.e. [`View::rebuild`]). -pub trait WithAttributes { - /// Needs to be invoked within a [`View::rebuild`] before traversing to descendent views, and before any modifications (with [`set_attribute`](`WithAttributes::set_attribute`)) are done in that view - fn rebuild_attribute_modifier(&mut self); - - /// Needs to be invoked after any modifications are done - fn mark_end_of_attribute_modifier(&mut self); - - /// Sets or removes (when value is `None`) an attribute from the underlying element. - /// - /// When in [`View::rebuild`] this has to be invoked *after* traversing the inner `View` with [`View::rebuild`] - fn set_attribute(&mut self, name: &CowStr, value: &Option); - - // TODO first find a use-case for this... - // fn get_attr(&self, name: &str) -> Option<&AttributeValue>; -} - -#[derive(Debug, PartialEq)] -enum AttributeModifier { - Remove(CowStr), - Set(CowStr, AttributeValue), - EndMarker(u16), -} - -const HYDRATING: u16 = 1 << 14; -const CREATING: u16 = 1 << 15; -const RESERVED_BIT_MASK: u16 = HYDRATING | CREATING; - -/// This contains all the current attributes of an [`Element`](`crate::interfaces::Element`) -#[derive(Debug, Default)] -pub struct Attributes { - attribute_modifiers: Vec, - updated_attributes: VecMap, - idx: u16, - /// the two most significant bits are reserved for whether this was just created (bit 15) and if it's currently being hydrated (bit 14) - start_idx: u16, -} - -impl Attributes { - pub(crate) fn new(size_hint: usize, #[cfg(feature = "hydration")] in_hydration: bool) -> Self { - #[allow(unused_mut)] - let mut start_idx = CREATING; - #[cfg(feature = "hydration")] - if in_hydration { - start_idx |= HYDRATING; - } - - Self { - attribute_modifiers: Vec::with_capacity(size_hint), - start_idx, - ..Default::default() - } - } -} - -fn set_attribute(element: &web_sys::Element, name: &str, value: &str) { - debug_assert_ne!( - name, "class", - "Using `class` as attribute is not supported, use the `el.class()` modifier instead" - ); - debug_assert_ne!( - name, "style", - "Using `style` as attribute is not supported, use the `el.style()` modifier instead" - ); - - // we have to special-case `value` because setting the value using `set_attribute` - // doesn't work after the value has been changed. - // TODO not sure, whether this is always a good idea, in case custom or other interfaces such as HtmlOptionElement elements are used that have "value" as an attribute name. - // We likely want to use the DOM attributes instead. - if name == "value" { - if let Some(input_element) = element.dyn_ref::() { - input_element.set_value(value); - } else { - element.set_attribute("value", value).unwrap_throw(); - } - } else if name == "checked" { - if let Some(input_element) = element.dyn_ref::() { - input_element.set_checked(true); - } else { - element.set_attribute("checked", value).unwrap_throw(); - } - } else { - element.set_attribute(name, value).unwrap_throw(); - } -} - -fn remove_attribute(element: &web_sys::Element, name: &str) { - debug_assert_ne!( - name, "class", - "Using `class` as attribute is not supported, use the `el.class()` modifier instead" - ); - debug_assert_ne!( - name, "style", - "Using `style` as attribute is not supported, use the `el.style()` modifier instead" - ); - // we have to special-case `checked` because setting the value using `set_attribute` - // doesn't work after the value has been changed. - if name == "checked" { - if let Some(input_element) = element.dyn_ref::() { - input_element.set_checked(false); - } else { - element.remove_attribute("checked").unwrap_throw(); - } - } else { - element.remove_attribute(name).unwrap_throw(); - } -} - -impl Attributes { - /// applies potential changes of the attributes of an element to the underlying DOM node - pub fn apply_attribute_changes(&mut self, element: &web_sys::Element) { - if (self.start_idx & HYDRATING) == HYDRATING { - self.start_idx &= !RESERVED_BIT_MASK; - return; - } - - if (self.start_idx & CREATING) == CREATING { - for modifier in self.attribute_modifiers.iter().rev() { - match modifier { - AttributeModifier::Remove(name) => { - remove_attribute(element, name); - } - AttributeModifier::Set(name, value) => { - set_attribute(element, name, &value.serialize()); - } - AttributeModifier::EndMarker(_) => (), - } - } - self.start_idx &= !RESERVED_BIT_MASK; - debug_assert!(self.updated_attributes.is_empty()); - return; - } - - if !self.updated_attributes.is_empty() { - for modifier in self.attribute_modifiers.iter().rev() { - match modifier { - AttributeModifier::Remove(name) => { - if self.updated_attributes.remove(name).is_some() { - remove_attribute(element, name); - } - } - AttributeModifier::Set(name, value) => { - if self.updated_attributes.remove(name).is_some() { - set_attribute(element, name, &value.serialize()); - } - } - AttributeModifier::EndMarker(_) => (), - } - } - debug_assert!(self.updated_attributes.is_empty()); - } - } -} - -impl WithAttributes for Attributes { - fn set_attribute(&mut self, name: &CowStr, value: &Option) { - if (self.start_idx & RESERVED_BIT_MASK) != 0 { - let modifier = if let Some(value) = value { - AttributeModifier::Set(name.clone(), value.clone()) - } else { - AttributeModifier::Remove(name.clone()) - }; - self.attribute_modifiers.push(modifier); - } else if let Some(modifier) = self.attribute_modifiers.get_mut(self.idx as usize) { - let dirty = match (&modifier, value) { - // early return if nothing has changed, avoids allocations - (AttributeModifier::Set(old_name, old_value), Some(new_value)) - if old_name == name => - { - if old_value == new_value { - false - } else { - self.updated_attributes.insert(name.clone(), ()); - true - } - } - (AttributeModifier::Remove(removed), None) if removed == name => false, - (AttributeModifier::Set(old_name, _), None) - | (AttributeModifier::Remove(old_name), Some(_)) - if old_name == name => - { - self.updated_attributes.insert(name.clone(), ()); - true - } - (AttributeModifier::EndMarker(_), None) - | (AttributeModifier::EndMarker(_), Some(_)) => { - self.updated_attributes.insert(name.clone(), ()); - true - } - (AttributeModifier::Set(old_name, _), _) - | (AttributeModifier::Remove(old_name), _) => { - self.updated_attributes.insert(name.clone(), ()); - self.updated_attributes.insert(old_name.clone(), ()); - true - } - }; - if dirty { - *modifier = if let Some(value) = value { - AttributeModifier::Set(name.clone(), value.clone()) - } else { - AttributeModifier::Remove(name.clone()) - }; - } - // else remove it out of updated_attributes? (because previous attributes are overwritten) not sure if worth it because potentially worse perf - } else { - let new_modifier = if let Some(value) = value { - AttributeModifier::Set(name.clone(), value.clone()) - } else { - AttributeModifier::Remove(name.clone()) - }; - self.updated_attributes.insert(name.clone(), ()); - self.attribute_modifiers.push(new_modifier); - } - self.idx += 1; - } - - fn rebuild_attribute_modifier(&mut self) { - if self.idx == 0 { - self.start_idx &= RESERVED_BIT_MASK; - } else { - let AttributeModifier::EndMarker(start_idx) = - self.attribute_modifiers[(self.idx - 1) as usize] - else { - unreachable!("this should not happen, as either `rebuild_attribute_modifier` happens first, or follows an `mark_end_of_attribute_modifier`") - }; - self.idx = start_idx; - self.start_idx = start_idx | (self.start_idx & RESERVED_BIT_MASK); - } - } - - fn mark_end_of_attribute_modifier(&mut self) { - let start_idx = self.start_idx & !RESERVED_BIT_MASK; - match self.attribute_modifiers.get_mut(self.idx as usize) { - Some(AttributeModifier::EndMarker(prev_start_idx)) if *prev_start_idx == start_idx => {} // attribute modifier hasn't changed - Some(modifier) => *modifier = AttributeModifier::EndMarker(start_idx), - None => self - .attribute_modifiers - .push(AttributeModifier::EndMarker(start_idx)), - } - self.idx += 1; - self.start_idx = self.idx | (self.start_idx & RESERVED_BIT_MASK); - } -} - -impl WithAttributes for ElementProps { - fn rebuild_attribute_modifier(&mut self) { - self.attributes().rebuild_attribute_modifier(); - } - - fn mark_end_of_attribute_modifier(&mut self) { - self.attributes().mark_end_of_attribute_modifier(); - } - - fn set_attribute(&mut self, name: &CowStr, value: &Option) { - self.attributes().set_attribute(name, value); - } -} - -impl WithAttributes for Pod -where - N: DomNode, - N::Props: WithAttributes, -{ - fn rebuild_attribute_modifier(&mut self) { - self.props.rebuild_attribute_modifier(); - } - - fn mark_end_of_attribute_modifier(&mut self) { - self.props.mark_end_of_attribute_modifier(); - } - - fn set_attribute(&mut self, name: &CowStr, value: &Option) { - self.props.set_attribute(name, value); - } -} - -impl WithAttributes for PodMut<'_, N> -where - N: DomNode, - N::Props: WithAttributes, -{ - fn rebuild_attribute_modifier(&mut self) { - self.props.rebuild_attribute_modifier(); - } - - fn mark_end_of_attribute_modifier(&mut self) { - self.props.mark_end_of_attribute_modifier(); - } - - fn set_attribute(&mut self, name: &CowStr, value: &Option) { - self.props.set_attribute(name, value); - } -} - -/// Syntax sugar for adding a type bound on the `ViewElement` of a view, such that both, [`ViewElement`] and [`ViewElement::Mut`] are bound to [`WithAttributes`] -pub trait ElementWithAttributes: - for<'a> ViewElement: WithAttributes> + WithAttributes -{ -} - -impl ElementWithAttributes for T -where - T: ViewElement + WithAttributes, - for<'a> T::Mut<'a>: WithAttributes, -{ -} - -/// A view to add or remove an attribute to/from an element, see [`Element::attr`](`crate::interfaces::Element::attr`) for how it's usually used. -#[derive(Clone, Debug)] -pub struct Attr { - el: E, - name: CowStr, - value: Option, - phantom: PhantomData (T, A)>, -} - -impl Attr { - pub fn new(el: E, name: CowStr, value: Option) -> Self { - Attr { - el, - name, - value, - phantom: PhantomData, - } - } -} - -impl ViewMarker for Attr {} -impl View for Attr -where - T: 'static, - A: 'static, - E: DomView>, -{ - type Element = E::Element; - - type ViewState = E::ViewState; - - fn build(&self, ctx: &mut ViewCtx) -> (Self::Element, Self::ViewState) { - ctx.add_modifier_size_hint::(1); - let (mut element, state) = self.el.build(ctx); - element.set_attribute(&self.name, &self.value); - element.mark_end_of_attribute_modifier(); - (element, state) - } - - fn rebuild( - &self, - prev: &Self, - view_state: &mut Self::ViewState, - ctx: &mut ViewCtx, - mut element: Mut, - ) { - element.rebuild_attribute_modifier(); - self.el - .rebuild(&prev.el, view_state, ctx, element.reborrow_mut()); - element.set_attribute(&self.name, &self.value); - element.mark_end_of_attribute_modifier(); - } - - fn teardown( - &self, - view_state: &mut Self::ViewState, - ctx: &mut ViewCtx, - element: Mut, - ) { - self.el.teardown(view_state, ctx, element); - } - - fn message( - &self, - view_state: &mut Self::ViewState, - id_path: &[ViewId], - message: DynMessage, - app_state: &mut T, - ) -> MessageResult { - self.el.message(view_state, id_path, message, app_state) - } -} diff --git a/xilem_web/src/class.rs b/xilem_web/src/class.rs deleted file mode 100644 index 73a2730a8..000000000 --- a/xilem_web/src/class.rs +++ /dev/null @@ -1,384 +0,0 @@ -// Copyright 2024 the Xilem Authors -// SPDX-License-Identifier: Apache-2.0 - -use crate::{ - core::{MessageResult, Mut, View, ViewElement, ViewId, ViewMarker}, - vecmap::VecMap, - DomNode, DomView, DynMessage, ElementProps, Pod, PodMut, ViewCtx, -}; -use std::marker::PhantomData; -use wasm_bindgen::{JsCast, UnwrapThrowExt}; - -type CowStr = std::borrow::Cow<'static, str>; - -/// Types implementing this trait can be used in the [`Class`] view, see also [`Element::class`](`crate::interfaces::Element::class`) -pub trait AsClassIter { - fn class_iter(&self) -> impl Iterator; -} - -impl AsClassIter for Option { - fn class_iter(&self) -> impl Iterator { - self.iter().flat_map(|c| c.class_iter()) - } -} - -impl AsClassIter for String { - fn class_iter(&self) -> impl Iterator { - std::iter::once(self.clone().into()) - } -} - -impl AsClassIter for &'static str { - fn class_iter(&self) -> impl Iterator { - std::iter::once(CowStr::from(*self)) - } -} - -impl AsClassIter for CowStr { - fn class_iter(&self) -> impl Iterator { - std::iter::once(self.clone()) - } -} - -impl AsClassIter for Vec -where - T: AsClassIter, -{ - fn class_iter(&self) -> impl Iterator { - self.iter().flat_map(|c| c.class_iter()) - } -} - -impl AsClassIter for [T; N] { - fn class_iter(&self) -> impl Iterator { - self.iter().flat_map(|c| c.class_iter()) - } -} - -/// This trait enables having classes (via `className`) on DOM [`Element`](`crate::interfaces::Element`)s. It is used within [`View`]s that modify the classes of an element. -/// -/// Modifications have to be done on the up-traversal of [`View::rebuild`], i.e. after [`View::rebuild`] was invoked for descendent views. -/// See [`Class::build`] and [`Class::rebuild`], how to use this for [`ViewElement`]s that implement this trait. -/// When these methods are used, they have to be used in every reconciliation pass (i.e. [`View::rebuild`]). -pub trait WithClasses { - /// Needs to be invoked within a [`View::rebuild`] before traversing to descendent views, and before any modifications (with [`add_class`](`WithClasses::add_class`) or [`remove_class`](`WithClasses::remove_class`)) are done in that view - fn rebuild_class_modifier(&mut self); - - /// Needs to be invoked after any modifications are done - fn mark_end_of_class_modifier(&mut self); - - /// Adds a class to the element - /// - /// When in [`View::rebuild`] this has to be invoked *after* traversing the inner `View` with [`View::rebuild`] - fn add_class(&mut self, class_name: &CowStr); - - /// Removes a possibly previously added class from the element - /// - /// When in [`View::rebuild`] this has to be invoked *after* traversing the inner `View` with [`View::rebuild`] - fn remove_class(&mut self, class_name: &CowStr); - - // TODO something like the following, but I'm not yet sure how to support that efficiently (and without much binary bloat) - // The modifiers possibly have to be applied then... - // fn classes(&self) -> impl Iterator; - // maybe also something like: - // fn has_class(&self, class_name: &str) -> bool - // Need to find a use-case for this first though (i.e. a modifier needs to read previously added classes) -} - -#[derive(Debug)] -enum ClassModifier { - Remove(CowStr), - Add(CowStr), - EndMarker(u16), -} - -const DIRTY: u16 = 1 << 14; -const HYDRATING: u16 = 1 << 15; -const RESERVED_BIT_MASK: u16 = HYDRATING | DIRTY; - -/// This contains all the current classes of an [`Element`](`crate::interfaces::Element`) -#[derive(Debug, Default)] -pub struct Classes { - // TODO maybe this attribute is redundant and can be formed just from the class_modifiers attribute - classes: VecMap, - class_modifiers: Vec, - class_name: String, - idx: u16, - /// the two most significant bits are reserved for whether it's currently being hydrated (bit 15), or is dirty (bit 14) - start_idx: u16, -} - -impl Classes { - pub(crate) fn new(size_hint: usize, #[cfg(feature = "hydration")] in_hydration: bool) -> Self { - #[allow(unused_mut)] - let mut start_idx = 0; - #[cfg(feature = "hydration")] - if in_hydration { - start_idx |= HYDRATING; - } - - Self { - class_modifiers: Vec::with_capacity(size_hint), - start_idx, - ..Default::default() - } - } -} - -impl Classes { - pub fn apply_class_changes(&mut self, element: &web_sys::Element) { - #[cfg(feature = "hydration")] - if (self.start_idx & HYDRATING) == HYDRATING { - self.start_idx &= !RESERVED_BIT_MASK; - return; - } - - if (self.start_idx & DIRTY) == DIRTY { - self.start_idx &= !RESERVED_BIT_MASK; - self.classes.clear(); - for modifier in &self.class_modifiers { - match modifier { - ClassModifier::Remove(class_name) => { - self.classes.remove(class_name); - } - ClassModifier::Add(class_name) => { - self.classes.insert(class_name.clone(), ()); - } - ClassModifier::EndMarker(_) => (), - } - } - // intersperse would be the right way to do this, but avoid extra dependencies just for this (and otherwise it's unstable in std)... - self.class_name.clear(); - let last_idx = self.classes.len().saturating_sub(1); - for (idx, class) in self.classes.keys().enumerate() { - self.class_name += class; - if idx != last_idx { - self.class_name += " "; - } - } - // Svg elements do have issues with className, see https://developer.mozilla.org/en-US/docs/Web/API/Element/className - if element.dyn_ref::().is_some() { - element - .set_attribute(wasm_bindgen::intern("class"), &self.class_name) - .unwrap_throw(); - } else { - element.set_class_name(&self.class_name); - } - } - } -} - -impl WithClasses for Classes { - fn rebuild_class_modifier(&mut self) { - if self.idx == 0 { - self.start_idx = 0; - } else { - let ClassModifier::EndMarker(start_idx) = self.class_modifiers[(self.idx - 1) as usize] - else { - unreachable!("this should not happen, as either `rebuild_class_modifier` is happens first, or follows an `mark_end_of_class_modifier`") - }; - self.idx = start_idx; - self.start_idx = start_idx | (self.start_idx & RESERVED_BIT_MASK); - } - } - - fn mark_end_of_class_modifier(&mut self) { - match self.class_modifiers.get_mut(self.idx as usize) { - Some(ClassModifier::EndMarker(_)) if self.start_idx & DIRTY != DIRTY => (), // class modifier hasn't changed - Some(modifier) => { - self.start_idx |= DIRTY; - *modifier = ClassModifier::EndMarker(self.start_idx & !RESERVED_BIT_MASK); - } - None => { - self.start_idx |= DIRTY; - self.class_modifiers.push(ClassModifier::EndMarker( - self.start_idx & !RESERVED_BIT_MASK, - )); - } - } - self.idx += 1; - self.start_idx = self.idx | (self.start_idx & RESERVED_BIT_MASK); - } - - fn add_class(&mut self, class_name: &CowStr) { - match self.class_modifiers.get_mut(self.idx as usize) { - Some(ClassModifier::Add(class)) if class == class_name => (), // class modifier hasn't changed - Some(modifier) => { - self.start_idx |= DIRTY; - *modifier = ClassModifier::Add(class_name.clone()); - } - None => { - self.start_idx |= DIRTY; - self.class_modifiers - .push(ClassModifier::Add(class_name.clone())); - } - } - self.idx += 1; - } - - fn remove_class(&mut self, class_name: &CowStr) { - // Same code as add_class but with remove... - match self.class_modifiers.get_mut(self.idx as usize) { - Some(ClassModifier::Remove(class)) if class == class_name => (), // class modifier hasn't changed - Some(modifier) => { - self.start_idx |= DIRTY; - *modifier = ClassModifier::Remove(class_name.clone()); - } - None => { - self.start_idx |= DIRTY; - self.class_modifiers - .push(ClassModifier::Remove(class_name.clone())); - } - } - self.idx += 1; - } -} - -impl WithClasses for ElementProps { - fn rebuild_class_modifier(&mut self) { - self.classes().rebuild_class_modifier(); - } - - fn mark_end_of_class_modifier(&mut self) { - self.classes().mark_end_of_class_modifier(); - } - - fn add_class(&mut self, class_name: &CowStr) { - self.classes().add_class(class_name); - } - - fn remove_class(&mut self, class_name: &CowStr) { - self.classes().remove_class(class_name); - } -} - -impl WithClasses for Pod -where - N::Props: WithClasses, -{ - fn rebuild_class_modifier(&mut self) { - self.props.rebuild_class_modifier(); - } - - fn mark_end_of_class_modifier(&mut self) { - self.props.mark_end_of_class_modifier(); - } - - fn add_class(&mut self, class_name: &CowStr) { - self.props.add_class(class_name); - } - - fn remove_class(&mut self, class_name: &CowStr) { - self.props.remove_class(class_name); - } -} - -impl WithClasses for PodMut<'_, N> -where - N::Props: WithClasses, -{ - fn rebuild_class_modifier(&mut self) { - self.props.rebuild_class_modifier(); - } - - fn mark_end_of_class_modifier(&mut self) { - self.props.mark_end_of_class_modifier(); - } - - fn add_class(&mut self, class_name: &CowStr) { - self.props.add_class(class_name); - } - - fn remove_class(&mut self, class_name: &CowStr) { - self.props.remove_class(class_name); - } -} - -/// Syntax sugar for adding a type bound on the `ViewElement` of a view, such that both, [`ViewElement`] and [`ViewElement::Mut`] are bound to [`WithClasses`] -pub trait ElementWithClasses: for<'a> ViewElement: WithClasses> + WithClasses {} - -impl ElementWithClasses for T -where - T: ViewElement + WithClasses, - for<'a> T::Mut<'a>: WithClasses, -{ -} - -/// A view to add classes to elements -#[derive(Clone, Debug)] -pub struct Class { - el: E, - classes: C, - phantom: PhantomData (T, A)>, -} - -impl Class { - pub fn new(el: E, classes: C) -> Self { - Class { - el, - classes, - phantom: PhantomData, - } - } -} - -impl ViewMarker for Class {} -impl View for Class -where - T: 'static, - A: 'static, - C: AsClassIter + 'static, - E: DomView>, -{ - type Element = E::Element; - - type ViewState = E::ViewState; - - fn build(&self, ctx: &mut ViewCtx) -> (Self::Element, Self::ViewState) { - let class_iter = self.classes.class_iter(); - ctx.add_modifier_size_hint::(class_iter.size_hint().0); - let (mut e, s) = self.el.build(ctx); - for class in class_iter { - e.add_class(&class); - } - e.mark_end_of_class_modifier(); - (e, s) - } - - fn rebuild( - &self, - prev: &Self, - view_state: &mut Self::ViewState, - ctx: &mut ViewCtx, - mut element: Mut, - ) { - // This has to happen, before any children are rebuilt, otherwise this state machine breaks... - // The actual modifiers also have to happen after the children are rebuilt, see `add_class` below. - element.rebuild_class_modifier(); - self.el - .rebuild(&prev.el, view_state, ctx, element.reborrow_mut()); - for class in self.classes.class_iter() { - element.add_class(&class); - } - element.mark_end_of_class_modifier(); - } - - fn teardown( - &self, - view_state: &mut Self::ViewState, - ctx: &mut ViewCtx, - element: Mut, - ) { - self.el.teardown(view_state, ctx, element); - } - - fn message( - &self, - view_state: &mut Self::ViewState, - id_path: &[ViewId], - message: DynMessage, - app_state: &mut T, - ) -> MessageResult { - self.el.message(view_state, id_path, message, app_state) - } -} diff --git a/xilem_web/src/context.rs b/xilem_web/src/context.rs index 053aa4566..71b9d4002 100644 --- a/xilem_web/src/context.rs +++ b/xilem_web/src/context.rs @@ -2,7 +2,6 @@ // SPDX-License-Identifier: Apache-2.0 use crate::vecmap::VecMap; -#[cfg(feature = "hydration")] use std::any::Any; use std::any::TypeId; use std::rc::Rc; @@ -40,13 +39,12 @@ pub struct ViewCtx { id_path: Vec, app_ref: Option>, pub(crate) fragment: Rc, - #[cfg(feature = "hydration")] hydration_node_stack: Vec, - #[cfg(feature = "hydration")] is_hydrating: bool, - #[cfg(feature = "hydration")] pub(crate) templates: VecMap)>, - modifier_size_hints: VecMap, + /// A stack containing modifier count size-hints for each element context, mostly to avoid unnecessary allocations. + modifier_size_hints: Vec>, + modifier_size_hint_stack_idx: usize, } impl Default for ViewCtx { @@ -55,13 +53,12 @@ impl Default for ViewCtx { id_path: Vec::default(), app_ref: None, fragment: Rc::new(crate::document().create_document_fragment()), - #[cfg(feature = "hydration")] templates: Default::default(), - #[cfg(feature = "hydration")] hydration_node_stack: Default::default(), - #[cfg(feature = "hydration")] is_hydrating: false, - modifier_size_hints: Default::default(), + // One element for the root `DomFragment`. will be extended with `Self::push_size_hints` + modifier_size_hints: vec![VecMap::default()], + modifier_size_hint_stack_idx: 0, } } } @@ -78,28 +75,33 @@ impl ViewCtx { self.app_ref = Some(Box::new(runner)); } - #[cfg(feature = "hydration")] - pub(crate) fn push_hydration_node(&mut self, node: web_sys::Node) { - self.hydration_node_stack.push(node); + /// Should be used when creating children of a DOM node, e.g. to handle hydration and size hints correctly. + pub fn with_build_children(&mut self, f: impl FnOnce(&mut Self) -> R) -> R { + self.enter_hydrating_children(); + self.push_size_hints(); + let r = f(self); + self.pop_size_hints(); + r } - #[cfg(feature = "hydration")] - pub(crate) fn enable_hydration(&mut self) { + pub fn with_hydration_node( + &mut self, + node: web_sys::Node, + f: impl FnOnce(&mut Self) -> R, + ) -> R { + self.hydration_node_stack.push(node); + let is_hydrating = self.is_hydrating; self.is_hydrating = true; + let r = f(self); + self.is_hydrating = is_hydrating; + r } - #[cfg(feature = "hydration")] - pub(crate) fn disable_hydration(&mut self) { - self.is_hydrating = false; - } - - #[cfg(feature = "hydration")] pub(crate) fn is_hydrating(&self) -> bool { self.is_hydrating } - #[cfg(feature = "hydration")] - pub(crate) fn enter_hydrating_children(&mut self) { + fn enter_hydrating_children(&mut self) { if let Some(node) = self.hydration_node_stack.last() { if let Some(child) = node.first_child() { self.hydration_node_stack.push(child); @@ -108,7 +110,6 @@ impl ViewCtx { } } - #[cfg(feature = "hydration")] /// Returns the current node, and goes to the `next_sibling`, if it's `None`, it's popping the stack pub(crate) fn hydrate_node(&mut self) -> Option { let node = self.hydration_node_stack.pop()?; @@ -118,21 +119,55 @@ impl ViewCtx { Some(node) } - pub fn add_modifier_size_hint(&mut self, request_size: usize) { + fn current_size_hints_mut(&mut self) -> &mut VecMap { + &mut self.modifier_size_hints[self.modifier_size_hint_stack_idx] + } + + fn add_modifier_size_hint(&mut self, request_size: usize) { let id = TypeId::of::(); - match self.modifier_size_hints.get_mut(&id) { - Some(hint) => *hint += request_size + 1, // + 1 because of the marker + let hints = self.current_size_hints_mut(); + match hints.get_mut(&id) { + Some(hint) => *hint += request_size, None => { - self.modifier_size_hints.insert(id, request_size + 1); + hints.insert(id, request_size); } - }; + } + } + + #[inline] + pub fn take_modifier_size_hint(&mut self) -> usize { + self.current_size_hints_mut() + .get_mut(&TypeId::of::()) + .map(std::mem::take) + .unwrap_or(0) } - pub fn modifier_size_hint(&mut self) -> usize { - match self.modifier_size_hints.get_mut(&TypeId::of::()) { - Some(hint) => std::mem::take(hint), - None => 0, + fn push_size_hints(&mut self) { + if self.modifier_size_hint_stack_idx == self.modifier_size_hints.len() - 1 { + self.modifier_size_hints.push(VecMap::default()); } + self.modifier_size_hint_stack_idx += 1; + } + + fn pop_size_hints(&mut self) { + debug_assert!( + self.modifier_size_hints[self.modifier_size_hint_stack_idx] + .iter() + .map(|(_, size_hint)| *size_hint) + .sum::() + == 0 + ); + self.modifier_size_hint_stack_idx -= 1; + } + + #[inline] + pub fn with_size_hint( + &mut self, + size: usize, + f: impl FnOnce(&mut Self) -> R, + ) -> R { + self.add_modifier_size_hint::(size); + f(self) } } diff --git a/xilem_web/src/diff.rs b/xilem_web/src/diff.rs new file mode 100644 index 000000000..a67a8f9f7 --- /dev/null +++ b/xilem_web/src/diff.rs @@ -0,0 +1,63 @@ +// Copyright 2024 the Xilem Authors +// SPDX-License-Identifier: Apache-2.0 + +//! Basic utility functions for diffing when rebuilding the views with [`View::rebuild`](`crate::core::View::rebuild`) + +use std::iter::Peekable; + +/// Diffs between two iterators with `Diff` as its [`Iterator::Item`] +pub fn diff_iters>(old: I, new: I) -> DiffIterator { + let next = new.peekable(); + let prev = old.peekable(); + DiffIterator { prev, next } +} + +#[derive(Clone, Copy, Eq, PartialEq, Debug)] +/// The [`Iterator::Item`] used in `DiffIterator`. +/// +/// `Remove` and `Skip` contain the count of elements that were deleted/skipped +pub enum Diff { + Add(T), + Remove(usize), + Change(T), + Skip(usize), +} + +/// An [`Iterator`] that diffs between two iterators with `Diff` as its [`Iterator::Item`] +pub struct DiffIterator> { + prev: Peekable, + next: Peekable, +} + +impl> Iterator for DiffIterator { + type Item = Diff; + fn next(&mut self) -> Option { + let mut skip_count = 0; + while let (Some(new), Some(old)) = (self.next.peek(), self.prev.peek()) { + if new == old { + skip_count += 1; + self.next.next(); + self.prev.next(); + continue; + } + if skip_count > 0 { + return Some(Diff::Skip(skip_count)); + } else { + let new = self.next.next().unwrap(); + self.prev.next(); + return Some(Diff::Change(new)); + } + } + let mut remove_count = 0; + while self.prev.next().is_some() { + remove_count += 1; + } + if remove_count > 0 { + return Some(Diff::Remove(remove_count)); + } + if let Some(new) = self.next.next() { + return Some(Diff::Add(new)); + } + None + } +} diff --git a/xilem_web/src/element_props.rs b/xilem_web/src/element_props.rs index 26584b913..2a69e0f73 100644 --- a/xilem_web/src/element_props.rs +++ b/xilem_web/src/element_props.rs @@ -1,16 +1,18 @@ // Copyright 2024 the Xilem Authors // SPDX-License-Identifier: Apache-2.0 -use crate::{attribute::Attributes, class::Classes, document, style::Styles, AnyPod, Pod, ViewCtx}; -#[cfg(feature = "hydration")] +use crate::{ + document, + modifiers::{Attributes, Children, Classes, Styles, With}, + AnyPod, Pod, ViewCtx, +}; use wasm_bindgen::JsCast; use wasm_bindgen::UnwrapThrowExt; // Lazy access to attributes etc. to avoid allocating unnecessary memory when it isn't needed // Benchmarks have shown, that this can significantly increase performance and reduce memory usage... -/// This holds all the state for a DOM [`Element`](`crate::interfaces::Element`), it is used for [`DomView::Props`](`crate::DomView::Props`) +/// This holds all the state for a DOM [`Element`](`crate::interfaces::Element`), it is used for [`DomNode::Props`](`crate::DomNode::Props`) pub struct ElementProps { - #[cfg(feature = "hydration")] pub(crate) in_hydration: bool, pub(crate) attributes: Option>, pub(crate) classes: Option>, @@ -18,47 +20,28 @@ pub struct ElementProps { pub(crate) children: Vec, } +impl With for ElementProps { + fn modifier(&mut self) -> &mut Children { + &mut self.children + } +} + impl ElementProps { pub fn new( children: Vec, attr_size_hint: usize, style_size_hint: usize, class_size_hint: usize, - #[cfg(feature = "hydration")] in_hydration: bool, + in_hydration: bool, ) -> Self { - let attributes = if attr_size_hint > 0 { - Some(Box::new(Attributes::new( - attr_size_hint, - #[cfg(feature = "hydration")] - in_hydration, - ))) - } else { - None - }; - let styles = if style_size_hint > 0 { - Some(Box::new(Styles::new( - style_size_hint, - #[cfg(feature = "hydration")] - in_hydration, - ))) - } else { - None - }; - let classes = if class_size_hint > 0 { - Some(Box::new(Classes::new( - class_size_hint, - #[cfg(feature = "hydration")] - in_hydration, - ))) - } else { - None - }; Self { - attributes, - classes, - styles, + attributes: (attr_size_hint > 0) + .then(|| Box::new(Attributes::new(attr_size_hint, in_hydration))), + classes: (class_size_hint > 0) + .then(|| Box::new(Classes::new(class_size_hint, in_hydration))), + styles: (style_size_hint > 0) + .then(|| Box::new(Styles::new(style_size_hint, in_hydration))), children, - #[cfg(feature = "hydration")] in_hydration, } } @@ -67,76 +50,46 @@ impl ElementProps { // because we want to minimize DOM traffic as much as possible (that's basically the bottleneck) pub fn update_element(&mut self, element: &web_sys::Element) { if let Some(attributes) = &mut self.attributes { - attributes.apply_attribute_changes(element); + attributes.apply_changes(element); } if let Some(classes) = &mut self.classes { - classes.apply_class_changes(element); + classes.apply_changes(element); } if let Some(styles) = &mut self.styles { - styles.apply_style_changes(element); + styles.apply_changes(element); } } + /// Lazily returns the [`Attributes`] modifier of this element. pub fn attributes(&mut self) -> &mut Attributes { - self.attributes.get_or_insert_with(|| { - Box::new(Attributes::new( - 0, - #[cfg(feature = "hydration")] - self.in_hydration, - )) - }) + self.attributes + .get_or_insert_with(|| Box::new(Attributes::new(0, self.in_hydration))) } + /// Lazily returns the [`Styles`] modifier of this element. pub fn styles(&mut self) -> &mut Styles { - self.styles.get_or_insert_with(|| { - Box::new(Styles::new( - 0, - #[cfg(feature = "hydration")] - self.in_hydration, - )) - }) + self.styles + .get_or_insert_with(|| Box::new(Styles::new(0, self.in_hydration))) } + /// Lazily returns the [`Classes`] modifier of this element. pub fn classes(&mut self) -> &mut Classes { - self.classes.get_or_insert_with(|| { - Box::new(Classes::new( - 0, - #[cfg(feature = "hydration")] - self.in_hydration, - )) - }) + self.classes + .get_or_insert_with(|| Box::new(Classes::new(0, self.in_hydration))) } } impl Pod { + /// Creates a new Pod with [`web_sys::Element`] as element and `ElementProps` as its [`DomNode::Props`](`crate::DomNode::Props`). pub fn new_element_with_ctx( children: Vec, ns: &str, elem_name: &str, ctx: &mut ViewCtx, ) -> Self { - let attr_size_hint = ctx.modifier_size_hint::(); - let class_size_hint = ctx.modifier_size_hint::(); - let style_size_hint = ctx.modifier_size_hint::(); - Self::new_element( - children, - ns, - elem_name, - attr_size_hint, - style_size_hint, - class_size_hint, - ) - } - - /// Creates a new Pod with [`web_sys::Element`] as element and `ElementProps` as its [`DomView::Props`](`crate::DomView::Props`) - pub fn new_element( - children: Vec, - ns: &str, - elem_name: &str, - attr_size_hint: usize, - style_size_hint: usize, - class_size_hint: usize, - ) -> Self { + let attr_size_hint = ctx.take_modifier_size_hint::(); + let class_size_hint = ctx.take_modifier_size_hint::(); + let style_size_hint = ctx.take_modifier_size_hint::(); let element = document() .create_element_ns( Some(wasm_bindgen::intern(ns)), @@ -155,38 +108,18 @@ impl Pod { attr_size_hint, style_size_hint, class_size_hint, - #[cfg(feature = "hydration")] false, ), } } - #[cfg(feature = "hydration")] - pub fn hydrate_element_with_ctx( - children: Vec, - element: web_sys::Node, - ctx: &mut ViewCtx, - ) -> Self { - let attr_size_hint = ctx.modifier_size_hint::(); - let class_size_hint = ctx.modifier_size_hint::(); - let style_size_hint = ctx.modifier_size_hint::(); - Self::hydrate_element( - children, - element, - attr_size_hint, - style_size_hint, - class_size_hint, - ) - } + /// Creates a new Pod that hydrates an existing node (within the `ViewCtx`) as [`web_sys::Element`] and [`ElementProps`] as its [`DomNode::Props`](`crate::DomNode::Props`). + pub fn hydrate_element_with_ctx(children: Vec, ctx: &mut ViewCtx) -> Self { + let attr_size_hint = ctx.take_modifier_size_hint::(); + let class_size_hint = ctx.take_modifier_size_hint::(); + let style_size_hint = ctx.take_modifier_size_hint::(); + let element = ctx.hydrate_node().unwrap_throw(); - #[cfg(feature = "hydration")] - pub fn hydrate_element( - children: Vec, - element: web_sys::Node, - attr_size_hint: usize, - style_size_hint: usize, - class_size_hint: usize, - ) -> Self { Self { node: element.unchecked_into(), props: ElementProps::new( @@ -194,7 +127,6 @@ impl Pod { attr_size_hint, style_size_hint, class_size_hint, - #[cfg(feature = "hydration")] true, ), } diff --git a/xilem_web/src/elements.rs b/xilem_web/src/elements.rs index d63cd8402..8aab2808a 100644 --- a/xilem_web/src/elements.rs +++ b/xilem_web/src/elements.rs @@ -11,11 +11,10 @@ use wasm_bindgen::{JsCast, UnwrapThrowExt}; use crate::{ core::{AppendVec, ElementSplice, MessageResult, Mut, View, ViewId, ViewMarker}, document, - element_props::ElementProps, + modifiers::{Children, With}, vec_splice::VecSplice, - AnyPod, DomFragment, DomNode, DynMessage, Pod, ViewCtx, HTML_NS, + AnyPod, DomFragment, DomNode, DynMessage, FromWithContext, Pod, ViewCtx, HTML_NS, }; -use crate::{Attributes, Classes, Styles}; // 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`. @@ -123,7 +122,6 @@ pub struct DomChildrenSplice<'a, 'b, 'c, 'd> { parent: &'d web_sys::Node, fragment: Rc, parent_was_removed: bool, - #[cfg(feature = "hydration")] in_hydration: bool, } @@ -135,7 +133,7 @@ impl<'a, 'b, 'c, 'd> DomChildrenSplice<'a, 'b, 'c, 'd> { parent: &'d web_sys::Node, fragment: Rc, parent_was_deleted: bool, - #[cfg(feature = "hydration")] hydrate: bool, + hydrate: bool, ) -> Self { Self { scratch, @@ -144,7 +142,6 @@ impl<'a, 'b, 'c, 'd> DomChildrenSplice<'a, 'b, 'c, 'd> { parent, fragment, parent_was_removed: parent_was_deleted, - #[cfg(feature = "hydration")] in_hydration: hydrate, } } @@ -154,13 +151,7 @@ impl<'a, 'b, 'c, 'd> ElementSplice for DomChildrenSplice<'a, 'b, 'c, 'd> fn with_scratch(&mut self, f: impl FnOnce(&mut AppendVec) -> R) -> R { let ret = f(self.scratch); if !self.scratch.is_empty() { - #[allow(unused_assignments, unused_mut)] - // reason: when the feature "hydration" is enabled/disabled, avoid warnings - let mut add_dom_children_to_parent = true; - #[cfg(feature = "hydration")] - { - add_dom_children_to_parent = !self.in_hydration; - } + let add_dom_children_to_parent = !self.in_hydration; for element in self.scratch.drain() { if add_dom_children_to_parent { @@ -246,44 +237,18 @@ where State: 'static, Action: 'static, Element: 'static, - Element: From>, + Element: FromWithContext>, { - // We need to get those size hints before traversing to the children, otherwise the hints are messed up - let attr_size_hint = ctx.modifier_size_hint::(); - let class_size_hint = ctx.modifier_size_hint::(); - let style_size_hint = ctx.modifier_size_hint::(); let mut elements = AppendVec::default(); - #[cfg(feature = "hydration")] - if ctx.is_hydrating() { - ctx.enter_hydrating_children(); - } - let state = ElementState::new(children.dyn_seq_build(ctx, &mut elements)); - #[cfg(feature = "hydration")] - if ctx.is_hydrating() { - let hydrating_node = ctx.hydrate_node().unwrap_throw(); - return ( - Pod::hydrate_element( - elements.into_inner(), - hydrating_node, - attr_size_hint, - style_size_hint, - class_size_hint, - ) - .into(), - state, - ); - } + let children_state = ctx.with_build_children(|ctx| children.dyn_seq_build(ctx, &mut elements)); + let element = if ctx.is_hydrating() { + Pod::hydrate_element_with_ctx(elements.into_inner(), ctx) + } else { + Pod::new_element_with_ctx(elements.into_inner(), ns, tag_name, ctx) + }; ( - Pod::new_element( - elements.into_inner(), - ns, - tag_name, - attr_size_hint, - style_size_hint, - class_size_hint, - ) - .into(), - state, + Element::from_with_ctx(element, ctx), + ElementState::new(children_state), ) } @@ -297,16 +262,15 @@ pub(crate) fn rebuild_element( State: 'static, Action: 'static, Element: 'static, - Element: DomNode, + Element: DomNode>, { let mut dom_children_splice = DomChildrenSplice::new( &mut state.append_scratch, - &mut element.props.children, + With::::modifier(element.props), &mut state.vec_splice_scratch, element.node.as_ref(), ctx.fragment.clone(), element.was_removed, - #[cfg(feature = "hydration")] ctx.is_hydrating(), ); children.dyn_seq_rebuild( @@ -326,16 +290,15 @@ pub(crate) fn teardown_element( State: 'static, Action: 'static, Element: 'static, - Element: DomNode, + Element: DomNode>, { let mut dom_children_splice = DomChildrenSplice::new( &mut state.append_scratch, - &mut element.props.children, + With::::modifier(element.props), &mut state.vec_splice_scratch, element.node.as_ref(), ctx.fragment.clone(), true, - #[cfg(feature = "hydration")] ctx.is_hydrating(), ); children.dyn_seq_teardown(&mut state.seq_state, ctx, &mut dom_children_splice); diff --git a/xilem_web/src/interfaces.rs b/xilem_web/src/interfaces.rs index a848bcc9c..09a4a1fe5 100644 --- a/xilem_web/src/interfaces.rs +++ b/xilem_web/src/interfaces.rs @@ -3,7 +3,7 @@ //! Opinionated extension traits roughly resembling their equivalently named DOM interfaces. //! -//! It is used for DOM elements, e.g. created with [`html::span`](`crate::elements::html::span`) to modify the underlying element, such as [`Element::attr`] or [`HtmlElement::style`] +//! It is used for DOM elements, e.g. created with [`html::span`](`crate::elements::html::span`) to modify the underlying element, such as [`Element::attr`] or [`Element::style`] //! //! These traits can also be used as return type of components to allow modifying the underlying DOM element that is returned. //! For example: @@ -15,10 +15,11 @@ use std::borrow::Cow; use crate::{ - attribute::{Attr, WithAttributes}, - class::{AsClassIter, Class, WithClasses}, events, - style::{IntoStyles, Rotate, Scale, ScaleValue, Style, WithStyle}, + modifiers::{ + Attr, Attributes, Class, ClassIter, Classes, Rotate, Scale, ScaleValue, Style, StyleIter, + Styles, With, + }, DomNode, DomView, IntoAttributeValue, OptionalAction, Pointer, PointerMsg, }; use wasm_bindgen::JsCast; @@ -54,7 +55,8 @@ pub trait Element: + DomView< State, Action, - DomNode: DomNode + AsRef, + DomNode: DomNode + With + With> + + AsRef, > { /// Set an attribute for an [`Element`] @@ -97,7 +99,7 @@ pub trait Element: /// .class(Some("optional-class")) /// # } /// ``` - fn class( + fn class( self, as_classes: AsClasses, ) -> Class { @@ -151,7 +153,7 @@ pub trait Element: /// # Examples /// /// ``` - /// use xilem_web::{style as s, elements::html::div, interfaces::Element}; + /// use xilem_web::{modifiers::style as s, elements::html::div, interfaces::Element}; /// /// # fn component() -> impl Element<()> { /// div(()) @@ -159,9 +161,7 @@ pub trait Element: /// .style(s("justify-content", "center")) /// # } /// ``` - fn style(self, style: impl IntoStyles) -> Style { - let mut styles = vec![]; - style.into_styles(&mut styles); + fn style(self, styles: AsStyles) -> Style { Style::new(self, styles) } @@ -169,7 +169,7 @@ pub trait Element: /// # Examples /// /// ``` - /// use xilem_web::{style as s, interfaces::Element, svg::kurbo::Rect}; + /// use xilem_web::{modifiers::style as s, interfaces::Element, svg::kurbo::Rect}; /// /// # fn component() -> impl Element<()> { /// Rect::from_origin_size((0.0, 10.0), (20.0, 30.0)) @@ -187,7 +187,7 @@ pub trait Element: /// # Examples /// /// ``` - /// use xilem_web::{style as s, interfaces::Element, svg::kurbo::Circle}; + /// use xilem_web::{modifiers::style as s, interfaces::Element, svg::kurbo::Circle}; /// /// # fn component() -> impl Element<()> { /// Circle::new((10.0, 20.0), 30.0) @@ -198,10 +198,7 @@ pub trait Element: /// // /// # } /// ``` - fn scale(self, scale: impl Into) -> Scale - where - ::Props: WithStyle, - { + fn scale(self, scale: impl Into) -> Scale { Scale::new(self, scale) } @@ -334,7 +331,7 @@ pub trait Element: impl Element for T where T: DomView, - ::Props: WithAttributes + WithClasses + WithStyle, + ::Props: With + With + With, T::DomNode: AsRef, { } @@ -559,7 +556,7 @@ where // #[cfg(feature = "HtmlElement")] pub trait HtmlElement: - Element + AsRef> + Element> { } @@ -568,7 +565,6 @@ impl HtmlElement for T where T: Element, T::DomNode: AsRef, - ::Props: WithStyle, { } @@ -1530,7 +1526,7 @@ where // #[cfg(feature = "SvgElement")] pub trait SvgElement: - Element + AsRef> + Element> { } @@ -1539,7 +1535,6 @@ impl SvgElement for T where T: Element, T::DomNode: AsRef, - ::Props: WithStyle, { } diff --git a/xilem_web/src/lib.rs b/xilem_web/src/lib.rs index 6d3defb77..4d80d09f3 100644 --- a/xilem_web/src/lib.rs +++ b/xilem_web/src/lib.rs @@ -29,9 +29,7 @@ pub const MATHML_NS: &str = "http://www.w3.org/1998/Math/MathML"; mod after_update; mod app; -mod attribute; mod attribute_value; -mod class; mod context; mod dom_helpers; mod element_props; @@ -39,17 +37,17 @@ mod message; mod one_of; mod optional_action; mod pointer; -mod style; -#[cfg(feature = "hydration")] mod templated; mod text; mod vec_splice; mod vecmap; pub mod concurrent; +pub mod diff; pub mod elements; pub mod events; pub mod interfaces; +pub mod modifiers; pub mod svg; pub use self::{ @@ -57,19 +55,15 @@ pub use self::{ after_build, after_rebuild, before_teardown, AfterBuild, AfterRebuild, BeforeTeardown, }, app::App, - attribute::{Attr, Attributes, ElementWithAttributes, WithAttributes}, attribute_value::{AttributeValue, IntoAttributeValue}, - class::{AsClassIter, Class, Classes, ElementWithClasses, WithClasses}, context::{MessageThunk, ViewCtx}, dom_helpers::{document, document_body, get_element_by_id, input_event_target_value}, element_props::ElementProps, message::{DynMessage, Message}, optional_action::{Action, OptionalAction}, pointer::{Pointer, PointerDetails, PointerMsg}, - style::{style, ElementWithStyle, IntoStyles, Style, Styles, WithStyle}, }; -#[cfg(feature = "hydration")] pub use templated::{templated, Templated}; pub use xilem_core as core; @@ -408,6 +402,16 @@ impl DomNode for web_sys::Text { fn apply_props(&self, (): &mut ()) {} } +pub trait FromWithContext: Sized { + fn from_with_ctx(value: T, ctx: &mut ViewCtx) -> Self; +} + +impl FromWithContext for T { + fn from_with_ctx(value: T, _ctx: &mut ViewCtx) -> Self { + value + } +} + // TODO specialize some of these elements, maybe via features? macro_rules! impl_dom_node_for_elements { ($($ty:ident, )*) => {$( @@ -419,8 +423,8 @@ macro_rules! impl_dom_node_for_elements { } } - impl From> for Pod { - fn from(value: Pod) -> Self { + impl FromWithContext> for Pod { + fn from_with_ctx(value: Pod, _ctx: &mut ViewCtx) -> Self { Self { node: value.node.unchecked_into(), props: value.props, diff --git a/xilem_web/src/modifiers/attribute.rs b/xilem_web/src/modifiers/attribute.rs new file mode 100644 index 000000000..c33f7069f --- /dev/null +++ b/xilem_web/src/modifiers/attribute.rs @@ -0,0 +1,362 @@ +// Copyright 2024 the Xilem Authors +// SPDX-License-Identifier: Apache-2.0 + +use crate::{ + core::{MessageResult, Mut, View, ViewElement, ViewId, ViewMarker}, + modifiers::With, + vecmap::VecMap, + AttributeValue, DomView, DynMessage, ElementProps, IntoAttributeValue, ViewCtx, +}; +use std::marker::PhantomData; +use wasm_bindgen::{JsCast, UnwrapThrowExt}; + +type CowStr = std::borrow::Cow<'static, str>; + +#[derive(Debug, PartialEq, Clone)] +/// An modifier element to either set or remove an attribute. +/// +/// It's used in [`Attributes`]. +pub enum AttributeModifier { + Set(CowStr, AttributeValue), + Remove(CowStr), +} + +impl AttributeModifier { + /// Returns the attribute name of this modifier. + pub fn name(&self) -> &CowStr { + let (AttributeModifier::Set(name, _) | AttributeModifier::Remove(name)) = self; + name + } + + /// Convert this modifier into its attribute name. + pub fn into_name(self) -> CowStr { + let (AttributeModifier::Set(name, _) | AttributeModifier::Remove(name)) = self; + name + } +} + +impl, V: IntoAttributeValue> From<(K, V)> for AttributeModifier { + fn from((name, value): (K, V)) -> Self { + match value.into_attr_value() { + Some(value) => AttributeModifier::Set(name.into(), value), + None => AttributeModifier::Remove(name.into()), + } + } +} + +#[derive(Default)] +/// An Element modifier that manages all attributes of an Element. +pub struct Attributes { + // TODO think about using a `VecSplice` for more efficient insertion etc., + // but this is an additional trade-off of memory-usage and complexity, + // while probably not helping much in the average case (of very few styles)... + modifiers: Vec, + updated: VecMap, + idx: u16, + in_hydration: bool, + was_created: bool, +} + +impl With for ElementProps { + fn modifier(&mut self) -> &mut Attributes { + self.attributes() + } +} + +impl Attributes { + /// Creates a new `Attributes` modifier. + /// + /// `size_hint` is used to avoid unnecessary allocations while traversing up the view-tree when adding modifiers in [`View::build`]. + pub(crate) fn new(size_hint: usize, in_hydration: bool) -> Self { + Self { + modifiers: Vec::with_capacity(size_hint), + was_created: true, + in_hydration, + ..Default::default() + } + } + + /// Applies potential changes of the attributes of an element to the underlying DOM node. + pub fn apply_changes(&mut self, element: &web_sys::Element) { + if self.in_hydration { + self.in_hydration = false; + self.was_created = false; + } else if self.was_created { + self.was_created = false; + for modifier in &self.modifiers { + match modifier { + AttributeModifier::Remove(n) => remove_attribute(element, n), + AttributeModifier::Set(n, v) => set_attribute(element, n, &v.serialize()), + } + } + } else if !self.updated.is_empty() { + for modifier in self.modifiers.iter().rev() { + match modifier { + AttributeModifier::Remove(name) if self.updated.remove(name).is_some() => { + remove_attribute(element, name); + } + AttributeModifier::Set(name, value) if self.updated.remove(name).is_some() => { + set_attribute(element, name, &value.serialize()); + } + _ => {} + } + } + // if there's any remaining key in updated, it means these are deleted keys + for (name, ()) in self.updated.drain() { + remove_attribute(element, &name); + } + } + debug_assert!(self.updated.is_empty()); + } + + #[inline] + /// Rebuilds the current element, while ensuring that the order of the modifiers stays correct. + /// Any children should be rebuilt in inside `f`, *before* modifying any other properties of [`Attributes`]. + pub fn rebuild>(mut element: E, prev_len: usize, f: impl FnOnce(E)) { + element.modifier().idx -= prev_len as u16; + f(element); + } + + #[inline] + /// Returns whether the underlying element has been built or rebuilt, this could e.g. happen, when `OneOf` changes a variant to a different element. + pub fn was_created(&self) -> bool { + self.was_created + } + + #[inline] + /// Pushes `modifier` at the end of the current modifiers. + /// + /// Must only be used when `self.was_created() == true`. + pub fn push(&mut self, modifier: impl Into) { + debug_assert!( + self.was_created(), + "This should never be called, when the underlying element wasn't (re)created. Use `Attributes::insert` instead." + ); + let modifier = modifier.into(); + self.modifiers.push(modifier); + self.idx += 1; + } + + #[inline] + /// Inserts `modifier` at the current index. + /// + /// Must only be used when `self.was_created() == false`. + pub fn insert(&mut self, modifier: impl Into) { + debug_assert!( + !self.was_created(), + "This should never be called, when the underlying element was (re)created, use `Attributes::push` instead." + ); + let modifier = modifier.into(); + self.updated.insert(modifier.name().clone(), ()); + // TODO this could potentially be expensive, maybe think about `VecSplice` again. + // Although in the average case, this is likely not relevant, as usually very few attributes are used, thus shifting is probably good enough + // I.e. a `VecSplice` is probably less optimal (either more complicated code, and/or more memory usage) + self.modifiers.insert(self.idx as usize, modifier); + self.idx += 1; + } + + #[inline] + /// Mutates the next modifier. + /// + /// Must only be used when `self.was_created() == false`. + pub fn mutate(&mut self, f: impl FnOnce(&mut AttributeModifier) -> R) -> R { + debug_assert!( + !self.was_created(), + "This should never be called, when the underlying element was (re)created." + ); + let modifier = &mut self.modifiers[self.idx as usize]; + let old = modifier.name().clone(); + let rv = f(modifier); + let new = modifier.name(); + if *new != old { + self.updated.insert(new.clone(), ()); + } + self.updated.insert(old, ()); + self.idx += 1; + rv + } + + /// Skips the next `count` modifiers. + /// + /// Must only be used when `self.was_created() == false`. + pub fn skip(&mut self, count: usize) { + debug_assert!( + !self.was_created(), + "This should never be called, when the underlying element was (re)created." + ); + self.idx += count as u16; + } + + /// Deletes the next `count` modifiers. + /// + /// Must only be used when `self.was_created() == false`. + pub fn delete(&mut self, count: usize) { + debug_assert!( + !self.was_created(), + "This should never be called, when the underlying element was (re)created." + ); + let start = self.idx as usize; + for modifier in self.modifiers.drain(start..(start + count)) { + self.updated.insert(modifier.into_name(), ()); + } + } + + /// Updates the next modifier, based on the diff of `prev` and `next`. + pub fn update(&mut self, prev: &AttributeModifier, next: &AttributeModifier) { + if self.was_created() { + self.push(next.clone()); + } else if next != prev { + self.mutate(|modifier| *modifier = next.clone()); + } else { + self.skip(1); + } + } + + /// Updates the next modifier, based on the diff of `prev` and `next`, this can be used only when the previous modifier has the same name `key`, and only its value has changed. + pub fn update_with_same_key( + &mut self, + key: impl Into, + prev: &Value, + next: &Value, + ) { + if self.was_created() { + self.push((key, next.clone())); + } else if next != prev { + self.mutate(|modifier| *modifier = (key, next.clone()).into()); + } else { + self.skip(1); + } + } +} + +fn set_attribute(element: &web_sys::Element, name: &str, value: &str) { + debug_assert_ne!( + name, "class", + "Using `class` as attribute is not supported, use the `el.class()` modifier instead" + ); + debug_assert_ne!( + name, "style", + "Using `style` as attribute is not supported, use the `el.style()` modifier instead" + ); + + // we have to special-case `value` because setting the value using `set_attribute` + // doesn't work after the value has been changed. + // TODO not sure, whether this is always a good idea, in case custom or other interfaces such as HtmlOptionElement elements are used that have "value" as an attribute name. + // We likely want to use the DOM attributes instead. + if name == "value" { + if let Some(input_element) = element.dyn_ref::() { + input_element.set_value(value); + } else { + element.set_attribute("value", value).unwrap_throw(); + } + } else if name == "checked" { + if let Some(input_element) = element.dyn_ref::() { + input_element.set_checked(true); + } else { + element.set_attribute("checked", value).unwrap_throw(); + } + } else { + element.set_attribute(name, value).unwrap_throw(); + } +} + +fn remove_attribute(element: &web_sys::Element, name: &str) { + debug_assert_ne!( + name, "class", + "Using `class` as attribute is not supported, use the `el.class()` modifier instead" + ); + debug_assert_ne!( + name, "style", + "Using `style` as attribute is not supported, use the `el.style()` modifier instead" + ); + // we have to special-case `checked` because setting the value using `set_attribute` + // doesn't work after the value has been changed. + if name == "checked" { + if let Some(input_element) = element.dyn_ref::() { + input_element.set_checked(false); + } else { + element.remove_attribute("checked").unwrap_throw(); + } + } else { + element.remove_attribute(name).unwrap_throw(); + } +} + +/// A view to add an attribute to [`Element`](`crate::interfaces::Element`) derived components. +/// +/// See [`Element::attr`](`crate::interfaces::Element::attr`) for more usage information. +pub struct Attr { + inner: V, + modifier: AttributeModifier, + phantom: PhantomData (State, Action)>, +} + +impl Attr { + /// Create an [`Attr`] view. When `value` is `None`, it means remove the `name` attribute. + /// + /// Usually [`Element::attr`](`crate::interfaces::Element::attr`) should be used instead of this function. + pub fn new(el: V, name: CowStr, value: Option) -> Self { + let modifier = match value { + Some(value) => AttributeModifier::Set(name, value), + None => AttributeModifier::Remove(name), + }; + Self { + inner: el, + modifier, + phantom: PhantomData, + } + } +} + +impl ViewMarker for Attr {} +impl View for Attr +where + State: 'static, + Action: 'static, + V: DomView>, + for<'a> ::Mut<'a>: With, +{ + type Element = V::Element; + + type ViewState = V::ViewState; + + fn build(&self, ctx: &mut ViewCtx) -> (Self::Element, Self::ViewState) { + let (mut element, state) = + ctx.with_size_hint::(1, |ctx| self.inner.build(ctx)); + element.modifier().push(self.modifier.clone()); + (element, state) + } + + fn rebuild( + &self, + prev: &Self, + view_state: &mut Self::ViewState, + ctx: &mut ViewCtx, + element: Mut, + ) { + Attributes::rebuild(element, 1, |mut element| { + self.inner + .rebuild(&prev.inner, view_state, ctx, element.reborrow_mut()); + element.modifier().update(&prev.modifier, &self.modifier); + }); + } + + fn teardown( + &self, + view_state: &mut Self::ViewState, + ctx: &mut ViewCtx, + element: Mut, + ) { + self.inner.teardown(view_state, ctx, element); + } + + fn message( + &self, + view_state: &mut Self::ViewState, + id_path: &[ViewId], + message: DynMessage, + app_state: &mut State, + ) -> MessageResult { + self.inner.message(view_state, id_path, message, app_state) + } +} diff --git a/xilem_web/src/modifiers/class.rs b/xilem_web/src/modifiers/class.rs new file mode 100644 index 000000000..1a8d78644 --- /dev/null +++ b/xilem_web/src/modifiers/class.rs @@ -0,0 +1,393 @@ +// Copyright 2024 the Xilem Authors +// SPDX-License-Identifier: Apache-2.0 + +use crate::{ + core::{MessageResult, Mut, View, ViewElement, ViewId, ViewMarker}, + diff::{diff_iters, Diff}, + modifiers::With, + vecmap::VecMap, + DomView, DynMessage, ElementProps, ViewCtx, +}; +use std::{fmt::Debug, marker::PhantomData}; +use wasm_bindgen::{JsCast, UnwrapThrowExt}; + +type CowStr = std::borrow::Cow<'static, str>; + +#[derive(Debug, PartialEq, Clone)] +/// An modifier element to either add or remove a class of an element. +/// +/// It's used in [`Classes`]. +pub enum ClassModifier { + Add(CowStr), + Remove(CowStr), +} + +impl ClassModifier { + /// Returns the class name of this modifier. + pub fn name(&self) -> &CowStr { + let (ClassModifier::Add(name) | ClassModifier::Remove(name)) = self; + name + } +} + +/// Types implementing this trait can be used in the [`Class`] view, see also [`Element::class`](`crate::interfaces::Element::class`). +pub trait ClassIter: PartialEq + Debug + 'static { + /// Returns an iterator of class compliant strings (e.g. the strings aren't allowed to contain spaces). + fn class_iter(&self) -> impl Iterator; + + /// Returns an iterator of additive classes, i.e. all classes of this iterator are added to the current element. + fn add_class_iter(&self) -> impl Iterator { + self.class_iter().map(ClassModifier::Add) + } + + /// Returns an iterator of to remove classes, i.e. all classes of this iterator are removed from the current element. + fn remove_class_iter(&self) -> impl Iterator { + self.class_iter().map(ClassModifier::Remove) + } +} + +impl ClassIter for Option { + fn class_iter(&self) -> impl Iterator { + self.iter().flat_map(|c| c.class_iter()) + } +} + +impl ClassIter for String { + fn class_iter(&self) -> impl Iterator { + std::iter::once(self.clone().into()) + } +} + +impl ClassIter for &'static str { + fn class_iter(&self) -> impl Iterator { + std::iter::once(CowStr::from(*self)) + } +} + +impl ClassIter for CowStr { + fn class_iter(&self) -> impl Iterator { + std::iter::once(self.clone()) + } +} + +impl ClassIter for Vec { + fn class_iter(&self) -> impl Iterator { + self.iter().flat_map(|c| c.class_iter()) + } +} + +impl ClassIter for [C; N] { + fn class_iter(&self) -> impl Iterator { + self.iter().flat_map(|c| c.class_iter()) + } +} + +const IN_HYDRATION: u8 = 1 << 0; +const WAS_CREATED: u8 = 1 << 1; + +#[derive(Default)] +/// An Element modifier that manages all classes of an Element. +pub struct Classes { + class_name: String, + // It would be nice to avoid this, as this results in extra allocations, when `Strings` are used as classes. + classes: VecMap, + modifiers: Vec, + idx: u16, + dirty: bool, + /// This is to avoid an additional alignment word with 2 booleans, it contains the two `IN_HYDRATION` and `WAS_CREATED` flags + flags: u8, +} + +impl With for ElementProps { + fn modifier(&mut self) -> &mut Classes { + self.classes() + } +} + +impl Classes { + /// Creates a new `Classes` modifier. + /// + /// `size_hint` is used to avoid unnecessary allocations while traversing up the view-tree when adding modifiers in [`View::build`]. + pub(crate) fn new(size_hint: usize, in_hydration: bool) -> Self { + let mut flags = WAS_CREATED; + if in_hydration { + flags |= IN_HYDRATION; + } + Self { + modifiers: Vec::with_capacity(size_hint), + flags, + ..Default::default() + } + } + + /// Applies potential changes of the classes of an element to the underlying DOM node. + pub fn apply_changes(&mut self, element: &web_sys::Element) { + if (self.flags & IN_HYDRATION) == IN_HYDRATION { + self.flags = 0; + self.dirty = false; + } else if self.dirty { + self.flags = 0; + self.dirty = false; + self.classes.clear(); + self.classes.reserve(self.modifiers.len()); + for modifier in &self.modifiers { + match modifier { + ClassModifier::Remove(class_name) => self.classes.remove(class_name), + ClassModifier::Add(class_name) => self.classes.insert(class_name.clone(), ()), + }; + } + self.class_name.clear(); + self.class_name + .reserve_exact(self.classes.keys().map(|k| k.len() + 1).sum()); + let last_idx = self.classes.len().saturating_sub(1); + for (idx, class) in self.classes.keys().enumerate() { + self.class_name += class; + if idx != last_idx { + self.class_name += " "; + } + } + // Svg elements do have issues with className, see https://developer.mozilla.org/en-US/docs/Web/API/Element/className + if element.dyn_ref::().is_some() { + element + .set_attribute(wasm_bindgen::intern("class"), &self.class_name) + .unwrap_throw(); + } else { + element.set_class_name(&self.class_name); + } + } + } + + #[inline] + /// Rebuilds the current element, while ensuring that the order of the modifiers stays correct. + /// Any children should be rebuilt in inside `f`, *before* modifying any other properties of [`Classes`]. + pub fn rebuild>(mut element: E, prev_len: usize, f: impl FnOnce(E)) { + element.modifier().idx -= prev_len as u16; + f(element); + } + + #[inline] + /// Returns whether the underlying element has been built or rebuilt, this could e.g. happen, when `OneOf` changes a variant to a different element. + pub fn was_created(&self) -> bool { + self.flags & WAS_CREATED != 0 + } + + #[inline] + /// Pushes `modifier` at the end of the current modifiers. + /// + /// Must only be used when `self.was_created() == true` + pub fn push(&mut self, modifier: ClassModifier) { + debug_assert!( + self.was_created(), + "This should never be called, when the underlying element wasn't (re)created. Use `Classes::insert` instead." + ); + self.dirty = true; + self.modifiers.push(modifier); + self.idx += 1; + } + + #[inline] + /// Inserts `modifier` at the current index. + /// + /// Must only be used when `self.was_created() == false` + pub fn insert(&mut self, modifier: ClassModifier) { + debug_assert!( + !self.was_created(), + "This should never be called, when the underlying element was (re)created, use `Classes::push` instead." + ); + + self.dirty = true; + // TODO this could potentially be expensive, maybe think about `VecSplice` again. + // Although in the average case, this is likely not relevant, as usually very few attributes are used, thus shifting is probably good enough + // I.e. a `VecSplice` is probably less optimal (either more complicated code, and/or more memory usage) + self.modifiers.insert(self.idx as usize, modifier); + self.idx += 1; + } + + #[inline] + /// Mutates the next modifier. + /// + /// Must only be used when `self.was_created() == false` + pub fn mutate(&mut self, f: impl FnOnce(&mut ClassModifier) -> R) -> R { + debug_assert!( + !self.was_created(), + "This should never be called, when the underlying element was (re)created, use `Classes::push` instead." + ); + + self.dirty = true; + let idx = self.idx; + self.idx += 1; + f(&mut self.modifiers[idx as usize]) + } + + #[inline] + /// Skips the next `count` modifiers. + /// + /// Must only be used when `self.was_created() == false` + pub fn skip(&mut self, count: usize) { + debug_assert!( + !self.was_created(), + "This should never be called, when the underlying element was (re)created" + ); + self.idx += count as u16; + } + + #[inline] + /// Deletes the next `count` modifiers. + /// + /// Must only be used when `self.was_created() == false` + pub fn delete(&mut self, count: usize) { + debug_assert!( + !self.was_created(), + "This should never be called, when the underlying element was (re)created." + ); + let start = self.idx as usize; + self.dirty = true; + self.modifiers.drain(start..(start + count)); + } + + #[inline] + /// Extends the current modifiers with an iterator of modifiers. Returns the count of `modifiers`. + /// + /// Must only be used when `self.was_created() == true` + pub fn extend(&mut self, modifiers: impl Iterator) -> usize { + debug_assert!( + self.was_created(), + "This should never be called, when the underlying element wasn't (re)created, use `Classes::apply_diff` instead." + ); + self.dirty = true; + let prev_len = self.modifiers.len(); + self.modifiers.extend(modifiers); + let new_len = self.modifiers.len() - prev_len; + self.idx += new_len as u16; + new_len + } + + #[inline] + /// Diffs between two iterators, and updates the underlying modifiers if they have changed, returns the `next` iterator count. + /// + /// Must only be used when `self.was_created() == false` + pub fn apply_diff>(&mut self, prev: T, next: T) -> usize { + debug_assert!( + !self.was_created(), + "This should never be called, when the underlying element was (re)created, use `Classes::extend` instead." + ); + let mut new_len = 0; + for change in diff_iters(prev, next) { + match change { + Diff::Add(modifier) => { + self.insert(modifier); + new_len += 1; + } + Diff::Remove(count) => self.delete(count), + Diff::Change(new_modifier) => { + self.mutate(|modifier| *modifier = new_modifier); + new_len += 1; + } + Diff::Skip(count) => { + self.skip(count); + new_len += count; + } + } + } + new_len + } + + /// Updates based on the diff between two class iterators (`prev`, `next`) interpreted as add modifiers. + /// + /// Updates the underlying modifiers if they have changed, returns the next iterator count. + /// Skips or adds modifiers, when nothing has changed, or the element was recreated. + pub fn update_as_add_class_iter( + &mut self, + prev_len: usize, + prev: &T, + next: &T, + ) -> usize { + if self.was_created() { + self.extend(next.add_class_iter()) + } else if next != prev { + self.apply_diff(prev.add_class_iter(), next.add_class_iter()) + } else { + self.skip(prev_len); + prev_len + } + } +} + +/// A view to add classes to `Element` derived elements. +/// +/// See [`Element::class`](`crate::interfaces::Element::class`) for more usage information. +#[derive(Clone, Debug)] +pub struct Class { + el: E, + classes: C, + phantom: PhantomData (T, A)>, +} + +impl Class { + /// Create a `Class` view. `classes` is a [`ClassIter`]. + /// + /// Usually [`Element::class`](`crate::interfaces::Element::class`) should be used instead of this function. + pub fn new(el: E, classes: C) -> Self { + Class { + el, + classes, + phantom: PhantomData, + } + } +} + +impl ViewMarker for Class {} +impl View for Class +where + State: 'static, + Action: 'static, + C: ClassIter, + V: DomView>, + for<'a> ::Mut<'a>: With, +{ + type Element = V::Element; + + type ViewState = (usize, V::ViewState); + + fn build(&self, ctx: &mut ViewCtx) -> (Self::Element, Self::ViewState) { + let add_class_iter = self.classes.add_class_iter(); + let (mut e, s) = ctx + .with_size_hint::(add_class_iter.size_hint().0, |ctx| self.el.build(ctx)); + let len = e.modifier().extend(add_class_iter); + (e, (len, s)) + } + + fn rebuild( + &self, + prev: &Self, + (len, view_state): &mut Self::ViewState, + ctx: &mut ViewCtx, + element: Mut, + ) { + Classes::rebuild(element, *len, |mut elem| { + self.el + .rebuild(&prev.el, view_state, ctx, elem.reborrow_mut()); + let classes = elem.modifier(); + *len = classes.update_as_add_class_iter(*len, &prev.classes, &self.classes); + }); + } + + fn teardown( + &self, + (_, view_state): &mut Self::ViewState, + ctx: &mut ViewCtx, + element: Mut, + ) { + self.el.teardown(view_state, ctx, element); + } + + fn message( + &self, + (_, view_state): &mut Self::ViewState, + id_path: &[ViewId], + message: DynMessage, + app_state: &mut State, + ) -> MessageResult { + self.el.message(view_state, id_path, message, app_state) + } +} diff --git a/xilem_web/src/modifiers/mod.rs b/xilem_web/src/modifiers/mod.rs new file mode 100644 index 000000000..099ac53ba --- /dev/null +++ b/xilem_web/src/modifiers/mod.rs @@ -0,0 +1,87 @@ +// Copyright 2024 the Xilem Authors +// SPDX-License-Identifier: Apache-2.0 + +//! This module contains DOM element modifiers, e.g. to add attributes/classes/styles. +//! +//! A modifier is usually a part/attribute of a [`ViewElement`](crate::core::ViewElement), +//! and has corresponding Views, usually meant to be used in a builder-style. +//! +//! One such example is setting attributes on a DOM element, like this: +//! ``` +//! use xilem_web::{interfaces::Element, elements::html::{a, canvas, input}}; +//! // ... +//! # use xilem_web::elements::html::div; +//! # fn component() -> impl Element<()> { +//! # div(( +//! a("a link to an anchor").attr("href", "#anchor"), +//! // attribute will only appear if condition is met +//! // previous attribute is overwritten (and removed if condition is false) +//! a("a link to a new anchor - *maybe*") +//! .attr("href", "#anchor") +//! .attr("href", true.then_some("#new-anchor")), +//! input(()).attr("autofocus", true), +//! canvas(()).attr("width", 300) +//! # )) +//! # } +//! ``` +//! +//! These modifiers have to fulfill some properties to be able to be used without unwanted side-effects. +//! As the modifier-views are usually depending on a bound on its `View::Element`, the following needs to be supported: +//! +//! ``` +//! use xilem_web::{ +//! core::{frozen, one_of::Either}, +//! interfaces::Element, +//! elements::html::{div, span}, +//! modifiers::style as s, +//! }; +//! // ... +//! # fn component() -> impl Element<()> { +//! # div(( +//! // Memoized views may never update their memoized modifiers: +//! frozen(|| div("this will be created only once").class("shadow")) +//! .class(["text-center", "flex"]), +//! // For some cases be able to read possibly memoized modifiers. +//! // Following results in the style attribute: +//! // `transform: translate(10px, 10px) scale(2.0)` and is updated, when `.scale` changes +//! frozen(|| div("transformed").style(s("transform", "translate(10px, 10px)"))) +//! .scale(2.0), +//! // OneOf/Either views can change their underlying element type, while supporting the same modifier: +//! (if true { Either::A(div("div").class("w-full")) } else { Either::B(span("span")) }) +//! .class("text-center") +//! # )) +//! # } +//! ``` +//! +//! They should also aim to produce as little DOM traffic (i.e. js calls to modify the DOM-tree) as possible to be efficient. +mod attribute; +pub use attribute::*; + +mod class; +pub use class::*; + +mod style; +pub use style::*; + +use crate::{AnyPod, DomNode, Pod, PodMut}; + +/// This is basically equivalent to [`AsMut`], it's intended to give access to modifiers of a [`ViewElement`](crate::core::ViewElement). +/// +/// The name is chosen, such that it reads nicely, e.g. in a trait bound: [`DomView>`](crate::DomView), while not behaving differently as [`AsRef`] on [`Pod`] and [`PodMut`]. +pub trait With { + fn modifier(&mut self) -> &mut M; +} + +impl>> With for Pod { + fn modifier(&mut self) -> &mut T { + >::modifier(&mut self.props) + } +} + +impl>> With for PodMut<'_, N> { + fn modifier(&mut self) -> &mut T { + >::modifier(self.props) + } +} + +pub type Children = Vec; diff --git a/xilem_web/src/modifiers/style.rs b/xilem_web/src/modifiers/style.rs new file mode 100644 index 000000000..2d3ef4d9d --- /dev/null +++ b/xilem_web/src/modifiers/style.rs @@ -0,0 +1,730 @@ +// Copyright 2024 the Xilem Authors +// SPDX-License-Identifier: Apache-2.0 + +use crate::{ + core::{MessageResult, Mut, View, ViewElement, ViewId, ViewMarker}, + diff::{diff_iters, Diff}, + modifiers::With, + vecmap::VecMap, + DomView, DynMessage, ElementProps, ViewCtx, +}; +use peniko::kurbo::Vec2; +use std::{ + collections::{BTreeMap, HashMap}, + fmt::{Debug, Display}, + hash::{BuildHasher, Hash}, + marker::PhantomData, +}; +use wasm_bindgen::{JsCast, UnwrapThrowExt}; + +type CowStr = std::borrow::Cow<'static, str>; + +#[derive(Debug, PartialEq, Clone)] +/// An modifier element to either set or remove an inline style. +/// +/// It's used in [`Styles`]. +pub enum StyleModifier { + Set(CowStr, CowStr), + Remove(CowStr), +} + +impl StyleModifier { + /// Returns the property name of this modifier. + pub fn name(&self) -> &CowStr { + let (StyleModifier::Set(name, _) | StyleModifier::Remove(name)) = self; + name + } + + /// Convert this modifier into its property name. + pub fn into_name(self) -> CowStr { + let (StyleModifier::Set(name, _) | StyleModifier::Remove(name)) = self; + name + } +} + +impl>, K: Into> From<(K, V)> for StyleModifier { + fn from((name, value): (K, V)) -> Self { + match value.into() { + Some(value) => StyleModifier::Set(name.into(), value), + None => StyleModifier::Remove(name.into()), + } + } +} + +/// A trait to make the style adding functions generic over collection types +pub trait StyleIter: PartialEq + Debug + 'static { + // TODO do a similar pattern as in ClassIter? (i.e. don't use an Option here, and be able to use it as boolean intersection?) + /// Iterates over key value pairs of style properties, `None` as value means remove the current value if it was previously set. + fn styles_iter(&self) -> impl Iterator)>; + + fn style_modifiers_iter(&self) -> impl Iterator { + self.styles_iter().map(From::from) + } +} + +#[derive(PartialEq, Debug)] +struct StyleTuple(T1, T2); + +// TODO should this also allow removing style values, via `None`? +/// Create a style from a style name and its value. +pub fn style( + name: impl Into + Clone + PartialEq + Debug + 'static, + value: impl Into + Clone + PartialEq + Debug + 'static, +) -> impl StyleIter { + StyleTuple(name, Some(value.into())) +} + +impl StyleIter for StyleTuple +where + T1: Into + Clone + PartialEq + Debug + 'static, + T2: Into> + Clone + PartialEq + Debug + 'static, +{ + fn styles_iter(&self) -> impl Iterator)> { + let StyleTuple(key, value) = self; + std::iter::once((key.clone().into(), value.clone().into())) + } +} + +impl StyleIter for Option { + fn styles_iter(&self) -> impl Iterator)> { + self.iter().flat_map(|c| c.styles_iter()) + } +} + +impl StyleIter for Vec { + fn styles_iter(&self) -> impl Iterator)> { + self.iter().flat_map(|c| c.styles_iter()) + } +} + +impl StyleIter for [T; N] { + fn styles_iter(&self) -> impl Iterator)> { + self.iter().flat_map(|c| c.styles_iter()) + } +} + +impl StyleIter for HashMap +where + T1: Into + Clone + PartialEq + Eq + Hash + Debug + 'static, + T2: Into> + Clone + PartialEq + Debug + 'static, + S: BuildHasher + 'static, +{ + fn styles_iter(&self) -> impl Iterator)> { + self.iter() + .map(|s| (s.0.clone().into(), s.1.clone().into())) + } +} + +impl StyleIter for BTreeMap +where + T1: Into + Clone + PartialEq + Debug + 'static, + T2: Into> + Clone + PartialEq + Debug + 'static, +{ + fn styles_iter(&self) -> impl Iterator)> { + self.iter() + .map(|s| (s.0.clone().into(), s.1.clone().into())) + } +} + +impl StyleIter for VecMap +where + T1: Into + Clone + PartialEq + Debug + 'static, + T2: Into> + Clone + PartialEq + Debug + 'static, +{ + fn styles_iter(&self) -> impl Iterator)> { + self.iter() + .map(|s| (s.0.clone().into(), s.1.clone().into())) + } +} + +#[derive(Default)] +/// An Element modifier that manages all inline styles of an Element. +pub struct Styles { + // TODO think about using a `VecSplice` for more efficient insertion etc., + // but this is an additional trade-off of memory-usage and complexity, + // while probably not helping much in the average case (of very few styles)... + modifiers: Vec, + updated: VecMap, + idx: u16, + in_hydration: bool, + was_created: bool, +} + +impl With for ElementProps { + fn modifier(&mut self) -> &mut Styles { + self.styles() + } +} + +fn set_style(element: &web_sys::Element, name: &str, value: &str) { + if let Some(el) = element.dyn_ref::() { + el.style().set_property(name, value).unwrap_throw(); + } else if let Some(el) = element.dyn_ref::() { + el.style().set_property(name, value).unwrap_throw(); + } +} + +fn remove_style(element: &web_sys::Element, name: &str) { + if let Some(el) = element.dyn_ref::() { + el.style().remove_property(name).unwrap_throw(); + } else if let Some(el) = element.dyn_ref::() { + el.style().remove_property(name).unwrap_throw(); + } +} + +impl Styles { + /// Creates a new `Styles` modifier. + /// + /// `size_hint` is used to avoid unnecessary allocations while traversing up the view-tree when adding modifiers in [`View::build`]. + pub(crate) fn new(size_hint: usize, in_hydration: bool) -> Self { + Self { + modifiers: Vec::with_capacity(size_hint), + was_created: true, + in_hydration, + ..Default::default() + } + } + + /// Applies potential changes of the inline styles of an element to the underlying DOM node. + pub fn apply_changes(&mut self, element: &web_sys::Element) { + if self.in_hydration { + self.in_hydration = false; + self.was_created = false; + } else if self.was_created { + self.was_created = false; + for modifier in &self.modifiers { + match modifier { + StyleModifier::Remove(name) => remove_style(element, name), + StyleModifier::Set(name, value) => set_style(element, name, value), + } + } + } else if !self.updated.is_empty() { + for modifier in self.modifiers.iter().rev() { + match modifier { + StyleModifier::Remove(name) if self.updated.remove(name).is_some() => { + remove_style(element, name); + } + StyleModifier::Set(name, value) if self.updated.remove(name).is_some() => { + set_style(element, name, value); + } + _ => {} + } + } + // if there's any remaining key in updated, it means these are deleted keys + for (name, ()) in self.updated.drain() { + remove_style(element, &name); + } + } + debug_assert!(self.updated.is_empty()); + } + + /// Returns a previous [`StyleModifier`], when `predicate` returns true, this is similar to [`Iterator::find`]. + pub fn get(&self, mut predicate: impl FnMut(&StyleModifier) -> bool) -> Option<&StyleModifier> { + self.modifiers[..self.idx as usize] + .iter() + .rev() + .find(|modifier| predicate(modifier)) + } + + #[inline] + /// Returns the current value of a style property with `name` if it is set. + pub fn get_style(&self, name: &str) -> Option<&CowStr> { + if let Some(StyleModifier::Set(_, value)) = self.get( + |m| matches!(m, StyleModifier::Remove(key) | StyleModifier::Set(key, _) if key == name), + ) { + Some(value) + } else { + None + } + } + + #[inline] + /// Rebuilds the current element, while ensuring that the order of the modifiers stays correct. + /// Any children should be rebuilt in inside `f`, *before* modifying any other properties of [`Styles`]. + pub fn rebuild>(mut element: E, prev_len: usize, f: impl FnOnce(E)) { + element.modifier().idx -= prev_len as u16; + f(element); + } + + #[inline] + /// Returns whether the underlying element has been built or rebuilt, this could e.g. happen, when `OneOf` changes a variant to a different element. + pub fn was_created(&self) -> bool { + self.was_created + } + + #[inline] + /// Returns whether the style with the `name` has been modified in the current reconciliation pass/rebuild. + fn was_updated(&self, name: &str) -> bool { + self.updated.contains_key(name) + } + + #[inline] + /// Pushes `modifier` at the end of the current modifiers + /// + /// Must only be used when `self.was_created() == true`, use `Styles::insert` otherwise. + pub fn push(&mut self, modifier: StyleModifier) { + debug_assert!( + self.was_created(), + "This should never be called, when the underlying element wasn't (re)created. Use `Styles::insert` instead." + ); + if !self.was_created && !self.in_hydration { + self.updated.insert(modifier.name().clone(), ()); + } + self.modifiers.push(modifier); + self.idx += 1; + } + + #[inline] + /// Inserts `modifier` at the current index + /// + /// Must only be used when `self.was_created() == false`, use `Styles::push` otherwise. + pub fn insert(&mut self, modifier: StyleModifier) { + debug_assert!( + !self.was_created(), + "This should never be called, when the underlying element was (re)created, use `Styles::push` instead." + ); + if !self.was_created && !self.in_hydration { + self.updated.insert(modifier.name().clone(), ()); + } + // TODO this could potentially be expensive, maybe think about `VecSplice` again. + // Although in the average case, this is likely not relevant, as usually very few attributes are used, thus shifting is probably good enough + // I.e. a `VecSplice` is probably less optimal (either more complicated code, and/or more memory usage) + self.modifiers.insert(self.idx as usize, modifier); + self.idx += 1; + } + + #[inline] + /// Mutates the next modifier. + /// + /// Must only be used when `self.was_created() == false`. + pub fn mutate(&mut self, f: impl FnOnce(&mut StyleModifier) -> R) -> R { + debug_assert!( + !self.was_created(), + "This should never be called, when the underlying element was (re)created." + ); + let modifier = &mut self.modifiers[self.idx as usize]; + let old = modifier.name().clone(); + let rv = f(modifier); + let new = modifier.name(); + if *new != old { + self.updated.insert(new.clone(), ()); + } + self.updated.insert(old, ()); + self.idx += 1; + rv + } + + #[inline] + /// Skips the next `count` modifiers. + /// + /// Must only be used when `self.was_created() == false`. + pub fn skip(&mut self, count: usize) { + debug_assert!( + !self.was_created(), + "This should never be called, when the underlying element was (re)created." + ); + self.idx += count as u16; + } + + #[inline] + /// Deletes the next `count` modifiers. + /// + /// Must only be used when `self.was_created() == false`. + pub fn delete(&mut self, count: usize) { + debug_assert!( + !self.was_created(), + "This should never be called, when the underlying element was (re)created." + ); + let start = self.idx as usize; + for modifier in self.modifiers.drain(start..(start + count)) { + self.updated.insert(modifier.into_name(), ()); + } + } + + #[inline] + /// Updates the next modifier, based on the diff of `prev` and `next`. + pub fn update(&mut self, prev: &StyleModifier, next: &StyleModifier) { + if self.was_created() { + self.push(next.clone()); + } else if next != prev { + self.mutate(|modifier| *modifier = next.clone()); + } else { + self.skip(1); + } + } + + #[inline] + /// Extends the current modifiers with an iterator of modifiers. Returns the count of `modifiers`. + /// + /// Must only be used when `self.was_created() == true`, use `Styles::apply_diff` otherwise. + pub fn extend(&mut self, modifiers: impl Iterator) -> usize { + debug_assert!( + self.was_created(), + "This should never be called, when the underlying element wasn't (re)created, use `Styles::apply_diff` instead." + ); + let prev_len = self.modifiers.len(); + self.modifiers.extend(modifiers); + let iter_count = self.modifiers.len() - prev_len; + if !self.was_created && !self.in_hydration && iter_count > 0 { + for modifier in &self.modifiers[prev_len..] { + self.updated.insert(modifier.name().clone(), ()); + } + } + self.idx += iter_count as u16; + iter_count + } + + #[inline] + /// Diffs between two iterators, and updates the underlying modifiers if they have changed, returns the `next` iterator count. + /// + /// Must only be used when `self.was_created() == false`, use [`Styles::extend`] otherwise. + pub fn apply_diff>(&mut self, prev: T, next: T) -> usize { + debug_assert!( + !self.was_created(), + "This should never be called, when the underlying element was (re)created, use `Styles::extend` instead." + ); + let mut count = 0; + for change in diff_iters(prev, next) { + match change { + Diff::Add(modifier) => { + self.insert(modifier); + count += 1; + } + Diff::Remove(count) => self.delete(count), + Diff::Change(new_modifier) => { + self.mutate(|modifier| *modifier = new_modifier); + count += 1; + } + Diff::Skip(c) => { + self.skip(c); + count += c; + } + } + } + count + } + + #[inline] + /// Updates styles defined by an iterator, returns the `next` iterator length. + pub fn update_style_modifier_iter( + &mut self, + prev_len: usize, + prev: &T, + next: &T, + ) -> usize { + if self.was_created() { + self.extend(next.style_modifiers_iter()) + } else if next != prev { + self.apply_diff(prev.style_modifiers_iter(), next.style_modifiers_iter()) + } else { + self.skip(prev_len); + prev_len + } + } + + #[inline] + /// Updates the style property `name` by modifying its previous value with `create_modifier`. + pub fn update_style_mutator( + &mut self, + name: &'static str, + prev: &T, + next: &T, + create_modifier: impl FnOnce(Option<&CowStr>, &T) -> StyleModifier, + ) { + if self.was_created() { + self.push(create_modifier(self.get_style(name), next)); + } else if prev != next || self.was_updated(name) { + let new_modifier = create_modifier(self.get_style(name), next); + self.mutate(|modifier| *modifier = new_modifier); + } else { + self.skip(1); + } + } +} + +#[derive(Clone, Debug)] +/// A view to add `style` properties to `Element` derived elements. +/// +/// See [`Element::style`](`crate::interfaces::Element::style`) for more usage information. +pub struct Style { + el: E, + styles: S, + phantom: PhantomData (T, A)>, +} + +impl Style { + /// Create a `Style` view. `styles` is a [`StyleIter`]. + /// + /// Usually [`Element::style`](`crate::interfaces::Element::style`) should be used instead of this function. + pub fn new(el: E, styles: S) -> Self { + Style { + el, + styles, + phantom: PhantomData, + } + } +} + +impl ViewMarker for Style {} +impl View for Style +where + State: 'static, + Action: 'static, + S: StyleIter, + V: DomView>, + for<'a> ::Mut<'a>: With, +{ + type Element = V::Element; + + type ViewState = (usize, V::ViewState); + + fn build(&self, ctx: &mut ViewCtx) -> (Self::Element, Self::ViewState) { + let style_iter = self.styles.style_modifiers_iter(); + let (mut e, s) = + ctx.with_size_hint::(style_iter.size_hint().0, |ctx| self.el.build(ctx)); + let len = e.modifier().extend(style_iter); + (e, (len, s)) + } + + fn rebuild( + &self, + prev: &Self, + (len, view_state): &mut Self::ViewState, + ctx: &mut ViewCtx, + element: Mut, + ) { + Styles::rebuild(element, *len, |mut elem| { + self.el + .rebuild(&prev.el, view_state, ctx, elem.reborrow_mut()); + let styles = elem.modifier(); + *len = styles.update_style_modifier_iter(*len, &prev.styles, &self.styles); + }); + } + + fn teardown( + &self, + (_, view_state): &mut Self::ViewState, + ctx: &mut ViewCtx, + element: Mut, + ) { + self.el.teardown(view_state, ctx, element); + } + + fn message( + &self, + (_, view_state): &mut Self::ViewState, + id_path: &[ViewId], + message: DynMessage, + app_state: &mut State, + ) -> MessageResult { + self.el.message(view_state, id_path, message, app_state) + } +} + +/// Add a `rotate(rad)` [transform-function](https://developer.mozilla.org/en-US/docs/Web/CSS/transform-function/rotate) to the current CSS `transform`. +pub struct Rotate { + el: E, + phantom: PhantomData (State, Action)>, + radians: f64, +} + +impl Rotate { + pub(crate) fn new(element: E, radians: f64) -> Self { + Rotate { + el: element, + phantom: PhantomData, + radians, + } + } +} + +fn rotate_transform_modifier(transform: Option<&CowStr>, radians: &f64) -> StyleModifier { + let value = if let Some(transform) = transform { + format!("{transform} rotate({radians}rad)") + } else { + format!("rotate({radians}rad)") + }; + StyleModifier::Set("transform".into(), CowStr::from(value)) +} + +impl ViewMarker for Rotate {} +impl View for Rotate +where + State: 'static, + Action: 'static, + V: DomView>, + for<'a> ::Mut<'a>: With, +{ + type Element = V::Element; + + type ViewState = V::ViewState; + + fn build(&self, ctx: &mut ViewCtx) -> (Self::Element, Self::ViewState) { + let (mut element, state) = ctx.with_size_hint::(1, |ctx| self.el.build(ctx)); + let styles = element.modifier(); + styles.push(rotate_transform_modifier( + styles.get_style("transform"), + &self.radians, + )); + (element, state) + } + + fn rebuild( + &self, + prev: &Self, + view_state: &mut Self::ViewState, + ctx: &mut ViewCtx, + element: Mut, + ) { + Styles::rebuild(element, 1, |mut element| { + self.el + .rebuild(&prev.el, view_state, ctx, element.reborrow_mut()); + element.modifier().update_style_mutator( + "transform", + &prev.radians, + &self.radians, + rotate_transform_modifier, + ); + }); + } + + fn teardown( + &self, + view_state: &mut Self::ViewState, + ctx: &mut ViewCtx, + element: Mut, + ) { + self.el.teardown(view_state, ctx, element); + } + + fn message( + &self, + view_state: &mut Self::ViewState, + id_path: &[ViewId], + message: DynMessage, + app_state: &mut State, + ) -> MessageResult { + self.el.message(view_state, id_path, message, app_state) + } +} + +#[derive(Clone, Copy, Debug, PartialEq)] +/// A wrapper, for some syntax sugar, such that `el.scale(1.5).scale((0.75, 2.0))` is possible. +pub enum ScaleValue { + Uniform(f64), + NonUniform(f64, f64), +} + +impl Display for ScaleValue { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ScaleValue::Uniform(uniform) => write!(f, "{uniform}"), + ScaleValue::NonUniform(x, y) => write!(f, "{x}, {y}"), + } + } +} + +impl From for ScaleValue { + fn from(value: f64) -> Self { + ScaleValue::Uniform(value) + } +} + +impl From<(f64, f64)> for ScaleValue { + fn from(value: (f64, f64)) -> Self { + ScaleValue::NonUniform(value.0, value.1) + } +} + +impl From for ScaleValue { + fn from(value: Vec2) -> Self { + ScaleValue::NonUniform(value.x, value.y) + } +} + +/// Add a `scale()` [transform-function](https://developer.mozilla.org/en-US/docs/Web/CSS/transform-function/scale) to the current CSS `transform`. +pub struct Scale { + el: E, + phantom: PhantomData (State, Action)>, + scale: ScaleValue, +} + +impl Scale { + pub(crate) fn new(element: E, scale: impl Into) -> Self { + Scale { + el: element, + phantom: PhantomData, + scale: scale.into(), + } + } +} + +fn scale_transform_modifier(transform: Option<&CowStr>, scale: &ScaleValue) -> StyleModifier { + let value = if let Some(transform) = transform { + format!("{transform} scale({scale})") + } else { + format!("scale({scale})") + }; + StyleModifier::Set("transform".into(), CowStr::from(value)) +} + +impl ViewMarker for Scale {} +impl View for Scale +where + State: 'static, + Action: 'static, + V: DomView>, + for<'a> ::Mut<'a>: With, +{ + type Element = V::Element; + + type ViewState = V::ViewState; + + fn build(&self, ctx: &mut ViewCtx) -> (Self::Element, Self::ViewState) { + let (mut element, state) = ctx.with_size_hint::(1, |ctx| self.el.build(ctx)); + let styles = element.modifier(); + styles.push(scale_transform_modifier( + styles.get_style("transform"), + &self.scale, + )); + (element, state) + } + + fn rebuild( + &self, + prev: &Self, + view_state: &mut Self::ViewState, + ctx: &mut ViewCtx, + element: Mut, + ) { + Styles::rebuild(element, 1, |mut element| { + self.el + .rebuild(&prev.el, view_state, ctx, element.reborrow_mut()); + element.modifier().update_style_mutator( + "transform", + &prev.scale, + &self.scale, + scale_transform_modifier, + ); + }); + } + + fn teardown( + &self, + view_state: &mut Self::ViewState, + ctx: &mut ViewCtx, + element: Mut, + ) { + self.el.teardown(view_state, ctx, element); + } + + fn message( + &self, + view_state: &mut Self::ViewState, + id_path: &[ViewId], + message: DynMessage, + app_state: &mut State, + ) -> MessageResult { + self.el.message(view_state, id_path, message, app_state) + } +} diff --git a/xilem_web/src/one_of.rs b/xilem_web/src/one_of.rs index 08fd4a888..65a18b9bf 100644 --- a/xilem_web/src/one_of.rs +++ b/xilem_web/src/one_of.rs @@ -2,19 +2,15 @@ // SPDX-License-Identifier: Apache-2.0 use crate::{ - attribute::WithAttributes, - class::WithClasses, core::{ one_of::{OneOf, OneOfCtx, PhantomElementCtx}, Mut, }, - style::WithStyle, - AttributeValue, DomNode, Pod, PodMut, ViewCtx, + modifiers::With, + DomNode, Pod, PodMut, ViewCtx, }; use wasm_bindgen::UnwrapThrowExt; -type CowStr = std::borrow::Cow<'static, str>; - impl OneOfCtx, Pod, Pod, Pod, Pod, Pod, Pod, Pod, Pod> for ViewCtx @@ -183,288 +179,6 @@ where } } -#[allow(unnameable_types)] // reason: Implementation detail, public because of trait visibility rules -pub enum Noop {} - -impl PhantomElementCtx for ViewCtx { - type PhantomElement = Pod; -} - -impl WithAttributes for Noop { - fn rebuild_attribute_modifier(&mut self) { - unreachable!() - } - - fn mark_end_of_attribute_modifier(&mut self) { - unreachable!() - } - - fn set_attribute(&mut self, _name: &CowStr, _value: &Option) { - unreachable!() - } -} - -impl WithClasses for Noop { - fn rebuild_class_modifier(&mut self) { - unreachable!() - } - - fn add_class(&mut self, _class_name: &CowStr) { - unreachable!() - } - - fn remove_class(&mut self, _class_name: &CowStr) { - unreachable!() - } - - fn mark_end_of_class_modifier(&mut self) { - unreachable!() - } -} - -impl WithStyle for Noop { - fn rebuild_style_modifier(&mut self) { - unreachable!() - } - - fn set_style(&mut self, _name: &CowStr, _value: &Option) { - unreachable!() - } - - fn mark_end_of_style_modifier(&mut self) { - unreachable!() - } - - fn get_style(&self, _name: &str) -> Option<&CowStr> { - unreachable!() - } - - fn was_updated(&self, _name: &str) -> bool { - unreachable!() - } -} - -impl AsRef for Noop { - fn as_ref(&self) -> &T { - unreachable!() - } -} - -impl DomNode for Noop { - fn apply_props(&self, _props: &mut Self::Props) { - unreachable!() - } - - type Props = Noop; -} - -impl< - E1: WithAttributes, - E2: WithAttributes, - E3: WithAttributes, - E4: WithAttributes, - E5: WithAttributes, - E6: WithAttributes, - E7: WithAttributes, - E8: WithAttributes, - E9: WithAttributes, - > WithAttributes for OneOf -{ - fn rebuild_attribute_modifier(&mut self) { - match self { - OneOf::A(e) => e.rebuild_attribute_modifier(), - OneOf::B(e) => e.rebuild_attribute_modifier(), - OneOf::C(e) => e.rebuild_attribute_modifier(), - OneOf::D(e) => e.rebuild_attribute_modifier(), - OneOf::E(e) => e.rebuild_attribute_modifier(), - OneOf::F(e) => e.rebuild_attribute_modifier(), - OneOf::G(e) => e.rebuild_attribute_modifier(), - OneOf::H(e) => e.rebuild_attribute_modifier(), - OneOf::I(e) => e.rebuild_attribute_modifier(), - } - } - - fn mark_end_of_attribute_modifier(&mut self) { - match self { - OneOf::A(e) => e.mark_end_of_attribute_modifier(), - OneOf::B(e) => e.mark_end_of_attribute_modifier(), - OneOf::C(e) => e.mark_end_of_attribute_modifier(), - OneOf::D(e) => e.mark_end_of_attribute_modifier(), - OneOf::E(e) => e.mark_end_of_attribute_modifier(), - OneOf::F(e) => e.mark_end_of_attribute_modifier(), - OneOf::G(e) => e.mark_end_of_attribute_modifier(), - OneOf::H(e) => e.mark_end_of_attribute_modifier(), - OneOf::I(e) => e.mark_end_of_attribute_modifier(), - } - } - - fn set_attribute(&mut self, name: &CowStr, value: &Option) { - match self { - OneOf::A(e) => e.set_attribute(name, value), - OneOf::B(e) => e.set_attribute(name, value), - OneOf::C(e) => e.set_attribute(name, value), - OneOf::D(e) => e.set_attribute(name, value), - OneOf::E(e) => e.set_attribute(name, value), - OneOf::F(e) => e.set_attribute(name, value), - OneOf::G(e) => e.set_attribute(name, value), - OneOf::H(e) => e.set_attribute(name, value), - OneOf::I(e) => e.set_attribute(name, value), - } - } -} - -impl< - E1: WithClasses, - E2: WithClasses, - E3: WithClasses, - E4: WithClasses, - E5: WithClasses, - E6: WithClasses, - E7: WithClasses, - E8: WithClasses, - E9: WithClasses, - > WithClasses for OneOf -{ - fn rebuild_class_modifier(&mut self) { - match self { - OneOf::A(e) => e.rebuild_class_modifier(), - OneOf::B(e) => e.rebuild_class_modifier(), - OneOf::C(e) => e.rebuild_class_modifier(), - OneOf::D(e) => e.rebuild_class_modifier(), - OneOf::E(e) => e.rebuild_class_modifier(), - OneOf::F(e) => e.rebuild_class_modifier(), - OneOf::G(e) => e.rebuild_class_modifier(), - OneOf::H(e) => e.rebuild_class_modifier(), - OneOf::I(e) => e.rebuild_class_modifier(), - } - } - - fn add_class(&mut self, class_name: &CowStr) { - match self { - OneOf::A(e) => e.add_class(class_name), - OneOf::B(e) => e.add_class(class_name), - OneOf::C(e) => e.add_class(class_name), - OneOf::D(e) => e.add_class(class_name), - OneOf::E(e) => e.add_class(class_name), - OneOf::F(e) => e.add_class(class_name), - OneOf::G(e) => e.add_class(class_name), - OneOf::H(e) => e.add_class(class_name), - OneOf::I(e) => e.add_class(class_name), - } - } - - fn remove_class(&mut self, class_name: &CowStr) { - match self { - OneOf::A(e) => e.remove_class(class_name), - OneOf::B(e) => e.remove_class(class_name), - OneOf::C(e) => e.remove_class(class_name), - OneOf::D(e) => e.remove_class(class_name), - OneOf::E(e) => e.remove_class(class_name), - OneOf::F(e) => e.remove_class(class_name), - OneOf::G(e) => e.remove_class(class_name), - OneOf::H(e) => e.remove_class(class_name), - OneOf::I(e) => e.remove_class(class_name), - } - } - - fn mark_end_of_class_modifier(&mut self) { - match self { - OneOf::A(e) => e.mark_end_of_class_modifier(), - OneOf::B(e) => e.mark_end_of_class_modifier(), - OneOf::C(e) => e.mark_end_of_class_modifier(), - OneOf::D(e) => e.mark_end_of_class_modifier(), - OneOf::E(e) => e.mark_end_of_class_modifier(), - OneOf::F(e) => e.mark_end_of_class_modifier(), - OneOf::G(e) => e.mark_end_of_class_modifier(), - OneOf::H(e) => e.mark_end_of_class_modifier(), - OneOf::I(e) => e.mark_end_of_class_modifier(), - } - } -} - -impl< - E1: WithStyle, - E2: WithStyle, - E3: WithStyle, - E4: WithStyle, - E5: WithStyle, - E6: WithStyle, - E7: WithStyle, - E8: WithStyle, - E9: WithStyle, - > WithStyle for OneOf -{ - fn rebuild_style_modifier(&mut self) { - match self { - OneOf::A(e) => e.rebuild_style_modifier(), - OneOf::B(e) => e.rebuild_style_modifier(), - OneOf::C(e) => e.rebuild_style_modifier(), - OneOf::D(e) => e.rebuild_style_modifier(), - OneOf::E(e) => e.rebuild_style_modifier(), - OneOf::F(e) => e.rebuild_style_modifier(), - OneOf::G(e) => e.rebuild_style_modifier(), - OneOf::H(e) => e.rebuild_style_modifier(), - OneOf::I(e) => e.rebuild_style_modifier(), - } - } - - fn set_style(&mut self, name: &CowStr, value: &Option) { - match self { - OneOf::A(e) => e.set_style(name, value), - OneOf::B(e) => e.set_style(name, value), - OneOf::C(e) => e.set_style(name, value), - OneOf::D(e) => e.set_style(name, value), - OneOf::E(e) => e.set_style(name, value), - OneOf::F(e) => e.set_style(name, value), - OneOf::G(e) => e.set_style(name, value), - OneOf::H(e) => e.set_style(name, value), - OneOf::I(e) => e.set_style(name, value), - } - } - - fn mark_end_of_style_modifier(&mut self) { - match self { - OneOf::A(e) => e.mark_end_of_style_modifier(), - OneOf::B(e) => e.mark_end_of_style_modifier(), - OneOf::C(e) => e.mark_end_of_style_modifier(), - OneOf::D(e) => e.mark_end_of_style_modifier(), - OneOf::E(e) => e.mark_end_of_style_modifier(), - OneOf::F(e) => e.mark_end_of_style_modifier(), - OneOf::G(e) => e.mark_end_of_style_modifier(), - OneOf::H(e) => e.mark_end_of_style_modifier(), - OneOf::I(e) => e.mark_end_of_style_modifier(), - } - } - - fn get_style(&self, name: &str) -> Option<&CowStr> { - match self { - OneOf::A(e) => e.get_style(name), - OneOf::B(e) => e.get_style(name), - OneOf::C(e) => e.get_style(name), - OneOf::D(e) => e.get_style(name), - OneOf::E(e) => e.get_style(name), - OneOf::F(e) => e.get_style(name), - OneOf::G(e) => e.get_style(name), - OneOf::H(e) => e.get_style(name), - OneOf::I(e) => e.get_style(name), - } - } - - fn was_updated(&self, name: &str) -> bool { - match self { - OneOf::A(e) => e.was_updated(name), - OneOf::B(e) => e.was_updated(name), - OneOf::C(e) => e.was_updated(name), - OneOf::D(e) => e.was_updated(name), - OneOf::E(e) => e.was_updated(name), - OneOf::F(e) => e.was_updated(name), - OneOf::G(e) => e.was_updated(name), - OneOf::H(e) => e.was_updated(name), - OneOf::I(e) => e.was_updated(name), - } - } -} - impl DomNode for OneOf where N1: DomNode, @@ -503,3 +217,63 @@ where } } } + +impl With for OneOf +where + A: With, + B: With, + C: With, + D: With, + E: With, + F: With, + G: With, + H: With, + I: With, +{ + fn modifier(&mut self) -> &mut T { + match self { + OneOf::A(e) => >::modifier(e), + OneOf::B(e) => >::modifier(e), + OneOf::C(e) => >::modifier(e), + OneOf::D(e) => >::modifier(e), + OneOf::E(e) => >::modifier(e), + OneOf::F(e) => >::modifier(e), + OneOf::G(e) => >::modifier(e), + OneOf::H(e) => >::modifier(e), + OneOf::I(e) => >::modifier(e), + } + } +} + +#[allow(unnameable_types)] // reason: Implementation detail, public because of trait visibility rules +pub enum Noop {} + +impl AsRef for Noop { + fn as_ref(&self) -> &T { + match *self {} + } +} + +impl AsMut for Noop { + fn as_mut(&mut self) -> &mut T { + match *self {} + } +} + +impl With for Noop { + fn modifier(&mut self) -> &mut T { + match *self {} + } +} + +impl PhantomElementCtx for ViewCtx { + type PhantomElement = Pod; +} + +impl DomNode for Noop { + fn apply_props(&self, _props: &mut Self::Props) { + match *self {} + } + + type Props = Noop; +} diff --git a/xilem_web/src/style.rs b/xilem_web/src/style.rs deleted file mode 100644 index 27beaf9c1..000000000 --- a/xilem_web/src/style.rs +++ /dev/null @@ -1,696 +0,0 @@ -// Copyright 2024 the Xilem Authors -// SPDX-License-Identifier: Apache-2.0 - -use crate::{ - core::{MessageResult, Mut, View, ViewElement, ViewId, ViewMarker}, - vecmap::VecMap, - DomNode, DomView, DynMessage, ElementProps, Pod, PodMut, ViewCtx, -}; -use peniko::kurbo::Vec2; -use std::{ - collections::{BTreeMap, HashMap}, - fmt::Display, - marker::PhantomData, -}; -use wasm_bindgen::{JsCast, UnwrapThrowExt}; - -type CowStr = std::borrow::Cow<'static, str>; - -/// A trait to make the class adding functions generic over collection type -pub trait IntoStyles { - fn into_styles(self, styles: &mut Vec<(CowStr, Option)>); -} - -struct StyleTuple(T1, T2); - -// TODO should this also allow removing style values, via `None`? -/// Create a style from a style name and its value. -pub fn style(name: impl Into, value: impl Into) -> impl IntoStyles { - StyleTuple(name, Some(value.into())) -} - -impl IntoStyles for StyleTuple -where - T1: Into, - T2: Into>, -{ - fn into_styles(self, styles: &mut Vec<(CowStr, Option)>) { - let StyleTuple(key, value) = self; - styles.push((key.into(), value.into())); - } -} - -impl IntoStyles for Option -where - T: IntoStyles, -{ - fn into_styles(self, styles: &mut Vec<(CowStr, Option)>) { - if let Some(t) = self { - t.into_styles(styles); - } - } -} - -impl IntoStyles for Vec -where - T: IntoStyles, -{ - fn into_styles(self, styles: &mut Vec<(CowStr, Option)>) { - for itm in self { - itm.into_styles(styles); - } - } -} - -impl IntoStyles for [T; N] { - fn into_styles(self, styles: &mut Vec<(CowStr, Option)>) { - for itm in self { - itm.into_styles(styles); - } - } -} - -impl IntoStyles for HashMap -where - T1: Into, - T2: Into>, -{ - fn into_styles(self, styles: &mut Vec<(CowStr, Option)>) { - for (key, value) in self { - styles.push((key.into(), value.into())); - } - } -} - -impl IntoStyles for BTreeMap -where - T1: Into, - T2: Into>, -{ - fn into_styles(self, styles: &mut Vec<(CowStr, Option)>) { - for (key, value) in self { - styles.push((key.into(), value.into())); - } - } -} - -impl IntoStyles for VecMap -where - T1: Into, - T2: Into>, -{ - fn into_styles(self, styles: &mut Vec<(CowStr, Option)>) { - for (key, value) in self { - styles.push((key.into(), value.into())); - } - } -} - -/// This trait allows (modifying) the `style` property of `HTMLElement`/`SVGElement`s -/// -/// It's e.g. used in the DOM interface traits [`HtmlElement`](`crate::interfaces::HtmlElement`) and [`SvgElement`](`crate::interfaces::SvgElement`). -/// Modifications have to be done on the up-traversal of [`View::rebuild`], i.e. after [`View::rebuild`] was invoked for descendent views. -/// See [`Style::build`] and [`Style::rebuild`], how to use this for [`ViewElement`]s that implement this trait. -/// When these methods are used, they have to be used in every reconciliation pass (i.e. [`View::rebuild`]). -pub trait WithStyle { - /// Needs to be invoked within a [`View::rebuild`] before traversing to descendent views, and before any modifications (with [`set_style`](`WithStyle::set_style`)) are done in that view - fn rebuild_style_modifier(&mut self); - - /// Needs to be invoked after any modifications are done - fn mark_end_of_style_modifier(&mut self); - - /// Sets or removes (when value is `None`) a style property from the underlying element. - /// - /// When in [`View::rebuild`] this has to be invoked *after* traversing the inner `View` with [`View::rebuild`] - fn set_style(&mut self, name: &CowStr, value: &Option); - - /// Gets a previously set style from this modifier. - /// - /// When in [`View::rebuild`] this has to be invoked *after* traversing the inner `View` with [`View::rebuild`] - fn get_style(&self, name: &str) -> Option<&CowStr>; - - /// Returns `true` if a style property `name` was updated. - /// - /// This can be useful, for modifying a previously set value. - /// When in [`View::rebuild`] this has to be invoked *after* traversing the inner `View` with [`View::rebuild`] - fn was_updated(&self, name: &str) -> bool; -} - -#[derive(Debug, PartialEq)] -enum StyleModifier { - Remove(CowStr), - Set(CowStr, CowStr), - EndMarker(u16), -} - -const HYDRATING: u16 = 1 << 14; -const CREATING: u16 = 1 << 15; -const RESERVED_BIT_MASK: u16 = HYDRATING | CREATING; - -#[derive(Debug, Default)] -/// This contains all the current style properties of an [`HtmlElement`](`crate::interfaces::Element`) or [`SvgElement`](`crate::interfaces::SvgElement`). -pub struct Styles { - style_modifiers: Vec, - updated_styles: VecMap, - idx: u16, - /// the two most significant bits are reserved for whether this was just created (bit 15) and if it's currently being hydrated (bit 14) - start_idx: u16, -} - -impl Styles { - pub(crate) fn new(size_hint: usize, #[cfg(feature = "hydration")] in_hydration: bool) -> Self { - #[allow(unused_mut)] - let mut start_idx = CREATING; - #[cfg(feature = "hydration")] - if in_hydration { - start_idx |= HYDRATING; - } - - Self { - style_modifiers: Vec::with_capacity(size_hint), - start_idx, - ..Default::default() - } - } -} - -fn set_style(element: &web_sys::Element, name: &str, value: &str) { - if let Some(el) = element.dyn_ref::() { - el.style().set_property(name, value).unwrap_throw(); - } else if let Some(el) = element.dyn_ref::() { - el.style().set_property(name, value).unwrap_throw(); - } -} - -fn remove_style(element: &web_sys::Element, name: &str) { - if let Some(el) = element.dyn_ref::() { - el.style().remove_property(name).unwrap_throw(); - } else if let Some(el) = element.dyn_ref::() { - el.style().remove_property(name).unwrap_throw(); - } -} - -impl Styles { - pub fn apply_style_changes(&mut self, element: &web_sys::Element) { - if (self.start_idx & HYDRATING) == HYDRATING { - self.start_idx &= !RESERVED_BIT_MASK; - debug_assert!(self.updated_styles.is_empty()); - return; - } - - if (self.start_idx & CREATING) == CREATING { - for modifier in self.style_modifiers.iter().rev() { - match modifier { - StyleModifier::Remove(name) => { - remove_style(element, name); - } - StyleModifier::Set(name, value) => { - set_style(element, name, value); - } - StyleModifier::EndMarker(_) => (), - } - } - self.start_idx &= !RESERVED_BIT_MASK; - debug_assert!(self.updated_styles.is_empty()); - return; - } - - if !self.updated_styles.is_empty() { - for modifier in self.style_modifiers.iter().rev() { - match modifier { - StyleModifier::Remove(name) => { - if self.updated_styles.remove(name).is_some() { - remove_style(element, name); - } - } - StyleModifier::Set(name, value) => { - if self.updated_styles.remove(name).is_some() { - set_style(element, name, value); - } - } - StyleModifier::EndMarker(_) => (), - } - } - debug_assert!(self.updated_styles.is_empty()); - } - } -} - -impl WithStyle for Styles { - fn set_style(&mut self, name: &CowStr, value: &Option) { - if (self.start_idx & RESERVED_BIT_MASK) != 0 { - let modifier = if let Some(value) = value { - StyleModifier::Set(name.clone(), value.clone()) - } else { - StyleModifier::Remove(name.clone()) - }; - self.style_modifiers.push(modifier); - } else if let Some(modifier) = self.style_modifiers.get_mut(self.idx as usize) { - let dirty = match (&modifier, value) { - // early return if nothing has changed, avoids allocations - (StyleModifier::Set(old_name, old_value), Some(new_value)) if old_name == name => { - if old_value == new_value { - false - } else { - self.updated_styles.insert(name.clone(), ()); - true - } - } - (StyleModifier::Remove(removed), None) if removed == name => false, - (StyleModifier::Set(old_name, _), None) - | (StyleModifier::Remove(old_name), Some(_)) - if old_name == name => - { - self.updated_styles.insert(name.clone(), ()); - true - } - (StyleModifier::EndMarker(_), None) | (StyleModifier::EndMarker(_), Some(_)) => { - self.updated_styles.insert(name.clone(), ()); - true - } - (StyleModifier::Set(old_name, _), _) | (StyleModifier::Remove(old_name), _) => { - self.updated_styles.insert(name.clone(), ()); - self.updated_styles.insert(old_name.clone(), ()); - true - } - }; - if dirty { - *modifier = if let Some(value) = value { - StyleModifier::Set(name.clone(), value.clone()) - } else { - StyleModifier::Remove(name.clone()) - }; - } - // else remove it out of updated_styles? (because previous styles are overwritten) not sure if worth it because potentially worse perf - } else { - let new_modifier = if let Some(value) = value { - StyleModifier::Set(name.clone(), value.clone()) - } else { - StyleModifier::Remove(name.clone()) - }; - self.updated_styles.insert(name.clone(), ()); - self.style_modifiers.push(new_modifier); - } - self.idx += 1; - } - - fn rebuild_style_modifier(&mut self) { - if self.idx == 0 { - self.start_idx &= RESERVED_BIT_MASK; - } else { - let StyleModifier::EndMarker(start_idx) = self.style_modifiers[(self.idx - 1) as usize] - else { - unreachable!("this should not happen, as either `rebuild_style_modifier` happens first, or follows an `mark_end_of_style_modifier`") - }; - self.idx = start_idx; - self.start_idx = start_idx | (self.start_idx & RESERVED_BIT_MASK); - } - } - - fn mark_end_of_style_modifier(&mut self) { - let start_idx = self.start_idx & !RESERVED_BIT_MASK; - match self.style_modifiers.get_mut(self.idx as usize) { - Some(StyleModifier::EndMarker(prev_start_idx)) if *prev_start_idx == start_idx => {} // style modifier hasn't changed - Some(modifier) => *modifier = StyleModifier::EndMarker(start_idx), - None => self - .style_modifiers - .push(StyleModifier::EndMarker(start_idx)), - } - self.idx += 1; - self.start_idx = self.idx | (self.start_idx & RESERVED_BIT_MASK); - } - - fn get_style(&self, name: &str) -> Option<&CowStr> { - for modifier in self.style_modifiers[..self.idx as usize].iter().rev() { - match modifier { - StyleModifier::Remove(removed) if removed == name => return None, - StyleModifier::Set(key, value) if key == name => return Some(value), - _ => (), - } - } - None - } - - fn was_updated(&self, name: &str) -> bool { - self.updated_styles.contains_key(name) - } -} - -impl WithStyle for ElementProps { - fn rebuild_style_modifier(&mut self) { - self.styles().rebuild_style_modifier(); - } - - fn mark_end_of_style_modifier(&mut self) { - self.styles().mark_end_of_style_modifier(); - } - - fn set_style(&mut self, name: &CowStr, value: &Option) { - self.styles().set_style(name, value); - } - - fn get_style(&self, name: &str) -> Option<&CowStr> { - self.styles - .as_deref() - .and_then(|styles| styles.get_style(name)) - } - - fn was_updated(&self, name: &str) -> bool { - self.styles - .as_deref() - .map(|styles| styles.was_updated(name)) - .unwrap_or(false) - } -} - -impl WithStyle for Pod -where - N::Props: WithStyle, -{ - fn rebuild_style_modifier(&mut self) { - self.props.rebuild_style_modifier(); - } - - fn mark_end_of_style_modifier(&mut self) { - self.props.mark_end_of_style_modifier(); - } - - fn set_style(&mut self, name: &CowStr, value: &Option) { - self.props.set_style(name, value); - } - - fn get_style(&self, name: &str) -> Option<&CowStr> { - self.props.get_style(name) - } - - fn was_updated(&self, name: &str) -> bool { - self.props.was_updated(name) - } -} - -impl WithStyle for PodMut<'_, N> -where - N::Props: WithStyle, -{ - fn rebuild_style_modifier(&mut self) { - self.props.rebuild_style_modifier(); - } - - fn mark_end_of_style_modifier(&mut self) { - self.props.mark_end_of_style_modifier(); - } - - fn set_style(&mut self, name: &CowStr, value: &Option) { - self.props.set_style(name, value); - } - - fn get_style(&self, name: &str) -> Option<&CowStr> { - self.props.get_style(name) - } - - fn was_updated(&self, name: &str) -> bool { - self.props.was_updated(name) - } -} - -/// Syntax sugar for adding a type bound on the `ViewElement` of a view, such that both, [`ViewElement`] and [`ViewElement::Mut`] are bound to [`WithStyle`] -pub trait ElementWithStyle: for<'a> ViewElement: WithStyle> + WithStyle {} - -impl ElementWithStyle for T -where - T: ViewElement + WithStyle, - for<'a> T::Mut<'a>: WithStyle, -{ -} - -#[derive(Clone, Debug)] -/// A view to add `style` properties of `HTMLElement` and `SVGElement` derived elements, -pub struct Style { - el: E, - styles: Vec<(CowStr, Option)>, - phantom: PhantomData (T, A)>, -} - -impl Style { - pub fn new(el: E, styles: Vec<(CowStr, Option)>) -> Self { - Style { - el, - styles, - phantom: PhantomData, - } - } -} - -impl ViewMarker for Style {} -impl View for Style -where - T: 'static, - A: 'static, - E: DomView>, -{ - type Element = E::Element; - - type ViewState = E::ViewState; - - fn build(&self, ctx: &mut ViewCtx) -> (Self::Element, Self::ViewState) { - ctx.add_modifier_size_hint::(self.styles.len()); - let (mut element, state) = self.el.build(ctx); - for (key, value) in &self.styles { - element.set_style(key, value); - } - element.mark_end_of_style_modifier(); - (element, state) - } - - fn rebuild( - &self, - prev: &Self, - view_state: &mut Self::ViewState, - ctx: &mut ViewCtx, - mut element: Mut, - ) { - element.rebuild_style_modifier(); - self.el - .rebuild(&prev.el, view_state, ctx, element.reborrow_mut()); - for (key, value) in &self.styles { - element.set_style(key, value); - } - element.mark_end_of_style_modifier(); - } - - fn teardown( - &self, - view_state: &mut Self::ViewState, - ctx: &mut ViewCtx, - element: Mut, - ) { - self.el.teardown(view_state, ctx, element); - } - - fn message( - &self, - view_state: &mut Self::ViewState, - id_path: &[ViewId], - message: DynMessage, - app_state: &mut T, - ) -> MessageResult { - self.el.message(view_state, id_path, message, app_state) - } -} - -/// Add a `rotate(rad)` [transform-function](https://developer.mozilla.org/en-US/docs/Web/CSS/transform-function) to the current CSS `transform` -pub struct Rotate { - el: E, - phantom: PhantomData (State, Action)>, - radians: f64, -} - -impl Rotate { - pub(crate) fn new(element: E, radians: f64) -> Self { - Rotate { - el: element, - phantom: PhantomData, - radians, - } - } -} - -fn modify_rotate_transform(transform: Option<&CowStr>, radians: f64) -> Option { - if let Some(transform) = transform { - Some(CowStr::from(format!("{transform} rotate({radians}rad)"))) - } else { - Some(CowStr::from(format!("rotate({radians}rad)"))) - } -} - -impl ViewMarker for Rotate {} -impl View for Rotate -where - T: 'static, - A: 'static, - E: DomView>, -{ - type Element = E::Element; - - type ViewState = (E::ViewState, Option); - - fn build(&self, ctx: &mut ViewCtx) -> (Self::Element, Self::ViewState) { - ctx.add_modifier_size_hint::(1); - let (mut element, state) = self.el.build(ctx); - let css_repr = modify_rotate_transform(element.get_style("transform"), self.radians); - element.set_style(&"transform".into(), &css_repr); - element.mark_end_of_style_modifier(); - (element, (state, css_repr)) - } - - fn rebuild( - &self, - prev: &Self, - (view_state, css_repr): &mut Self::ViewState, - ctx: &mut ViewCtx, - mut element: Mut, - ) { - element.rebuild_style_modifier(); - self.el - .rebuild(&prev.el, view_state, ctx, element.reborrow_mut()); - if prev.radians != self.radians || element.was_updated("transform") { - *css_repr = modify_rotate_transform(element.get_style("transform"), self.radians); - } - element.set_style(&"transform".into(), css_repr); - element.mark_end_of_style_modifier(); - } - - fn teardown( - &self, - (view_state, _): &mut Self::ViewState, - ctx: &mut ViewCtx, - element: Mut, - ) { - self.el.teardown(view_state, ctx, element); - } - - fn message( - &self, - (view_state, _): &mut Self::ViewState, - id_path: &[ViewId], - message: DynMessage, - app_state: &mut T, - ) -> MessageResult { - self.el.message(view_state, id_path, message, app_state) - } -} - -#[derive(Clone, Copy, Debug, PartialEq)] -pub enum ScaleValue { - Uniform(f64), - NonUniform(f64, f64), -} - -impl Display for ScaleValue { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - ScaleValue::Uniform(uniform) => write!(f, "{uniform}"), - ScaleValue::NonUniform(x, y) => write!(f, "{x}, {y}"), - } - } -} - -impl From for ScaleValue { - fn from(value: f64) -> Self { - ScaleValue::Uniform(value) - } -} - -impl From<(f64, f64)> for ScaleValue { - fn from(value: (f64, f64)) -> Self { - ScaleValue::NonUniform(value.0, value.1) - } -} - -impl From for ScaleValue { - fn from(value: Vec2) -> Self { - ScaleValue::NonUniform(value.x, value.y) - } -} - -/// Add a `rotate(rad)` [transform-function](https://developer.mozilla.org/en-US/docs/Web/CSS/transform-function) to the current CSS `transform` -pub struct Scale { - el: E, - phantom: PhantomData (State, Action)>, - scale: ScaleValue, -} - -impl Scale { - pub(crate) fn new(element: E, scale: impl Into) -> Self { - Scale { - el: element, - phantom: PhantomData, - scale: scale.into(), - } - } -} - -fn modify_scale_transform(transform: Option<&CowStr>, scale: ScaleValue) -> Option { - if let Some(transform) = transform { - Some(CowStr::from(format!("{transform} scale({scale})"))) - } else { - Some(CowStr::from(format!("scale({scale})"))) - } -} - -impl ViewMarker for Scale {} -impl View for Scale -where - T: 'static, - A: 'static, - E: DomView>, -{ - type Element = E::Element; - - type ViewState = (E::ViewState, Option); - - fn build(&self, ctx: &mut ViewCtx) -> (Self::Element, Self::ViewState) { - ctx.add_modifier_size_hint::(1); - let (mut element, state) = self.el.build(ctx); - let css_repr = modify_scale_transform(element.get_style("transform"), self.scale); - element.set_style(&"transform".into(), &css_repr); - element.mark_end_of_style_modifier(); - (element, (state, css_repr)) - } - - fn rebuild( - &self, - prev: &Self, - (view_state, css_repr): &mut Self::ViewState, - ctx: &mut ViewCtx, - mut element: Mut, - ) { - element.rebuild_style_modifier(); - self.el - .rebuild(&prev.el, view_state, ctx, element.reborrow_mut()); - if prev.scale != self.scale || element.was_updated("transform") { - *css_repr = modify_scale_transform(element.get_style("transform"), self.scale); - } - element.set_style(&"transform".into(), css_repr); - element.mark_end_of_style_modifier(); - } - - fn teardown( - &self, - (view_state, _): &mut Self::ViewState, - ctx: &mut ViewCtx, - element: Mut, - ) { - self.el.teardown(view_state, ctx, element); - } - - fn message( - &self, - (view_state, _): &mut Self::ViewState, - id_path: &[ViewId], - message: DynMessage, - app_state: &mut T, - ) -> MessageResult { - self.el.message(view_state, id_path, message, app_state) - } -} diff --git a/xilem_web/src/svg/common_attrs.rs b/xilem_web/src/svg/common_attrs.rs index e175a27bc..6ec7ba8f8 100644 --- a/xilem_web/src/svg/common_attrs.rs +++ b/xilem_web/src/svg/common_attrs.rs @@ -2,11 +2,12 @@ // SPDX-License-Identifier: Apache-2.0 use crate::{ - attribute::WithAttributes, - core::{MessageResult, Mut, View, ViewId, ViewMarker}, - AttributeValue, DomNode, DomView, DynMessage, IntoAttributeValue, ViewCtx, + core::{MessageResult, Mut, View, ViewElement, ViewId, ViewMarker}, + modifiers::With, + modifiers::{AttributeModifier, Attributes}, + DomView, DynMessage, ViewCtx, }; -use peniko::Brush; +use peniko::{kurbo, Brush}; use std::fmt::Write as _; use std::marker::PhantomData; @@ -21,7 +22,7 @@ pub struct Stroke { child: V, // This could reasonably be static Cow also, but keep things simple brush: Brush, - style: peniko::kurbo::Stroke, + style: kurbo::Stroke, phantom: PhantomData (State, Action)>, } @@ -36,7 +37,7 @@ pub fn fill(child: V, brush: impl Into) -> Fill( child: V, brush: impl Into, - style: peniko::kurbo::Stroke, + style: kurbo::Stroke, ) -> Stroke { Stroke { child, @@ -77,12 +78,13 @@ fn brush_to_string(brush: &Brush) -> String { } } -fn add_opacity_to_element(brush: &Brush, element: &mut impl WithAttributes, attr: &'static str) { +fn opacity_attr_modifier(attr: &'static str, brush: &Brush) -> AttributeModifier { let opacity = match brush { Brush::Solid(color) if color.a != u8::MAX => Some(color.a as f64 / 255.0), _ => None, }; - element.set_attribute(&attr.into(), &opacity.into_attr_value()); + + (attr, opacity).into() } impl ViewMarker for Fill {} @@ -90,36 +92,42 @@ impl View for Fill>, + V: DomView>, + for<'a> ::Mut<'a>: With, { - type ViewState = (Option, V::ViewState); + type ViewState = V::ViewState; type Element = V::Element; fn build(&self, ctx: &mut ViewCtx) -> (Self::Element, Self::ViewState) { - let (mut element, child_state) = self.child.build(ctx); - let brush_svg_repr = brush_to_string(&self.brush).into_attr_value(); - element.set_attribute(&"fill".into(), &brush_svg_repr); - add_opacity_to_element(&self.brush, &mut element, "fill-opacity"); - element.mark_end_of_attribute_modifier(); - (element, (brush_svg_repr, child_state)) + let (mut element, state) = + ctx.with_size_hint::(2, |ctx| self.child.build(ctx)); + let attrs = element.modifier(); + attrs.push(("fill", brush_to_string(&self.brush))); + attrs.push(opacity_attr_modifier("fill-opacity", &self.brush)); + (element, state) } fn rebuild( &self, prev: &Self, - (brush_svg_repr, child_state): &mut Self::ViewState, + view_state: &mut Self::ViewState, ctx: &mut ViewCtx, - mut element: Mut, + element: Mut, ) { - element.rebuild_attribute_modifier(); - self.child - .rebuild(&prev.child, child_state, ctx, element.reborrow_mut()); - if self.brush != prev.brush { - *brush_svg_repr = brush_to_string(&self.brush).into_attr_value(); - } - element.set_attribute(&"fill".into(), brush_svg_repr); - add_opacity_to_element(&self.brush, &mut element, "fill-opacity"); - element.mark_end_of_attribute_modifier(); + Attributes::rebuild(element, 2, |mut element| { + self.child + .rebuild(&prev.child, view_state, ctx, element.reborrow_mut()); + let attrs = element.modifier(); + if attrs.was_created() { + attrs.push(("fill", brush_to_string(&self.brush))); + attrs.push(opacity_attr_modifier("fill-opacity", &self.brush)); + } else if self.brush != prev.brush { + attrs.mutate(|m| *m = ("fill", brush_to_string(&self.brush)).into()); + attrs.mutate(|m| *m = opacity_attr_modifier("fill-opacity", &self.brush)); + } else { + attrs.skip(2); + } + }); } fn teardown( @@ -128,12 +136,12 @@ where ctx: &mut ViewCtx, element: Mut, ) { - self.child.teardown(&mut view_state.1, ctx, element); + self.child.teardown(view_state, ctx, element); } fn message( &self, - (_, child_state): &mut Self::ViewState, + child_state: &mut Self::ViewState, id_path: &[ViewId], message: DynMessage, app_state: &mut State, @@ -142,11 +150,52 @@ where } } -#[allow(unnameable_types)] // reason: Implementation detail, public because of trait visibility rules -pub struct StrokeState { - brush_svg_repr: Option, - stroke_dash_pattern_svg_repr: Option, - child_state: ChildState, +fn push_stroke_modifiers(attrs: &mut Attributes, stroke: &kurbo::Stroke, brush: &Brush) { + let dash_pattern = + (!stroke.dash_pattern.is_empty()).then(|| join(&mut stroke.dash_pattern.iter(), " ")); + attrs.push(("stroke-dasharray", dash_pattern)); + + let dash_offset = (stroke.dash_offset != 0.0).then_some(stroke.dash_offset); + attrs.push(("stroke-dashoffset", dash_offset)); + attrs.push(("stroke-width", stroke.width)); + attrs.push(opacity_attr_modifier("stroke-opacity", brush)); +} + +// This function is not inlined to avoid unnecessary monomorphization, which may result in a bigger binary. +fn update_stroke_modifiers( + attrs: &mut Attributes, + prev_stroke: &kurbo::Stroke, + next_stroke: &kurbo::Stroke, + prev_brush: &Brush, + next_brush: &Brush, +) { + if attrs.was_created() { + push_stroke_modifiers(attrs, next_stroke, next_brush); + } else { + if next_stroke.dash_pattern != prev_stroke.dash_pattern { + let dash_pattern = (!next_stroke.dash_pattern.is_empty()) + .then(|| join(&mut next_stroke.dash_pattern.iter(), " ")); + attrs.mutate(|m| *m = ("stroke-dasharray", dash_pattern).into()); + } else { + attrs.skip(1); + } + if next_stroke.dash_offset != prev_stroke.dash_offset { + let dash_offset = (next_stroke.dash_offset != 0.0).then_some(next_stroke.dash_offset); + attrs.mutate(|m| *m = ("stroke-dashoffset", dash_offset).into()); + } else { + attrs.skip(1); + } + if next_stroke.width != prev_stroke.width { + attrs.mutate(|m| *m = ("stroke-width", next_stroke.width).into()); + } else { + attrs.skip(1); + } + if next_brush != prev_brush { + attrs.mutate(|m| *m = opacity_attr_modifier("stroke-opacity", next_brush)); + } else { + attrs.skip(1); + } + } } impl ViewMarker for Stroke {} @@ -154,67 +203,37 @@ impl View for Stroke>, + V: DomView>, + for<'a> ::Mut<'a>: With, { - type ViewState = StrokeState; + type ViewState = V::ViewState; type Element = V::Element; fn build(&self, ctx: &mut ViewCtx) -> (Self::Element, Self::ViewState) { - let (mut element, child_state) = self.child.build(ctx); - let brush_svg_repr = brush_to_string(&self.brush).into_attr_value(); - element.set_attribute(&"stroke".into(), &brush_svg_repr); - let stroke_dash_pattern_svg_repr = (!self.style.dash_pattern.is_empty()) - .then(|| join(&mut self.style.dash_pattern.iter(), " ").into_attr_value()) - .flatten(); - element.set_attribute(&"stroke-dasharray".into(), &stroke_dash_pattern_svg_repr); - let dash_offset = (self.style.dash_offset != 0.0).then_some(self.style.dash_offset); - element.set_attribute(&"stroke-dashoffset".into(), &dash_offset.into_attr_value()); - element.set_attribute(&"stroke-width".into(), &self.style.width.into_attr_value()); - add_opacity_to_element(&self.brush, &mut element, "stroke-opacity"); - - element.mark_end_of_attribute_modifier(); - ( - element, - StrokeState { - brush_svg_repr, - stroke_dash_pattern_svg_repr, - child_state, - }, - ) + let (mut element, state) = + ctx.with_size_hint::(4, |ctx| self.child.build(ctx)); + push_stroke_modifiers(element.modifier(), &self.style, &self.brush); + (element, state) } fn rebuild( &self, prev: &Self, - StrokeState { - brush_svg_repr, - stroke_dash_pattern_svg_repr, - child_state, - }: &mut Self::ViewState, + view_state: &mut Self::ViewState, ctx: &mut ViewCtx, - mut element: Mut, + element: Mut, ) { - element.rebuild_attribute_modifier(); - - self.child - .rebuild(&prev.child, child_state, ctx, element.reborrow_mut()); - - if self.brush != prev.brush { - *brush_svg_repr = brush_to_string(&self.brush).into_attr_value(); - } - element.set_attribute(&"stroke".into(), brush_svg_repr); - if self.style.dash_pattern != prev.style.dash_pattern { - *stroke_dash_pattern_svg_repr = (!self.style.dash_pattern.is_empty()) - .then(|| join(&mut self.style.dash_pattern.iter(), " ").into_attr_value()) - .flatten(); - } - element.set_attribute(&"stroke-dasharray".into(), stroke_dash_pattern_svg_repr); - let dash_offset = (self.style.dash_offset != 0.0).then_some(self.style.dash_offset); - element.set_attribute(&"stroke-dashoffset".into(), &dash_offset.into_attr_value()); - element.set_attribute(&"stroke-width".into(), &self.style.width.into_attr_value()); - add_opacity_to_element(&self.brush, &mut element, "stroke-opacity"); - - element.mark_end_of_attribute_modifier(); + Attributes::rebuild(element, 4, |mut element| { + self.child + .rebuild(&prev.child, view_state, ctx, element.reborrow_mut()); + update_stroke_modifiers( + element.modifier(), + &prev.style, + &self.style, + &prev.brush, + &self.brush, + ); + }); } fn teardown( @@ -223,8 +242,7 @@ where ctx: &mut ViewCtx, element: Mut, ) { - self.child - .teardown(&mut view_state.child_state, ctx, element); + self.child.teardown(view_state, ctx, element); } fn message( @@ -234,7 +252,6 @@ where message: DynMessage, app_state: &mut State, ) -> MessageResult { - self.child - .message(&mut view_state.child_state, id_path, message, app_state) + self.child.message(view_state, id_path, message, app_state) } } diff --git a/xilem_web/src/svg/kurbo_shape.rs b/xilem_web/src/svg/kurbo_shape.rs index d5200a71e..8a9795387 100644 --- a/xilem_web/src/svg/kurbo_shape.rs +++ b/xilem_web/src/svg/kurbo_shape.rs @@ -4,22 +4,26 @@ //! Implementation of the View trait for various kurbo shapes. use crate::{ - attribute::WithAttributes, core::{MessageResult, Mut, OrphanView, ViewId}, - AttributeValue, Attributes, DynMessage, IntoAttributeValue, Pod, ViewCtx, SVG_NS, + modifiers::{Attributes, With}, + DynMessage, FromWithContext, Pod, ViewCtx, SVG_NS, }; use peniko::kurbo::{BezPath, Circle, Line, Rect}; -fn create_element(name: &str, ctx: &mut ViewCtx, attr_size_hint: usize) -> Pod { - ctx.add_modifier_size_hint::(attr_size_hint); - #[cfg(feature = "hydration")] - if ctx.is_hydrating() { - Pod::hydrate_element_with_ctx(Vec::new(), ctx.hydrate_node().unwrap(), ctx) - } else { - Pod::new_element_with_ctx(Vec::new(), SVG_NS, name, ctx) - } - #[cfg(not(feature = "hydration"))] - Pod::new_element_with_ctx(Vec::new(), SVG_NS, name, ctx) +fn create_element( + name: &str, + ctx: &mut ViewCtx, + attr_size_hint: usize, + f: impl FnOnce(Pod, &mut ViewCtx) -> R, +) -> R { + ctx.with_size_hint::(attr_size_hint, |ctx| { + let element = if ctx.is_hydrating() { + Pod::hydrate_element_with_ctx(Vec::new(), ctx) + } else { + Pod::new_element_with_ctx(Vec::new(), SVG_NS, name, ctx) + }; + f(element, ctx) + }) } impl OrphanView for ViewCtx { @@ -30,28 +34,31 @@ impl OrphanView (Self::OrphanElement, Self::OrphanViewState) { - let mut element: Self::OrphanElement = create_element("line", ctx, 4).into(); - element.set_attribute(&"x1".into(), &view.p0.x.into_attr_value()); - element.set_attribute(&"y1".into(), &view.p0.y.into_attr_value()); - element.set_attribute(&"x2".into(), &view.p1.x.into_attr_value()); - element.set_attribute(&"y2".into(), &view.p1.y.into_attr_value()); - element.mark_end_of_attribute_modifier(); - (element, ()) + create_element("line", ctx, 4, |element, ctx| { + let mut element = Self::OrphanElement::from_with_ctx(element, ctx); + let attrs: &mut Attributes = element.modifier(); + attrs.push(("x1", view.p0.x)); + attrs.push(("y1", view.p0.y)); + attrs.push(("x2", view.p1.x)); + attrs.push(("y2", view.p1.y)); + (element, ()) + }) } fn orphan_rebuild( new: &Line, - _prev: &Line, + prev: &Line, (): &mut Self::OrphanViewState, _ctx: &mut ViewCtx, - mut element: Mut, + element: Mut, ) { - element.rebuild_attribute_modifier(); - element.set_attribute(&"x1".into(), &new.p0.x.into_attr_value()); - element.set_attribute(&"y1".into(), &new.p0.y.into_attr_value()); - element.set_attribute(&"x2".into(), &new.p1.x.into_attr_value()); - element.set_attribute(&"y2".into(), &new.p1.y.into_attr_value()); - element.mark_end_of_attribute_modifier(); + Attributes::rebuild(element, 4, |mut element| { + let attrs: &mut Attributes = element.modifier(); + attrs.update_with_same_key("x1", &prev.p0.x, &new.p0.x); + attrs.update_with_same_key("y1", &prev.p0.y, &new.p0.y); + attrs.update_with_same_key("x2", &prev.p1.x, &new.p1.x); + attrs.update_with_same_key("y2", &prev.p1.y, &new.p1.y); + }); } fn orphan_teardown( @@ -81,28 +88,31 @@ impl OrphanView (Self::OrphanElement, Self::OrphanViewState) { - let mut element: Self::OrphanElement = create_element("rect", ctx, 4).into(); - element.set_attribute(&"x".into(), &view.x0.into_attr_value()); - element.set_attribute(&"y".into(), &view.y0.into_attr_value()); - element.set_attribute(&"width".into(), &view.width().into_attr_value()); - element.set_attribute(&"height".into(), &view.height().into_attr_value()); - element.mark_end_of_attribute_modifier(); - (element, ()) + create_element("rect", ctx, 4, |element, ctx| { + let mut element = Self::OrphanElement::from_with_ctx(element, ctx); + let attrs: &mut Attributes = element.modifier(); + attrs.push(("x", view.x0)); + attrs.push(("y", view.y0)); + attrs.push(("width", view.width())); + attrs.push(("height", view.height())); + (element, ()) + }) } fn orphan_rebuild( new: &Rect, - _prev: &Rect, + prev: &Rect, (): &mut Self::OrphanViewState, _ctx: &mut ViewCtx, - mut element: Mut, + element: Mut, ) { - element.rebuild_attribute_modifier(); - element.set_attribute(&"x".into(), &new.x0.into_attr_value()); - element.set_attribute(&"y".into(), &new.y0.into_attr_value()); - element.set_attribute(&"width".into(), &new.width().into_attr_value()); - element.set_attribute(&"height".into(), &new.height().into_attr_value()); - element.mark_end_of_attribute_modifier(); + Attributes::rebuild(element, 4, |mut element| { + let attrs: &mut Attributes = element.modifier(); + attrs.update_with_same_key("x", &prev.x0, &new.x0); + attrs.update_with_same_key("y", &prev.y0, &new.y0); + attrs.update_with_same_key("width", &prev.width(), &new.width()); + attrs.update_with_same_key("height", &prev.height(), &new.height()); + }); } fn orphan_teardown( @@ -132,26 +142,29 @@ impl OrphanView (Self::OrphanElement, Self::OrphanViewState) { - let mut element: Self::OrphanElement = create_element("circle", ctx, 3).into(); - element.set_attribute(&"cx".into(), &view.center.x.into_attr_value()); - element.set_attribute(&"cy".into(), &view.center.y.into_attr_value()); - element.set_attribute(&"r".into(), &view.radius.into_attr_value()); - element.mark_end_of_attribute_modifier(); - (element, ()) + create_element("circle", ctx, 3, |element, ctx| { + let mut element = Self::OrphanElement::from_with_ctx(element, ctx); + let attrs: &mut Attributes = element.modifier(); + attrs.push(("cx", view.center.x)); + attrs.push(("cy", view.center.y)); + attrs.push(("r", view.radius)); + (element, ()) + }) } fn orphan_rebuild( new: &Circle, - _prev: &Circle, + prev: &Circle, (): &mut Self::OrphanViewState, _ctx: &mut ViewCtx, - mut element: Mut, + element: Mut, ) { - element.rebuild_attribute_modifier(); - element.set_attribute(&"cx".into(), &new.center.x.into_attr_value()); - element.set_attribute(&"cy".into(), &new.center.y.into_attr_value()); - element.set_attribute(&"r".into(), &new.radius.into_attr_value()); - element.mark_end_of_attribute_modifier(); + Attributes::rebuild(element, 3, |mut element| { + let attrs: &mut Attributes = element.modifier(); + attrs.update_with_same_key("cx", &prev.center.x, &new.center.x); + attrs.update_with_same_key("cy", &prev.center.y, &new.center.y); + attrs.update_with_same_key("height", &prev.radius, &new.radius); + }); } fn orphan_teardown( @@ -174,34 +187,37 @@ impl OrphanView OrphanView for ViewCtx { - type OrphanViewState = Option; + type OrphanViewState = (); type OrphanElement = Pod; fn orphan_build( view: &BezPath, ctx: &mut ViewCtx, ) -> (Self::OrphanElement, Self::OrphanViewState) { - let mut element: Self::OrphanElement = create_element("path", ctx, 1).into(); - let svg_repr = view.to_svg().into_attr_value(); - element.set_attribute(&"d".into(), &svg_repr); - element.mark_end_of_attribute_modifier(); - (element, svg_repr) + create_element("path", ctx, 1, |element, ctx| { + let mut element = Self::OrphanElement::from_with_ctx(element, ctx); + element.props.attributes().push(("path", view.to_svg())); + (element, ()) + }) } fn orphan_rebuild( new: &BezPath, prev: &BezPath, - svg_repr: &mut Self::OrphanViewState, + (): &mut Self::OrphanViewState, _ctx: &mut ViewCtx, - mut element: Mut, + element: Mut, ) { - // slight optimization to avoid serialization/allocation - if new != prev { - *svg_repr = new.to_svg().into_attr_value(); - } - element.rebuild_attribute_modifier(); - element.set_attribute(&"d".into(), svg_repr); - element.mark_end_of_attribute_modifier(); + Attributes::rebuild(element, 1, |mut element| { + let attrs: &mut Attributes = element.modifier(); + if attrs.was_created() { + attrs.push(("path", new.to_svg())); + } else if new != prev { + attrs.mutate(|m| *m = ("path", new.to_svg()).into()); + } else { + attrs.skip(1); + } + }); } fn orphan_teardown( diff --git a/xilem_web/src/templated.rs b/xilem_web/src/templated.rs index 52c52d900..8b052a29b 100644 --- a/xilem_web/src/templated.rs +++ b/xilem_web/src/templated.rs @@ -35,14 +35,8 @@ where let prev = view.clone(); let prev = prev.downcast_ref::().unwrap_throw(); let node = template_node.clone_node_with_deep(true).unwrap_throw(); - let is_already_hydrating = ctx.is_hydrating(); - ctx.enable_hydration(); - ctx.push_hydration_node(node); - let (mut el, mut state) = prev.build(ctx); + let (mut el, mut state) = ctx.with_hydration_node(node, |ctx| prev.build(ctx)); el.node.apply_props(&mut el.props); - if !is_already_hydrating { - ctx.disable_hydration(); - } let pod_mut = PodMut::new(&mut el.node, &mut el.props, None, false); self.0.rebuild(prev, &mut state, ctx, pod_mut); diff --git a/xilem_web/src/text.rs b/xilem_web/src/text.rs index aacf1ddbe..cf20f7bec 100644 --- a/xilem_web/src/text.rs +++ b/xilem_web/src/text.rs @@ -5,7 +5,6 @@ use crate::{ core::{MessageResult, Mut, OrphanView, ViewId}, DynMessage, Pod, ViewCtx, }; -#[cfg(feature = "hydration")] use wasm_bindgen::JsCast; // strings -> text nodes @@ -18,16 +17,13 @@ macro_rules! impl_string_view { fn orphan_build( view: &$ty, - #[cfg_attr(not(feature = "hydration"), allow(unused_variables))] ctx: &mut ViewCtx, + ctx: &mut ViewCtx, ) -> (Self::OrphanElement, Self::OrphanViewState) { - #[cfg(feature = "hydration")] let node = if ctx.is_hydrating() { ctx.hydrate_node().unwrap().unchecked_into() } else { web_sys::Text::new_with_data(view).unwrap() }; - #[cfg(not(feature = "hydration"))] - let node = web_sys::Text::new_with_data(view).unwrap(); (Pod { node, props: () }, ()) } @@ -77,16 +73,13 @@ macro_rules! impl_to_string_view { fn orphan_build( view: &$ty, - #[cfg_attr(not(feature = "hydration"), allow(unused_variables))] ctx: &mut ViewCtx, + ctx: &mut ViewCtx, ) -> (Self::OrphanElement, Self::OrphanViewState) { - #[cfg(feature = "hydration")] let node = if ctx.is_hydrating() { ctx.hydrate_node().unwrap().unchecked_into() } else { web_sys::Text::new_with_data(&view.to_string()).unwrap() }; - #[cfg(not(feature = "hydration"))] - let node = web_sys::Text::new_with_data(&view.to_string()).unwrap(); (Pod { node, props: () }, ()) } diff --git a/xilem_web/src/vecmap.rs b/xilem_web/src/vecmap.rs index dee2db573..658f3817f 100644 --- a/xilem_web/src/vecmap.rs +++ b/xilem_web/src/vecmap.rs @@ -1,7 +1,7 @@ // Copyright 2023 the Xilem Authors // SPDX-License-Identifier: Apache-2.0 -use std::{borrow::Borrow, fmt, ops::Index}; +use std::{borrow::Borrow, fmt, ops::Index, vec::Drain}; /// Basically an ordered `Map` (similar as `BTreeMap`) with a `Vec` as backend for very few elements /// As it uses linear search instead of a tree traversal, @@ -14,6 +14,13 @@ impl Default for VecMap { } } +impl Eq for VecMap {} +impl PartialEq for VecMap { + fn eq(&self, other: &Self) -> bool { + self.0 == other.0 + } +} + impl fmt::Debug for VecMap { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { f.debug_map().entries(self.iter()).finish() @@ -143,6 +150,33 @@ impl VecMap { self.0.iter().map(|(k, v)| (k, v)) } + /// Clears the map, returning all key-value pairs as an iterator. Keeps the + /// allocated memory for reuse. + /// + /// If the returned iterator is dropped before being fully consumed, it + /// drops the remaining key-value pairs. The returned iterator keeps a + /// mutable borrow on the map to optimize its implementation. + /// + /// # Examples + /// + /// ```ignore + /// use crate::vecmap::VecMap; + /// + /// let mut a = VecMap::default(); + /// a.insert(1, "a"); + /// a.insert(2, "b"); + /// + /// for (k, v) in a.drain().take(1) { + /// assert!(k == 1 || k == 2); + /// assert!(v == "a" || v == "b"); + /// } + /// + /// assert!(a.is_empty()); + /// ``` + pub fn drain(&mut self) -> Drain<'_, (K, V)> { + self.0.drain(..) + } + /// Inserts a key-value pair into the map. /// /// If the map did not have this key present, `None` is returned. @@ -248,6 +282,39 @@ impl VecMap { pub fn len(&self) -> usize { self.0.len() } + + /// Reserves capacity for at least `additional` more elements to be inserted + /// in the given `Vec`. The collection may reserve more space to + /// speculatively avoid frequent reallocations. After calling `reserve`, + /// capacity will be greater than or equal to `self.len() + additional`. + /// Does nothing if capacity is already sufficient. + /// + /// # Panics + /// + /// Panics if the new capacity exceeds `isize::MAX` _bytes_. + pub fn reserve(&mut self, additional: usize) { + self.0.reserve(additional); + } + + /// Reserves the minimum capacity for at least `additional` more elements to + /// be inserted in the given `VecMap`. Unlike [`reserve`], this will not + /// deliberately over-allocate to speculatively avoid frequent allocations. + /// After calling `reserve_exact`, capacity will be greater than or equal to + /// `self.len() + additional`. Does nothing if the capacity is already + /// sufficient. + /// + /// Note that the allocator may give the collection more space than it + /// requests. Therefore, capacity can not be relied upon to be precisely + /// minimal. Prefer [`reserve`] if future insertions are expected. + /// + /// [`reserve`]: VecMap::reserve + /// + /// # Panics + /// + /// Panics if the new capacity exceeds `isize::MAX` _bytes_. + pub fn reserve_exact(&mut self, additional: usize) { + self.0.reserve_exact(additional); + } } impl Index<&Q> for VecMap @@ -278,6 +345,19 @@ impl<'a, K, V> IntoIterator for &'a VecMap { } } +impl<'a, K, V> IntoIterator for &'a mut VecMap { + type Item = (&'a mut K, &'a mut V); + + type IntoIter = std::iter::Map< + std::slice::IterMut<'a, (K, V)>, + fn(&'a mut (K, V)) -> (&'a mut K, &'a mut V), + >; + + fn into_iter(self) -> Self::IntoIter { + self.0.iter_mut().map(|(k, v)| (k, v)) + } +} + impl IntoIterator for VecMap { type Item = (K, V); @@ -341,6 +421,20 @@ mod tests { assert_eq!((*first_key, *first_value), (1, "a")); } + #[test] + fn drain() { + let mut a = VecMap::default(); + a.insert(1, "a"); + a.insert(2, "b"); + + for (k, v) in a.drain().take(1) { + assert!(k == 1 || k == 2); + assert!(v == "a" || v == "b"); + } + + assert!(a.is_empty()); + } + #[test] fn insert() { let mut map = VecMap::default(); diff --git a/xilem_web/web_examples/mathml_svg/src/main.rs b/xilem_web/web_examples/mathml_svg/src/main.rs index 9432a9992..fcd9eba7d 100644 --- a/xilem_web/web_examples/mathml_svg/src/main.rs +++ b/xilem_web/web_examples/mathml_svg/src/main.rs @@ -4,7 +4,7 @@ use wasm_bindgen::{JsCast, UnwrapThrowExt}; use xilem_web::{ document_body, elements::html, elements::mathml as ml, elements::svg, interfaces::*, - style as s, App, + modifiers::style as s, App, }; struct Triangle { diff --git a/xilem_web/web_examples/svgtoy/src/main.rs b/xilem_web/web_examples/svgtoy/src/main.rs index 3ba516e54..460d56e02 100644 --- a/xilem_web/web_examples/svgtoy/src/main.rs +++ b/xilem_web/web_examples/svgtoy/src/main.rs @@ -5,7 +5,7 @@ use xilem_web::{ document_body, elements::svg::{g, svg, text}, interfaces::*, - style as s, + modifiers::style as s, svg::{ kurbo::{Circle, Line, Rect, Stroke}, peniko::Color, diff --git a/xilem_web/web_examples/todomvc/src/main.rs b/xilem_web/web_examples/todomvc/src/main.rs index fe18459d7..73e857bae 100644 --- a/xilem_web/web_examples/todomvc/src/main.rs +++ b/xilem_web/web_examples/todomvc/src/main.rs @@ -11,7 +11,8 @@ use xilem_web::{ elements::html as el, get_element_by_id, interfaces::*, - style as s, Action, App, DomView, + modifiers::style as s, + Action, App, DomView, }; // All of these actions arise from within a `Todo`, but we need access to the full state to reduce