diff --git a/crates/xilem_web/Cargo.toml b/crates/xilem_web/Cargo.toml index 43d04e426..e6c13502a 100644 --- a/crates/xilem_web/Cargo.toml +++ b/crates/xilem_web/Cargo.toml @@ -34,7 +34,9 @@ peniko = { git = "https://github.com/linebender/peniko", rev = "629fc3325b016a8c version = "0.3.4" features = [ "console", + "CssStyleDeclaration", "Document", + "DomTokenList", "Element", "Event", "HtmlElement", diff --git a/crates/xilem_web/src/class.rs b/crates/xilem_web/src/class.rs new file mode 100644 index 000000000..da2f58769 --- /dev/null +++ b/crates/xilem_web/src/class.rs @@ -0,0 +1,124 @@ +use std::{borrow::Cow, marker::PhantomData}; + +use xilem_core::{Id, MessageResult}; + +use crate::{ + interfaces::{sealed::Sealed, Element}, + ChangeFlags, Cx, View, ViewMarker, +}; + +/// A trait to make the class adding functions generic over collection type +pub trait IntoClasses { + fn into_classes(self, classes: &mut Vec>); +} + +impl IntoClasses for String { + fn into_classes(self, classes: &mut Vec>) { + classes.push(self.into()); + } +} + +impl IntoClasses for &'static str { + fn into_classes(self, classes: &mut Vec>) { + classes.push(self.into()); + } +} + +impl IntoClasses for Cow<'static, str> { + fn into_classes(self, classes: &mut Vec>) { + classes.push(self); + } +} + +impl IntoClasses for Option +where + T: IntoClasses, +{ + fn into_classes(self, classes: &mut Vec>) { + if let Some(t) = self { + t.into_classes(classes); + } + } +} + +impl IntoClasses for Vec +where + T: IntoClasses, +{ + fn into_classes(self, classes: &mut Vec>) { + for itm in self { + itm.into_classes(classes); + } + } +} + +macro_rules! impl_tuple_intoclasses { + ($($name:ident : $type:ident),* $(,)?) => { + impl<$($type),*> IntoClasses for ($($type,)*) + where + $($type: IntoClasses),* + { + #[allow(unused_variables)] + fn into_classes(self, classes: &mut Vec>) { + let ($($name,)*) = self; + $( + $name.into_classes(classes); + )* + } + } + }; +} + +impl_tuple_intoclasses!(); +impl_tuple_intoclasses!(t1: T1); +impl_tuple_intoclasses!(t1: T1, t2: T2); +impl_tuple_intoclasses!(t1: T1, t2: T2, t3: T3); +impl_tuple_intoclasses!(t1: T1, t2: T2, t3: T3, t4: T4); + +/// Applies a class to the underlying element. +pub struct Class { + pub(crate) element: E, + pub(crate) class_names: Vec>, + pub(crate) phantom: PhantomData (T, A)>, +} + +impl ViewMarker for Class {} +impl Sealed for Class {} + +impl, T, A> View for Class { + type State = E::State; + type Element = E::Element; + + fn build(&self, cx: &mut Cx) -> (Id, Self::State, Self::Element) { + for class_name in &self.class_names { + cx.add_class_to_element(class_name); + } + self.element.build(cx) + } + + fn rebuild( + &self, + cx: &mut Cx, + prev: &Self, + id: &mut Id, + state: &mut Self::State, + element: &mut Self::Element, + ) -> ChangeFlags { + for class_name in &self.class_names { + cx.add_class_to_element(class_name); + } + self.element.rebuild(cx, &prev.element, id, state, element) + } + + fn message( + &self, + id_path: &[Id], + state: &mut Self::State, + message: Box, + app_state: &mut T, + ) -> MessageResult { + self.element.message(id_path, state, message, app_state) + } +} + +crate::interfaces::impl_dom_interfaces_for_ty!(Element, Class); diff --git a/crates/xilem_web/src/context.rs b/crates/xilem_web/src/context.rs index 2ae261d7a..ce793851d 100644 --- a/crates/xilem_web/src/context.rs +++ b/crates/xilem_web/src/context.rs @@ -16,6 +16,131 @@ use crate::{ type CowStr = std::borrow::Cow<'static, str>; +#[derive(Debug, Default)] +pub struct HtmlProps { + pub(crate) attributes: VecMap, + pub(crate) classes: VecMap, + pub(crate) styles: VecMap, +} + +impl HtmlProps { + fn apply(&mut self, el: &web_sys::Element) -> Self { + let attributes = self.apply_attributes(el); + let classes = self.apply_classes(el); + let styles = self.apply_styles(el); + Self { + attributes, + classes, + styles, + } + } + + fn apply_attributes(&mut self, element: &web_sys::Element) -> VecMap { + let mut attributes = VecMap::default(); + std::mem::swap(&mut attributes, &mut self.attributes); + for (name, value) in attributes.iter() { + set_attribute(element, name, &value.serialize()); + } + attributes + } + + fn apply_classes(&mut self, element: &web_sys::Element) -> VecMap { + let mut classes = VecMap::default(); + std::mem::swap(&mut classes, &mut self.classes); + for (class_name, ()) in classes.iter() { + set_class(element, class_name); + } + classes + } + + fn apply_styles(&mut self, element: &web_sys::Element) -> VecMap { + let mut styles = VecMap::default(); + std::mem::swap(&mut styles, &mut self.styles); + for (name, value) in styles.iter() { + set_style(element, name, value); + } + styles + } + + fn apply_changes(&mut self, element: &web_sys::Element, props: &mut HtmlProps) -> ChangeFlags { + self.apply_attribute_changes(element, &mut props.attributes) + | self.apply_class_changes(element, &mut props.classes) + | self.apply_style_changes(element, &mut props.styles) + } + + pub(crate) fn apply_attribute_changes( + &mut self, + element: &web_sys::Element, + attributes: &mut VecMap, + ) -> ChangeFlags { + let mut changed = ChangeFlags::empty(); + // update attributes + for itm in diff_kv_iterables(&*attributes, &self.attributes) { + match itm { + Diff::Add(name, value) | Diff::Change(name, value) => { + set_attribute(element, name, &value.serialize()); + changed |= ChangeFlags::OTHER_CHANGE; + } + Diff::Remove(name) => { + remove_attribute(element, name); + changed |= ChangeFlags::OTHER_CHANGE; + } + } + } + std::mem::swap(attributes, &mut self.attributes); + self.attributes.clear(); + changed + } + + pub(crate) fn apply_class_changes( + &mut self, + element: &web_sys::Element, + classes: &mut VecMap, + ) -> ChangeFlags { + let mut changed = ChangeFlags::empty(); + // update attributes + for itm in diff_kv_iterables(&*classes, &self.classes) { + match itm { + Diff::Add(class_name, ()) | Diff::Change(class_name, ()) => { + set_class(element, class_name); + changed |= ChangeFlags::OTHER_CHANGE; + } + Diff::Remove(class_name) => { + remove_class(element, class_name); + changed |= ChangeFlags::OTHER_CHANGE; + } + } + } + std::mem::swap(classes, &mut self.classes); + self.classes.clear(); + changed + } + + pub(crate) fn apply_style_changes( + &mut self, + element: &web_sys::Element, + styles: &mut VecMap, + ) -> ChangeFlags { + let mut changed = ChangeFlags::empty(); + // update attributes + for itm in diff_kv_iterables(&*styles, &self.styles) { + match itm { + Diff::Add(name, value) | Diff::Change(name, value) => { + set_style(element, name, value); + changed |= ChangeFlags::OTHER_CHANGE; + } + Diff::Remove(name) => { + remove_style(element, name); + changed |= ChangeFlags::OTHER_CHANGE; + } + } + } + std::mem::swap(styles, &mut self.styles); + self.styles.clear(); + changed + } +} + fn set_attribute(element: &web_sys::Element, name: &str, value: &str) { // we have to special-case `value` because setting the value using `set_attribute` // doesn't work after the value has been changed. @@ -41,12 +166,52 @@ fn remove_attribute(element: &web_sys::Element, name: &str) { } } +fn set_class(element: &web_sys::Element, class_name: &str) { + debug_assert!( + !class_name.is_empty(), + "class names cannot be the empty string" + ); + debug_assert!( + !class_name.contains(' '), + "class names cannot contain the ascii space character" + ); + element.class_list().add_1(class_name).unwrap_throw(); +} + +fn remove_class(element: &web_sys::Element, class_name: &str) { + debug_assert!( + !class_name.is_empty(), + "class names cannot be the empty string" + ); + debug_assert!( + !class_name.contains(' '), + "class names cannot contain the ascii space character" + ); + element.class_list().remove_1(class_name).unwrap_throw(); +} + +fn set_style(element: &web_sys::Element, name: &str, value: &str) { + if let Some(el) = element.dyn_ref::() { + el.style().set_property(name, value).unwrap_throw(); + } else if let Some(el) = element.dyn_ref::() { + el.style().set_property(name, value).unwrap_throw(); + } +} + +fn remove_style(element: &web_sys::Element, name: &str) { + if let Some(el) = element.dyn_ref::() { + el.style().remove_property(name).unwrap_throw(); + } else if let Some(el) = element.dyn_ref::() { + el.style().remove_property(name).unwrap_throw(); + } +} + // Note: xilem has derive Clone here. Not sure. pub struct Cx { id_path: IdPath, document: Document, // TODO There's likely a cleaner more robust way to propagate the attributes to an element - pub(crate) current_element_attributes: VecMap, + pub(crate) current_element_props: HtmlProps, app_ref: Option>, } @@ -69,7 +234,7 @@ impl Cx { id_path: Vec::new(), document: crate::document(), app_ref: None, - current_element_attributes: Default::default(), + current_element_props: Default::default(), } } @@ -141,73 +306,62 @@ impl Cx { &self.document } - pub(crate) fn build_element( - &mut self, - ns: &str, - name: &str, - ) -> (web_sys::Element, VecMap) { + pub(crate) fn build_element(&mut self, ns: &str, name: &str) -> (web_sys::Element, HtmlProps) { let el = self .document .create_element_ns(Some(ns), name) .expect("could not create element"); - let attributes = self.apply_attributes(&el); - (el, attributes) + let props = self.current_element_props.apply(&el); + (el, props) } pub(crate) fn rebuild_element( &mut self, element: &web_sys::Element, - attributes: &mut VecMap, + props: &mut HtmlProps, ) -> ChangeFlags { - self.apply_attribute_changes(element, attributes) + self.current_element_props.apply_changes(element, props) } // TODO Not sure how multiple attribute definitions with the same name should be handled (e.g. `e.attr("class", "a").attr("class", "b")`) // Currently the outer most (in the example above "b") defines the attribute (when it isn't `None`, in that case the inner attr defines the value) pub(crate) fn add_attr_to_element(&mut self, name: &CowStr, value: &Option) { + // Panic in dev if "class" is used as an attribute. In production the result is undefined. + debug_assert!( + name != "class", + "classes should be set using the `class` method" + ); + // Panic in dev if "style" is used as an attribute. In production the result is undefined. + debug_assert!( + name != "style", + "styles should be set using the `style` method" + ); + if let Some(value) = value { // could be slightly optimized via something like this: `new_attrs.entry(name).or_insert_with(|| value)` - if !self.current_element_attributes.contains_key(name) { - self.current_element_attributes + if !self.current_element_props.attributes.contains_key(name) { + self.current_element_props + .attributes .insert(name.clone(), value.clone()); } } } - pub(crate) fn apply_attributes( - &mut self, - element: &web_sys::Element, - ) -> VecMap { - let mut attributes = VecMap::default(); - std::mem::swap(&mut attributes, &mut self.current_element_attributes); - for (name, value) in attributes.iter() { - set_attribute(element, name, &value.serialize()); + pub(crate) fn add_class_to_element(&mut self, class_name: &CowStr) { + // Don't strictly need this check but I assume its better for perf (might not be though) + if !self.current_element_props.classes.contains_key(class_name) { + self.current_element_props + .classes + .insert(class_name.clone(), ()); } - attributes } - pub(crate) fn apply_attribute_changes( - &mut self, - element: &web_sys::Element, - attributes: &mut VecMap, - ) -> ChangeFlags { - let mut changed = ChangeFlags::empty(); - // update attributes - for itm in diff_kv_iterables(&*attributes, &self.current_element_attributes) { - match itm { - Diff::Add(name, value) | Diff::Change(name, value) => { - set_attribute(element, name, &value.serialize()); - changed |= ChangeFlags::OTHER_CHANGE; - } - Diff::Remove(name) => { - remove_attribute(element, name); - changed |= ChangeFlags::OTHER_CHANGE; - } - } + pub(crate) fn add_style_to_element(&mut self, name: &CowStr, value: &CowStr) { + if !self.current_element_props.styles.contains_key(name) { + self.current_element_props + .styles + .insert(name.clone(), value.clone()); } - std::mem::swap(attributes, &mut self.current_element_attributes); - self.current_element_attributes.clear(); - changed } pub fn message_thunk(&self) -> MessageThunk { diff --git a/crates/xilem_web/src/elements.rs b/crates/xilem_web/src/elements.rs index 1a48bf596..ee4b31e67 100644 --- a/crates/xilem_web/src/elements.rs +++ b/crates/xilem_web/src/elements.rs @@ -4,8 +4,8 @@ use wasm_bindgen::{JsCast, UnwrapThrowExt}; use xilem_core::{Id, MessageResult, VecSplice}; use crate::{ - interfaces::sealed::Sealed, vecmap::VecMap, view::DomNode, AttributeValue, ChangeFlags, Cx, - ElementsSplice, Pod, View, ViewMarker, ViewSequence, HTML_NS, + context::HtmlProps, interfaces::sealed::Sealed, view::DomNode, ChangeFlags, Cx, ElementsSplice, + Pod, View, ViewMarker, ViewSequence, HTML_NS, }; use super::interfaces::Element; @@ -17,7 +17,7 @@ type CowStr = std::borrow::Cow<'static, str>; /// Stores handles to the child elements and any child state, as well as attributes and event listeners pub struct ElementState { pub(crate) children_states: ViewSeqState, - pub(crate) attributes: VecMap, + pub(crate) props: HtmlProps, pub(crate) child_elements: Vec, /// This is temporary cache for elements while updating/diffing, /// after usage it shouldn't contain any elements, @@ -150,7 +150,7 @@ where type Element = web_sys::HtmlElement; fn build(&self, cx: &mut Cx) -> (Id, Self::State, Self::Element) { - let (el, attributes) = cx.build_element(HTML_NS, &self.name); + let (el, props) = cx.build_element(HTML_NS, &self.name); let mut child_elements = vec![]; let mut scratch = vec![]; @@ -171,7 +171,7 @@ where children_states, child_elements, scratch, - attributes, + props, }; (id, state, el) } @@ -193,8 +193,8 @@ where .parent_element() .expect_throw("this element was mounted and so should have a parent"); parent.remove_child(element).unwrap_throw(); - let (new_element, attributes) = cx.build_element(HTML_NS, self.node_name()); - state.attributes = attributes; + let (new_element, props) = cx.build_element(HTML_NS, self.node_name()); + state.props = props; // TODO could this be combined with child updates? while let Some(child) = element.child_nodes().get(0) { new_element.append_child(&child).unwrap_throw(); @@ -203,7 +203,7 @@ where changed |= ChangeFlags::STRUCTURE; } - changed |= cx.rebuild_element(element, &mut state.attributes); + changed |= cx.rebuild_element(element, &mut state.props); // update children let mut splice = @@ -280,7 +280,7 @@ macro_rules! define_element { type Element = web_sys::$dom_interface; fn build(&self, cx: &mut Cx) -> (Id, Self::State, Self::Element) { - let (el, attributes) = cx.build_element($ns, $tag_name); + let (el, props) = cx.build_element($ns, $tag_name); let mut child_elements = vec![]; let mut scratch = vec![]; @@ -300,7 +300,7 @@ macro_rules! define_element { children_states, child_elements, scratch, - attributes, + props, }; (id, state, el) } @@ -315,7 +315,7 @@ macro_rules! define_element { ) -> ChangeFlags { let mut changed = ChangeFlags::empty(); - changed |= cx.rebuild_element(element, &mut state.attributes); + changed |= cx.rebuild_element(element, &mut state.props); // update children let mut splice = ChildrenSplice::new(&mut state.child_elements, &mut state.scratch, element); diff --git a/crates/xilem_web/src/interfaces.rs b/crates/xilem_web/src/interfaces.rs index 56a37038a..62f98b6ae 100644 --- a/crates/xilem_web/src/interfaces.rs +++ b/crates/xilem_web/src/interfaces.rs @@ -1,5 +1,9 @@ -use crate::{Pointer, PointerMsg, View, ViewMarker}; -use std::borrow::Cow; +use crate::{ + class::{Class, IntoClasses}, + style::{IntoStyles, Style}, + Pointer, PointerMsg, View, ViewMarker, +}; +use std::{borrow::Cow, marker::PhantomData}; use gloo::events::EventListenerOptions; use wasm_bindgen::JsCast; @@ -91,11 +95,30 @@ where } } - // TODO should some methods extend some properties automatically, - // instead of overwriting the (possibly set) inner value - // or should there be (extra) "modifier" methods like `add_class` and/or `remove_class` - fn class(self, class: impl Into>) -> Attr { - self.attr("class", class.into()) + /// Add 0 or more classes to the wrapped element. + /// + /// Can pass a string, &'static str, Option, tuple, or vec + /// + /// If multiple classes are added, all will be applied to the element. + fn class(self, class: impl IntoClasses) -> Class { + let mut class_names = vec![]; + class.into_classes(&mut class_names); + Class { + element: self, + class_names, + phantom: PhantomData, + } + } + + /// Set a style attribute + fn style(self, style: impl IntoStyles) -> Style { + let mut styles = vec![]; + style.into_styles(&mut styles); + Style { + element: self, + styles, + phantom: PhantomData, + } } // event list from diff --git a/crates/xilem_web/src/lib.rs b/crates/xilem_web/src/lib.rs index a7c6d1e3d..3821ecf79 100644 --- a/crates/xilem_web/src/lib.rs +++ b/crates/xilem_web/src/lib.rs @@ -10,6 +10,7 @@ use wasm_bindgen::JsCast; mod app; mod attribute; mod attribute_value; +mod class; mod context; mod diff; pub mod elements; @@ -18,6 +19,7 @@ pub mod interfaces; mod one_of; mod optional_action; mod pointer; +mod style; pub mod svg; mod vecmap; mod view; @@ -35,6 +37,7 @@ pub use one_of::{ }; pub use optional_action::{Action, OptionalAction}; pub use pointer::{Pointer, PointerDetails, PointerMsg}; +pub use style::style; pub use view::{ memoize, static_view, Adapt, AdaptState, AdaptThunk, AnyView, BoxedView, ElementsSplice, Memoize, MemoizeState, Pod, View, ViewMarker, ViewSequence, diff --git a/crates/xilem_web/src/style.rs b/crates/xilem_web/src/style.rs new file mode 100644 index 000000000..ddd7364fe --- /dev/null +++ b/crates/xilem_web/src/style.rs @@ -0,0 +1,152 @@ +use std::collections::BTreeMap; +use std::marker::PhantomData; +use std::{borrow::Cow, collections::HashMap}; + +use xilem_core::{Id, MessageResult}; + +use crate::{interfaces::sealed::Sealed, ChangeFlags, Cx, View, ViewMarker}; + +use super::interfaces::Element; + +/// A trait to make the class adding functions generic over collection type +pub trait IntoStyles { + fn into_styles(self, styles: &mut Vec<(Cow<'static, str>, Cow<'static, str>)>); +} + +struct StyleTuple(T1, T2); + +/// Create a style from a style name and its value. +pub fn style(name: T1, value: T2) -> impl IntoStyles +where + T1: Into>, + T2: Into>, +{ + StyleTuple(name, value) +} + +impl IntoStyles for StyleTuple +where + T1: Into>, + T2: Into>, +{ + fn into_styles(self, styles: &mut Vec<(Cow<'static, str>, Cow<'static, str>)>) { + let StyleTuple(key, value) = self; + styles.push((key.into(), value.into())); + } +} + +impl IntoStyles for Option +where + T: IntoStyles, +{ + fn into_styles(self, styles: &mut Vec<(Cow<'static, str>, Cow<'static, str>)>) { + if let Some(t) = self { + t.into_styles(styles); + } + } +} + +impl IntoStyles for Vec +where + T: IntoStyles, +{ + fn into_styles(self, styles: &mut Vec<(Cow<'static, str>, Cow<'static, str>)>) { + for itm in self { + itm.into_styles(styles); + } + } +} + +impl IntoStyles for HashMap +where + T1: Into>, + T2: Into>, +{ + fn into_styles(self, styles: &mut Vec<(Cow<'static, str>, Cow<'static, str>)>) { + for (key, value) in self { + styles.push((key.into(), value.into())); + } + } +} + +impl IntoStyles for BTreeMap +where + T1: Into>, + T2: Into>, +{ + fn into_styles(self, styles: &mut Vec<(Cow<'static, str>, Cow<'static, str>)>) { + for (key, value) in self { + styles.push((key.into(), value.into())); + } + } +} + +macro_rules! impl_tuple_intostyles { + ($($name:ident : $type:ident),* $(,)?) => { + impl<$($type),*> IntoStyles for ($($type,)*) + where + $($type: IntoStyles),* + { + #[allow(unused_variables)] + fn into_styles(self, styles: &mut Vec<(Cow<'static, str>, Cow<'static, str>)>) { + let ($($name,)*) = self; + $( + $name.into_styles(styles); + )* + } + } + }; +} + +impl_tuple_intostyles!(); +impl_tuple_intostyles!(t1: T1); +impl_tuple_intostyles!(t1: T1, t2: T2); +impl_tuple_intostyles!(t1: T1, t2: T2, t3: T3); +impl_tuple_intostyles!(t1: T1, t2: T2, t3: T3, t4: T4); + +pub struct Style { + pub(crate) element: E, + pub(crate) styles: Vec<(Cow<'static, str>, Cow<'static, str>)>, + pub(crate) phantom: PhantomData (T, A)>, +} + +impl ViewMarker for Style {} +impl Sealed for Style {} + +impl, T, A> View for Style { + type State = E::State; + type Element = E::Element; + + fn build(&self, cx: &mut Cx) -> (Id, Self::State, Self::Element) { + for (key, value) in &self.styles { + cx.add_style_to_element(key, value); + } + self.element.build(cx) + } + + fn rebuild( + &self, + cx: &mut Cx, + prev: &Self, + id: &mut Id, + state: &mut Self::State, + element: &mut Self::Element, + ) -> ChangeFlags { + for (key, value) in &self.styles { + cx.add_style_to_element(key, value); + } + self.element.rebuild(cx, &prev.element, id, state, element) + } + + fn message( + &self, + id_path: &[Id], + state: &mut Self::State, + message: Box, + app_state: &mut T, + ) -> MessageResult { + self.element.message(id_path, state, message, app_state) + } +} + +crate::interfaces::impl_dom_interfaces_for_ty!(Element, Style); diff --git a/crates/xilem_web/src/svg/kurbo_shape.rs b/crates/xilem_web/src/svg/kurbo_shape.rs index cd7c92029..454319dd8 100644 --- a/crates/xilem_web/src/svg/kurbo_shape.rs +++ b/crates/xilem_web/src/svg/kurbo_shape.rs @@ -9,11 +9,10 @@ use std::borrow::Cow; use xilem_core::{Id, MessageResult}; use crate::{ - context::{ChangeFlags, Cx}, + context::{ChangeFlags, Cx, HtmlProps}, interfaces::sealed::Sealed, - vecmap::VecMap, view::{View, ViewMarker}, - AttributeValue, IntoAttributeValue, SVG_NS, + IntoAttributeValue, SVG_NS, }; macro_rules! generate_dom_interface_impl { @@ -29,7 +28,7 @@ impl ViewMarker for Line {} impl Sealed for Line {} impl View for Line { - type State = VecMap, AttributeValue>; + type State = HtmlProps; type Element = web_sys::Element; fn build(&self, cx: &mut Cx) -> (Id, Self::State, Self::Element) { @@ -37,9 +36,9 @@ impl View for Line { cx.add_attr_to_element(&"y1".into(), &self.p0.y.into_attr_value()); cx.add_attr_to_element(&"x2".into(), &self.p1.x.into_attr_value()); cx.add_attr_to_element(&"y2".into(), &self.p1.y.into_attr_value()); - let (el, attributes) = cx.build_element(SVG_NS, "line"); + let (el, props) = cx.build_element(SVG_NS, "line"); let id = Id::next(); - (id, attributes, el) + (id, props, el) } fn rebuild( @@ -47,14 +46,14 @@ impl View for Line { cx: &mut Cx, _prev: &Self, _id: &mut Id, - attributes: &mut Self::State, + props: &mut Self::State, element: &mut Self::Element, ) -> ChangeFlags { cx.add_attr_to_element(&"x1".into(), &self.p0.x.into_attr_value()); cx.add_attr_to_element(&"y1".into(), &self.p0.y.into_attr_value()); cx.add_attr_to_element(&"x2".into(), &self.p1.x.into_attr_value()); cx.add_attr_to_element(&"y2".into(), &self.p1.y.into_attr_value()); - cx.rebuild_element(element, attributes) + cx.rebuild_element(element, props) } fn message( @@ -75,7 +74,7 @@ impl ViewMarker for Rect {} impl Sealed for Rect {} impl View for Rect { - type State = VecMap, AttributeValue>; + type State = HtmlProps; type Element = web_sys::Element; fn build(&self, cx: &mut Cx) -> (Id, Self::State, Self::Element) { @@ -84,9 +83,9 @@ impl View for Rect { let size = self.size(); cx.add_attr_to_element(&"width".into(), &size.width.into_attr_value()); cx.add_attr_to_element(&"height".into(), &size.height.into_attr_value()); - let (el, attributes) = cx.build_element(SVG_NS, "rect"); + let (el, props) = cx.build_element(SVG_NS, "rect"); let id = Id::next(); - (id, attributes, el) + (id, props, el) } fn rebuild( @@ -94,7 +93,7 @@ impl View for Rect { cx: &mut Cx, _prev: &Self, _id: &mut Id, - attributes: &mut Self::State, + props: &mut Self::State, element: &mut Self::Element, ) -> ChangeFlags { cx.add_attr_to_element(&"x".into(), &self.x0.into_attr_value()); @@ -102,7 +101,7 @@ impl View for Rect { let size = self.size(); cx.add_attr_to_element(&"width".into(), &size.width.into_attr_value()); cx.add_attr_to_element(&"height".into(), &size.height.into_attr_value()); - cx.rebuild_element(element, attributes) + cx.rebuild_element(element, props) } fn message( @@ -123,16 +122,16 @@ impl ViewMarker for Circle {} impl Sealed for Circle {} impl View for Circle { - type State = VecMap, AttributeValue>; + type State = HtmlProps; type Element = web_sys::Element; fn build(&self, cx: &mut Cx) -> (Id, Self::State, Self::Element) { cx.add_attr_to_element(&"cx".into(), &self.center.x.into_attr_value()); cx.add_attr_to_element(&"cy".into(), &self.center.y.into_attr_value()); cx.add_attr_to_element(&"r".into(), &self.radius.into_attr_value()); - let (el, attributes) = cx.build_element(SVG_NS, "circle"); + let (el, props) = cx.build_element(SVG_NS, "circle"); let id = Id::next(); - (id, attributes, el) + (id, props, el) } fn rebuild( @@ -140,13 +139,13 @@ impl View for Circle { cx: &mut Cx, _prev: &Self, _id: &mut Id, - attributes: &mut Self::State, + props: &mut Self::State, element: &mut Self::Element, ) -> ChangeFlags { cx.add_attr_to_element(&"cx".into(), &self.center.x.into_attr_value()); cx.add_attr_to_element(&"cy".into(), &self.center.y.into_attr_value()); cx.add_attr_to_element(&"r".into(), &self.radius.into_attr_value()); - cx.rebuild_element(element, attributes) + cx.rebuild_element(element, props) } fn message( @@ -167,15 +166,15 @@ impl ViewMarker for BezPath {} impl Sealed for BezPath {} impl View for BezPath { - type State = (Cow<'static, str>, VecMap, AttributeValue>); + type State = (Cow<'static, str>, HtmlProps); type Element = web_sys::Element; fn build(&self, cx: &mut Cx) -> (Id, Self::State, Self::Element) { let svg_repr = Cow::from(self.to_svg()); cx.add_attr_to_element(&"d".into(), &svg_repr.clone().into_attr_value()); - let (el, attributes) = cx.build_element(SVG_NS, "path"); + let (el, props) = cx.build_element(SVG_NS, "path"); let id = Id::next(); - (id, (svg_repr, attributes), el) + (id, (svg_repr, props), el) } fn rebuild( @@ -183,7 +182,7 @@ impl View for BezPath { cx: &mut Cx, prev: &Self, _id: &mut Id, - (svg_repr, attributes): &mut Self::State, + (svg_repr, props): &mut Self::State, element: &mut Self::Element, ) -> ChangeFlags { // slight optimization to avoid serialization/allocation @@ -191,7 +190,7 @@ impl View for BezPath { *svg_repr = Cow::from(self.to_svg()); } cx.add_attr_to_element(&"d".into(), &svg_repr.clone().into_attr_value()); - cx.rebuild_element(element, attributes) + cx.rebuild_element(element, props) } fn message( diff --git a/crates/xilem_web/src/vecmap.rs b/crates/xilem_web/src/vecmap.rs index 3a76cb777..10d1ddfe4 100644 --- a/crates/xilem_web/src/vecmap.rs +++ b/crates/xilem_web/src/vecmap.rs @@ -1,4 +1,4 @@ -use std::{borrow::Borrow, ops::Index}; +use std::{borrow::Borrow, fmt, ops::Index}; /// Basically an ordered Map (similar as BTreeMap) with a Vec as backend for very few elements /// As it uses linear search instead of a tree traversal, @@ -11,6 +11,12 @@ impl Default for VecMap { } } +impl fmt::Debug for VecMap { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_map().entries(self.iter()).finish() + } +} + impl VecMap { /// Returns a reference to the value corresponding to the key. /// diff --git a/crates/xilem_web/web_examples/counter/src/main.rs b/crates/xilem_web/web_examples/counter/src/main.rs index 850a75024..de1b9b90e 100644 --- a/crates/xilem_web/web_examples/counter/src/main.rs +++ b/crates/xilem_web/web_examples/counter/src/main.rs @@ -8,7 +8,7 @@ use xilem_web::{ #[derive(Default)] struct AppState { clicks: i32, - class: &'static str, + class: Option<&'static str>, text: String, } @@ -23,10 +23,10 @@ impl AppState { self.clicks = 0; } fn change_class(&mut self) { - if self.class == "gray" { - self.class = "green"; + if self.class == Some("gray") { + self.class = Some("green"); } else { - self.class = "gray"; + self.class = Some("gray"); } } @@ -49,7 +49,7 @@ fn btn( fn app_logic(state: &mut AppState) -> impl View { el::div(( - el::span(format!("clicked {} times", state.clicks)).attr("class", state.class), + el::span(format!("clicked {} times", state.clicks)).class(state.class), el::br(()), btn("+1 click", |state, _| state.increment()), btn("-1 click", |state, _| state.decrement()), diff --git a/crates/xilem_web/web_examples/todomvc/src/main.rs b/crates/xilem_web/web_examples/todomvc/src/main.rs index b49cc3bf8..74a6651d8 100644 --- a/crates/xilem_web/web_examples/todomvc/src/main.rs +++ b/crates/xilem_web/web_examples/todomvc/src/main.rs @@ -4,7 +4,8 @@ use state::{AppState, Filter, Todo}; use wasm_bindgen::JsCast; use xilem_web::{ - elements::html as el, get_element_by_id, interfaces::*, Action, Adapt, App, MessageResult, View, + elements::html as el, get_element_by_id, interfaces::*, style as s, Action, Adapt, App, + MessageResult, View, }; // All of these actions arise from within a `Todo`, but we need access to the full state to reduce @@ -19,16 +20,8 @@ enum TodoAction { impl Action for TodoAction {} fn todo_item(todo: &mut Todo, editing: bool) -> impl Element { - let mut class = String::new(); - if todo.completed { - class.push_str(" completed"); - } - if editing { - class.push_str(" editing"); - } - let checkbox = el::input(()) - .attr("class", "toggle") + .class("toggle") .attr("type", "checkbox") .attr("checked", todo.completed) .on_click(|state: &mut Todo, _| state.completed = !state.completed); @@ -39,13 +32,13 @@ fn todo_item(todo: &mut Todo, editing: bool) -> impl Element { el::label(todo.title.clone()) .on_dblclick(|state: &mut Todo, _| TodoAction::SetEditing(state.id)), el::button(()) - .attr("class", "destroy") + .class("destroy") .on_click(|state: &mut Todo, _| TodoAction::Destroy(state.id)), )) - .attr("class", "view"), + .class("view"), el::input(()) .attr("value", todo.title_editing.clone()) - .attr("class", "edit") + .class("edit") .on_keydown(|state: &mut Todo, evt| { let key = evt.key(); if key == "Enter" { @@ -70,7 +63,8 @@ fn todo_item(todo: &mut Todo, editing: bool) -> impl Element { .passive(true) .on_blur(|_, _| TodoAction::CancelEditing), )) - .attr("class", class) + .class(todo.completed.then_some("completed")) + .class(editing.then_some("editing")) } fn footer_view(state: &mut AppState, should_display: bool) -> impl Element { @@ -82,7 +76,7 @@ fn footer_view(state: &mut AppState, should_display: bool) -> impl Element 0).then(|| { Element::on_click( - el::button("Clear completed").attr("class", "clear-completed"), + el::button("Clear completed").class("clear-completed"), |state: &mut AppState, _| { state.todos.retain(|todo| !todo.completed); }, @@ -96,12 +90,12 @@ fn footer_view(state: &mut AppState, should_display: bool) -> impl Element impl Element impl Element impl Element { @@ -158,17 +152,17 @@ fn main_view(state: &mut AppState, should_display: bool) -> impl Element impl View { @@ -177,7 +171,7 @@ fn app_logic(state: &mut AppState) -> impl View { let main = main_view(state, some_todos); let footer = footer_view(state, some_todos); let input = el::input(()) - .attr("class", "new-todo") + .class("new-todo") .attr("placeholder", "What needs to be done?") .attr("value", state.new_todo.clone()) .attr("autofocus", true); @@ -202,7 +196,7 @@ fn app_logic(state: &mut AppState) -> impl View { }) .passive(false), )) - .attr("class", "header"), + .class("header"), main, footer, ))