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