add class and style interfaces (#193)

* add class interface

* add style interface

* remove parse style/class logic, and panic instead (in dev)

* combine attributes, styles, classes into single struct

* make `class` and `style` methods polymorphic

* fix clippy

* Update Cargo.toml

Co-authored-by: Philipp Mildenberger <philipp@mildenberger.me>

* Update crates/xilem_web/src/context.rs

Co-authored-by: Philipp Mildenberger <philipp@mildenberger.me>

* Update crates/xilem_web/src/context.rs

Co-authored-by: Philipp Mildenberger <philipp@mildenberger.me>

* Update crates/xilem_web/src/class.rs

Co-authored-by: Philipp Mildenberger <philipp@mildenberger.me>

---------

Co-authored-by: Philipp Mildenberger <philipp@mildenberger.me>
This commit is contained in:
Richard Dodd (dodj) 2024-03-31 23:15:32 +01:00 committed by GitHub
parent 072358e293
commit 74bc6fcee8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 575 additions and 118 deletions

View File

@ -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",

View File

@ -0,0 +1,124 @@
use std::{borrow::Cow, marker::PhantomData};
use xilem_core::{Id, MessageResult};
use crate::{
interfaces::{sealed::Sealed, Element},
ChangeFlags, Cx, View, ViewMarker,
};
/// A trait to make the class adding functions generic over collection type
pub trait IntoClasses {
fn into_classes(self, classes: &mut Vec<Cow<'static, str>>);
}
impl IntoClasses for String {
fn into_classes(self, classes: &mut Vec<Cow<'static, str>>) {
classes.push(self.into());
}
}
impl IntoClasses for &'static str {
fn into_classes(self, classes: &mut Vec<Cow<'static, str>>) {
classes.push(self.into());
}
}
impl IntoClasses for Cow<'static, str> {
fn into_classes(self, classes: &mut Vec<Cow<'static, str>>) {
classes.push(self);
}
}
impl<T> IntoClasses for Option<T>
where
T: IntoClasses,
{
fn into_classes(self, classes: &mut Vec<Cow<'static, str>>) {
if let Some(t) = self {
t.into_classes(classes);
}
}
}
impl<T> IntoClasses for Vec<T>
where
T: IntoClasses,
{
fn into_classes(self, classes: &mut Vec<Cow<'static, str>>) {
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<Cow<'static, str>>) {
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<E, T, A> {
pub(crate) element: E,
pub(crate) class_names: Vec<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) {
for class_name in &self.class_names {
cx.add_class_to_element(class_name);
}
self.element.build(cx)
}
fn rebuild(
&self,
cx: &mut Cx,
prev: &Self,
id: &mut Id,
state: &mut Self::State,
element: &mut Self::Element,
) -> ChangeFlags {
for class_name in &self.class_names {
cx.add_class_to_element(class_name);
}
self.element.rebuild(cx, &prev.element, id, state, element)
}
fn message(
&self,
id_path: &[Id],
state: &mut Self::State,
message: Box<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);

View File

@ -16,6 +16,131 @@ use crate::{
type CowStr = std::borrow::Cow<'static, str>;
#[derive(Debug, Default)]
pub struct HtmlProps {
pub(crate) attributes: VecMap<CowStr, AttributeValue>,
pub(crate) classes: VecMap<CowStr, ()>,
pub(crate) styles: VecMap<CowStr, CowStr>,
}
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<CowStr, AttributeValue> {
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<CowStr, ()> {
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<CowStr, CowStr> {
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<CowStr, AttributeValue>,
) -> 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<CowStr, ()>,
) -> 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<CowStr, CowStr>,
) -> ChangeFlags {
let mut changed = ChangeFlags::empty();
// update attributes
for itm in diff_kv_iterables(&*styles, &self.styles) {
match itm {
Diff::Add(name, value) | Diff::Change(name, value) => {
set_style(element, name, value);
changed |= ChangeFlags::OTHER_CHANGE;
}
Diff::Remove(name) => {
remove_style(element, name);
changed |= ChangeFlags::OTHER_CHANGE;
}
}
}
std::mem::swap(styles, &mut self.styles);
self.styles.clear();
changed
}
}
fn set_attribute(element: &web_sys::Element, name: &str, value: &str) {
// we have to special-case `value` because setting the value using `set_attribute`
// doesn't work after the value has been changed.
@ -41,12 +166,52 @@ fn remove_attribute(element: &web_sys::Element, name: &str) {
}
}
fn set_class(element: &web_sys::Element, class_name: &str) {
debug_assert!(
!class_name.is_empty(),
"class names cannot be the empty string"
);
debug_assert!(
!class_name.contains(' '),
"class names cannot contain the ascii space character"
);
element.class_list().add_1(class_name).unwrap_throw();
}
fn remove_class(element: &web_sys::Element, class_name: &str) {
debug_assert!(
!class_name.is_empty(),
"class names cannot be the empty string"
);
debug_assert!(
!class_name.contains(' '),
"class names cannot contain the ascii space character"
);
element.class_list().remove_1(class_name).unwrap_throw();
}
fn set_style(element: &web_sys::Element, name: &str, value: &str) {
if let Some(el) = element.dyn_ref::<web_sys::HtmlElement>() {
el.style().set_property(name, value).unwrap_throw();
} else if let Some(el) = element.dyn_ref::<web_sys::SvgElement>() {
el.style().set_property(name, value).unwrap_throw();
}
}
fn remove_style(element: &web_sys::Element, name: &str) {
if let Some(el) = element.dyn_ref::<web_sys::HtmlElement>() {
el.style().remove_property(name).unwrap_throw();
} else if let Some(el) = element.dyn_ref::<web_sys::SvgElement>() {
el.style().remove_property(name).unwrap_throw();
}
}
// Note: xilem has derive Clone here. Not sure.
pub struct Cx {
id_path: IdPath,
document: Document,
// TODO There's likely a cleaner more robust way to propagate the attributes to an element
pub(crate) current_element_attributes: VecMap<CowStr, AttributeValue>,
pub(crate) current_element_props: HtmlProps,
app_ref: Option<Box<dyn AppRunner>>,
}
@ -69,7 +234,7 @@ impl Cx {
id_path: Vec::new(),
document: crate::document(),
app_ref: None,
current_element_attributes: Default::default(),
current_element_props: Default::default(),
}
}
@ -141,73 +306,62 @@ impl Cx {
&self.document
}
pub(crate) fn build_element(
&mut self,
ns: &str,
name: &str,
) -> (web_sys::Element, VecMap<CowStr, AttributeValue>) {
pub(crate) fn build_element(&mut self, ns: &str, name: &str) -> (web_sys::Element, HtmlProps) {
let el = self
.document
.create_element_ns(Some(ns), name)
.expect("could not create element");
let attributes = self.apply_attributes(&el);
(el, attributes)
let props = self.current_element_props.apply(&el);
(el, props)
}
pub(crate) fn rebuild_element(
&mut self,
element: &web_sys::Element,
attributes: &mut VecMap<CowStr, AttributeValue>,
props: &mut HtmlProps,
) -> ChangeFlags {
self.apply_attribute_changes(element, attributes)
self.current_element_props.apply_changes(element, props)
}
// TODO Not sure how multiple attribute definitions with the same name should be handled (e.g. `e.attr("class", "a").attr("class", "b")`)
// Currently the outer most (in the example above "b") defines the attribute (when it isn't `None`, in that case the inner attr defines the value)
pub(crate) fn add_attr_to_element(&mut self, name: &CowStr, value: &Option<AttributeValue>) {
// Panic in dev if "class" is used as an attribute. In production the result is undefined.
debug_assert!(
name != "class",
"classes should be set using the `class` method"
);
// Panic in dev if "style" is used as an attribute. In production the result is undefined.
debug_assert!(
name != "style",
"styles should be set using the `style` method"
);
if let Some(value) = value {
// could be slightly optimized via something like this: `new_attrs.entry(name).or_insert_with(|| value)`
if !self.current_element_attributes.contains_key(name) {
self.current_element_attributes
if !self.current_element_props.attributes.contains_key(name) {
self.current_element_props
.attributes
.insert(name.clone(), value.clone());
}
}
}
pub(crate) fn apply_attributes(
&mut self,
element: &web_sys::Element,
) -> VecMap<CowStr, AttributeValue> {
let mut attributes = VecMap::default();
std::mem::swap(&mut attributes, &mut self.current_element_attributes);
for (name, value) in attributes.iter() {
set_attribute(element, name, &value.serialize());
pub(crate) fn add_class_to_element(&mut self, class_name: &CowStr) {
// Don't strictly need this check but I assume its better for perf (might not be though)
if !self.current_element_props.classes.contains_key(class_name) {
self.current_element_props
.classes
.insert(class_name.clone(), ());
}
attributes
}
pub(crate) fn apply_attribute_changes(
&mut self,
element: &web_sys::Element,
attributes: &mut VecMap<CowStr, AttributeValue>,
) -> ChangeFlags {
let mut changed = ChangeFlags::empty();
// update attributes
for itm in diff_kv_iterables(&*attributes, &self.current_element_attributes) {
match itm {
Diff::Add(name, value) | Diff::Change(name, value) => {
set_attribute(element, name, &value.serialize());
changed |= ChangeFlags::OTHER_CHANGE;
}
Diff::Remove(name) => {
remove_attribute(element, name);
changed |= ChangeFlags::OTHER_CHANGE;
}
}
pub(crate) fn add_style_to_element(&mut self, name: &CowStr, value: &CowStr) {
if !self.current_element_props.styles.contains_key(name) {
self.current_element_props
.styles
.insert(name.clone(), value.clone());
}
std::mem::swap(attributes, &mut self.current_element_attributes);
self.current_element_attributes.clear();
changed
}
pub fn message_thunk(&self) -> MessageThunk {

View File

@ -4,8 +4,8 @@ use wasm_bindgen::{JsCast, UnwrapThrowExt};
use xilem_core::{Id, MessageResult, VecSplice};
use crate::{
interfaces::sealed::Sealed, vecmap::VecMap, view::DomNode, AttributeValue, ChangeFlags, Cx,
ElementsSplice, Pod, View, ViewMarker, ViewSequence, HTML_NS,
context::HtmlProps, interfaces::sealed::Sealed, view::DomNode, ChangeFlags, Cx, ElementsSplice,
Pod, View, ViewMarker, ViewSequence, HTML_NS,
};
use super::interfaces::Element;
@ -17,7 +17,7 @@ type CowStr = std::borrow::Cow<'static, str>;
/// Stores handles to the child elements and any child state, as well as attributes and event listeners
pub struct ElementState<ViewSeqState> {
pub(crate) children_states: ViewSeqState,
pub(crate) attributes: VecMap<CowStr, AttributeValue>,
pub(crate) props: HtmlProps,
pub(crate) child_elements: Vec<Pod>,
/// This is temporary cache for elements while updating/diffing,
/// after usage it shouldn't contain any elements,
@ -150,7 +150,7 @@ where
type Element = web_sys::HtmlElement;
fn build(&self, cx: &mut Cx) -> (Id, Self::State, Self::Element) {
let (el, attributes) = cx.build_element(HTML_NS, &self.name);
let (el, props) = cx.build_element(HTML_NS, &self.name);
let mut child_elements = vec![];
let mut scratch = vec![];
@ -171,7 +171,7 @@ where
children_states,
child_elements,
scratch,
attributes,
props,
};
(id, state, el)
}
@ -193,8 +193,8 @@ where
.parent_element()
.expect_throw("this element was mounted and so should have a parent");
parent.remove_child(element).unwrap_throw();
let (new_element, attributes) = cx.build_element(HTML_NS, self.node_name());
state.attributes = attributes;
let (new_element, props) = cx.build_element(HTML_NS, self.node_name());
state.props = props;
// TODO could this be combined with child updates?
while let Some(child) = element.child_nodes().get(0) {
new_element.append_child(&child).unwrap_throw();
@ -203,7 +203,7 @@ where
changed |= ChangeFlags::STRUCTURE;
}
changed |= cx.rebuild_element(element, &mut state.attributes);
changed |= cx.rebuild_element(element, &mut state.props);
// update children
let mut splice =
@ -280,7 +280,7 @@ macro_rules! define_element {
type Element = web_sys::$dom_interface;
fn build(&self, cx: &mut Cx) -> (Id, Self::State, Self::Element) {
let (el, attributes) = cx.build_element($ns, $tag_name);
let (el, props) = cx.build_element($ns, $tag_name);
let mut child_elements = vec![];
let mut scratch = vec![];
@ -300,7 +300,7 @@ macro_rules! define_element {
children_states,
child_elements,
scratch,
attributes,
props,
};
(id, state, el)
}
@ -315,7 +315,7 @@ macro_rules! define_element {
) -> ChangeFlags {
let mut changed = ChangeFlags::empty();
changed |= cx.rebuild_element(element, &mut state.attributes);
changed |= cx.rebuild_element(element, &mut state.props);
// update children
let mut splice = ChildrenSplice::new(&mut state.child_elements, &mut state.scratch, element);

View File

@ -1,5 +1,9 @@
use crate::{Pointer, PointerMsg, View, ViewMarker};
use std::borrow::Cow;
use crate::{
class::{Class, IntoClasses},
style::{IntoStyles, Style},
Pointer, PointerMsg, View, ViewMarker,
};
use std::{borrow::Cow, marker::PhantomData};
use gloo::events::EventListenerOptions;
use wasm_bindgen::JsCast;
@ -91,11 +95,30 @@ where
}
}
// TODO should some methods extend some properties automatically,
// instead of overwriting the (possibly set) inner value
// or should there be (extra) "modifier" methods like `add_class` and/or `remove_class`
fn class(self, class: impl Into<Cow<'static, str>>) -> Attr<Self, T, A> {
self.attr("class", class.into())
/// Add 0 or more classes to the wrapped element.
///
/// Can pass a string, &'static str, Option, tuple, or vec
///
/// If multiple classes are added, all will be applied to the element.
fn class(self, class: impl IntoClasses) -> Class<Self, T, A> {
let mut class_names = vec![];
class.into_classes(&mut class_names);
Class {
element: self,
class_names,
phantom: PhantomData,
}
}
/// Set a style attribute
fn style(self, style: impl IntoStyles) -> Style<Self, T, A> {
let mut styles = vec![];
style.into_styles(&mut styles);
Style {
element: self,
styles,
phantom: PhantomData,
}
}
// event list from

View File

@ -10,6 +10,7 @@ use wasm_bindgen::JsCast;
mod app;
mod attribute;
mod attribute_value;
mod class;
mod context;
mod diff;
pub mod elements;
@ -18,6 +19,7 @@ pub mod interfaces;
mod one_of;
mod optional_action;
mod pointer;
mod style;
pub mod svg;
mod vecmap;
mod view;
@ -35,6 +37,7 @@ pub use one_of::{
};
pub use optional_action::{Action, OptionalAction};
pub use pointer::{Pointer, PointerDetails, PointerMsg};
pub use style::style;
pub use view::{
memoize, static_view, Adapt, AdaptState, AdaptThunk, AnyView, BoxedView, ElementsSplice,
Memoize, MemoizeState, Pod, View, ViewMarker, ViewSequence,

View File

@ -0,0 +1,152 @@
use std::collections::BTreeMap;
use std::marker::PhantomData;
use std::{borrow::Cow, collections::HashMap};
use xilem_core::{Id, MessageResult};
use crate::{interfaces::sealed::Sealed, ChangeFlags, Cx, View, ViewMarker};
use super::interfaces::Element;
/// A trait to make the class adding functions generic over collection type
pub trait IntoStyles {
fn into_styles(self, styles: &mut Vec<(Cow<'static, str>, Cow<'static, str>)>);
}
struct StyleTuple<T1, T2>(T1, T2);
/// Create a style from a style name and its value.
pub fn style<T1, T2>(name: T1, value: T2) -> impl IntoStyles
where
T1: Into<Cow<'static, str>>,
T2: Into<Cow<'static, str>>,
{
StyleTuple(name, value)
}
impl<T1, T2> IntoStyles for StyleTuple<T1, T2>
where
T1: Into<Cow<'static, str>>,
T2: Into<Cow<'static, str>>,
{
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<T> IntoStyles for Option<T>
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<T> IntoStyles for Vec<T>
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<T1, T2, S> IntoStyles for HashMap<T1, T2, S>
where
T1: Into<Cow<'static, str>>,
T2: Into<Cow<'static, str>>,
{
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<T1, T2> IntoStyles for BTreeMap<T1, T2>
where
T1: Into<Cow<'static, str>>,
T2: Into<Cow<'static, str>>,
{
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<E, T, A> {
pub(crate) element: E,
pub(crate) styles: Vec<(Cow<'static, str>, Cow<'static, str>)>,
pub(crate) phantom: PhantomData<fn() -> (T, A)>,
}
impl<E, T, A> ViewMarker for Style<E, T, A> {}
impl<E, T, A> Sealed for Style<E, T, A> {}
impl<E: Element<T, A>, T, A> View<T, A> for Style<E, T, A> {
type State = E::State;
type Element = E::Element;
fn build(&self, cx: &mut Cx) -> (Id, Self::State, Self::Element) {
for (key, value) in &self.styles {
cx.add_style_to_element(key, value);
}
self.element.build(cx)
}
fn rebuild(
&self,
cx: &mut Cx,
prev: &Self,
id: &mut Id,
state: &mut Self::State,
element: &mut Self::Element,
) -> ChangeFlags {
for (key, value) in &self.styles {
cx.add_style_to_element(key, value);
}
self.element.rebuild(cx, &prev.element, id, state, element)
}
fn message(
&self,
id_path: &[Id],
state: &mut Self::State,
message: Box<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, Style);

View File

@ -9,11 +9,10 @@ use std::borrow::Cow;
use xilem_core::{Id, MessageResult};
use crate::{
context::{ChangeFlags, Cx},
context::{ChangeFlags, Cx, HtmlProps},
interfaces::sealed::Sealed,
vecmap::VecMap,
view::{View, ViewMarker},
AttributeValue, IntoAttributeValue, SVG_NS,
IntoAttributeValue, SVG_NS,
};
macro_rules! generate_dom_interface_impl {
@ -29,7 +28,7 @@ impl ViewMarker for Line {}
impl Sealed for Line {}
impl<T, A> View<T, A> for Line {
type State = VecMap<Cow<'static, str>, AttributeValue>;
type State = HtmlProps;
type Element = web_sys::Element;
fn build(&self, cx: &mut Cx) -> (Id, Self::State, Self::Element) {
@ -37,9 +36,9 @@ impl<T, A> View<T, A> for Line {
cx.add_attr_to_element(&"y1".into(), &self.p0.y.into_attr_value());
cx.add_attr_to_element(&"x2".into(), &self.p1.x.into_attr_value());
cx.add_attr_to_element(&"y2".into(), &self.p1.y.into_attr_value());
let (el, attributes) = cx.build_element(SVG_NS, "line");
let (el, props) = cx.build_element(SVG_NS, "line");
let id = Id::next();
(id, attributes, el)
(id, props, el)
}
fn rebuild(
@ -47,14 +46,14 @@ impl<T, A> View<T, A> for Line {
cx: &mut Cx,
_prev: &Self,
_id: &mut Id,
attributes: &mut Self::State,
props: &mut Self::State,
element: &mut Self::Element,
) -> ChangeFlags {
cx.add_attr_to_element(&"x1".into(), &self.p0.x.into_attr_value());
cx.add_attr_to_element(&"y1".into(), &self.p0.y.into_attr_value());
cx.add_attr_to_element(&"x2".into(), &self.p1.x.into_attr_value());
cx.add_attr_to_element(&"y2".into(), &self.p1.y.into_attr_value());
cx.rebuild_element(element, attributes)
cx.rebuild_element(element, props)
}
fn message(
@ -75,7 +74,7 @@ impl ViewMarker for Rect {}
impl Sealed for Rect {}
impl<T, A> View<T, A> for Rect {
type State = VecMap<Cow<'static, str>, AttributeValue>;
type State = HtmlProps;
type Element = web_sys::Element;
fn build(&self, cx: &mut Cx) -> (Id, Self::State, Self::Element) {
@ -84,9 +83,9 @@ impl<T, A> View<T, A> for Rect {
let size = self.size();
cx.add_attr_to_element(&"width".into(), &size.width.into_attr_value());
cx.add_attr_to_element(&"height".into(), &size.height.into_attr_value());
let (el, attributes) = cx.build_element(SVG_NS, "rect");
let (el, props) = cx.build_element(SVG_NS, "rect");
let id = Id::next();
(id, attributes, el)
(id, props, el)
}
fn rebuild(
@ -94,7 +93,7 @@ impl<T, A> View<T, A> for Rect {
cx: &mut Cx,
_prev: &Self,
_id: &mut Id,
attributes: &mut Self::State,
props: &mut Self::State,
element: &mut Self::Element,
) -> ChangeFlags {
cx.add_attr_to_element(&"x".into(), &self.x0.into_attr_value());
@ -102,7 +101,7 @@ impl<T, A> View<T, A> for Rect {
let size = self.size();
cx.add_attr_to_element(&"width".into(), &size.width.into_attr_value());
cx.add_attr_to_element(&"height".into(), &size.height.into_attr_value());
cx.rebuild_element(element, attributes)
cx.rebuild_element(element, props)
}
fn message(
@ -123,16 +122,16 @@ impl ViewMarker for Circle {}
impl Sealed for Circle {}
impl<T, A> View<T, A> for Circle {
type State = VecMap<Cow<'static, str>, AttributeValue>;
type State = HtmlProps;
type Element = web_sys::Element;
fn build(&self, cx: &mut Cx) -> (Id, Self::State, Self::Element) {
cx.add_attr_to_element(&"cx".into(), &self.center.x.into_attr_value());
cx.add_attr_to_element(&"cy".into(), &self.center.y.into_attr_value());
cx.add_attr_to_element(&"r".into(), &self.radius.into_attr_value());
let (el, attributes) = cx.build_element(SVG_NS, "circle");
let (el, props) = cx.build_element(SVG_NS, "circle");
let id = Id::next();
(id, attributes, el)
(id, props, el)
}
fn rebuild(
@ -140,13 +139,13 @@ impl<T, A> View<T, A> for Circle {
cx: &mut Cx,
_prev: &Self,
_id: &mut Id,
attributes: &mut Self::State,
props: &mut Self::State,
element: &mut Self::Element,
) -> ChangeFlags {
cx.add_attr_to_element(&"cx".into(), &self.center.x.into_attr_value());
cx.add_attr_to_element(&"cy".into(), &self.center.y.into_attr_value());
cx.add_attr_to_element(&"r".into(), &self.radius.into_attr_value());
cx.rebuild_element(element, attributes)
cx.rebuild_element(element, props)
}
fn message(
@ -167,15 +166,15 @@ impl ViewMarker for BezPath {}
impl Sealed for BezPath {}
impl<T, A> View<T, A> for BezPath {
type State = (Cow<'static, str>, VecMap<Cow<'static, str>, AttributeValue>);
type State = (Cow<'static, str>, HtmlProps);
type Element = web_sys::Element;
fn build(&self, cx: &mut Cx) -> (Id, Self::State, Self::Element) {
let svg_repr = Cow::from(self.to_svg());
cx.add_attr_to_element(&"d".into(), &svg_repr.clone().into_attr_value());
let (el, attributes) = cx.build_element(SVG_NS, "path");
let (el, props) = cx.build_element(SVG_NS, "path");
let id = Id::next();
(id, (svg_repr, attributes), el)
(id, (svg_repr, props), el)
}
fn rebuild(
@ -183,7 +182,7 @@ impl<T, A> View<T, A> for BezPath {
cx: &mut Cx,
prev: &Self,
_id: &mut Id,
(svg_repr, attributes): &mut Self::State,
(svg_repr, props): &mut Self::State,
element: &mut Self::Element,
) -> ChangeFlags {
// slight optimization to avoid serialization/allocation
@ -191,7 +190,7 @@ impl<T, A> View<T, A> for BezPath {
*svg_repr = Cow::from(self.to_svg());
}
cx.add_attr_to_element(&"d".into(), &svg_repr.clone().into_attr_value());
cx.rebuild_element(element, attributes)
cx.rebuild_element(element, props)
}
fn message(

View File

@ -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<K, V> Default for VecMap<K, V> {
}
}
impl<K: fmt::Debug, V: fmt::Debug> fmt::Debug for VecMap<K, V> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.debug_map().entries(self.iter()).finish()
}
}
impl<K, V> VecMap<K, V> {
/// Returns a reference to the value corresponding to the key.
///

View File

@ -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<AppState> {
el::div((
el::span(format!("clicked {} times", state.clicks)).attr("class", state.class),
el::span(format!("clicked {} times", state.clicks)).class(state.class),
el::br(()),
btn("+1 click", |state, _| state.increment()),
btn("-1 click", |state, _| state.decrement()),

View File

@ -4,7 +4,8 @@ use state::{AppState, Filter, Todo};
use wasm_bindgen::JsCast;
use xilem_web::{
elements::html as el, get_element_by_id, interfaces::*, Action, Adapt, App, MessageResult, View,
elements::html as el, get_element_by_id, interfaces::*, style as s, Action, Adapt, App,
MessageResult, View,
};
// All of these actions arise from within a `Todo`, but we need access to the full state to reduce
@ -19,16 +20,8 @@ enum TodoAction {
impl Action for TodoAction {}
fn todo_item(todo: &mut Todo, editing: bool) -> impl Element<Todo, TodoAction> {
let mut class = String::new();
if todo.completed {
class.push_str(" completed");
}
if editing {
class.push_str(" editing");
}
let checkbox = el::input(())
.attr("class", "toggle")
.class("toggle")
.attr("type", "checkbox")
.attr("checked", todo.completed)
.on_click(|state: &mut Todo, _| state.completed = !state.completed);
@ -39,13 +32,13 @@ fn todo_item(todo: &mut Todo, editing: bool) -> impl Element<Todo, TodoAction> {
el::label(todo.title.clone())
.on_dblclick(|state: &mut Todo, _| TodoAction::SetEditing(state.id)),
el::button(())
.attr("class", "destroy")
.class("destroy")
.on_click(|state: &mut Todo, _| TodoAction::Destroy(state.id)),
))
.attr("class", "view"),
.class("view"),
el::input(())
.attr("value", todo.title_editing.clone())
.attr("class", "edit")
.class("edit")
.on_keydown(|state: &mut Todo, evt| {
let key = evt.key();
if key == "Enter" {
@ -70,7 +63,8 @@ fn todo_item(todo: &mut Todo, editing: bool) -> impl Element<Todo, TodoAction> {
.passive(true)
.on_blur(|_, _| TodoAction::CancelEditing),
))
.attr("class", class)
.class(todo.completed.then_some("completed"))
.class(editing.then_some("editing"))
}
fn footer_view(state: &mut AppState, should_display: bool) -> impl Element<AppState> {
@ -82,7 +76,7 @@ fn footer_view(state: &mut AppState, should_display: bool) -> impl Element<AppSt
let clear_button = (state.todos.iter().filter(|todo| todo.completed).count() > 0).then(|| {
Element::on_click(
el::button("Clear completed").attr("class", "clear-completed"),
el::button("Clear completed").class("clear-completed"),
|state: &mut AppState, _| {
state.todos.retain(|todo| !todo.completed);
},
@ -96,12 +90,12 @@ fn footer_view(state: &mut AppState, should_display: bool) -> impl Element<AppSt
el::strong(state.todos.len().to_string()),
format!(" {} left", item_str),
))
.attr("class", "todo-count"),
.class("todo-count"),
el::ul((
el::li(Element::on_click(
el::a("All")
.attr("href", "#/")
.attr("class", filter_class(Filter::All)),
.class(filter_class(Filter::All)),
|state: &mut AppState, _| {
state.filter = Filter::All;
},
@ -110,7 +104,7 @@ fn footer_view(state: &mut AppState, should_display: bool) -> impl Element<AppSt
el::li(Element::on_click(
el::a("Active")
.attr("href", "#/active")
.attr("class", filter_class(Filter::Active)),
.class(filter_class(Filter::Active)),
|state: &mut AppState, _| {
state.filter = Filter::Active;
},
@ -119,17 +113,17 @@ fn footer_view(state: &mut AppState, should_display: bool) -> impl Element<AppSt
el::li(Element::on_click(
el::a("Completed")
.attr("href", "#/completed")
.attr("class", filter_class(Filter::Completed)),
.class(filter_class(Filter::Completed)),
|state: &mut AppState, _| {
state.filter = Filter::Completed;
},
)),
))
.attr("class", "filters"),
.class("filters"),
clear_button,
))
.attr("class", "footer")
.attr("style", (!should_display).then_some("display:none;"))
.class("footer")
.style((!should_display).then_some(s("display", "none")))
}
fn main_view(state: &mut AppState, should_display: bool) -> impl Element<AppState> {
@ -158,17 +152,17 @@ fn main_view(state: &mut AppState, should_display: bool) -> impl Element<AppStat
.collect();
let toggle_all = el::input(())
.attr("id", "toggle-all")
.attr("class", "toggle-all")
.class("toggle-all")
.attr("type", "checkbox")
.attr("checked", state.are_all_complete());
el::section((
toggle_all.on_click(|state: &mut AppState, _| state.toggle_all_complete()),
el::label(()).attr("for", "toggle-all"),
el::ul(todos).attr("class", "todo-list"),
el::ul(todos).class("todo-list"),
))
.attr("class", "main")
.attr("style", (!should_display).then_some("display:none;"))
.class("main")
.style((!should_display).then_some(s("display", "none")))
}
fn app_logic(state: &mut AppState) -> impl View<AppState> {
@ -177,7 +171,7 @@ fn app_logic(state: &mut AppState) -> impl View<AppState> {
let main = main_view(state, some_todos);
let footer = footer_view(state, some_todos);
let input = el::input(())
.attr("class", "new-todo")
.class("new-todo")
.attr("placeholder", "What needs to be done?")
.attr("value", state.new_todo.clone())
.attr("autofocus", true);
@ -202,7 +196,7 @@ fn app_logic(state: &mut AppState) -> impl View<AppState> {
})
.passive(false),
))
.attr("class", "header"),
.class("header"),
main,
footer,
))