diff --git a/xilem_web/src/element_props.rs b/xilem_web/src/element_props.rs deleted file mode 100644 index 2a69e0f73..000000000 --- a/xilem_web/src/element_props.rs +++ /dev/null @@ -1,134 +0,0 @@ -// Copyright 2024 the Xilem Authors -// SPDX-License-Identifier: Apache-2.0 - -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 [`DomNode::Props`](`crate::DomNode::Props`) -pub struct ElementProps { - pub(crate) in_hydration: bool, - pub(crate) attributes: Option>, - pub(crate) classes: Option>, - pub(crate) styles: Option>, - 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, - in_hydration: bool, - ) -> Self { - Self { - 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, - in_hydration, - } - } - - // All of this is slightly more complicated than it should be, - // 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_changes(element); - } - if let Some(classes) = &mut self.classes { - classes.apply_changes(element); - } - if let Some(styles) = &mut self.styles { - 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, 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, 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, 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.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)), - wasm_bindgen::intern(elem_name), - ) - .unwrap_throw(); - - for child in children.iter() { - let _ = element.append_child(child.node.as_ref()); - } - - Self { - node: element, - props: ElementProps::new( - children, - attr_size_hint, - style_size_hint, - class_size_hint, - false, - ), - } - } - - /// 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(); - - Self { - node: element.unchecked_into(), - props: ElementProps::new( - children, - attr_size_hint, - style_size_hint, - class_size_hint, - true, - ), - } - } -} diff --git a/xilem_web/src/elements.rs b/xilem_web/src/elements.rs index 8aab2808a..63abc6bf6 100644 --- a/xilem_web/src/elements.rs +++ b/xilem_web/src/elements.rs @@ -11,7 +11,7 @@ use wasm_bindgen::{JsCast, UnwrapThrowExt}; use crate::{ core::{AppendVec, ElementSplice, MessageResult, Mut, View, ViewId, ViewMarker}, document, - modifiers::{Children, With}, + modifiers::{Children, WithModifier}, vec_splice::VecSplice, AnyPod, DomFragment, DomNode, DynMessage, FromWithContext, Pod, ViewCtx, HTML_NS, }; @@ -262,11 +262,11 @@ 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, - With::::modifier(element.props), + WithModifier::::modifier(element.props).modifier, &mut state.vec_splice_scratch, element.node.as_ref(), ctx.fragment.clone(), @@ -290,11 +290,11 @@ 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, - With::::modifier(element.props), + WithModifier::::modifier(element.props).modifier, &mut state.vec_splice_scratch, element.node.as_ref(), ctx.fragment.clone(), diff --git a/xilem_web/src/interfaces.rs b/xilem_web/src/interfaces.rs index 09a4a1fe5..4c966d894 100644 --- a/xilem_web/src/interfaces.rs +++ b/xilem_web/src/interfaces.rs @@ -16,10 +16,8 @@ use std::borrow::Cow; use crate::{ events, - modifiers::{ - Attr, Attributes, Class, ClassIter, Classes, Rotate, Scale, ScaleValue, Style, StyleIter, - Styles, With, - }, + modifiers::{Attr, Class, ClassIter, Rotate, Scale, ScaleValue, Style, StyleIter}, + props::{WithElementProps, WithHtmlInputElementProps}, DomNode, DomView, IntoAttributeValue, OptionalAction, Pointer, PointerMsg, }; use wasm_bindgen::JsCast; @@ -51,13 +49,7 @@ macro_rules! event_handler_mixin { } pub trait Element: - Sized - + DomView< - State, - Action, - DomNode: DomNode + With + With> - + AsRef, - > + Sized + DomView + AsRef> { /// Set an attribute for an [`Element`] /// @@ -331,7 +323,7 @@ pub trait Element: impl Element for T where T: DomView, - ::Props: With + With + With, + ::Props: WithElementProps, T::DomNode: AsRef, { } @@ -758,10 +750,92 @@ where { } +use crate::modifiers::html_input_element; // #[cfg(feature = "HtmlInputElement")] pub trait HtmlInputElement: - HtmlElement> + HtmlElement< + State, + Action, + DomNode: DomNode + AsRef, +> { + /// See for more details. + /// + /// # Examples + /// + /// ``` + /// use xilem_web::{interfaces::{Element, HtmlInputElement}, elements::html::input}; + /// + /// # fn component() -> impl HtmlInputElement<()> { + /// input(()).attr("type", "checkbox").checked(true) // results in + /// # } + /// ``` + fn checked(self, checked: bool) -> html_input_element::view::Checked { + html_input_element::view::Checked::new(self, checked) + } + + /// See for more details. + /// + /// # Examples + /// + /// ``` + /// use xilem_web::{interfaces::{Element, HtmlInputElement}, elements::html::input}; + /// + /// # fn component() -> impl HtmlInputElement<()> { + /// input(()).attr("type", "radio").default_checked(true) // results in + /// # } + /// ``` + fn default_checked( + self, + default_checked: bool, + ) -> html_input_element::view::DefaultChecked { + html_input_element::view::DefaultChecked::new(self, default_checked) + } + + /// See for more details. + /// + /// # Examples + /// + /// ``` + /// use xilem_web::{interfaces::{Element, HtmlInputElement}, elements::html::input}; + /// + /// # fn component() -> impl HtmlInputElement<()> { + /// input(()).disabled(true) // results in + /// # } + /// ``` + fn disabled(self, disabled: bool) -> html_input_element::view::Disabled { + html_input_element::view::Disabled::new(self, disabled) + } + + /// See for more details. + /// + /// # Examples + /// + /// ``` + /// use xilem_web::{interfaces::{Element, HtmlInputElement}, elements::html::input}; + /// + /// # fn component() -> impl HtmlInputElement<()> { + /// input(()).required(true) // results in + /// # } + /// ``` + fn required(self, required: bool) -> html_input_element::view::Required { + html_input_element::view::Required::new(self, required) + } + + /// See for more details. + /// + /// # Examples + /// + /// ``` + /// use xilem_web::{interfaces::{Element, HtmlInputElement}, elements::html::input}; + /// + /// # fn component() -> impl HtmlInputElement<()> { + /// input(()).multiple(true) // results in + /// # } + /// ``` + fn multiple(self, multiple: bool) -> html_input_element::view::Multiple { + html_input_element::view::Multiple::new(self, multiple) + } } // #[cfg(feature = "HtmlInputElement")] @@ -769,6 +843,7 @@ impl HtmlInputElement for T where T: HtmlElement, T::DomNode: AsRef, + ::Props: WithHtmlInputElementProps, { } diff --git a/xilem_web/src/lib.rs b/xilem_web/src/lib.rs index 4d80d09f3..b2bbe14ae 100644 --- a/xilem_web/src/lib.rs +++ b/xilem_web/src/lib.rs @@ -32,7 +32,6 @@ mod app; mod attribute_value; mod context; mod dom_helpers; -mod element_props; mod message; mod one_of; mod optional_action; @@ -48,6 +47,7 @@ pub mod elements; pub mod events; pub mod interfaces; pub mod modifiers; +pub mod props; pub mod svg; pub use self::{ @@ -58,7 +58,6 @@ pub use self::{ attribute_value::{AttributeValue, IntoAttributeValue}, 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}, @@ -251,11 +250,11 @@ pub struct Pod { pub type AnyPod = Pod>; impl Pod { - pub fn into_any_pod(node: N, mut props: N::Props) -> AnyPod { - node.apply_props(&mut props); + pub fn into_any_pod(mut pod: Pod) -> AnyPod { + pod.node.apply_props(&mut pod.props); Pod { - node: Box::new(node), - props: Box::new(props), + node: Box::new(pod.node), + props: Box::new(pod.props), } } } @@ -281,7 +280,7 @@ impl ViewElement for Pod { impl SuperElement, ViewCtx> for AnyPod { fn upcast(_ctx: &mut ViewCtx, child: Pod) -> Self { - Pod::into_any_pod(child.node, child.props) + Pod::into_any_pod(child) } fn with_downcast_val( @@ -389,9 +388,9 @@ impl + DomNode> AsRef for PodMut<'_, N> { } impl DomNode for web_sys::Element { - type Props = ElementProps; + type Props = props::Element; - fn apply_props(&self, props: &mut ElementProps) { + fn apply_props(&self, props: &mut props::Element) { props.update_element(self); } } @@ -402,6 +401,14 @@ impl DomNode for web_sys::Text { fn apply_props(&self, (): &mut ()) {} } +impl DomNode for web_sys::HtmlInputElement { + type Props = props::HtmlInputElement; + + fn apply_props(&self, props: &mut props::HtmlInputElement) { + props.update_element(self); + } +} + pub trait FromWithContext: Sized { fn from_with_ctx(value: T, ctx: &mut ViewCtx) -> Self; } @@ -416,9 +423,9 @@ impl FromWithContext for T { macro_rules! impl_dom_node_for_elements { ($($ty:ident, )*) => {$( impl DomNode for web_sys::$ty { - type Props = ElementProps; + type Props = props::Element; - fn apply_props(&self, props: &mut ElementProps) { + fn apply_props(&self, props: &mut props::Element) { props.update_element(self); } } @@ -464,7 +471,7 @@ impl_dom_node_for_elements!( // HtmlHtmlElement, TODO include metadata? HtmlIFrameElement, HtmlImageElement, - HtmlInputElement, + // HtmlInputElement, has specialized impl HtmlLabelElement, HtmlLegendElement, HtmlLiElement, diff --git a/xilem_web/src/modifiers/attribute.rs b/xilem_web/src/modifiers/attribute.rs index c33f7069f..008c05a75 100644 --- a/xilem_web/src/modifiers/attribute.rs +++ b/xilem_web/src/modifiers/attribute.rs @@ -3,9 +3,9 @@ use crate::{ core::{MessageResult, Mut, View, ViewElement, ViewId, ViewMarker}, - modifiers::With, + modifiers::{Modifier, WithModifier}, vecmap::VecMap, - AttributeValue, DomView, DynMessage, ElementProps, IntoAttributeValue, ViewCtx, + AttributeValue, DomView, DynMessage, IntoAttributeValue, ViewCtx, }; use std::marker::PhantomData; use wasm_bindgen::{JsCast, UnwrapThrowExt}; @@ -52,184 +52,191 @@ pub struct Attributes { // 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() - } + idx: usize, } 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 { + pub(crate) fn new(size_hint: usize) -> 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 { + pub fn apply_changes(this: Modifier<'_, Self>, element: &web_sys::Element) { + let Modifier { modifier, flags } = this; + + if !flags.in_hydration() && flags.was_created() { + for modifier in &modifier.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() => { + } else if !modifier.updated.is_empty() { + for m in modifier.modifiers.iter().rev() { + match m { + AttributeModifier::Remove(name) if modifier.updated.remove(name).is_some() => { remove_attribute(element, name); } - AttributeModifier::Set(name, value) if self.updated.remove(name).is_some() => { + AttributeModifier::Set(name, value) + if modifier.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() { + for (name, ()) in modifier.updated.drain() { remove_attribute(element, &name); } } - debug_assert!(self.updated.is_empty()); + debug_assert!(modifier.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; + pub fn rebuild>(mut element: E, prev_len: usize, f: impl FnOnce(E)) { + element.modifier().modifier.idx -= prev_len; 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) { + /// Must only be used when `this.flags.was_created() == true`. + pub fn push(this: &mut Modifier<'_, Self>, modifier: impl Into) { debug_assert!( - self.was_created(), + this.flags.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; + this.flags.set_needs_update(); + this.modifier.modifiers.push(modifier.into()); + this.modifier.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) { + /// Must only be used when `this.flags.was_created() == false`. + pub fn insert(this: &mut Modifier<'_, Self>, modifier: impl Into) { debug_assert!( - !self.was_created(), + !this.flags.was_created(), "This should never be called, when the underlying element was (re)created, use `Attributes::push` instead." ); + this.flags.set_needs_update(); let modifier = modifier.into(); - self.updated.insert(modifier.name().clone(), ()); + this.modifier.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; + this.modifier.modifiers.insert(this.modifier.idx, modifier); + this.modifier.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 { + /// Must only be used when `this.flags.was_created() == false`. + pub fn mutate( + this: &mut Modifier<'_, Self>, + f: impl FnOnce(&mut AttributeModifier) -> R, + ) -> R { debug_assert!( - !self.was_created(), + !this.flags.was_created(), "This should never be called, when the underlying element was (re)created." ); - let modifier = &mut self.modifiers[self.idx as usize]; + this.flags.set_needs_update(); + let modifier = &mut this.modifier.modifiers[this.modifier.idx]; let old = modifier.name().clone(); let rv = f(modifier); let new = modifier.name(); if *new != old { - self.updated.insert(new.clone(), ()); + this.modifier.updated.insert(new.clone(), ()); } - self.updated.insert(old, ()); - self.idx += 1; + this.modifier.updated.insert(old, ()); + this.modifier.idx += 1; rv } /// Skips the next `count` modifiers. /// - /// Must only be used when `self.was_created() == false`. - pub fn skip(&mut self, count: usize) { + /// Must only be used when `this.flags.was_created() == false`. + pub fn skip(this: &mut Modifier<'_, Self>, count: usize) { debug_assert!( - !self.was_created(), + !this.flags.was_created(), "This should never be called, when the underlying element was (re)created." ); - self.idx += count as u16; + this.modifier.idx += count; } /// Deletes the next `count` modifiers. /// - /// Must only be used when `self.was_created() == false`. - pub fn delete(&mut self, count: usize) { + /// Must only be used when `this.flags.was_created() == false`. + pub fn delete(this: &mut Modifier<'_, Self>, count: usize) { debug_assert!( - !self.was_created(), + !this.flags.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(), ()); + let start = this.modifier.idx; + this.flags.set_needs_update(); + for modifier in this.modifier.modifiers.drain(start..(start + count)) { + this.modifier.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()); + pub fn update( + this: &mut Modifier<'_, Self>, + prev: &AttributeModifier, + next: &AttributeModifier, + ) { + if this.flags.was_created() { + Attributes::push(this, next.clone()); } else if next != prev { - self.mutate(|modifier| *modifier = next.clone()); + Attributes::mutate(this, |modifier| *modifier = next.clone()); } else { - self.skip(1); + Attributes::skip(this, 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, + this: &mut Modifier<'_, Self>, key: impl Into, prev: &Value, next: &Value, ) { - if self.was_created() { - self.push((key, next.clone())); + if this.flags.was_created() { + Attributes::push(this, (key, next.clone())); } else if next != prev { - self.mutate(|modifier| *modifier = (key, next.clone()).into()); + Attributes::mutate(this, |modifier| *modifier = (key, next.clone()).into()); } else { - self.skip(1); + Attributes::skip(this, 1); } } } -fn set_attribute(element: &web_sys::Element, name: &str, value: &str) { +fn html_input_attr_assertions(element: &web_sys::Element, name: &str) { + debug_assert!( + !(element.is_instance_of::() && name == "checked"), + "Using `checked` as attribute on a checkbox is not supported, \ + use the `el.checked()` or `el.default_checked()` modifier instead." + ); + debug_assert!( + !(element.is_instance_of::() && name == "disabled"), + "Using `disabled` as attribute on an input element is not supported, \ + use the `el.checked()` modifier instead." + ); +} + +fn element_attr_assertions(element: &web_sys::Element, name: &str) { debug_assert_ne!( name, "class", "Using `class` as attribute is not supported, use the `el.class()` modifier instead" @@ -238,6 +245,11 @@ fn set_attribute(element: &web_sys::Element, name: &str, value: &str) { name, "style", "Using `style` as attribute is not supported, use the `el.style()` modifier instead" ); + html_input_attr_assertions(element, name); +} + +fn set_attribute(element: &web_sys::Element, name: &str, value: &str) { + element_attr_assertions(element, name); // we have to special-case `value` because setting the value using `set_attribute` // doesn't work after the value has been changed. @@ -249,37 +261,14 @@ fn set_attribute(element: &web_sys::Element, name: &str, value: &str) { } 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(); - } + element_attr_assertions(element, name); + element.remove_attribute(name).unwrap_throw(); } /// A view to add an attribute to [`Element`](`crate::interfaces::Element`) derived components. @@ -313,8 +302,8 @@ impl View for Attr>, - for<'a> ::Mut<'a>: With, + V: DomView>, + for<'a> ::Mut<'a>: WithModifier, { type Element = V::Element; @@ -323,7 +312,7 @@ where 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()); + Attributes::push(&mut element.modifier(), self.modifier.clone()); (element, state) } @@ -337,7 +326,7 @@ where Attributes::rebuild(element, 1, |mut element| { self.inner .rebuild(&prev.inner, view_state, ctx, element.reborrow_mut()); - element.modifier().update(&prev.modifier, &self.modifier); + Attributes::update(&mut element.modifier(), &prev.modifier, &self.modifier); }); } diff --git a/xilem_web/src/modifiers/class.rs b/xilem_web/src/modifiers/class.rs index 1a8d78644..5f6b26d68 100644 --- a/xilem_web/src/modifiers/class.rs +++ b/xilem_web/src/modifiers/class.rs @@ -4,9 +4,9 @@ use crate::{ core::{MessageResult, Mut, View, ViewElement, ViewId, ViewMarker}, diff::{diff_iters, Diff}, - modifiers::With, + modifiers::{Modifier, WithModifier}, vecmap::VecMap, - DomView, DynMessage, ElementProps, ViewCtx, + DomView, DynMessage, ViewCtx, }; use std::{fmt::Debug, marker::PhantomData}; use wasm_bindgen::{JsCast, UnwrapThrowExt}; @@ -82,9 +82,6 @@ impl ClassIter for [C; N] { } } -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 { @@ -94,65 +91,55 @@ pub struct Classes { 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; - } + pub(crate) fn new(size_hint: usize) -> Self { 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(), ()), + pub fn apply_changes(this: Modifier<'_, Self>, element: &web_sys::Element) { + let Modifier { modifier, flags } = this; + + if flags.in_hydration() { + modifier.dirty = false; + } else if modifier.dirty { + modifier.dirty = false; + modifier.classes.clear(); + modifier.classes.reserve(modifier.modifiers.len()); + for m in &modifier.modifiers { + match m { + ClassModifier::Remove(class_name) => modifier.classes.remove(class_name), + ClassModifier::Add(class_name) => { + modifier.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; + modifier.class_name.clear(); + modifier + .class_name + .reserve_exact(modifier.classes.keys().map(|k| k.len() + 1).sum()); + let last_idx = modifier.classes.len().saturating_sub(1); + for (idx, class) in modifier.classes.keys().enumerate() { + modifier.class_name += class; if idx != last_idx { - self.class_name += " "; + modifier.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) + .set_attribute(wasm_bindgen::intern("class"), &modifier.class_name) .unwrap_throw(); } else { - element.set_class_name(&self.class_name); + element.set_class_name(&modifier.class_name); } } } @@ -160,131 +147,139 @@ impl Classes { #[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; + pub fn rebuild>(mut element: E, prev_len: usize, f: impl FnOnce(E)) { + element.modifier().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) { + /// Must only be used when `this.flags.was_created() == true` + pub fn push(this: &mut Modifier<'_, Self>, modifier: ClassModifier) { debug_assert!( - self.was_created(), + this.flags.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; + this.modifier.dirty = true; + this.flags.set_needs_update(); + this.modifier.modifiers.push(modifier); + this.modifier.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) { + /// Must only be used when `this.flags.was_created() == false` + pub fn insert(this: &mut Modifier<'_, Self>, modifier: ClassModifier) { debug_assert!( - !self.was_created(), + !this.flags.was_created(), "This should never be called, when the underlying element was (re)created, use `Classes::push` instead." ); - self.dirty = true; + this.modifier.dirty = true; + this.flags.set_needs_update(); // 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; + this.modifier + .modifiers + .insert(this.modifier.idx as usize, modifier); + this.modifier.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 { + /// Must only be used when `this.flags.was_created() == false` + pub fn mutate(this: &mut Modifier<'_, Self>, f: impl FnOnce(&mut ClassModifier) -> R) -> R { debug_assert!( - !self.was_created(), + !this.flags.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]) + this.modifier.dirty = true; + this.flags.set_needs_update(); + let idx = this.modifier.idx; + this.modifier.idx += 1; + f(&mut this.modifier.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) { + /// Must only be used when `this.flags.was_created() == false` + pub fn skip(this: &mut Modifier<'_, Self>, count: usize) { debug_assert!( - !self.was_created(), + !this.flags.was_created(), "This should never be called, when the underlying element was (re)created" ); - self.idx += count as u16; + this.modifier.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) { + /// Must only be used when `this.flags.was_created() == false` + pub fn delete(this: &mut Modifier<'_, Self>, count: usize) { debug_assert!( - !self.was_created(), + !this.flags.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)); + let start = this.modifier.idx as usize; + this.modifier.dirty = true; + this.flags.set_needs_update(); + this.modifier.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 { + /// Must only be used when `this.flags.was_created() == true` + pub fn extend( + this: &mut Modifier<'_, Self>, + modifiers: impl Iterator, + ) -> usize { debug_assert!( - self.was_created(), + this.flags.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; + this.modifier.dirty = true; + this.flags.set_needs_update(); + let prev_len = this.modifier.modifiers.len(); + this.modifier.modifiers.extend(modifiers); + let new_len = this.modifier.modifiers.len() - prev_len; + this.modifier.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 { + /// Must only be used when `this.flags.was_created() == false` + pub fn apply_diff>( + this: &mut Modifier<'_, Self>, + prev: T, + next: T, + ) -> usize { debug_assert!( - !self.was_created(), + !this.flags.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); + Classes::insert(this, modifier); new_len += 1; } - Diff::Remove(count) => self.delete(count), + Diff::Remove(count) => Classes::delete(this, count), Diff::Change(new_modifier) => { - self.mutate(|modifier| *modifier = new_modifier); + Classes::mutate(this, |modifier| *modifier = new_modifier); new_len += 1; } Diff::Skip(count) => { - self.skip(count); + Classes::skip(this, count); new_len += count; } } @@ -297,17 +292,17 @@ impl Classes { /// 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, + this: &mut Modifier<'_, Self>, prev_len: usize, prev: &T, next: &T, ) -> usize { - if self.was_created() { - self.extend(next.add_class_iter()) + if this.flags.was_created() { + Classes::extend(this, next.add_class_iter()) } else if next != prev { - self.apply_diff(prev.add_class_iter(), next.add_class_iter()) + Classes::apply_diff(this, prev.add_class_iter(), next.add_class_iter()) } else { - self.skip(prev_len); + Classes::skip(this, prev_len); prev_len } } @@ -342,8 +337,8 @@ where State: 'static, Action: 'static, C: ClassIter, - V: DomView>, - for<'a> ::Mut<'a>: With, + V: DomView>, + for<'a> ::Mut<'a>: WithModifier, { type Element = V::Element; @@ -353,7 +348,7 @@ where 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); + let len = Classes::extend(&mut e.modifier(), add_class_iter); (e, (len, s)) } @@ -367,8 +362,12 @@ where 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); + *len = Classes::update_as_add_class_iter( + &mut elem.modifier(), + *len, + &prev.classes, + &self.classes, + ); }); } diff --git a/xilem_web/src/modifiers/elements.rs b/xilem_web/src/modifiers/elements.rs new file mode 100644 index 000000000..448067189 --- /dev/null +++ b/xilem_web/src/modifiers/elements.rs @@ -0,0 +1,20 @@ +// Copyright 2024 the Xilem Authors +// SPDX-License-Identifier: Apache-2.0 + +pub mod html_input_element { + use crate::overwrite_bool_modifier; + overwrite_bool_modifier!(Checked); + overwrite_bool_modifier!(DefaultChecked); + overwrite_bool_modifier!(Disabled); + overwrite_bool_modifier!(Required); + overwrite_bool_modifier!(Multiple); + + pub mod view { + use crate::overwrite_bool_modifier_view; + overwrite_bool_modifier_view!(Checked); + overwrite_bool_modifier_view!(DefaultChecked); + overwrite_bool_modifier_view!(Disabled); + overwrite_bool_modifier_view!(Required); + overwrite_bool_modifier_view!(Multiple); + } +} diff --git a/xilem_web/src/modifiers/mod.rs b/xilem_web/src/modifiers/mod.rs index 099ac53ba..cfc2937b5 100644 --- a/xilem_web/src/modifiers/mod.rs +++ b/xilem_web/src/modifiers/mod.rs @@ -63,24 +63,40 @@ pub use class::*; mod style; pub use style::*; -use crate::{AnyPod, DomNode, Pod, PodMut}; +mod overwrite; +pub use overwrite::*; -/// 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; +mod elements; +pub use elements::*; + +use crate::{props::ElementFlags, AnyPod, DomNode, Pod, PodMut}; + +/// This struct is a container, with the current Element state (e.g. whether it was created/hydrated or generally needs an update), and the modifier itself. +pub struct Modifier<'a, M> { + pub modifier: &'a mut M, + pub flags: &'a mut ElementFlags, +} + +impl<'a, M> Modifier<'a, M> { + pub fn new(modifier: &'a mut M, flags: &'a mut ElementFlags) -> Self { + Self { modifier, flags } + } +} + +/// This trait is intended to give access to modifiers of a [`ViewElement`](crate::core::ViewElement). +pub trait WithModifier { + fn modifier(&mut self) -> Modifier<'_, M>; } -impl>> With for Pod { - fn modifier(&mut self) -> &mut T { - >::modifier(&mut self.props) +impl>> WithModifier for Pod { + fn modifier(&mut self) -> Modifier<'_, T> { + >::modifier(&mut self.props) } } -impl>> With for PodMut<'_, N> { - fn modifier(&mut self) -> &mut T { - >::modifier(self.props) +impl>> WithModifier for PodMut<'_, N> { + fn modifier(&mut self) -> Modifier<'_, T> { + >::modifier(self.props) } } diff --git a/xilem_web/src/modifiers/overwrite.rs b/xilem_web/src/modifiers/overwrite.rs new file mode 100644 index 000000000..a8253cda4 --- /dev/null +++ b/xilem_web/src/modifiers/overwrite.rs @@ -0,0 +1,451 @@ +// Copyright 2024 the Xilem Authors +// SPDX-License-Identifier: Apache-2.0 + +use super::Modifier; + +#[derive(Default)] +/// A specialized optimized boolean overwrite modifier, with support of up to 32 boolean elements, to avoid allocations. +/// +/// It is intended for just overwriting a possibly previous set value. +/// For example `input_el.checked(true).checked(false)` will set the `checked` attribute to `false`. +/// This will usually be used for boolean attributes such as `input.checked`, or `button.disabled` +/// If used for more than 32 it will panic. +pub struct OverwriteBool { + /// The underlying boolean flags encoded as bitflags. + modifiers: u32, + /// the current index of the modifiers. After reconciliation it should equal `len`. In rebuild it is decremented with [`OverwriteBool::rebuild`] + pub(crate) idx: u8, + /// The amount of boolean flags used in this modifier. + len: u8, + /// A dirty flag, to indicate whether `apply_changes` should update (i.e. call `f`) the underlying value. + needs_update: bool, +} + +macro_rules! assert_overflow { + ($index: expr) => { + debug_assert!( + $index < 32, + "If you ever see this,\ + please open an issue at https://github.com/linebender/xilem/, \ + we would appreciate to know what caused this. There are known solutions.\ + This is currently limited to 32 booleans to be more efficient." + ); + }; +} + +impl OverwriteBool { + #[inline] + pub fn rebuild(&mut self, prev_len: u8) { + self.idx -= prev_len; + } + + /// Returns if `modifier` has changed. + fn set(&mut self, modifier: bool) -> bool { + let before = self.modifiers & (1 << self.idx); + if modifier { + self.modifiers |= 1 << self.idx; + } else { + self.modifiers &= !(1 << self.idx); + } + (self.modifiers & (1 << self.idx)) != before + } + + /// Returns the current boolean modifier (at `self.idx`) + fn get(&self) -> bool { + let bit = 1 << self.idx; + self.modifiers & bit == bit + } + + #[inline] + /// Pushes `modifier` at the end of the current modifiers. + /// + /// Must only be used when `this.flags.was_created() == true`. + pub fn push(this: &mut Modifier<'_, Self>, modifier: bool) { + debug_assert!( + this.flags.was_created(), + "This should never be called, when the underlying element wasn't (re)created." + ); + this.modifier.set(modifier); + this.modifier.needs_update = true; + this.flags.set_needs_update(); + this.modifier.idx += 1; + this.modifier.len += 1; + assert_overflow!(this.modifier.len); + } + + #[inline] + /// Mutates the next modifier. + /// + /// Must only be used when `this.flags.was_created() == false`. + pub fn mutate(this: &mut Modifier<'_, Self>, f: impl FnOnce(&mut bool) -> R) -> R { + debug_assert!( + !this.flags.was_created(), + "This should never be called, when the underlying element was (re)created." + ); + let mut modifier = this.modifier.get(); + let retval = f(&mut modifier); + let dirty = this.modifier.set(modifier); + this.modifier.idx += 1; + this.modifier.needs_update |= this.modifier.len == this.modifier.idx && dirty; + if this.modifier.needs_update { + this.flags.set_needs_update(); + } + retval + } + + #[inline] + /// Skips the next `count` modifiers. + /// + /// Must only be used when `this.flags.was_created() == false`. + pub fn skip(this: &mut Modifier<'_, Self>, count: u8) { + debug_assert!( + !this.flags.was_created(), + "This should never be called, when the underlying element was (re)created." + ); + this.modifier.idx += count; + } + + #[inline] + /// Updates the next modifier, based on the diff of `prev` and `next`. + /// + /// It can also be used when the underlying element was recreated. + pub fn update(this: &mut Modifier<'_, Self>, prev: bool, next: bool) { + if this.flags.was_created() { + Self::push(this, next); + } else if next != prev { + Self::mutate(this, |modifier| *modifier = next); + } else { + Self::skip(this, 1); + } + } + + #[inline] + /// Applies potential changes with `f`. + /// + /// Argument of `f` is the new value, `Some(_)` if set, and `None` if all values were deleted. + pub fn apply_changes(&mut self, f: impl FnOnce(Option)) { + let needs_update = self.needs_update; + self.needs_update = false; + if needs_update { + let bit = 1 << (self.idx - 1); + let modifier = (self.len > 0).then_some(self.modifiers & bit == bit); + f(modifier); + } + } + // TODO implement delete etc. +} + +#[macro_export] +/// A macro to create a boolean overwrite modifier. +macro_rules! overwrite_bool_modifier { + ($modifier: ident) => { + #[derive(Default)] + pub struct $modifier($crate::modifiers::OverwriteBool); + + impl $modifier { + fn as_overwrite_bool_modifier( + this: $crate::modifiers::Modifier<'_, Self>, + ) -> $crate::modifiers::Modifier<'_, $crate::modifiers::OverwriteBool> { + $crate::modifiers::Modifier::new(&mut this.modifier.0, this.flags) + } + + pub fn apply_changes(&mut self, f: impl FnOnce(Option)) { + self.0.apply_changes(f); + } + } + }; +} + +#[macro_export] +/// A macro to create a boolean overwrite modifier view for a modifier that's in the parent module with the same name. +macro_rules! overwrite_bool_modifier_view { + ($modifier: ident) => { + pub struct $modifier { + value: bool, + inner: V, + phantom: std::marker::PhantomData (State, Action)>, + } + + impl $modifier { + pub fn new(inner: V, value: bool) -> Self { + $modifier { + inner, + value, + phantom: std::marker::PhantomData, + } + } + } + + impl $crate::core::ViewMarker for $modifier {} + impl + $crate::core::View + for $modifier + where + State: 'static, + Action: 'static, + V: $crate::DomView< + State, + Action, + Element: $crate::modifiers::WithModifier, + >, + for<'a> ::Mut<'a>: + $crate::modifiers::WithModifier, + { + type Element = V::Element; + + type ViewState = V::ViewState; + + fn build(&self, ctx: &mut $crate::ViewCtx) -> (Self::Element, Self::ViewState) { + use $crate::modifiers::WithModifier; + let (mut el, state) = self.inner.build(ctx); + let modifier = &mut super::$modifier::as_overwrite_bool_modifier(el.modifier()); + $crate::modifiers::OverwriteBool::push(modifier, self.value); + (el, state) + } + + fn rebuild( + &self, + prev: &Self, + view_state: &mut Self::ViewState, + ctx: &mut $crate::ViewCtx, + mut element: $crate::core::Mut, + ) { + use $crate::modifiers::WithModifier; + element.modifier().modifier.0.rebuild(1); + self.inner + .rebuild(&prev.inner, view_state, ctx, element.reborrow_mut()); + let mut modifier = super::$modifier::as_overwrite_bool_modifier(element.modifier()); + $crate::modifiers::OverwriteBool::update(&mut modifier, prev.value, self.value); + } + + fn teardown( + &self, + view_state: &mut Self::ViewState, + ctx: &mut $crate::ViewCtx, + element: $crate::core::Mut, + ) { + self.inner.teardown(view_state, ctx, element); + } + + fn message( + &self, + view_state: &mut Self::ViewState, + id_path: &[$crate::core::ViewId], + message: $crate::DynMessage, + app_state: &mut State, + ) -> $crate::core::MessageResult { + self.inner.message(view_state, id_path, message, app_state) + } + } + }; +} + +#[cfg(test)] +mod tests { + use crate::props::ElementFlags; + + use super::*; + + #[test] + fn overwrite_bool_push() { + let mut modifier = OverwriteBool::default(); + let flags = &mut ElementFlags::new(false); + let m = &mut Modifier::new(&mut modifier, flags); + assert!(!m.flags.needs_update()); + OverwriteBool::push(m, true); + assert!(m.flags.needs_update()); + assert_eq!(m.modifier.len, 1); + OverwriteBool::push(m, false); + assert!(m.flags.needs_update()); + assert_eq!(m.modifier.len, 2); + assert_eq!(m.modifier.idx, 2); + let mut was_applied = false; + m.modifier.apply_changes(|value| { + was_applied = true; + assert_eq!(value, Some(false)); + }); + assert!(was_applied); + assert!(!m.modifier.needs_update); + } + + #[test] + fn overwrite_bool_mutate() { + let mut modifier = OverwriteBool::default(); + let flags = &mut ElementFlags::new(false); + let m = &mut Modifier::new(&mut modifier, flags); + OverwriteBool::push(m, true); + OverwriteBool::push(m, false); + OverwriteBool::push(m, true); + OverwriteBool::push(m, false); + assert!(m.modifier.needs_update); + let mut was_applied = false; + m.modifier.apply_changes(|value| { + was_applied = true; + assert_eq!(value, Some(false)); + }); + assert!(was_applied); + m.flags.clear(); + assert!(!m.flags.needs_update()); + assert_eq!(m.modifier.len, 4); + assert_eq!(m.modifier.idx, 4); + m.modifier.rebuild(4); + assert_eq!(m.modifier.idx, 0); + assert_eq!(m.modifier.len, 4); + OverwriteBool::mutate(m, |first| *first = true); + assert!(!m.modifier.needs_update); + OverwriteBool::mutate(m, |second| *second = false); + assert!(!m.modifier.needs_update); + OverwriteBool::mutate(m, |third| *third = false); + assert!(!m.modifier.needs_update); + OverwriteBool::mutate(m, |fourth| *fourth = true); + assert!(m.modifier.needs_update); + let mut was_applied = false; + m.modifier.apply_changes(|value| { + was_applied = true; + assert_eq!(value, Some(true)); + }); + assert!(was_applied); + m.flags.clear(); + assert!(!m.modifier.needs_update); + assert_eq!(m.modifier.len, 4); + assert_eq!(m.modifier.idx, 4); + } + + #[test] + fn overwrite_bool_skip() { + let mut modifier = OverwriteBool::default(); + let flags = &mut ElementFlags::new(false); + let m = &mut Modifier::new(&mut modifier, flags); + OverwriteBool::push(m, true); + OverwriteBool::push(m, false); + OverwriteBool::push(m, false); + let mut was_applied = false; + m.modifier.apply_changes(|value| { + was_applied = true; + assert_eq!(value, Some(false)); + }); + assert!(was_applied); + m.flags.clear(); + assert!(!m.modifier.needs_update); + + assert_eq!(m.modifier.idx, 3); + m.modifier.rebuild(3); + assert_eq!(m.modifier.len, 3); + assert_eq!(m.modifier.idx, 0); + OverwriteBool::mutate(m, |first| *first = false); // is overwritten, so don't dirty-flag this. + assert_eq!(m.modifier.idx, 1); + assert!(!m.modifier.needs_update); + OverwriteBool::skip(m, 2); + assert_eq!(m.modifier.len, 3); + assert_eq!(m.modifier.idx, 3); + assert!(!m.modifier.needs_update); + let mut was_applied = false; + m.modifier.apply_changes(|value| { + was_applied = true; + assert_eq!(value, Some(false)); + }); + m.flags.clear(); + // don't apply if nothing has changed... + assert!(!was_applied); + } + + #[test] + fn overwrite_bool_update() { + let mut modifier = OverwriteBool::default(); + let flags = &mut ElementFlags::new(false); + let m = &mut Modifier::new(&mut modifier, flags); + OverwriteBool::push(m, true); + OverwriteBool::push(m, false); + OverwriteBool::push(m, false); + let mut was_applied = false; + m.modifier.apply_changes(|value| { + was_applied = true; + assert_eq!(value, Some(false)); + }); + m.flags.clear(); + assert!(was_applied); + assert!(!m.modifier.needs_update); + + assert_eq!(m.modifier.idx, 3); + m.modifier.rebuild(3); + assert_eq!(m.modifier.len, 3); + assert_eq!(m.modifier.idx, 0); + assert_eq!(m.modifier.modifiers, 1); + // on rebuild + OverwriteBool::update(m, true, false); + assert_eq!(m.modifier.idx, 1); + assert_eq!(m.modifier.modifiers, 0); + assert!(!m.modifier.needs_update); + OverwriteBool::update(m, false, true); + assert_eq!(m.modifier.idx, 2); + assert_eq!(m.modifier.modifiers, 1 << 1); + assert!(!m.modifier.needs_update); + OverwriteBool::update(m, false, true); + assert_eq!(m.modifier.modifiers, 3 << 1); + assert_eq!(m.modifier.idx, 3); + assert!(m.modifier.needs_update); + let mut was_applied = false; + m.modifier.apply_changes(|value| { + was_applied = true; + assert_eq!(value, Some(true)); + }); + m.flags.clear(); + assert!(was_applied); + + // test recreation + let mut modifier = OverwriteBool::default(); + let flags = &mut ElementFlags::new(false); + let modifier = &mut Modifier::new(&mut modifier, flags); + assert_eq!(modifier.modifier.len, 0); + assert_eq!(modifier.modifier.idx, 0); + OverwriteBool::update(modifier, false, true); + assert_eq!(modifier.modifier.idx, 1); + assert_eq!(modifier.modifier.modifiers, 1); + assert!(modifier.modifier.needs_update); + OverwriteBool::update(modifier, true, false); + OverwriteBool::update(modifier, true, false); + assert_eq!(modifier.modifier.len, 3); + assert_eq!(modifier.modifier.idx, 3); + assert_eq!(modifier.modifier.modifiers, 1); + let mut was_applied = false; + modifier.modifier.apply_changes(|value| { + was_applied = true; + assert_eq!(value, Some(false)); + }); + modifier.flags.clear(); + assert!(was_applied); + } + + #[test] + #[should_panic( + expected = "This should never be called, when the underlying element was (re)created." + )] + fn panic_if_use_mutate_on_creation() { + let mut modifier = OverwriteBool::default(); + let flags = &mut ElementFlags::new(false); + let m = &mut Modifier::new(&mut modifier, flags); + assert!(m.flags.was_created()); + OverwriteBool::mutate(m, |m| *m = false); + } + + #[test] + #[should_panic( + expected = "This should never be called, when the underlying element wasn't (re)created." + )] + fn panic_if_use_push_on_rebuild() { + let mut modifier = OverwriteBool::default(); + let flags = &mut ElementFlags::new(false); + let m = &mut Modifier::new(&mut modifier, flags); + assert!(m.flags.was_created()); + OverwriteBool::push(m, true); + let mut was_applied = false; + m.modifier.apply_changes(|value| { + was_applied = true; + assert_eq!(value, Some(true)); + }); + assert!(was_applied); + m.flags.clear(); + assert!(!m.flags.was_created()); + OverwriteBool::push(m, true); + } +} diff --git a/xilem_web/src/modifiers/style.rs b/xilem_web/src/modifiers/style.rs index 2d3ef4d9d..4d17cd642 100644 --- a/xilem_web/src/modifiers/style.rs +++ b/xilem_web/src/modifiers/style.rs @@ -4,9 +4,8 @@ use crate::{ core::{MessageResult, Mut, View, ViewElement, ViewId, ViewMarker}, diff::{diff_iters, Diff}, - modifiers::With, vecmap::VecMap, - DomView, DynMessage, ElementProps, ViewCtx, + DomView, DynMessage, ViewCtx, }; use peniko::kurbo::Vec2; use std::{ @@ -17,6 +16,8 @@ use std::{ }; use wasm_bindgen::{JsCast, UnwrapThrowExt}; +use super::{Modifier, WithModifier}; + type CowStr = std::borrow::Cow<'static, str>; #[derive(Debug, PartialEq, Clone)] @@ -145,15 +146,7 @@ pub struct Styles { // 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() - } + idx: usize, } fn set_style(element: &web_sys::Element, name: &str, value: &str) { @@ -176,51 +169,49 @@ 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 { + pub(crate) fn new(size_hint: usize) -> 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 { + pub fn apply_changes(this: Modifier<'_, Self>, element: &web_sys::Element) { + if this.flags.in_hydration() { + return; + } else if this.flags.was_created() { + for modifier in &this.modifier.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() { + } else if !this.modifier.updated.is_empty() { + for modifier in this.modifier.modifiers.iter().rev() { match modifier { - StyleModifier::Remove(name) if self.updated.remove(name).is_some() => { + StyleModifier::Remove(name) if this.modifier.updated.remove(name).is_some() => { remove_style(element, name); } - StyleModifier::Set(name, value) if self.updated.remove(name).is_some() => { + StyleModifier::Set(name, value) + if this.modifier.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() { + for (name, ()) in this.modifier.updated.drain() { remove_style(element, &name); } } - debug_assert!(self.updated.is_empty()); + debug_assert!(this.modifier.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] + self.modifiers[..self.idx] .iter() .rev() .find(|modifier| predicate(modifier)) @@ -241,17 +232,11 @@ impl Styles { #[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; + pub fn rebuild>(mut element: E, prev_len: usize, f: impl FnOnce(E)) { + element.modifier().modifier.idx -= prev_len; 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 { @@ -261,142 +246,144 @@ impl Styles { #[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) { + /// Must only be used when `this.flags.was_created() == true`, use `Styles::insert` otherwise. + pub fn push(this: &mut Modifier<'_, Self>, modifier: StyleModifier) { debug_assert!( - self.was_created(), + this.flags.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; + this.flags.set_needs_update(); + this.modifier.modifiers.push(modifier); + this.modifier.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) { + /// Must only be used when `this.flags.was_created() == false`, use `Styles::push` otherwise. + pub fn insert(this: &mut Modifier<'_, Self>, modifier: StyleModifier) { debug_assert!( - !self.was_created(), + !this.flags.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(), ()); - } + this.modifier.updated.insert(modifier.name().clone(), ()); + this.flags.set_needs_update(); // 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; + this.modifier.modifiers.insert(this.modifier.idx, modifier); + this.modifier.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 { + /// Must only be used when `this.flags.was_created() == false`. + pub fn mutate(this: &mut Modifier<'_, Self>, f: impl FnOnce(&mut StyleModifier) -> R) -> R { debug_assert!( - !self.was_created(), + !this.flags.was_created(), "This should never be called, when the underlying element was (re)created." ); - let modifier = &mut self.modifiers[self.idx as usize]; + let modifier = &mut this.modifier.modifiers[this.modifier.idx]; let old = modifier.name().clone(); let rv = f(modifier); let new = modifier.name(); if *new != old { - self.updated.insert(new.clone(), ()); + this.modifier.updated.insert(new.clone(), ()); } - self.updated.insert(old, ()); - self.idx += 1; + this.flags.set_needs_update(); + this.modifier.updated.insert(old, ()); + this.modifier.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) { + /// Must only be used when `this.flags.was_created() == false`. + pub fn skip(this: &mut Modifier<'_, Self>, count: usize) { debug_assert!( - !self.was_created(), + !this.flags.was_created(), "This should never be called, when the underlying element was (re)created." ); - self.idx += count as u16; + this.modifier.idx += count; } #[inline] /// Deletes the next `count` modifiers. /// - /// Must only be used when `self.was_created() == false`. - pub fn delete(&mut self, count: usize) { + /// Must only be used when `this.flags.was_created() == false`. + pub fn delete(this: &mut Modifier<'_, Self>, count: usize) { debug_assert!( - !self.was_created(), + !this.flags.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(), ()); + let start = this.modifier.idx; + this.flags.set_needs_update(); + for modifier in this.modifier.modifiers.drain(start..(start + count)) { + this.modifier.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()); + pub fn update(this: &mut Modifier<'_, Self>, prev: &StyleModifier, next: &StyleModifier) { + if this.flags.was_created() { + Self::push(this, next.clone()); } else if next != prev { - self.mutate(|modifier| *modifier = next.clone()); + Self::mutate(this, |modifier| *modifier = next.clone()); } else { - self.skip(1); + Self::skip(this, 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 { + /// Must only be used when `this.flags.was_created() == true`, use `Styles::apply_diff` otherwise. + pub fn extend( + this: &mut Modifier<'_, Self>, + modifiers: impl Iterator, + ) -> usize { debug_assert!( - self.was_created(), + this.flags.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; + let prev_len = this.modifier.modifiers.len(); + this.modifier.modifiers.extend(modifiers); + let iter_count = this.modifier.modifiers.len() - prev_len; + this.flags.set_needs_update(); + this.modifier.idx += iter_count; 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 { + /// Must only be used when `this.flags.was_created() == false`, use [`Styles::extend`] otherwise. + pub fn apply_diff>( + this: &mut Modifier<'_, Self>, + prev: T, + next: T, + ) -> usize { debug_assert!( - !self.was_created(), + !this.flags.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); + Self::insert(this, modifier); count += 1; } - Diff::Remove(count) => self.delete(count), + Diff::Remove(count) => Self::delete(this, count), Diff::Change(new_modifier) => { - self.mutate(|modifier| *modifier = new_modifier); + Self::mutate(this, |modifier| *modifier = new_modifier); count += 1; } Diff::Skip(c) => { - self.skip(c); + Self::skip(this, c); count += c; } } @@ -407,37 +394,41 @@ impl Styles { #[inline] /// Updates styles defined by an iterator, returns the `next` iterator length. pub fn update_style_modifier_iter( - &mut self, + this: &mut Modifier<'_, Self>, prev_len: usize, prev: &T, next: &T, ) -> usize { - if self.was_created() { - self.extend(next.style_modifiers_iter()) + if this.flags.was_created() { + Self::extend(this, next.style_modifiers_iter()) } else if next != prev { - self.apply_diff(prev.style_modifiers_iter(), next.style_modifiers_iter()) + Self::apply_diff( + this, + prev.style_modifiers_iter(), + next.style_modifiers_iter(), + ) } else { - self.skip(prev_len); + Self::skip(this, 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, + pub fn update_with_modify_style( + this: &mut Modifier<'_, 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); + if this.flags.was_created() { + Self::push(this, create_modifier(this.modifier.get_style(name), next)); + } else if prev != next || this.modifier.was_updated(name) { + let new_modifier = create_modifier(this.modifier.get_style(name), next); + Self::mutate(this, |modifier| *modifier = new_modifier); } else { - self.skip(1); + Self::skip(this, 1); } } } @@ -471,8 +462,8 @@ where State: 'static, Action: 'static, S: StyleIter, - V: DomView>, - for<'a> ::Mut<'a>: With, + V: DomView>, + for<'a> ::Mut<'a>: WithModifier, { type Element = V::Element; @@ -482,7 +473,7 @@ where 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); + let len = Styles::extend(&mut e.modifier(), style_iter); (e, (len, s)) } @@ -496,8 +487,8 @@ where 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); + let styles = &mut elem.modifier(); + *len = Styles::update_style_modifier_iter(styles, *len, &prev.styles, &self.styles); }); } @@ -552,8 +543,8 @@ impl View for Rotate>, - for<'a> ::Mut<'a>: With, + V: DomView>, + for<'a> ::Mut<'a>: WithModifier, { type Element = V::Element; @@ -561,11 +552,11 @@ where 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, - )); + let styles = &mut element.modifier(); + Styles::push( + styles, + rotate_transform_modifier(styles.modifier.get_style("transform"), &self.radians), + ); (element, state) } @@ -579,7 +570,9 @@ where Styles::rebuild(element, 1, |mut element| { self.el .rebuild(&prev.el, view_state, ctx, element.reborrow_mut()); - element.modifier().update_style_mutator( + let mut styles = element.modifier(); + Styles::update_with_modify_style( + &mut styles, "transform", &prev.radians, &self.radians, @@ -673,8 +666,8 @@ impl View for Scale>, - for<'a> ::Mut<'a>: With, + V: DomView>, + for<'a> ::Mut<'a>: WithModifier, { type Element = V::Element; @@ -682,11 +675,11 @@ where 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, - )); + let styles = &mut element.modifier(); + Styles::push( + styles, + scale_transform_modifier(styles.modifier.get_style("transform"), &self.scale), + ); (element, state) } @@ -700,7 +693,9 @@ where Styles::rebuild(element, 1, |mut element| { self.el .rebuild(&prev.el, view_state, ctx, element.reborrow_mut()); - element.modifier().update_style_mutator( + let styles = &mut element.modifier(); + Styles::update_with_modify_style( + styles, "transform", &prev.scale, &self.scale, diff --git a/xilem_web/src/one_of.rs b/xilem_web/src/one_of.rs index 65a18b9bf..825d299e2 100644 --- a/xilem_web/src/one_of.rs +++ b/xilem_web/src/one_of.rs @@ -6,7 +6,7 @@ use crate::{ one_of::{OneOf, OneOfCtx, PhantomElementCtx}, Mut, }, - modifiers::With, + modifiers::{Modifier, WithModifier}, DomNode, Pod, PodMut, ViewCtx, }; use wasm_bindgen::UnwrapThrowExt; @@ -218,29 +218,29 @@ where } } -impl With for OneOf +impl WithModifier for OneOf where - A: With, - B: With, - C: With, - D: With, - E: With, - F: With, - G: With, - H: With, - I: With, + A: WithModifier, + B: WithModifier, + C: WithModifier, + D: WithModifier, + E: WithModifier, + F: WithModifier, + G: WithModifier, + H: WithModifier, + I: WithModifier, { - fn modifier(&mut self) -> &mut T { + fn modifier(&mut self) -> Modifier<'_, 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), + 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), } } } @@ -260,8 +260,8 @@ impl AsMut for Noop { } } -impl With for Noop { - fn modifier(&mut self) -> &mut T { +impl WithModifier for Noop { + fn modifier(&mut self) -> Modifier<'_, T> { match *self {} } } diff --git a/xilem_web/src/props/element.rs b/xilem_web/src/props/element.rs new file mode 100644 index 000000000..7f765facf --- /dev/null +++ b/xilem_web/src/props/element.rs @@ -0,0 +1,196 @@ +// Copyright 2024 the Xilem Authors +// SPDX-License-Identifier: Apache-2.0 + +use crate::{ + document, + modifiers::{Attributes, Children, Classes, Modifier, Styles, WithModifier}, + AnyPod, Pod, ViewCtx, +}; +use wasm_bindgen::JsCast; +use wasm_bindgen::UnwrapThrowExt; + +// TODO maybe use bitflags for this, but not sure if it's worth it to pull the dependency in just for this. +/// General flags describing the current state of the element (in hydration, was created, needs update (in general for optimization)) +pub struct ElementFlags(u8); + +impl ElementFlags { + const IN_HYDRATION: u8 = 1 << 0; + const WAS_CREATED: u8 = 1 << 1; + const NEEDS_UPDATE: u8 = 1 << 2; + + pub(crate) fn new(in_hydration: bool) -> Self { + if in_hydration { + ElementFlags(Self::WAS_CREATED | Self::IN_HYDRATION) + } else { + ElementFlags(Self::WAS_CREATED) + } + } + + /// This should only be used in tests, other than within the [`Element`] props + pub(crate) fn clear(&mut self) { + self.0 = 0; + } + + /// Whether the current element was just created, this is usually `true` within `View::build`, but can also happen, e.g. within a `OneOf` variant change. + pub fn was_created(&self) -> bool { + self.0 & Self::WAS_CREATED != 0 + } + + /// Whether the current element is within a hydration context, that could e.g. happen when inside a [`Templated`](crate::Templated) view. + pub fn in_hydration(&self) -> bool { + self.0 & Self::IN_HYDRATION != 0 + } + + /// Whether the current element generally needs to be updated, this serves as cheap preliminary check whether anything changed at all. + pub fn needs_update(&self) -> bool { + self.0 & Self::NEEDS_UPDATE != 0 + } + + /// This should be called as soon as anything has changed for the current element (except children, as they're handled within the element views). + pub fn set_needs_update(&mut self) { + self.0 |= Self::NEEDS_UPDATE; + } +} + +// 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 [`DomNode::Props`](`crate::DomNode::Props`) +pub struct Element { + pub(crate) flags: ElementFlags, + pub(crate) attributes: Option>, + pub(crate) classes: Option>, + pub(crate) styles: Option>, + pub(crate) children: Vec, +} + +impl Element { + pub fn new( + children: Vec, + attr_size_hint: usize, + style_size_hint: usize, + class_size_hint: usize, + in_hydration: bool, + ) -> Self { + Self { + attributes: (attr_size_hint > 0).then(|| Attributes::new(attr_size_hint).into()), + classes: (class_size_hint > 0).then(|| Classes::new(class_size_hint).into()), + styles: (style_size_hint > 0).then(|| Styles::new(style_size_hint).into()), + children, + flags: ElementFlags::new(in_hydration), + } + } + + // All of this is slightly more complicated than it should be, + // 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 self.flags.needs_update() { + if let Some(attributes) = &mut self.attributes { + Attributes::apply_changes(Modifier::new(attributes, &mut self.flags), element); + } + if let Some(classes) = &mut self.classes { + Classes::apply_changes(Modifier::new(classes, &mut self.flags), element); + } + if let Some(styles) = &mut self.styles { + Styles::apply_changes(Modifier::new(styles, &mut self.flags), element); + } + } + self.flags.clear(); + } +} + +impl Pod { + /// Creates a new Pod with [`web_sys::Element`] as element and [`Element`] 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.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)), + wasm_bindgen::intern(elem_name), + ) + .unwrap_throw(); + + for child in children.iter() { + let _ = element.append_child(child.node.as_ref()); + } + + Self { + node: element, + props: Element::new( + children, + attr_size_hint, + style_size_hint, + class_size_hint, + false, + ), + } + } + + /// Creates a new Pod that hydrates an existing node (within the `ViewCtx`) as [`web_sys::Element`] and [`Element`] 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(); + + Self { + node: element.unchecked_into(), + props: Element::new( + children, + attr_size_hint, + style_size_hint, + class_size_hint, + true, + ), + } + } +} + +impl WithModifier for Element { + fn modifier(&mut self) -> Modifier<'_, Attributes> { + let modifier = self + .attributes + .get_or_insert_with(|| Attributes::new(0).into()); + Modifier::new(modifier, &mut self.flags) + } +} + +impl WithModifier for Element { + fn modifier(&mut self) -> Modifier<'_, Children> { + Modifier::new(&mut self.children, &mut self.flags) + } +} + +impl WithModifier for Element { + fn modifier(&mut self) -> Modifier<'_, Classes> { + let modifier = self.classes.get_or_insert_with(|| Classes::new(0).into()); + Modifier::new(modifier, &mut self.flags) + } +} + +impl WithModifier for Element { + fn modifier(&mut self) -> Modifier<'_, Styles> { + let modifier = self.styles.get_or_insert_with(|| Styles::new(0).into()); + Modifier::new(modifier, &mut self.flags) + } +} + +/// An alias trait to sum up all modifiers that a DOM `Element` can have. It's used to avoid a lot of boilerplate in public APIs. +pub trait WithElementProps: + WithModifier + WithModifier + WithModifier + WithModifier +{ +} +impl< + T: WithModifier + + WithModifier + + WithModifier + + WithModifier, + > WithElementProps for T +{ +} diff --git a/xilem_web/src/props/html_input_element.rs b/xilem_web/src/props/html_input_element.rs new file mode 100644 index 000000000..1666ae6d1 --- /dev/null +++ b/xilem_web/src/props/html_input_element.rs @@ -0,0 +1,133 @@ +// Copyright 2024 the Xilem Authors +// SPDX-License-Identifier: Apache-2.0 + +use crate::modifiers::html_input_element::{Checked, DefaultChecked, Disabled, Multiple, Required}; +use crate::modifiers::{Modifier, WithModifier}; +use crate::{props, FromWithContext, Pod, ViewCtx}; +use wasm_bindgen::JsCast as _; + +use super::WithElementProps; + +/// Props specific to an input element. +pub struct HtmlInputElement { + element_props: props::Element, + checked: Checked, + default_checked: DefaultChecked, + disabled: Disabled, + required: Required, + multiple: Multiple, +} + +impl HtmlInputElement { + pub(crate) fn update_element(&mut self, element: &web_sys::HtmlInputElement) { + if self.element_props.flags.needs_update() { + let in_hydration = self.element_props.flags.in_hydration(); + + // Set booleans to `false` as this is the default, + // if we wouldn't do that, possibly the previous value would persist, which is likely unwanted. + self.checked.apply_changes(|value| { + if !in_hydration { + element.set_checked(value.unwrap_or(false)); + } + }); + self.default_checked.apply_changes(|value| { + if !in_hydration { + element.set_default_checked(value.unwrap_or(false)); + } + }); + self.disabled.apply_changes(|value| { + if !in_hydration { + element.set_disabled(value.unwrap_or(false)); + } + }); + self.required.apply_changes(|value| { + if !in_hydration { + element.set_required(value.unwrap_or(false)); + } + }); + self.multiple.apply_changes(|value| { + if !in_hydration { + element.set_multiple(value.unwrap_or(false)); + } + }); + } + // flags are cleared in the following call + self.element_props.update_element(element); + } +} + +impl FromWithContext> for Pod { + fn from_with_ctx(value: Pod, _ctx: &mut ViewCtx) -> Self { + Pod { + node: value.node.unchecked_into(), + props: HtmlInputElement { + checked: Checked::default(), + default_checked: DefaultChecked::default(), + disabled: Disabled::default(), + required: Required::default(), + multiple: Multiple::default(), + element_props: value.props, + }, + } + } +} + +impl WithModifier for HtmlInputElement +where + props::Element: WithModifier, +{ + fn modifier(&mut self) -> Modifier<'_, T> { + self.element_props.modifier() + } +} + +impl WithModifier for HtmlInputElement { + fn modifier(&mut self) -> Modifier<'_, Checked> { + Modifier::new(&mut self.checked, &mut self.element_props.flags) + } +} + +impl WithModifier for HtmlInputElement { + fn modifier(&mut self) -> Modifier<'_, DefaultChecked> { + Modifier::new(&mut self.default_checked, &mut self.element_props.flags) + } +} + +impl WithModifier for HtmlInputElement { + fn modifier(&mut self) -> Modifier<'_, Disabled> { + Modifier::new(&mut self.disabled, &mut self.element_props.flags) + } +} + +impl WithModifier for HtmlInputElement { + fn modifier(&mut self) -> Modifier<'_, Required> { + Modifier::new(&mut self.required, &mut self.element_props.flags) + } +} + +impl WithModifier for HtmlInputElement { + fn modifier(&mut self) -> Modifier<'_, Multiple> { + Modifier::new(&mut self.multiple, &mut self.element_props.flags) + } +} + +/// An alias trait to sum up all modifiers that a DOM `HTMLInputElement` can have. It's used to avoid a lot of boilerplate in public APIs. +pub trait WithHtmlInputElementProps: + WithElementProps + + WithModifier + + WithModifier + + WithModifier + + WithModifier + + WithModifier +{ +} +impl< + T: WithElementProps + + WithModifier + + WithModifier + + WithModifier + + WithModifier + + WithModifier, + > WithHtmlInputElementProps for T +{ +} diff --git a/xilem_web/src/props/mod.rs b/xilem_web/src/props/mod.rs new file mode 100644 index 000000000..82a255621 --- /dev/null +++ b/xilem_web/src/props/mod.rs @@ -0,0 +1,10 @@ +// Copyright 2024 the Xilem Authors +// SPDX-License-Identifier: Apache-2.0 + +//! This module contains the state (called props in this crate) of the virtual DOM. + +mod element; +pub use element::*; + +mod html_input_element; +pub use html_input_element::*; diff --git a/xilem_web/src/svg/common_attrs.rs b/xilem_web/src/svg/common_attrs.rs index 6ec7ba8f8..f629015f2 100644 --- a/xilem_web/src/svg/common_attrs.rs +++ b/xilem_web/src/svg/common_attrs.rs @@ -3,8 +3,7 @@ use crate::{ core::{MessageResult, Mut, View, ViewElement, ViewId, ViewMarker}, - modifiers::With, - modifiers::{AttributeModifier, Attributes}, + modifiers::{AttributeModifier, Attributes, Modifier, WithModifier}, DomView, DynMessage, ViewCtx, }; use peniko::{kurbo, Brush}; @@ -92,8 +91,8 @@ impl View for Fill>, - for<'a> ::Mut<'a>: With, + V: DomView>, + for<'a> ::Mut<'a>: WithModifier, { type ViewState = V::ViewState; type Element = V::Element; @@ -101,9 +100,12 @@ where fn build(&self, ctx: &mut ViewCtx) -> (Self::Element, Self::ViewState) { 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)); + let mut attrs = element.modifier(); + Attributes::push(&mut attrs, ("fill", brush_to_string(&self.brush))); + Attributes::push( + &mut attrs, + opacity_attr_modifier("fill-opacity", &self.brush), + ); (element, state) } @@ -117,15 +119,22 @@ where 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)); + let mut attrs = element.modifier(); + if attrs.flags.was_created() { + Attributes::push(&mut attrs, ("fill", brush_to_string(&self.brush))); + Attributes::push( + &mut attrs, + 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)); + Attributes::mutate(&mut attrs, |m| { + *m = ("fill", brush_to_string(&self.brush)).into(); + }); + Attributes::mutate(&mut attrs, |m| { + *m = opacity_attr_modifier("fill-opacity", &self.brush); + }); } else { - attrs.skip(2); + Attributes::skip(&mut attrs, 2); } }); } @@ -150,50 +159,66 @@ where } } -fn push_stroke_modifiers(attrs: &mut Attributes, stroke: &kurbo::Stroke, brush: &Brush) { +fn push_stroke_modifiers( + mut attrs: Modifier<'_, 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)); + Attributes::push(&mut attrs, ("stroke", brush_to_string(brush))); + Attributes::push(&mut attrs, opacity_attr_modifier("stroke-opacity", brush)); + Attributes::push(&mut attrs, ("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)); + Attributes::push(&mut attrs, ("stroke-dashoffset", dash_offset)); + Attributes::push(&mut attrs, ("stroke-width", stroke.width)); } // This function is not inlined to avoid unnecessary monomorphization, which may result in a bigger binary. fn update_stroke_modifiers( - attrs: &mut Attributes, + mut attrs: Modifier<'_, Attributes>, prev_stroke: &kurbo::Stroke, next_stroke: &kurbo::Stroke, prev_brush: &Brush, next_brush: &Brush, ) { - if attrs.was_created() { + if attrs.flags.was_created() { push_stroke_modifiers(attrs, next_stroke, next_brush); } else { + if next_brush != prev_brush { + Attributes::mutate(&mut attrs, |m| { + *m = ("stroke", brush_to_string(next_brush)).into(); + }); + Attributes::mutate(&mut attrs, |m| { + *m = opacity_attr_modifier("stroke-opacity", next_brush); + }); + } else { + Attributes::skip(&mut attrs, 2); + } 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()); + Attributes::mutate(&mut attrs, |m| { + *m = ("stroke-dasharray", dash_pattern).into(); + }); } else { - attrs.skip(1); + Attributes::skip(&mut attrs, 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()); + Attributes::mutate(&mut attrs, |m| { + *m = ("stroke-dashoffset", dash_offset).into(); + }); } else { - attrs.skip(1); + Attributes::skip(&mut attrs, 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)); + Attributes::mutate(&mut attrs, |m| { + *m = ("stroke-width", next_stroke.width).into(); + }); } else { - attrs.skip(1); + Attributes::skip(&mut attrs, 1); } } } @@ -203,15 +228,15 @@ impl View for Stroke>, - for<'a> ::Mut<'a>: With, + V: DomView>, + for<'a> ::Mut<'a>: WithModifier, { type ViewState = V::ViewState; type Element = V::Element; fn build(&self, ctx: &mut ViewCtx) -> (Self::Element, Self::ViewState) { let (mut element, state) = - ctx.with_size_hint::(4, |ctx| self.child.build(ctx)); + ctx.with_size_hint::(5, |ctx| self.child.build(ctx)); push_stroke_modifiers(element.modifier(), &self.style, &self.brush); (element, state) } @@ -223,7 +248,7 @@ where ctx: &mut ViewCtx, element: Mut, ) { - Attributes::rebuild(element, 4, |mut element| { + Attributes::rebuild(element, 5, |mut element| { self.child .rebuild(&prev.child, view_state, ctx, element.reborrow_mut()); update_stroke_modifiers( diff --git a/xilem_web/src/svg/kurbo_shape.rs b/xilem_web/src/svg/kurbo_shape.rs index cbfbe697a..ccf96fb30 100644 --- a/xilem_web/src/svg/kurbo_shape.rs +++ b/xilem_web/src/svg/kurbo_shape.rs @@ -5,7 +5,7 @@ use crate::{ core::{MessageResult, Mut, OrphanView, ViewId}, - modifiers::{Attributes, With}, + modifiers::{Attributes, WithModifier}, DynMessage, FromWithContext, Pod, ViewCtx, SVG_NS, }; use peniko::kurbo::{BezPath, Circle, Line, Rect}; @@ -36,11 +36,11 @@ impl OrphanView (Self::OrphanElement, Self::OrphanViewState) { 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)); + let attrs = &mut element.modifier(); + Attributes::push(attrs, ("x1", view.p0.x)); + Attributes::push(attrs, ("y1", view.p0.y)); + Attributes::push(attrs, ("x2", view.p1.x)); + Attributes::push(attrs, ("y2", view.p1.y)); (element, ()) }) } @@ -53,11 +53,11 @@ impl OrphanView, ) { 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); + let attrs = &mut element.modifier(); + Attributes::update_with_same_key(attrs, "x1", &prev.p0.x, &new.p0.x); + Attributes::update_with_same_key(attrs, "y1", &prev.p0.y, &new.p0.y); + Attributes::update_with_same_key(attrs, "x2", &prev.p1.x, &new.p1.x); + Attributes::update_with_same_key(attrs, "y2", &prev.p1.y, &new.p1.y); }); } @@ -90,11 +90,11 @@ impl OrphanView (Self::OrphanElement, Self::OrphanViewState) { 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())); + let attrs = &mut element.modifier(); + Attributes::push(attrs, ("x", view.x0)); + Attributes::push(attrs, ("y", view.y0)); + Attributes::push(attrs, ("width", view.width())); + Attributes::push(attrs, ("height", view.height())); (element, ()) }) } @@ -107,11 +107,11 @@ impl OrphanView, ) { 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()); + let attrs = &mut element.modifier(); + Attributes::update_with_same_key(attrs, "x", &prev.x0, &new.x0); + Attributes::update_with_same_key(attrs, "y", &prev.y0, &new.y0); + Attributes::update_with_same_key(attrs, "width", &prev.width(), &new.width()); + Attributes::update_with_same_key(attrs, "height", &prev.height(), &new.height()); }); } @@ -144,10 +144,10 @@ impl OrphanView (Self::OrphanElement, Self::OrphanViewState) { 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)); + let attrs = &mut element.modifier(); + Attributes::push(attrs, ("cx", view.center.x)); + Attributes::push(attrs, ("cy", view.center.y)); + Attributes::push(attrs, ("r", view.radius)); (element, ()) }) } @@ -160,10 +160,10 @@ impl OrphanView, ) { 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); + let attrs = &mut element.modifier(); + Attributes::update_with_same_key(attrs, "cx", &prev.center.x, &new.center.x); + Attributes::update_with_same_key(attrs, "cy", &prev.center.y, &new.center.y); + Attributes::update_with_same_key(attrs, "height", &prev.radius, &new.radius); }); } @@ -196,7 +196,8 @@ impl OrphanView (Self::OrphanElement, Self::OrphanViewState) { create_element("path", ctx, 1, |element, ctx| { let mut element = Self::OrphanElement::from_with_ctx(element, ctx); - element.props.attributes().push(("d", view.to_svg())); + let attrs = &mut element.modifier(); + Attributes::push(attrs, ("d", view.to_svg())); (element, ()) }) } @@ -209,13 +210,13 @@ impl OrphanView, ) { Attributes::rebuild(element, 1, |mut element| { - let attrs: &mut Attributes = element.modifier(); - if attrs.was_created() { - attrs.push(("d", new.to_svg())); + let attrs = &mut element.modifier(); + if attrs.flags.was_created() { + Attributes::push(attrs, ("d", new.to_svg())); } else if new != prev { - attrs.mutate(|m| *m = ("d", new.to_svg()).into()); + Attributes::mutate(attrs, |m| *m = ("d", new.to_svg()).into()); } else { - attrs.skip(1); + Attributes::skip(attrs, 1); } }); } diff --git a/xilem_web/web_examples/fetch/src/main.rs b/xilem_web/web_examples/fetch/src/main.rs index 6cc8bb734..e630a5262 100644 --- a/xilem_web/web_examples/fetch/src/main.rs +++ b/xilem_web/web_examples/fetch/src/main.rs @@ -9,7 +9,7 @@ use xilem_web::{ core::{fork, one_of::Either}, document_body, elements::html::*, - interfaces::{Element, HtmlDivElement, HtmlImageElement, HtmlLabelElement}, + interfaces::{Element, HtmlDivElement, HtmlImageElement, HtmlInputElement, HtmlLabelElement}, App, }; @@ -188,7 +188,7 @@ fn cat_fetch_controls(state: &AppState) -> impl Element { td(input(()) .id("reset-debounce-update") .attr("type", "checkbox") - .attr("checked", state.reset_debounce_on_update) + .checked(state.reset_debounce_on_update) .on_input(|state: &mut AppState, event: web_sys::Event| { state.reset_debounce_on_update = input_target(&event).checked(); })), diff --git a/xilem_web/web_examples/todomvc/src/main.rs b/xilem_web/web_examples/todomvc/src/main.rs index 73e857bae..97c4d990d 100644 --- a/xilem_web/web_examples/todomvc/src/main.rs +++ b/xilem_web/web_examples/todomvc/src/main.rs @@ -30,7 +30,7 @@ fn todo_item(todo: &mut Todo, editing: bool) -> impl Element { let checkbox = el::input(()) .class("toggle") .attr("type", "checkbox") - .attr("checked", todo.completed) + .checked(todo.completed) .on_click(|state: &mut Todo, _| state.completed = !state.completed); el::li(( @@ -160,7 +160,7 @@ fn main_view(state: &mut AppState, should_display: bool) -> impl Element