Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add class and style interfaces #193

Merged
merged 10 commits into from
Mar 31, 2024
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
richard-uk1 marked this conversation as resolved.
Show resolved Hide resolved
]

[workspace.package]
Expand Down
2 changes: 2 additions & 0 deletions crates/xilem_web/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
56 changes: 56 additions & 0 deletions crates/xilem_web/src/class.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
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<E, T, A> {
pub(crate) element: E,
pub(crate) class_name: Option<Cow<'static, str>>,
pub(crate) phantom: PhantomData<fn() -> (T, A)>,
}

impl<E, T, A> ViewMarker for Class<E, T, A> {}
impl<E, T, A> Sealed for Class<E, T, A> {}

impl<E: Element<T, A>, T, A> View<T, A> for Class<E, T, A> {
type State = E::State;
type Element = E::Element;

fn build(&self, cx: &mut Cx) -> (Id, Self::State, Self::Element) {
if let Some(class_name) = &self.class_name {
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 {
if let Some(class_name) = &self.class_name {
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<dyn std::any::Any>,
app_state: &mut T,
) -> MessageResult<A> {
self.element.message(id_path, state, message, app_state)
}
}

crate::interfaces::impl_dom_interfaces_for_ty!(Element, Class);
177 changes: 175 additions & 2 deletions crates/xilem_web/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,52 @@ 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()
richard-uk1 marked this conversation as resolved.
Show resolved Hide resolved
}

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::<web_sys::HtmlElement>() {
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::<web_sys::HtmlElement>() {
el.style().remove_property(name).unwrap_throw();
}
richard-uk1 marked this conversation as resolved.
Show resolved Hide resolved
}

// 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<CowStr, AttributeValue>,
pub(crate) current_element_classes: VecMap<CowStr, ()>,
pub(crate) current_element_styles: VecMap<CowStr, CowStr>,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not that it is particularly relevant to this PR, more a brain-dump/thoughts about it, I'd like to share.

I'm more and more thinking about getting the temporary state of elements out of the context again, likely via associated type bounds as described here, since they're stabilized now. This would allow (safe) implementations downstream (i.e. remove the "need" for the Sealed trait in the interfaces), better separation of concerns (especially if there's a lot of logic added for all kinds of dom elements), possibly it would also help stripping down to only relevant code (e.g. when style is not used, it could be stripped away in the resulting binary).
And generally make everything cleaner I think. I think it's probably worth the added compilation times. But this probably needs deeper investigation/experimentation (again).

app_ref: Option<Box<dyn AppRunner>>,
}

Expand All @@ -70,6 +110,8 @@ impl Cx {
document: crate::document(),
app_ref: None,
current_element_attributes: Default::default(),
current_element_classes: Default::default(),
current_element_styles: Default::default(),
}
}

Expand Down Expand Up @@ -145,26 +187,77 @@ impl Cx {
&mut self,
ns: &str,
name: &str,
) -> (web_sys::Element, VecMap<CowStr, AttributeValue>) {
) -> (
web_sys::Element,
VecMap<CowStr, AttributeValue>,
VecMap<CowStr, ()>,
VecMap<CowStr, CowStr>,
) {
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);
let styles = self.apply_styles(&el);
(el, attributes, classes, styles)
}

pub(crate) fn rebuild_element(
&mut self,
element: &web_sys::Element,
attributes: &mut VecMap<CowStr, AttributeValue>,
classes: &mut VecMap<CowStr, ()>,
styles: &mut VecMap<CowStr, CowStr>,
) -> 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<AttributeValue>) {
// Special-case class so it works with the `class` method
richard-uk1 marked this conversation as resolved.
Show resolved Hide resolved
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" {
richard-uk1 marked this conversation as resolved.
Show resolved Hide resolved
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) {
Expand All @@ -174,6 +267,20 @@ 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 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,
Expand Down Expand Up @@ -210,6 +317,72 @@ impl Cx {
changed
}

pub(crate) fn apply_classes(&mut self, element: &web_sys::Element) -> VecMap<CowStr, ()> {
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<CowStr, ()>,
) -> 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<CowStr, CowStr> {
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<CowStr, CowStr>,
) -> 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(),
Expand Down
24 changes: 19 additions & 5 deletions crates/xilem_web/src/elements.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ type CowStr = std::borrow::Cow<'static, str>;
pub struct ElementState<ViewSeqState> {
pub(crate) children_states: ViewSeqState,
pub(crate) attributes: VecMap<CowStr, AttributeValue>,
pub(crate) classes: VecMap<CowStr, ()>,
pub(crate) styles: VecMap<CowStr, CowStr>,
pub(crate) child_elements: Vec<Pod>,
/// This is temporary cache for elements while updating/diffing,
/// after usage it shouldn't contain any elements,
Expand Down Expand Up @@ -150,7 +152,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, styles) = cx.build_element(HTML_NS, &self.name);

let mut child_elements = vec![];
let mut scratch = vec![];
Expand All @@ -172,6 +174,8 @@ where
child_elements,
scratch,
attributes,
classes,
styles,
};
(id, state, el)
}
Expand All @@ -193,8 +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) = 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();
Expand All @@ -203,7 +210,12 @@ where
changed |= ChangeFlags::STRUCTURE;
}

changed |= cx.rebuild_element(element, &mut state.attributes);
changed |= cx.rebuild_element(
element,
&mut state.attributes,
&mut state.classes,
&mut state.styles,
);

// update children
let mut splice =
Expand Down Expand Up @@ -280,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) = 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![];
Expand All @@ -301,6 +313,8 @@ macro_rules! define_element {
child_elements,
scratch,
attributes,
classes,
styles,
};
(id, state, el)
}
Expand All @@ -315,7 +329,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, &mut state.styles);

// update children
let mut splice = ChildrenSplice::new(&mut state.child_elements, &mut state.scratch, element);
Expand Down
Loading
Loading