From 8f31522080d263867cad838b299b1011e74a4765 Mon Sep 17 00:00:00 2001 From: Richard Dodd Date: Sun, 24 Mar 2024 18:11:48 +0000 Subject: [PATCH 01/10] add class interface --- Cargo.lock | 124 ++++++++++++++++++++++++ Cargo.toml | 3 +- crates/xilem_web/Cargo.toml | 1 + crates/xilem_web/src/class.rs | 52 ++++++++++ crates/xilem_web/src/context.rs | 61 +++++++++++- crates/xilem_web/src/elements.rs | 14 ++- crates/xilem_web/src/lib.rs | 1 + crates/xilem_web/src/svg/kurbo_shape.rs | 53 ++++++---- 8 files changed, 281 insertions(+), 28 deletions(-) create mode 100644 crates/xilem_web/src/class.rs diff --git a/Cargo.lock b/Cargo.lock index 8c5406794..6d529617c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -184,6 +184,15 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "block-sys" version = "0.2.1" @@ -418,12 +427,31 @@ dependencies = [ "xilem_web", ] +[[package]] +name = "cpufeatures" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" +dependencies = [ + "libc", +] + [[package]] name = "crossbeam-utils" version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + [[package]] name = "cursor-icon" version = "1.1.0" @@ -440,6 +468,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + [[package]] name = "dispatch" version = "0.2.0" @@ -589,6 +627,16 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "gethostname" version = "0.4.3" @@ -1278,6 +1326,51 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +[[package]] +name = "pest" +version = "2.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f8023d0fb78c8e03784ea1c7f3fa36e68a723138990b8d5a47d916b651e7a8" +dependencies = [ + "memchr", + "thiserror", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0d24f72393fd16ab6ac5738bc33cdb6a9aa73f8b902e8fe29cf4e67d7dd1026" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdc17e2a6c7d0a492f0158d7a4bd66cc17280308bbaff78d5bef566dca35ab80" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.52", +] + +[[package]] +name = "pest_meta" +version = "2.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "934cd7631c050f4674352a6e835d5f6711ffbfb9345c2fc0107155ac495ae293" +dependencies = [ + "once_cell", + "pest", + "sha2", +] + [[package]] name = "pin-project-lite" version = "0.2.13" @@ -1528,6 +1621,17 @@ dependencies = [ "serde", ] +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "sharded-slab" version = "0.1.7" @@ -1911,6 +2015,18 @@ version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17f77d76d837a7830fe1d4f12b7b4ba4192c1888001c7164257e4bc6d21d96b4" +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "ucd-trie" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" + [[package]] name = "unicode-ident" version = "1.0.12" @@ -2671,6 +2787,14 @@ dependencies = [ "xilem_core", ] +[[package]] +name = "xilem_web_spec" +version = "0.1.0" +dependencies = [ + "pest", + "pest_derive", +] + [[package]] name = "xkbcommon-dl" version = "0.4.2" diff --git a/Cargo.toml b/Cargo.toml index 2039d2571..f525eb58e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,11 +2,12 @@ members = [ "crates/xilem_core", "crates/xilem_web", + "crates/xilem_web/xilem_web_spec", "crates/xilem_web/web_examples/counter", "crates/xilem_web/web_examples/counter_custom_element", "crates/xilem_web/web_examples/todomvc", "crates/xilem_web/web_examples/mathml_svg", - "crates/xilem_web/web_examples/svgtoy", + "crates/xilem_web/web_examples/svgtoy", ] [workspace.package] diff --git a/crates/xilem_web/Cargo.toml b/crates/xilem_web/Cargo.toml index 43d04e426..0f9932346 100644 --- a/crates/xilem_web/Cargo.toml +++ b/crates/xilem_web/Cargo.toml @@ -35,6 +35,7 @@ version = "0.3.4" features = [ "console", "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..540de57f4 --- /dev/null +++ b/crates/xilem_web/src/class.rs @@ -0,0 +1,52 @@ +use std::{borrow::Cow, marker::PhantomData}; + +use xilem_core::{Id, MessageResult}; + +use crate::{ + interfaces::{sealed::Sealed, Element}, + ChangeFlags, Cx, View, ViewMarker, +}; + +/// Applies a class to the underlying element. +pub struct Class { + pub(crate) element: E, + pub(crate) class_name: Cow<'static, str>, + 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) { + cx.add_class_to_element(&self.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 { + cx.add_class_to_element(&self.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..e34981842 100644 --- a/crates/xilem_web/src/context.rs +++ b/crates/xilem_web/src/context.rs @@ -41,12 +41,21 @@ fn remove_attribute(element: &web_sys::Element, name: &str) { } } +fn set_class(element: &web_sys::Element, class_name: &str) { + element.class_list().add_1(class_name).unwrap_throw() +} + +fn remove_class(element: &web_sys::Element, class_name: &str) { + element.class_list().remove_1(class_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_classes: VecMap, app_ref: Option>, } @@ -70,6 +79,7 @@ impl Cx { document: crate::document(), app_ref: None, current_element_attributes: Default::default(), + current_element_classes: Default::default(), } } @@ -145,21 +155,28 @@ impl Cx { &mut self, ns: &str, name: &str, - ) -> (web_sys::Element, VecMap) { + ) -> ( + web_sys::Element, + VecMap, + VecMap, + ) { let el = self .document .create_element_ns(Some(ns), name) .expect("could not create element"); let attributes = self.apply_attributes(&el); - (el, attributes) + let classes = self.apply_classes(&el); + (el, attributes, classes) } pub(crate) fn rebuild_element( &mut self, element: &web_sys::Element, attributes: &mut VecMap, + classes: &mut VecMap, ) -> ChangeFlags { self.apply_attribute_changes(element, attributes) + | self.apply_class_changes(element, classes) } // TODO Not sure how multiple attribute definitions with the same name should be handled (e.g. `e.attr("class", "a").attr("class", "b")`) @@ -174,6 +191,13 @@ impl Cx { } } + 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_classes.contains_key(class_name) { + self.current_element_classes.insert(class_name.clone(), ()); + } + } + pub(crate) fn apply_attributes( &mut self, element: &web_sys::Element, @@ -210,6 +234,39 @@ impl Cx { changed } + pub(crate) fn apply_classes(&mut self, element: &web_sys::Element) -> VecMap { + let mut classes = VecMap::default(); + std::mem::swap(&mut classes, &mut self.current_element_classes); + for (class_name, ()) in classes.iter() { + set_class(element, class_name); + } + classes + } + + 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.current_element_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.current_element_classes); + self.current_element_classes.clear(); + changed + } + pub fn message_thunk(&self) -> MessageThunk { MessageThunk { id_path: self.id_path.clone(), diff --git a/crates/xilem_web/src/elements.rs b/crates/xilem_web/src/elements.rs index 1a48bf596..1003e9414 100644 --- a/crates/xilem_web/src/elements.rs +++ b/crates/xilem_web/src/elements.rs @@ -18,6 +18,7 @@ type CowStr = std::borrow::Cow<'static, str>; pub struct ElementState { pub(crate) children_states: ViewSeqState, pub(crate) attributes: VecMap, + pub(crate) classes: VecMap, pub(crate) child_elements: Vec, /// This is temporary cache for elements while updating/diffing, /// after usage it shouldn't contain any elements, @@ -150,7 +151,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, attributes, classes) = cx.build_element(HTML_NS, &self.name); let mut child_elements = vec![]; let mut scratch = vec![]; @@ -172,6 +173,7 @@ where child_elements, scratch, attributes, + classes, }; (id, state, el) } @@ -193,8 +195,9 @@ 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()); + let (new_element, attributes, classes) = cx.build_element(HTML_NS, self.node_name()); state.attributes = attributes; + state.classes = classes; // 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 +206,7 @@ where changed |= ChangeFlags::STRUCTURE; } - changed |= cx.rebuild_element(element, &mut state.attributes); + changed |= cx.rebuild_element(element, &mut state.attributes, &mut state.classes); // update children let mut splice = @@ -280,7 +283,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, attributes, classes) = cx.build_element($ns, $tag_name); let mut child_elements = vec![]; let mut scratch = vec![]; @@ -301,6 +304,7 @@ macro_rules! define_element { child_elements, scratch, attributes, + classes, }; (id, state, el) } @@ -315,7 +319,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.attributes, &mut state.classes); // update children let mut splice = ChildrenSplice::new(&mut state.child_elements, &mut state.scratch, element); diff --git a/crates/xilem_web/src/lib.rs b/crates/xilem_web/src/lib.rs index a7c6d1e3d..6f40ca133 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; diff --git a/crates/xilem_web/src/svg/kurbo_shape.rs b/crates/xilem_web/src/svg/kurbo_shape.rs index cd7c92029..d40c7136a 100644 --- a/crates/xilem_web/src/svg/kurbo_shape.rs +++ b/crates/xilem_web/src/svg/kurbo_shape.rs @@ -29,7 +29,10 @@ impl ViewMarker for Line {} impl Sealed for Line {} impl View for Line { - type State = VecMap, AttributeValue>; + type State = ( + VecMap, AttributeValue>, + VecMap, ()>, + ); type Element = web_sys::Element; fn build(&self, cx: &mut Cx) -> (Id, Self::State, Self::Element) { @@ -37,9 +40,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, attributes, classes) = cx.build_element(SVG_NS, "line"); let id = Id::next(); - (id, attributes, el) + (id, (attributes, classes), el) } fn rebuild( @@ -47,14 +50,14 @@ impl View for Line { cx: &mut Cx, _prev: &Self, _id: &mut Id, - attributes: &mut Self::State, + (attributes, classes): &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, attributes, classes) } fn message( @@ -75,7 +78,10 @@ impl ViewMarker for Rect {} impl Sealed for Rect {} impl View for Rect { - type State = VecMap, AttributeValue>; + type State = ( + VecMap, AttributeValue>, + VecMap, ()>, + ); type Element = web_sys::Element; fn build(&self, cx: &mut Cx) -> (Id, Self::State, Self::Element) { @@ -84,9 +90,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, attributes, classes) = cx.build_element(SVG_NS, "rect"); let id = Id::next(); - (id, attributes, el) + (id, (attributes, classes), el) } fn rebuild( @@ -94,7 +100,7 @@ impl View for Rect { cx: &mut Cx, _prev: &Self, _id: &mut Id, - attributes: &mut Self::State, + (attributes, classes): &mut Self::State, element: &mut Self::Element, ) -> ChangeFlags { cx.add_attr_to_element(&"x".into(), &self.x0.into_attr_value()); @@ -102,7 +108,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, attributes, classes) } fn message( @@ -123,16 +129,19 @@ impl ViewMarker for Circle {} impl Sealed for Circle {} impl View for Circle { - type State = VecMap, AttributeValue>; + type State = ( + VecMap, AttributeValue>, + VecMap, ()>, + ); 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, attributes, classes) = cx.build_element(SVG_NS, "circle"); let id = Id::next(); - (id, attributes, el) + (id, (attributes, classes), el) } fn rebuild( @@ -140,13 +149,13 @@ impl View for Circle { cx: &mut Cx, _prev: &Self, _id: &mut Id, - attributes: &mut Self::State, + (attributes, classes): &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, attributes, classes) } fn message( @@ -167,15 +176,19 @@ impl ViewMarker for BezPath {} impl Sealed for BezPath {} impl View for BezPath { - type State = (Cow<'static, str>, VecMap, AttributeValue>); + type State = ( + Cow<'static, str>, + VecMap, AttributeValue>, + VecMap, ()>, + ); 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, attributes, classes) = cx.build_element(SVG_NS, "path"); let id = Id::next(); - (id, (svg_repr, attributes), el) + (id, (svg_repr, attributes, classes), el) } fn rebuild( @@ -183,7 +196,7 @@ impl View for BezPath { cx: &mut Cx, prev: &Self, _id: &mut Id, - (svg_repr, attributes): &mut Self::State, + (svg_repr, attributes, classes): &mut Self::State, element: &mut Self::Element, ) -> ChangeFlags { // slight optimization to avoid serialization/allocation @@ -191,7 +204,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, attributes, classes) } fn message( From c2ca085035794663126cbeccc1b24aef10c8342b Mon Sep 17 00:00:00 2001 From: Richard Dodd Date: Sun, 24 Mar 2024 23:16:23 +0000 Subject: [PATCH 02/10] add style interface --- Cargo.lock | 124 ------------------ Cargo.toml | 1 - crates/xilem_web/Cargo.toml | 1 + crates/xilem_web/src/class.rs | 10 +- crates/xilem_web/src/context.rs | 118 ++++++++++++++++- crates/xilem_web/src/elements.rs | 20 ++- crates/xilem_web/src/interfaces.rs | 48 ++++++- crates/xilem_web/src/lib.rs | 1 + crates/xilem_web/src/style.rs | 56 ++++++++ crates/xilem_web/src/svg/kurbo_shape.rs | 36 ++--- .../web_examples/counter/src/main.rs | 10 +- .../web_examples/todomvc/src/main.rs | 47 +++---- 12 files changed, 283 insertions(+), 189 deletions(-) create mode 100644 crates/xilem_web/src/style.rs diff --git a/Cargo.lock b/Cargo.lock index 6d529617c..8c5406794 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -184,15 +184,6 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] - [[package]] name = "block-sys" version = "0.2.1" @@ -427,31 +418,12 @@ dependencies = [ "xilem_web", ] -[[package]] -name = "cpufeatures" -version = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" -dependencies = [ - "libc", -] - [[package]] name = "crossbeam-utils" version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" -[[package]] -name = "crypto-common" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" -dependencies = [ - "generic-array", - "typenum", -] - [[package]] name = "cursor-icon" version = "1.1.0" @@ -468,16 +440,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer", - "crypto-common", -] - [[package]] name = "dispatch" version = "0.2.0" @@ -627,16 +589,6 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", -] - [[package]] name = "gethostname" version = "0.4.3" @@ -1326,51 +1278,6 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" -[[package]] -name = "pest" -version = "2.7.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f8023d0fb78c8e03784ea1c7f3fa36e68a723138990b8d5a47d916b651e7a8" -dependencies = [ - "memchr", - "thiserror", - "ucd-trie", -] - -[[package]] -name = "pest_derive" -version = "2.7.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0d24f72393fd16ab6ac5738bc33cdb6a9aa73f8b902e8fe29cf4e67d7dd1026" -dependencies = [ - "pest", - "pest_generator", -] - -[[package]] -name = "pest_generator" -version = "2.7.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdc17e2a6c7d0a492f0158d7a4bd66cc17280308bbaff78d5bef566dca35ab80" -dependencies = [ - "pest", - "pest_meta", - "proc-macro2", - "quote", - "syn 2.0.52", -] - -[[package]] -name = "pest_meta" -version = "2.7.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "934cd7631c050f4674352a6e835d5f6711ffbfb9345c2fc0107155ac495ae293" -dependencies = [ - "once_cell", - "pest", - "sha2", -] - [[package]] name = "pin-project-lite" version = "0.2.13" @@ -1621,17 +1528,6 @@ dependencies = [ "serde", ] -[[package]] -name = "sha2" -version = "0.10.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - [[package]] name = "sharded-slab" version = "0.1.7" @@ -2015,18 +1911,6 @@ version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17f77d76d837a7830fe1d4f12b7b4ba4192c1888001c7164257e4bc6d21d96b4" -[[package]] -name = "typenum" -version = "1.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" - -[[package]] -name = "ucd-trie" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" - [[package]] name = "unicode-ident" version = "1.0.12" @@ -2787,14 +2671,6 @@ dependencies = [ "xilem_core", ] -[[package]] -name = "xilem_web_spec" -version = "0.1.0" -dependencies = [ - "pest", - "pest_derive", -] - [[package]] name = "xkbcommon-dl" version = "0.4.2" diff --git a/Cargo.toml b/Cargo.toml index f525eb58e..9339113a2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,6 @@ members = [ "crates/xilem_core", "crates/xilem_web", - "crates/xilem_web/xilem_web_spec", "crates/xilem_web/web_examples/counter", "crates/xilem_web/web_examples/counter_custom_element", "crates/xilem_web/web_examples/todomvc", diff --git a/crates/xilem_web/Cargo.toml b/crates/xilem_web/Cargo.toml index 0f9932346..e6c13502a 100644 --- a/crates/xilem_web/Cargo.toml +++ b/crates/xilem_web/Cargo.toml @@ -34,6 +34,7 @@ peniko = { git = "https://github.com/linebender/peniko", rev = "629fc3325b016a8c version = "0.3.4" features = [ "console", + "CssStyleDeclaration", "Document", "DomTokenList", "Element", diff --git a/crates/xilem_web/src/class.rs b/crates/xilem_web/src/class.rs index 540de57f4..267050085 100644 --- a/crates/xilem_web/src/class.rs +++ b/crates/xilem_web/src/class.rs @@ -10,7 +10,7 @@ use crate::{ /// Applies a class to the underlying element. pub struct Class { pub(crate) element: E, - pub(crate) class_name: Cow<'static, str>, + pub(crate) class_name: Option>, pub(crate) phantom: PhantomData (T, A)>, } @@ -22,7 +22,9 @@ impl, T, A> View for Class { type Element = E::Element; fn build(&self, cx: &mut Cx) -> (Id, Self::State, Self::Element) { - cx.add_class_to_element(&self.class_name); + if let Some(class_name) = &self.class_name { + cx.add_class_to_element(class_name); + } self.element.build(cx) } @@ -34,7 +36,9 @@ impl, T, A> View for Class { state: &mut Self::State, element: &mut Self::Element, ) -> ChangeFlags { - cx.add_class_to_element(&self.class_name); + if let Some(class_name) = &self.class_name { + cx.add_class_to_element(class_name); + } self.element.rebuild(cx, &prev.element, id, state, element) } diff --git a/crates/xilem_web/src/context.rs b/crates/xilem_web/src/context.rs index e34981842..055cfab69 100644 --- a/crates/xilem_web/src/context.rs +++ b/crates/xilem_web/src/context.rs @@ -42,13 +42,43 @@ fn remove_attribute(element: &web_sys::Element, name: &str) { } fn set_class(element: &web_sys::Element, class_name: &str) { + #[cfg(debug_assertions)] + if class_name.is_empty() { + panic!("class names cannot be the empty string"); + } + #[cfg(debug_assertions)] + if class_name.contains(' ') { + panic!("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) { + #[cfg(debug_assertions)] + if class_name.is_empty() { + panic!("class names cannot be the empty string"); + } + #[cfg(debug_assertions)] + if class_name.contains(' ') { + panic!("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) { + // styles will be ignored for non-html elements (e.g. SVG) + if let Some(el) = element.dyn_ref::() { + el.style().set_property(name, value).unwrap_throw() + } +} + +fn remove_style(element: &web_sys::Element, name: &str) { + // styles will be ignored for non-html elements (e.g. SVG) + 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, @@ -56,6 +86,7 @@ pub struct Cx { // 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_classes: VecMap, + pub(crate) current_element_styles: VecMap, app_ref: Option>, } @@ -80,6 +111,7 @@ impl Cx { app_ref: None, current_element_attributes: Default::default(), current_element_classes: Default::default(), + current_element_styles: Default::default(), } } @@ -159,6 +191,7 @@ impl Cx { web_sys::Element, VecMap, VecMap, + VecMap, ) { let el = self .document @@ -166,7 +199,8 @@ impl Cx { .expect("could not create element"); let attributes = self.apply_attributes(&el); let classes = self.apply_classes(&el); - (el, attributes, classes) + let styles = self.apply_styles(&el); + (el, attributes, classes, styles) } pub(crate) fn rebuild_element( @@ -174,14 +208,56 @@ impl Cx { element: &web_sys::Element, attributes: &mut VecMap, classes: &mut VecMap, + styles: &mut VecMap, ) -> ChangeFlags { self.apply_attribute_changes(element, attributes) | self.apply_class_changes(element, classes) + | self.apply_style_changes(element, styles) } // 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) { + // Special-case class so it works with the `class` method + if name == "class" { + if let Some(value) = value { + let value = value.serialize(); + for class_name in value.split_ascii_whitespace() { + if !class_name.is_empty() + && !self.current_element_classes.contains_key(class_name) + { + self.current_element_classes + .insert(class_name.to_string().into(), ()); + } + } + } + return; + } + + // parse styles + if name == "style" { + if let Some(value) = value { + let value = value.serialize(); + for pair in value.split(';') { + let mut iter = pair.splitn(2, ':'); + let Some(name) = iter.next() else { + continue; + }; + let Some(value) = iter.next() else { + continue; + }; + if name.is_empty() || value.is_empty() { + continue; + } + if !self.current_element_styles.contains_key(name) { + self.current_element_styles + .insert(name.to_string().into(), value.to_string().into()); + } + } + } + return; + } + 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) { @@ -198,6 +274,13 @@ impl Cx { } } + pub(crate) fn add_style_to_element(&mut self, name: &CowStr, value: &CowStr) { + if !self.current_element_styles.contains_key(name) { + self.current_element_styles + .insert(name.clone(), value.clone()); + } + } + pub(crate) fn apply_attributes( &mut self, element: &web_sys::Element, @@ -267,6 +350,39 @@ impl Cx { changed } + pub(crate) fn apply_styles(&mut self, element: &web_sys::Element) -> VecMap { + let mut styles = VecMap::default(); + std::mem::swap(&mut styles, &mut self.current_element_styles); + for (name, value) in styles.iter() { + set_style(element, name, value); + } + styles + } + + 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.current_element_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.current_element_styles); + self.current_element_styles.clear(); + changed + } + pub fn message_thunk(&self) -> MessageThunk { MessageThunk { id_path: self.id_path.clone(), diff --git a/crates/xilem_web/src/elements.rs b/crates/xilem_web/src/elements.rs index 1003e9414..bd65a9213 100644 --- a/crates/xilem_web/src/elements.rs +++ b/crates/xilem_web/src/elements.rs @@ -19,6 +19,7 @@ pub struct ElementState { pub(crate) children_states: ViewSeqState, pub(crate) attributes: VecMap, pub(crate) classes: VecMap, + pub(crate) styles: VecMap, pub(crate) child_elements: Vec, /// This is temporary cache for elements while updating/diffing, /// after usage it shouldn't contain any elements, @@ -151,7 +152,7 @@ where type Element = web_sys::HtmlElement; fn build(&self, cx: &mut Cx) -> (Id, Self::State, Self::Element) { - let (el, attributes, classes) = cx.build_element(HTML_NS, &self.name); + let (el, attributes, classes, styles) = cx.build_element(HTML_NS, &self.name); let mut child_elements = vec![]; let mut scratch = vec![]; @@ -174,6 +175,7 @@ where scratch, attributes, classes, + styles, }; (id, state, el) } @@ -195,9 +197,11 @@ 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, classes) = cx.build_element(HTML_NS, self.node_name()); + let (new_element, attributes, classes, styles) = + cx.build_element(HTML_NS, self.node_name()); state.attributes = attributes; state.classes = classes; + state.styles = styles; // TODO could this be combined with child updates? while let Some(child) = element.child_nodes().get(0) { new_element.append_child(&child).unwrap_throw(); @@ -206,7 +210,12 @@ where changed |= ChangeFlags::STRUCTURE; } - changed |= cx.rebuild_element(element, &mut state.attributes, &mut state.classes); + changed |= cx.rebuild_element( + element, + &mut state.attributes, + &mut state.classes, + &mut state.styles, + ); // update children let mut splice = @@ -283,7 +292,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, classes) = cx.build_element($ns, $tag_name); + let (el, attributes, classes, styles) = cx.build_element($ns, $tag_name); let mut child_elements = vec![]; let mut scratch = vec![]; @@ -305,6 +314,7 @@ macro_rules! define_element { scratch, attributes, classes, + styles, }; (id, state, el) } @@ -319,7 +329,7 @@ macro_rules! define_element { ) -> ChangeFlags { let mut changed = ChangeFlags::empty(); - changed |= cx.rebuild_element(element, &mut state.attributes, &mut state.classes); + changed |= cx.rebuild_element(element, &mut state.attributes, &mut state.classes, &mut state.styles); // 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..388277a93 100644 --- a/crates/xilem_web/src/interfaces.rs +++ b/crates/xilem_web/src/interfaces.rs @@ -1,5 +1,5 @@ -use crate::{Pointer, PointerMsg, View, ViewMarker}; -use std::borrow::Cow; +use crate::{class::Class, style::Style, Pointer, PointerMsg, View, ViewMarker}; +use std::{borrow::Cow, marker::PhantomData}; use gloo::events::EventListenerOptions; use wasm_bindgen::JsCast; @@ -91,11 +91,45 @@ 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 a class to the wrapped element. + /// + /// If multiple classes are added, all will be applied to the element. + fn class(self, class: impl Into>) -> Class { + self.class_opt(Some(class)) + } + + /// Add an optional class to the wrapped element. + /// + /// If multiple classes are added, all will be applied to the element. + fn class_opt(self, class: Option>>) -> Class { + Class { + element: self, + class_name: class.map(Into::into), + phantom: PhantomData, + } + } + + /// Set a style attribute + fn style( + self, + name: impl Into>, + value: impl Into>, + ) -> Style { + self.style_opt(name, Some(value)) + } + + /// Set a style attribute + fn style_opt( + self, + name: impl Into>, + value: Option>>, + ) -> Style { + Style { + element: self, + name: name.into(), + value: value.map(Into::into), + phantom: PhantomData, + } } // event list from diff --git a/crates/xilem_web/src/lib.rs b/crates/xilem_web/src/lib.rs index 6f40ca133..d0d74d3a7 100644 --- a/crates/xilem_web/src/lib.rs +++ b/crates/xilem_web/src/lib.rs @@ -19,6 +19,7 @@ pub mod interfaces; mod one_of; mod optional_action; mod pointer; +mod style; pub mod svg; mod vecmap; mod view; diff --git a/crates/xilem_web/src/style.rs b/crates/xilem_web/src/style.rs new file mode 100644 index 000000000..59fcdb6fb --- /dev/null +++ b/crates/xilem_web/src/style.rs @@ -0,0 +1,56 @@ +use std::borrow::Cow; +use std::marker::PhantomData; + +use xilem_core::{Id, MessageResult}; + +use crate::{interfaces::sealed::Sealed, ChangeFlags, Cx, View, ViewMarker}; + +use super::interfaces::Element; + +pub struct Style { + pub(crate) element: E, + pub(crate) name: Cow<'static, str>, + pub(crate) value: Option>, + 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) { + if let Some(value) = &self.value { + cx.add_style_to_element(&self.name, 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 { + if let Some(value) = &self.value { + cx.add_style_to_element(&self.name, 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 d40c7136a..e65014ce4 100644 --- a/crates/xilem_web/src/svg/kurbo_shape.rs +++ b/crates/xilem_web/src/svg/kurbo_shape.rs @@ -32,6 +32,7 @@ impl View for Line { type State = ( VecMap, AttributeValue>, VecMap, ()>, + VecMap, Cow<'static, str>>, ); type Element = web_sys::Element; @@ -40,9 +41,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, classes) = cx.build_element(SVG_NS, "line"); + let (el, attributes, classes, styles) = cx.build_element(SVG_NS, "line"); let id = Id::next(); - (id, (attributes, classes), el) + (id, (attributes, classes, styles), el) } fn rebuild( @@ -50,14 +51,14 @@ impl View for Line { cx: &mut Cx, _prev: &Self, _id: &mut Id, - (attributes, classes): &mut Self::State, + (attributes, classes, styles): &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, classes) + cx.rebuild_element(element, attributes, classes, styles) } fn message( @@ -81,6 +82,7 @@ impl View for Rect { type State = ( VecMap, AttributeValue>, VecMap, ()>, + VecMap, Cow<'static, str>>, ); type Element = web_sys::Element; @@ -90,9 +92,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, classes) = cx.build_element(SVG_NS, "rect"); + let (el, attributes, classes, styles) = cx.build_element(SVG_NS, "rect"); let id = Id::next(); - (id, (attributes, classes), el) + (id, (attributes, classes, styles), el) } fn rebuild( @@ -100,7 +102,7 @@ impl View for Rect { cx: &mut Cx, _prev: &Self, _id: &mut Id, - (attributes, classes): &mut Self::State, + (attributes, classes, styles): &mut Self::State, element: &mut Self::Element, ) -> ChangeFlags { cx.add_attr_to_element(&"x".into(), &self.x0.into_attr_value()); @@ -108,7 +110,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, classes) + cx.rebuild_element(element, attributes, classes, styles) } fn message( @@ -132,6 +134,7 @@ impl View for Circle { type State = ( VecMap, AttributeValue>, VecMap, ()>, + VecMap, Cow<'static, str>>, ); type Element = web_sys::Element; @@ -139,9 +142,9 @@ impl View for Circle { 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, classes) = cx.build_element(SVG_NS, "circle"); + let (el, attributes, classes, styles) = cx.build_element(SVG_NS, "circle"); let id = Id::next(); - (id, (attributes, classes), el) + (id, (attributes, classes, styles), el) } fn rebuild( @@ -149,13 +152,13 @@ impl View for Circle { cx: &mut Cx, _prev: &Self, _id: &mut Id, - (attributes, classes): &mut Self::State, + (attributes, classes, styles): &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, classes) + cx.rebuild_element(element, attributes, classes, styles) } fn message( @@ -180,15 +183,16 @@ impl View for BezPath { Cow<'static, str>, VecMap, AttributeValue>, VecMap, ()>, + VecMap, Cow<'static, str>>, ); 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, classes) = cx.build_element(SVG_NS, "path"); + let (el, attributes, classes, styles) = cx.build_element(SVG_NS, "path"); let id = Id::next(); - (id, (svg_repr, attributes, classes), el) + (id, (svg_repr, attributes, classes, styles), el) } fn rebuild( @@ -196,7 +200,7 @@ impl View for BezPath { cx: &mut Cx, prev: &Self, _id: &mut Id, - (svg_repr, attributes, classes): &mut Self::State, + (svg_repr, attributes, classes, styles): &mut Self::State, element: &mut Self::Element, ) -> ChangeFlags { // slight optimization to avoid serialization/allocation @@ -204,7 +208,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, classes) + cx.rebuild_element(element, attributes, classes, styles) } fn message( diff --git a/crates/xilem_web/web_examples/counter/src/main.rs b/crates/xilem_web/web_examples/counter/src/main.rs index 850a75024..03782df73 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_opt(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..e33d067ac 100644 --- a/crates/xilem_web/web_examples/todomvc/src/main.rs +++ b/crates/xilem_web/web_examples/todomvc/src/main.rs @@ -19,16 +19,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 +31,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 +62,8 @@ fn todo_item(todo: &mut Todo, editing: bool) -> impl Element { .passive(true) .on_blur(|_, _| TodoAction::CancelEditing), )) - .attr("class", class) + .class_opt(todo.completed.then_some("completed")) + .class_opt(editing.then_some("editing")) } fn footer_view(state: &mut AppState, should_display: bool) -> impl Element { @@ -82,7 +75,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 +89,12 @@ fn footer_view(state: &mut AppState, should_display: bool) -> impl Element impl Element impl Element impl Element { @@ -158,17 +151,17 @@ fn main_view(state: &mut AppState, should_display: bool) -> impl Element impl View { @@ -177,7 +170,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 +195,7 @@ fn app_logic(state: &mut AppState) -> impl View { }) .passive(false), )) - .attr("class", "header"), + .class("header"), main, footer, )) From a00b158e24e4feb1d7e499236582c47a5055d77b Mon Sep 17 00:00:00 2001 From: Richard Dodd Date: Tue, 26 Mar 2024 21:32:58 +0000 Subject: [PATCH 03/10] remove parse style/class logic, and panic instead (in dev) --- crates/xilem_web/src/context.rs | 81 +++++++++++---------------------- 1 file changed, 26 insertions(+), 55 deletions(-) diff --git a/crates/xilem_web/src/context.rs b/crates/xilem_web/src/context.rs index 055cfab69..4106958dd 100644 --- a/crates/xilem_web/src/context.rs +++ b/crates/xilem_web/src/context.rs @@ -42,26 +42,26 @@ fn remove_attribute(element: &web_sys::Element, name: &str) { } fn set_class(element: &web_sys::Element, class_name: &str) { - #[cfg(debug_assertions)] - if class_name.is_empty() { - panic!("class names cannot be the empty string"); - } - #[cfg(debug_assertions)] - if class_name.contains(' ') { - panic!("class names cannot contain the ascii space character"); - } + 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) { - #[cfg(debug_assertions)] - if class_name.is_empty() { - panic!("class names cannot be the empty string"); - } - #[cfg(debug_assertions)] - if class_name.contains(' ') { - panic!("class names cannot contain the ascii space character"); - } + 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() } @@ -218,45 +218,16 @@ impl Cx { // 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) { - // Special-case class so it works with the `class` method - if name == "class" { - if let Some(value) = value { - let value = value.serialize(); - for class_name in value.split_ascii_whitespace() { - if !class_name.is_empty() - && !self.current_element_classes.contains_key(class_name) - { - self.current_element_classes - .insert(class_name.to_string().into(), ()); - } - } - } - return; - } - - // parse styles - if name == "style" { - if let Some(value) = value { - let value = value.serialize(); - for pair in value.split(';') { - let mut iter = pair.splitn(2, ':'); - let Some(name) = iter.next() else { - continue; - }; - let Some(value) = iter.next() else { - continue; - }; - if name.is_empty() || value.is_empty() { - continue; - } - if !self.current_element_styles.contains_key(name) { - self.current_element_styles - .insert(name.to_string().into(), value.to_string().into()); - } - } - } - return; - } + // 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)` From c4faf530922356f71a8d4e720890eabac269ee7a Mon Sep 17 00:00:00 2001 From: Richard Dodd Date: Fri, 29 Mar 2024 17:02:37 +0000 Subject: [PATCH 04/10] combine attributes, styles, classes into single struct --- crates/xilem_web/src/context.rs | 276 ++++++++++++------------ crates/xilem_web/src/elements.rs | 36 +--- crates/xilem_web/src/svg/kurbo_shape.rs | 62 ++---- crates/xilem_web/src/vecmap.rs | 8 +- 4 files changed, 182 insertions(+), 200 deletions(-) diff --git a/crates/xilem_web/src/context.rs b/crates/xilem_web/src/context.rs index 4106958dd..b3d2d7a9a 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. @@ -84,9 +209,7 @@ 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_classes: VecMap, - pub(crate) current_element_styles: VecMap, + pub(crate) current_element_props: HtmlProps, app_ref: Option>, } @@ -109,9 +232,7 @@ impl Cx { id_path: Vec::new(), document: crate::document(), app_ref: None, - current_element_attributes: Default::default(), - current_element_classes: Default::default(), - current_element_styles: Default::default(), + current_element_props: Default::default(), } } @@ -183,36 +304,21 @@ impl Cx { &self.document } - pub(crate) fn build_element( - &mut self, - ns: &str, - name: &str, - ) -> ( - web_sys::Element, - VecMap, - VecMap, - 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); - let classes = self.apply_classes(&el); - let styles = self.apply_styles(&el); - (el, attributes, classes, styles) + let props = self.current_element_props.apply(&el); + (el, props) } pub(crate) fn rebuild_element( &mut self, element: &web_sys::Element, - attributes: &mut VecMap, - classes: &mut VecMap, - styles: &mut VecMap, + props: &mut HtmlProps, ) -> ChangeFlags { - self.apply_attribute_changes(element, attributes) - | self.apply_class_changes(element, classes) - | self.apply_style_changes(element, styles) + 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")`) @@ -231,8 +337,9 @@ impl Cx { 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()); } } @@ -240,120 +347,21 @@ impl Cx { 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_classes.contains_key(class_name) { - self.current_element_classes.insert(class_name.clone(), ()); + if !self.current_element_props.classes.contains_key(class_name) { + self.current_element_props + .classes + .insert(class_name.clone(), ()); } } pub(crate) fn add_style_to_element(&mut self, name: &CowStr, value: &CowStr) { - if !self.current_element_styles.contains_key(name) { - self.current_element_styles + if !self.current_element_props.styles.contains_key(name) { + self.current_element_props + .styles .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()); - } - 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; - } - } - } - std::mem::swap(attributes, &mut self.current_element_attributes); - self.current_element_attributes.clear(); - changed - } - - pub(crate) fn apply_classes(&mut self, element: &web_sys::Element) -> VecMap { - let mut classes = VecMap::default(); - std::mem::swap(&mut classes, &mut self.current_element_classes); - for (class_name, ()) in classes.iter() { - set_class(element, class_name); - } - classes - } - - 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.current_element_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.current_element_classes); - self.current_element_classes.clear(); - changed - } - - pub(crate) fn apply_styles(&mut self, element: &web_sys::Element) -> VecMap { - let mut styles = VecMap::default(); - std::mem::swap(&mut styles, &mut self.current_element_styles); - for (name, value) in styles.iter() { - set_style(element, name, value); - } - styles - } - - 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.current_element_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.current_element_styles); - self.current_element_styles.clear(); - changed - } - pub fn message_thunk(&self) -> MessageThunk { MessageThunk { id_path: self.id_path.clone(), diff --git a/crates/xilem_web/src/elements.rs b/crates/xilem_web/src/elements.rs index bd65a9213..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,9 +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) classes: VecMap, - pub(crate) styles: 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, @@ -152,7 +150,7 @@ where type Element = web_sys::HtmlElement; fn build(&self, cx: &mut Cx) -> (Id, Self::State, Self::Element) { - let (el, attributes, classes, styles) = 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![]; @@ -173,9 +171,7 @@ where children_states, child_elements, scratch, - attributes, - classes, - styles, + props, }; (id, state, el) } @@ -197,11 +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, classes, styles) = - cx.build_element(HTML_NS, self.node_name()); - state.attributes = attributes; - state.classes = classes; - state.styles = styles; + 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(); @@ -210,12 +203,7 @@ where changed |= ChangeFlags::STRUCTURE; } - changed |= cx.rebuild_element( - element, - &mut state.attributes, - &mut state.classes, - &mut state.styles, - ); + changed |= cx.rebuild_element(element, &mut state.props); // update children let mut splice = @@ -292,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, classes, styles) = cx.build_element($ns, $tag_name); + let (el, props) = cx.build_element($ns, $tag_name); let mut child_elements = vec![]; let mut scratch = vec![]; @@ -312,9 +300,7 @@ macro_rules! define_element { children_states, child_elements, scratch, - attributes, - classes, - styles, + props, }; (id, state, el) } @@ -329,7 +315,7 @@ macro_rules! define_element { ) -> ChangeFlags { let mut changed = ChangeFlags::empty(); - changed |= cx.rebuild_element(element, &mut state.attributes, &mut state.classes, &mut state.styles); + 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/svg/kurbo_shape.rs b/crates/xilem_web/src/svg/kurbo_shape.rs index e65014ce4..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,11 +28,7 @@ impl ViewMarker for Line {} impl Sealed for Line {} impl View for Line { - type State = ( - VecMap, AttributeValue>, - VecMap, ()>, - VecMap, Cow<'static, str>>, - ); + type State = HtmlProps; type Element = web_sys::Element; fn build(&self, cx: &mut Cx) -> (Id, Self::State, Self::Element) { @@ -41,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, classes, styles) = cx.build_element(SVG_NS, "line"); + let (el, props) = cx.build_element(SVG_NS, "line"); let id = Id::next(); - (id, (attributes, classes, styles), el) + (id, props, el) } fn rebuild( @@ -51,14 +46,14 @@ impl View for Line { cx: &mut Cx, _prev: &Self, _id: &mut Id, - (attributes, classes, styles): &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, classes, styles) + cx.rebuild_element(element, props) } fn message( @@ -79,11 +74,7 @@ impl ViewMarker for Rect {} impl Sealed for Rect {} impl View for Rect { - type State = ( - VecMap, AttributeValue>, - VecMap, ()>, - VecMap, Cow<'static, str>>, - ); + type State = HtmlProps; type Element = web_sys::Element; fn build(&self, cx: &mut Cx) -> (Id, Self::State, Self::Element) { @@ -92,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, classes, styles) = cx.build_element(SVG_NS, "rect"); + let (el, props) = cx.build_element(SVG_NS, "rect"); let id = Id::next(); - (id, (attributes, classes, styles), el) + (id, props, el) } fn rebuild( @@ -102,7 +93,7 @@ impl View for Rect { cx: &mut Cx, _prev: &Self, _id: &mut Id, - (attributes, classes, styles): &mut Self::State, + props: &mut Self::State, element: &mut Self::Element, ) -> ChangeFlags { cx.add_attr_to_element(&"x".into(), &self.x0.into_attr_value()); @@ -110,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, classes, styles) + cx.rebuild_element(element, props) } fn message( @@ -131,20 +122,16 @@ impl ViewMarker for Circle {} impl Sealed for Circle {} impl View for Circle { - type State = ( - VecMap, AttributeValue>, - VecMap, ()>, - VecMap, Cow<'static, str>>, - ); + 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, classes, styles) = cx.build_element(SVG_NS, "circle"); + let (el, props) = cx.build_element(SVG_NS, "circle"); let id = Id::next(); - (id, (attributes, classes, styles), el) + (id, props, el) } fn rebuild( @@ -152,13 +139,13 @@ impl View for Circle { cx: &mut Cx, _prev: &Self, _id: &mut Id, - (attributes, classes, styles): &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, classes, styles) + cx.rebuild_element(element, props) } fn message( @@ -179,20 +166,15 @@ impl ViewMarker for BezPath {} impl Sealed for BezPath {} impl View for BezPath { - type State = ( - Cow<'static, str>, - VecMap, AttributeValue>, - VecMap, ()>, - VecMap, Cow<'static, str>>, - ); + 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, classes, styles) = cx.build_element(SVG_NS, "path"); + let (el, props) = cx.build_element(SVG_NS, "path"); let id = Id::next(); - (id, (svg_repr, attributes, classes, styles), el) + (id, (svg_repr, props), el) } fn rebuild( @@ -200,7 +182,7 @@ impl View for BezPath { cx: &mut Cx, prev: &Self, _id: &mut Id, - (svg_repr, attributes, classes, styles): &mut Self::State, + (svg_repr, props): &mut Self::State, element: &mut Self::Element, ) -> ChangeFlags { // slight optimization to avoid serialization/allocation @@ -208,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, classes, styles) + 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. /// From cbeef67bd86bfd9f4952c0208d305052b4640b4d Mon Sep 17 00:00:00 2001 From: Richard Dodd Date: Fri, 29 Mar 2024 20:26:45 +0000 Subject: [PATCH 05/10] make `class` and `style` methods polymorphic --- crates/xilem_web/src/class.rs | 68 ++++++++++- crates/xilem_web/src/interfaces.rs | 41 +++---- crates/xilem_web/src/lib.rs | 1 + crates/xilem_web/src/style.rs | 110 ++++++++++++++++-- .../web_examples/counter/src/main.rs | 2 +- .../web_examples/todomvc/src/main.rs | 17 +-- 6 files changed, 194 insertions(+), 45 deletions(-) diff --git a/crates/xilem_web/src/class.rs b/crates/xilem_web/src/class.rs index 267050085..515f67d8a 100644 --- a/crates/xilem_web/src/class.rs +++ b/crates/xilem_web/src/class.rs @@ -7,10 +7,72 @@ use crate::{ 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 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_name: Option>, + pub(crate) class_names: Vec>, pub(crate) phantom: PhantomData (T, A)>, } @@ -22,7 +84,7 @@ impl, T, A> View for Class { type Element = E::Element; fn build(&self, cx: &mut Cx) -> (Id, Self::State, Self::Element) { - if let Some(class_name) = &self.class_name { + for class_name in &self.class_names { cx.add_class_to_element(class_name); } self.element.build(cx) @@ -36,7 +98,7 @@ impl, T, A> View for Class { state: &mut Self::State, element: &mut Self::Element, ) -> ChangeFlags { - if let Some(class_name) = &self.class_name { + for class_name in &self.class_names { cx.add_class_to_element(class_name); } self.element.rebuild(cx, &prev.element, id, state, element) diff --git a/crates/xilem_web/src/interfaces.rs b/crates/xilem_web/src/interfaces.rs index 388277a93..62f98b6ae 100644 --- a/crates/xilem_web/src/interfaces.rs +++ b/crates/xilem_web/src/interfaces.rs @@ -1,4 +1,8 @@ -use crate::{class::Class, style::Style, Pointer, PointerMsg, View, ViewMarker}; +use crate::{ + class::{Class, IntoClasses}, + style::{IntoStyles, Style}, + Pointer, PointerMsg, View, ViewMarker, +}; use std::{borrow::Cow, marker::PhantomData}; use gloo::events::EventListenerOptions; @@ -91,43 +95,28 @@ where } } - /// Add a class to the wrapped element. + /// Add 0 or more classes to the wrapped element. /// - /// If multiple classes are added, all will be applied to the element. - fn class(self, class: impl Into>) -> Class { - self.class_opt(Some(class)) - } - - /// Add an optional class 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_opt(self, class: Option>>) -> Class { + fn class(self, class: impl IntoClasses) -> Class { + let mut class_names = vec![]; + class.into_classes(&mut class_names); Class { element: self, - class_name: class.map(Into::into), + class_names, phantom: PhantomData, } } /// Set a style attribute - fn style( - self, - name: impl Into>, - value: impl Into>, - ) -> Style { - self.style_opt(name, Some(value)) - } - - /// Set a style attribute - fn style_opt( - self, - name: impl Into>, - value: Option>>, - ) -> Style { + fn style(self, style: impl IntoStyles) -> Style { + let mut styles = vec![]; + style.into_styles(&mut styles); Style { element: self, - name: name.into(), - value: value.map(Into::into), + styles, phantom: PhantomData, } } diff --git a/crates/xilem_web/src/lib.rs b/crates/xilem_web/src/lib.rs index d0d74d3a7..3821ecf79 100644 --- a/crates/xilem_web/src/lib.rs +++ b/crates/xilem_web/src/lib.rs @@ -37,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 index 59fcdb6fb..d94db758f 100644 --- a/crates/xilem_web/src/style.rs +++ b/crates/xilem_web/src/style.rs @@ -1,5 +1,6 @@ -use std::borrow::Cow; +use std::collections::BTreeMap; use std::marker::PhantomData; +use std::{borrow::Cow, collections::HashMap}; use xilem_core::{Id, MessageResult}; @@ -7,10 +8,105 @@ 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) name: Cow<'static, str>, - pub(crate) value: Option>, + pub(crate) styles: Vec<(Cow<'static, str>, Cow<'static, str>)>, pub(crate) phantom: PhantomData (T, A)>, } @@ -22,8 +118,8 @@ impl, T, A> View for Style { type Element = E::Element; fn build(&self, cx: &mut Cx) -> (Id, Self::State, Self::Element) { - if let Some(value) = &self.value { - cx.add_style_to_element(&self.name, value); + for (key, value) in &self.styles { + cx.add_style_to_element(key, value); } self.element.build(cx) } @@ -36,8 +132,8 @@ impl, T, A> View for Style { state: &mut Self::State, element: &mut Self::Element, ) -> ChangeFlags { - if let Some(value) = &self.value { - cx.add_style_to_element(&self.name, value); + for (key, value) in &self.styles { + cx.add_style_to_element(key, value); } self.element.rebuild(cx, &prev.element, id, state, element) } diff --git a/crates/xilem_web/web_examples/counter/src/main.rs b/crates/xilem_web/web_examples/counter/src/main.rs index 03782df73..de1b9b90e 100644 --- a/crates/xilem_web/web_examples/counter/src/main.rs +++ b/crates/xilem_web/web_examples/counter/src/main.rs @@ -49,7 +49,7 @@ fn btn( fn app_logic(state: &mut AppState) -> impl View { el::div(( - el::span(format!("clicked {} times", state.clicks)).class_opt(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 e33d067ac..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 @@ -62,8 +63,8 @@ fn todo_item(todo: &mut Todo, editing: bool) -> impl Element { .passive(true) .on_blur(|_, _| TodoAction::CancelEditing), )) - .class_opt(todo.completed.then_some("completed")) - .class_opt(editing.then_some("editing")) + .class(todo.completed.then_some("completed")) + .class(editing.then_some("editing")) } fn footer_view(state: &mut AppState, should_display: bool) -> impl Element { @@ -94,7 +95,7 @@ fn footer_view(state: &mut AppState, should_display: bool) -> impl Element impl Element impl Element impl Element impl Element { @@ -161,7 +162,7 @@ fn main_view(state: &mut AppState, should_display: bool) -> impl Element impl View { From 76bb9946c459ae9eba01b0dfc448b12bcfd42e40 Mon Sep 17 00:00:00 2001 From: Richard Dodd Date: Sat, 30 Mar 2024 11:45:17 +0000 Subject: [PATCH 06/10] fix clippy --- crates/xilem_web/src/class.rs | 4 ++-- crates/xilem_web/src/context.rs | 6 +++--- crates/xilem_web/src/style.rs | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/xilem_web/src/class.rs b/crates/xilem_web/src/class.rs index 515f67d8a..64e4074b4 100644 --- a/crates/xilem_web/src/class.rs +++ b/crates/xilem_web/src/class.rs @@ -20,7 +20,7 @@ impl IntoClasses for String { impl IntoClasses for &'static str { fn into_classes(self, classes: &mut Vec>) { - classes.push(self.into()) + classes.push(self.into()); } } @@ -30,7 +30,7 @@ where { fn into_classes(self, classes: &mut Vec>) { if let Some(t) = self { - t.into_classes(classes) + t.into_classes(classes); } } } diff --git a/crates/xilem_web/src/context.rs b/crates/xilem_web/src/context.rs index b3d2d7a9a..460239c7e 100644 --- a/crates/xilem_web/src/context.rs +++ b/crates/xilem_web/src/context.rs @@ -175,7 +175,7 @@ fn set_class(element: &web_sys::Element, class_name: &str) { !class_name.contains(' '), "class names cannot contain the ascii space character" ); - element.class_list().add_1(class_name).unwrap_throw() + element.class_list().add_1(class_name).unwrap_throw(); } fn remove_class(element: &web_sys::Element, class_name: &str) { @@ -187,13 +187,13 @@ fn remove_class(element: &web_sys::Element, class_name: &str) { !class_name.contains(' '), "class names cannot contain the ascii space character" ); - element.class_list().remove_1(class_name).unwrap_throw() + element.class_list().remove_1(class_name).unwrap_throw(); } fn set_style(element: &web_sys::Element, name: &str, value: &str) { // styles will be ignored for non-html elements (e.g. SVG) if let Some(el) = element.dyn_ref::() { - el.style().set_property(name, value).unwrap_throw() + el.style().set_property(name, value).unwrap_throw(); } } diff --git a/crates/xilem_web/src/style.rs b/crates/xilem_web/src/style.rs index d94db758f..ddd7364fe 100644 --- a/crates/xilem_web/src/style.rs +++ b/crates/xilem_web/src/style.rs @@ -41,7 +41,7 @@ where { fn into_styles(self, styles: &mut Vec<(Cow<'static, str>, Cow<'static, str>)>) { if let Some(t) = self { - t.into_styles(styles) + t.into_styles(styles); } } } From 6240b7f1e376bf0bc991dc23a12a10e7010f3ef8 Mon Sep 17 00:00:00 2001 From: "Richard Dodd (dodj)" Date: Sun, 31 Mar 2024 10:12:44 +0100 Subject: [PATCH 07/10] Update Cargo.toml Co-authored-by: Philipp Mildenberger --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 9339113a2..2039d2571 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,7 @@ members = [ "crates/xilem_web/web_examples/counter_custom_element", "crates/xilem_web/web_examples/todomvc", "crates/xilem_web/web_examples/mathml_svg", - "crates/xilem_web/web_examples/svgtoy", + "crates/xilem_web/web_examples/svgtoy", ] [workspace.package] From f29a402bc86470a4736c14428017dae5498abac4 Mon Sep 17 00:00:00 2001 From: "Richard Dodd (dodj)" Date: Sun, 31 Mar 2024 10:13:10 +0100 Subject: [PATCH 08/10] Update crates/xilem_web/src/context.rs Co-authored-by: Philipp Mildenberger --- crates/xilem_web/src/context.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/xilem_web/src/context.rs b/crates/xilem_web/src/context.rs index 460239c7e..20ef714b7 100644 --- a/crates/xilem_web/src/context.rs +++ b/crates/xilem_web/src/context.rs @@ -198,9 +198,10 @@ fn set_style(element: &web_sys::Element, name: &str, value: &str) { } fn remove_style(element: &web_sys::Element, name: &str) { - // styles will be ignored for non-html elements (e.g. SVG) 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(); } } From 92f26a1ebe51421eb4af208b51ec3370a4732e53 Mon Sep 17 00:00:00 2001 From: "Richard Dodd (dodj)" Date: Sun, 31 Mar 2024 10:13:19 +0100 Subject: [PATCH 09/10] Update crates/xilem_web/src/context.rs Co-authored-by: Philipp Mildenberger --- crates/xilem_web/src/context.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/xilem_web/src/context.rs b/crates/xilem_web/src/context.rs index 20ef714b7..ce793851d 100644 --- a/crates/xilem_web/src/context.rs +++ b/crates/xilem_web/src/context.rs @@ -191,9 +191,10 @@ fn remove_class(element: &web_sys::Element, class_name: &str) { } fn set_style(element: &web_sys::Element, name: &str, value: &str) { - // styles will be ignored for non-html elements (e.g. SVG) 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(); } } From 678ced50696e3c7484e97f83f72c59b8c59d8b66 Mon Sep 17 00:00:00 2001 From: "Richard Dodd (dodj)" Date: Sun, 31 Mar 2024 10:13:48 +0100 Subject: [PATCH 10/10] Update crates/xilem_web/src/class.rs Co-authored-by: Philipp Mildenberger --- crates/xilem_web/src/class.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/crates/xilem_web/src/class.rs b/crates/xilem_web/src/class.rs index 64e4074b4..da2f58769 100644 --- a/crates/xilem_web/src/class.rs +++ b/crates/xilem_web/src/class.rs @@ -24,6 +24,12 @@ impl IntoClasses for &'static str { } } +impl IntoClasses for Cow<'static, str> { + fn into_classes(self, classes: &mut Vec>) { + classes.push(self); + } +} + impl IntoClasses for Option where T: IntoClasses,