From d40a94fa8a3fef5804ad026bb9fd8e7655106a75 Mon Sep 17 00:00:00 2001 From: Philipp Mildenberger Date: Thu, 23 Nov 2023 17:25:42 +0100 Subject: [PATCH] xilem_html: Significant refactor and introduce DOM interface traits (#141) * xilem_html: Introduce DOM interface traits to make per DOM element typing possible and flexible * Cleanup a little bit (remove artifacts from refactor) * Use separate sealed interfaces to avoid blanket trait impl collisions * Add generic params T and A to event views and Attr via PhantomData, to avoid inference issues with composed types * xilem_html: Refactor DOM interface macros * Add ancestor *and* descendent composable macros * Added a macro to correctly and conveniently implement dom interfaces for interface restricted Views, this is used currently for the Attr view and the event views * xilem_html: Add namespace to element generation and refactor element/attribute logic into separate functions * xilem_html: Remove unnecessary features * xilem_html: Implement DOM interface traits for Adapt and AdaptState * xilem_html: Refactor OneOf views * Eliminate associated trait bound for the View, for easier use * Add additional OneSeqOf view sequences, to avoid removing the ViewMarker super trait bound on Element * implement all dom interfaces for the OneOf views * xilem_html: Reorder generic type parameters in composing views such as `Attr` --- Cargo.lock | 3 +- Cargo.toml | 2 +- crates/xilem_html/Cargo.toml | 89 +++- crates/xilem_html/src/attribute.rs | 52 ++ .../src/{element => }/attribute_value.rs | 2 +- crates/xilem_html/src/class.rs | 75 --- crates/xilem_html/src/context.rs | 112 ++++- crates/xilem_html/src/element/elements.rs | 232 --------- crates/xilem_html/src/element/mod.rs | 274 ----------- crates/xilem_html/src/elements.rs | 405 ++++++++++++++++ crates/xilem_html/src/event/events.rs | 273 ----------- crates/xilem_html/src/event/mod.rs | 211 -------- crates/xilem_html/src/events.rs | 381 +++++++++++++++ crates/xilem_html/src/interfaces.rs | 449 ++++++++++++++++++ crates/xilem_html/src/lib.rs | 32 +- crates/xilem_html/src/one_of.rs | 121 +++-- crates/xilem_html/src/optional_action.rs | 35 ++ crates/xilem_html/src/vecmap.rs | 37 ++ crates/xilem_html/src/view.rs | 18 +- crates/xilem_html/src/view_ext.rs | 68 +-- .../web_examples/counter/src/main.rs | 13 +- .../Cargo.toml | 6 +- .../index.html | 0 .../src/main.rs | 29 +- .../web_examples/todomvc/src/main.rs | 73 +-- 25 files changed, 1706 insertions(+), 1286 deletions(-) create mode 100644 crates/xilem_html/src/attribute.rs rename crates/xilem_html/src/{element => }/attribute_value.rs (98%) delete mode 100644 crates/xilem_html/src/class.rs delete mode 100644 crates/xilem_html/src/element/elements.rs delete mode 100644 crates/xilem_html/src/element/mod.rs create mode 100644 crates/xilem_html/src/elements.rs delete mode 100644 crates/xilem_html/src/event/events.rs delete mode 100644 crates/xilem_html/src/event/mod.rs create mode 100644 crates/xilem_html/src/events.rs create mode 100644 crates/xilem_html/src/interfaces.rs create mode 100644 crates/xilem_html/src/optional_action.rs rename crates/xilem_html/web_examples/{counter_untyped => counter_custom_element}/Cargo.toml (52%) rename crates/xilem_html/web_examples/{counter_untyped => counter_custom_element}/index.html (100%) rename crates/xilem_html/web_examples/{counter_untyped => counter_custom_element}/src/main.rs (56%) diff --git a/Cargo.lock b/Cargo.lock index 1acbd903..c405b6b2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -593,7 +593,7 @@ dependencies = [ ] [[package]] -name = "counter_untyped" +name = "counter_custom_element" version = "0.1.0" dependencies = [ "console_error_panic_hook", @@ -3030,6 +3030,7 @@ dependencies = [ "gloo", "kurbo 0.9.5", "log", + "paste", "wasm-bindgen", "web-sys", "xilem_core", diff --git a/Cargo.toml b/Cargo.toml index e346fce1..d3de1c07 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ members = [ "crates/xilem_core", "crates/xilem_html", "crates/xilem_html/web_examples/counter", - "crates/xilem_html/web_examples/counter_untyped", + "crates/xilem_html/web_examples/counter_custom_element", "crates/xilem_html/web_examples/todomvc", "crates/xilem_svg", "crates/xilem_svg/web_examples/svgtoy", diff --git a/crates/xilem_html/Cargo.toml b/crates/xilem_html/Cargo.toml index 35fecb98..01efb1a4 100644 --- a/crates/xilem_html/Cargo.toml +++ b/crates/xilem_html/Cargo.toml @@ -17,36 +17,12 @@ default-target = "x86_64-pc-windows-msvc" # rustdoc-scrape-examples tracking issue https://github.com/rust-lang/rust/issues/88791 cargo-args = ["-Zunstable-options", "-Zrustdoc-scrape-examples"] -[features] -default = ["typed"] -typed = [ - "web-sys/FocusEvent", "web-sys/HtmlAnchorElement", "web-sys/HtmlAreaElement", - "web-sys/HtmlAudioElement", "web-sys/HtmlBrElement", "web-sys/HtmlButtonElement", - "web-sys/HtmlCanvasElement", "web-sys/HtmlDataElement", "web-sys/HtmlDataListElement", - "web-sys/HtmlDetailsElement", "web-sys/HtmlDialogElement", "web-sys/HtmlDivElement", - "web-sys/HtmlDListElement", "web-sys/HtmlEmbedElement", "web-sys/HtmlFieldSetElement", - "web-sys/HtmlFormElement", "web-sys/HtmlHeadingElement", "web-sys/HtmlHrElement", - "web-sys/HtmlIFrameElement", "web-sys/HtmlImageElement", "web-sys/HtmlInputElement", - "web-sys/HtmlLabelElement", "web-sys/HtmlLegendElement", "web-sys/HtmlLiElement", - "web-sys/HtmlMapElement", "web-sys/HtmlMenuElement", "web-sys/HtmlMeterElement", - "web-sys/HtmlModElement", "web-sys/HtmlObjectElement", "web-sys/HtmlOListElement", - "web-sys/HtmlOptGroupElement", "web-sys/HtmlOptionElement", "web-sys/HtmlOutputElement", - "web-sys/HtmlParagraphElement", "web-sys/HtmlPictureElement", "web-sys/HtmlPreElement", - "web-sys/HtmlProgressElement", "web-sys/HtmlQuoteElement", "web-sys/HtmlScriptElement", - "web-sys/HtmlSelectElement", "web-sys/HtmlSlotElement", "web-sys/HtmlSourceElement", - "web-sys/HtmlSpanElement", "web-sys/HtmlTableElement", "web-sys/HtmlTableCellElement", - "web-sys/HtmlTableColElement", "web-sys/HtmlTableCaptionElement", "web-sys/HtmlTableRowElement", - "web-sys/HtmlTableSectionElement", "web-sys/HtmlTemplateElement", "web-sys/HtmlTextAreaElement", - "web-sys/HtmlTimeElement", "web-sys/HtmlTrackElement", "web-sys/HtmlUListElement", - "web-sys/HtmlVideoElement", "web-sys/InputEvent", "web-sys/KeyboardEvent", "web-sys/MouseEvent", - "web-sys/PointerEvent", "web-sys/WheelEvent", -] - [dependencies] xilem_core.workspace = true kurbo.workspace = true bitflags = "2" wasm-bindgen = "0.2.87" +paste = "1" log = "0.4.19" gloo = { version = "0.8.1", default-features = false, features = ["events", "utils"] } @@ -63,4 +39,67 @@ features = [ "SvgElement", "Text", "Window", + "FocusEvent", + "HtmlInputElement", + "InputEvent", + "KeyboardEvent", + "MouseEvent", + "PointerEvent", + "WheelEvent", + "HtmlAnchorElement", + "HtmlAreaElement", + "HtmlAudioElement", + "HtmlBrElement", + "HtmlButtonElement", + "HtmlCanvasElement", + "HtmlDataElement", + "HtmlDataListElement", + "HtmlDetailsElement", + "HtmlDialogElement", + "HtmlDivElement", + "HtmlDListElement", + "HtmlEmbedElement", + "HtmlFieldSetElement", + "HtmlFormElement", + "HtmlHeadingElement", + "HtmlHrElement", + "HtmlIFrameElement", + "HtmlImageElement", + "HtmlInputElement", + "HtmlLabelElement", + "HtmlLegendElement", + "HtmlLiElement", + "HtmlLinkElement", + "HtmlMapElement", + "HtmlMediaElement", + "HtmlMenuElement", + "HtmlMeterElement", + "HtmlModElement", + "HtmlObjectElement", + "HtmlOListElement", + "HtmlOptGroupElement", + "HtmlOptionElement", + "HtmlOutputElement", + "HtmlParagraphElement", + "HtmlPictureElement", + "HtmlPreElement", + "HtmlProgressElement", + "HtmlQuoteElement", + "HtmlScriptElement", + "HtmlSelectElement", + "HtmlSlotElement", + "HtmlSourceElement", + "HtmlSpanElement", + "HtmlTableCaptionElement", + "HtmlTableCellElement", + "HtmlTableColElement", + "HtmlTableElement", + "HtmlTableRowElement", + "HtmlTableSectionElement", + "HtmlTemplateElement", + "HtmlTimeElement", + "HtmlTextAreaElement", + "HtmlTrackElement", + "HtmlUListElement", + "HtmlVideoElement", ] diff --git a/crates/xilem_html/src/attribute.rs b/crates/xilem_html/src/attribute.rs new file mode 100644 index 00000000..b05f3397 --- /dev/null +++ b/crates/xilem_html/src/attribute.rs @@ -0,0 +1,52 @@ +use std::borrow::Cow; +use std::marker::PhantomData; + +use xilem_core::{Id, MessageResult}; + +use crate::{interfaces::sealed::Sealed, AttributeValue, ChangeFlags, Cx, View, ViewMarker}; + +use super::interfaces::Element; + +pub struct Attr { + pub(crate) element: E, + pub(crate) name: Cow<'static, str>, + pub(crate) value: Option, + pub(crate) phantom: PhantomData (T, A)>, +} + +impl ViewMarker for Attr {} +impl Sealed for Attr {} + +impl, T, A> View for Attr { + type State = E::State; + type Element = E::Element; + + fn build(&self, cx: &mut Cx) -> (Id, Self::State, Self::Element) { + cx.add_new_attribute_to_current_element(&self.name, &self.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 { + cx.add_new_attribute_to_current_element(&self.name, &self.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, Attr); diff --git a/crates/xilem_html/src/element/attribute_value.rs b/crates/xilem_html/src/attribute_value.rs similarity index 98% rename from crates/xilem_html/src/element/attribute_value.rs rename to crates/xilem_html/src/attribute_value.rs index 050a3a68..eb78bd51 100644 --- a/crates/xilem_html/src/element/attribute_value.rs +++ b/crates/xilem_html/src/attribute_value.rs @@ -1,6 +1,6 @@ type CowStr = std::borrow::Cow<'static, str>; -#[derive(PartialEq, Debug)] +#[derive(PartialEq, Clone, Debug, PartialOrd)] pub enum AttributeValue { True, // for the boolean true, this serializes to an empty string (e.g. for ) I32(i32), diff --git a/crates/xilem_html/src/class.rs b/crates/xilem_html/src/class.rs deleted file mode 100644 index 1513c35a..00000000 --- a/crates/xilem_html/src/class.rs +++ /dev/null @@ -1,75 +0,0 @@ -// Copyright 2023 the Druid Authors. -// SPDX-License-Identifier: Apache-2.0 - -use std::{any::Any, borrow::Cow}; - -use xilem_core::{Id, MessageResult}; - -use crate::{ - context::{ChangeFlags, Cx}, - view::{DomElement, View, ViewMarker}, -}; - -pub struct Class { - child: V, - // This could reasonably be static Cow also, but keep things simple - class: Cow<'static, str>, -} - -pub fn class(child: V, class: impl Into>) -> Class { - Class { - child, - class: class.into(), - } -} - -impl ViewMarker for Class {} - -// TODO: make generic over A (probably requires Phantom) -impl View for Class -where - V: View, - V::Element: DomElement, -{ - type State = V::State; - type Element = V::Element; - - fn build(&self, cx: &mut Cx) -> (Id, Self::State, Self::Element) { - let (id, child_state, element) = self.child.build(cx); - element - .as_element_ref() - .set_attribute("class", &self.class) - .unwrap(); - (id, child_state, element) - } - - fn rebuild( - &self, - cx: &mut Cx, - prev: &Self, - id: &mut Id, - state: &mut Self::State, - element: &mut V::Element, - ) -> ChangeFlags { - let prev_id = *id; - let mut changed = self.child.rebuild(cx, &prev.child, id, state, element); - if self.class != prev.class || prev_id != *id { - element - .as_element_ref() - .set_attribute("class", &self.class) - .unwrap(); - changed.insert(ChangeFlags::OTHER_CHANGE); - } - changed - } - - fn message( - &self, - id_path: &[Id], - state: &mut Self::State, - message: Box, - app_state: &mut T, - ) -> MessageResult<()> { - self.child.message(id_path, state, message, app_state) - } -} diff --git a/crates/xilem_html/src/context.rs b/crates/xilem_html/src/context.rs index 57e9d42e..4929c9c7 100644 --- a/crates/xilem_html/src/context.rs +++ b/crates/xilem_html/src/context.rs @@ -1,17 +1,51 @@ use std::any::Any; use bitflags::bitflags; -use wasm_bindgen::JsCast; +use wasm_bindgen::{JsCast, UnwrapThrowExt}; use web_sys::Document; use xilem_core::{Id, IdPath}; -use crate::{app::AppRunner, Message, HTML_NS, SVG_NS}; +use crate::{ + app::AppRunner, + diff::{diff_kv_iterables, Diff}, + vecmap::VecMap, + AttributeValue, Message, +}; + +type CowStr = std::borrow::Cow<'static, str>; + +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. + if name == "value" { + let element: &web_sys::HtmlInputElement = element.dyn_ref().unwrap_throw(); + element.set_value(value) + } else if name == "checked" { + let element: &web_sys::HtmlInputElement = element.dyn_ref().unwrap_throw(); + element.set_checked(true) + } else { + element.set_attribute(name, value).unwrap_throw(); + } +} + +fn remove_attribute(element: &web_sys::Element, name: &str) { + // we have to special-case `checked` because setting the value using `set_attribute` + // doesn't work after the value has been changed. + if name == "checked" { + let element: &web_sys::HtmlInputElement = element.dyn_ref().unwrap_throw(); + element.set_checked(false) + } else { + element.remove_attribute(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, app_ref: Option>, } @@ -34,6 +68,7 @@ impl Cx { id_path: Vec::new(), document: crate::document(), app_ref: None, + current_element_attributes: Default::default(), } } @@ -75,18 +110,77 @@ impl Cx { &self.document } - pub fn create_element(&self, ns: &str, name: &str) -> web_sys::Element { - self.document + pub(crate) fn build_element( + &mut self, + ns: &str, + name: &str, + ) -> (web_sys::Element, VecMap) { + let el = self + .document .create_element_ns(Some(ns), name) - .expect("could not create element") + .expect("could not create element"); + let attributes = self.apply_attributes(&el); + (el, attributes) } - pub fn create_html_element(&self, name: &str) -> web_sys::HtmlElement { - self.create_element(HTML_NS, name).unchecked_into() + pub(crate) fn rebuild_element( + &mut self, + element: &web_sys::Element, + attributes: &mut VecMap, + ) -> ChangeFlags { + self.apply_attribute_changes(element, attributes) } - pub fn create_svg_element(&self, name: &str) -> web_sys::SvgElement { - self.create_element(SVG_NS, name).unchecked_into() + // 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_new_attribute_to_current_element( + &mut self, + name: &CowStr, + value: &Option, + ) { + 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 + .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 fn message_thunk(&self) -> MessageThunk { diff --git a/crates/xilem_html/src/element/elements.rs b/crates/xilem_html/src/element/elements.rs deleted file mode 100644 index 03b1dba9..00000000 --- a/crates/xilem_html/src/element/elements.rs +++ /dev/null @@ -1,232 +0,0 @@ -//! Types that wrap [`Element`][super::Element] and represent specific element types. -//! -macro_rules! elements { - () => {}; - (($ty_name:ident, $name:ident, $web_sys_ty:ty), $($rest:tt)*) => { - element!($ty_name, $name, $web_sys_ty); - elements!($($rest)*); - }; -} - -macro_rules! element { - ($ty_name:ident, $name:ident, $web_sys_ty:ty) => { - /// A view representing a - #[doc = concat!("`", stringify!($name), "`")] - /// element. - pub struct $ty_name(crate::Element<$web_sys_ty, ViewSeq>); - - /// Builder function for a - #[doc = concat!("`", stringify!($name), "`")] - /// view. - pub fn $name(children: ViewSeq) -> $ty_name { - $ty_name(crate::element(stringify!($name), children)) - } - - impl $ty_name { - /// Set an attribute on this element. - /// - /// # Panics - /// - /// If the name contains characters that are not valid in an attribute name, - /// then the `View::build`/`View::rebuild` functions will panic for this view. - pub fn attr( - mut self, - name: impl Into>, - value: impl crate::IntoAttributeValue, - ) -> Self { - self.0.set_attr(name, value); - self - } - - /// Set an attribute on this element. - /// - /// # Panics - /// - /// If the name contains characters that are not valid in an attribute name, - /// then the `View::build`/`View::rebuild` functions will panic for this view. - pub fn set_attr( - &mut self, - name: impl Into>, - value: impl crate::IntoAttributeValue, - ) -> &mut Self { - self.0.set_attr(name, value); - self - } - - pub fn remove_attr(&mut self, name: &str) -> &mut Self { - self.0.remove_attr(name); - self - } - - pub fn after_update(mut self, after_update: impl Fn(&$web_sys_ty) + 'static) -> Self { - self.0 = self.0.after_update(after_update); - self - } - } - - impl crate::view::ViewMarker for $ty_name {} - - impl crate::view::View for $ty_name - where - ViewSeq: crate::view::ViewSequence, - { - type State = crate::ElementState; - type Element = $web_sys_ty; - - fn build( - &self, - cx: &mut crate::context::Cx, - ) -> (xilem_core::Id, Self::State, Self::Element) { - self.0.build(cx) - } - - fn rebuild( - &self, - cx: &mut crate::context::Cx, - prev: &Self, - id: &mut xilem_core::Id, - state: &mut Self::State, - element: &mut Self::Element, - ) -> crate::ChangeFlags { - self.0.rebuild(cx, &prev.0, id, state, element) - } - - fn message( - &self, - id_path: &[xilem_core::Id], - state: &mut Self::State, - message: Box, - app_state: &mut T_, - ) -> xilem_core::MessageResult { - self.0.message(id_path, state, message, app_state) - } - } - }; -} - -// void elements (those without children) are `area`, `base`, `br`, `col`, -// `embed`, `hr`, `img`, `input`, `link`, `meta`, `source`, `track`, `wbr` -elements!( - // the order is copied from - // https://developer.mozilla.org/en-US/docs/Web/HTML/Element - // DOM interfaces copied from https://html.spec.whatwg.org/multipage/grouping-content.html and friends - - // content sectioning - (Address, address, web_sys::HtmlElement), - (Article, article, web_sys::HtmlElement), - (Aside, aside, web_sys::HtmlElement), - (Footer, footer, web_sys::HtmlElement), - (Header, header, web_sys::HtmlElement), - (H1, h1, web_sys::HtmlHeadingElement), - (H2, h2, web_sys::HtmlHeadingElement), - (H3, h3, web_sys::HtmlHeadingElement), - (H4, h4, web_sys::HtmlHeadingElement), - (H5, h5, web_sys::HtmlHeadingElement), - (H6, h6, web_sys::HtmlHeadingElement), - (Hgroup, hgroup, web_sys::HtmlElement), - (Main, main, web_sys::HtmlElement), - (Nav, nav, web_sys::HtmlElement), - (Section, section, web_sys::HtmlElement), - // text content - (Blockquote, blockquote, web_sys::HtmlQuoteElement), - (Dd, dd, web_sys::HtmlElement), - (Div, div, web_sys::HtmlDivElement), - (Dl, dl, web_sys::HtmlDListElement), - (Dt, dt, web_sys::HtmlElement), - (Figcaption, figcaption, web_sys::HtmlElement), - (Figure, figure, web_sys::HtmlElement), - (Hr, hr, web_sys::HtmlHrElement), - (Li, li, web_sys::HtmlLiElement), - (Menu, menu, web_sys::HtmlMenuElement), - (Ol, ol, web_sys::HtmlOListElement), - (P, p, web_sys::HtmlParagraphElement), - (Pre, pre, web_sys::HtmlPreElement), - (Ul, ul, web_sys::HtmlUListElement), - // inline text - (A, a, web_sys::HtmlAnchorElement), - (Abbr, abbr, web_sys::HtmlElement), - (B, b, web_sys::HtmlElement), - (Bdi, bdi, web_sys::HtmlElement), - (Bdo, bdo, web_sys::HtmlElement), - (Br, br, web_sys::HtmlBrElement), - (Cite, cite, web_sys::HtmlElement), - (Code, code, web_sys::HtmlElement), - (Data, data, web_sys::HtmlDataElement), - (Dfn, dfn, web_sys::HtmlElement), - (Em, em, web_sys::HtmlElement), - (I, i, web_sys::HtmlElement), - (Kbd, kbd, web_sys::HtmlElement), - (Mark, mark, web_sys::HtmlElement), - (Q, q, web_sys::HtmlQuoteElement), - (Rp, rp, web_sys::HtmlElement), - (Rt, rt, web_sys::HtmlElement), - (Ruby, ruby, web_sys::HtmlElement), - (S, s, web_sys::HtmlElement), - (Samp, samp, web_sys::HtmlElement), - (Small, small, web_sys::HtmlElement), - (Span, span, web_sys::HtmlSpanElement), - (Strong, strong, web_sys::HtmlElement), - (Sub, sub, web_sys::HtmlElement), - (Sup, sup, web_sys::HtmlElement), - (Time, time, web_sys::HtmlTimeElement), - (U, u, web_sys::HtmlElement), - (Var, var, web_sys::HtmlElement), - (Wbr, wbr, web_sys::HtmlElement), - // image and multimedia - (Area, area, web_sys::HtmlAreaElement), - (Audio, audio, web_sys::HtmlAudioElement), - (Img, img, web_sys::HtmlImageElement), - (Map, map, web_sys::HtmlMapElement), - (Track, track, web_sys::HtmlTrackElement), - (Video, video, web_sys::HtmlVideoElement), - // embedded content - (Embed, embed, web_sys::HtmlEmbedElement), - (Iframe, iframe, web_sys::HtmlIFrameElement), - (Object, object, web_sys::HtmlObjectElement), - (Picture, picture, web_sys::HtmlPictureElement), - (Portal, portal, web_sys::HtmlElement), - (Source, source, web_sys::HtmlSourceElement), - // SVG and MathML (TODO, svg and mathml elements) - (Svg, svg, web_sys::HtmlElement), - (Math, math, web_sys::HtmlElement), - // scripting - (Canvas, canvas, web_sys::HtmlCanvasElement), - (Noscript, noscript, web_sys::HtmlElement), - (Script, script, web_sys::HtmlScriptElement), - // demarcating edits - (Del, del, web_sys::HtmlModElement), - (Ins, ins, web_sys::HtmlModElement), - // tables - (Caption, caption, web_sys::HtmlTableCaptionElement), - (Col, col, web_sys::HtmlTableColElement), - (Colgroup, colgroup, web_sys::HtmlTableColElement), - (Table, table, web_sys::HtmlTableElement), - (Tbody, tbody, web_sys::HtmlTableSectionElement), - (Td, td, web_sys::HtmlTableCellElement), - (Tfoot, tfoot, web_sys::HtmlTableSectionElement), - (Th, th, web_sys::HtmlTableCellElement), - (Thead, thead, web_sys::HtmlTableSectionElement), - (Tr, tr, web_sys::HtmlTableRowElement), - // forms - (Button, button, web_sys::HtmlButtonElement), - (Datalist, datalist, web_sys::HtmlDataListElement), - (Fieldset, fieldset, web_sys::HtmlFieldSetElement), - (Form, form, web_sys::HtmlFormElement), - (Input, input, web_sys::HtmlInputElement), - (Label, label, web_sys::HtmlLabelElement), - (Legend, legend, web_sys::HtmlLegendElement), - (Meter, meter, web_sys::HtmlMeterElement), - (Optgroup, optgroup, web_sys::HtmlOptGroupElement), - (OptionElement, option, web_sys::HtmlOptionElement), - (Output, output, web_sys::HtmlOutputElement), - (Progress, progress, web_sys::HtmlProgressElement), - (Select, select, web_sys::HtmlSelectElement), - (Textarea, textarea, web_sys::HtmlTextAreaElement), - // interactive elements, - (Details, details, web_sys::HtmlDetailsElement), - (Dialog, dialog, web_sys::HtmlDialogElement), - (Summary, summary, web_sys::HtmlElement), - // web components, - (Slot, slot, web_sys::HtmlSlotElement), - (Template, template, web_sys::HtmlTemplateElement), -); diff --git a/crates/xilem_html/src/element/mod.rs b/crates/xilem_html/src/element/mod.rs deleted file mode 100644 index d3dca3d6..00000000 --- a/crates/xilem_html/src/element/mod.rs +++ /dev/null @@ -1,274 +0,0 @@ -//! The HTML element view and associated types/functions. -//! -//! If you are writing your own views, we recommend adding -//! `use xilem_html::elements as el` or similar to the top of your file. -use crate::{ - context::{ChangeFlags, Cx}, - diff::{diff_kv_iterables, Diff}, - vecmap::VecMap, - view::{DomElement, Pod, View, ViewMarker, ViewSequence}, -}; - -use std::{borrow::Cow, fmt}; -use wasm_bindgen::{JsCast, UnwrapThrowExt}; -use xilem_core::{Id, MessageResult, VecSplice}; - -mod attribute_value; -#[cfg(feature = "typed")] -pub mod elements; - -pub use attribute_value::{AttributeValue, IntoAttributeValue}; - -type CowStr = Cow<'static, str>; - -/// A view representing a HTML element. -/// -/// If the element has no children, use the unit type (e.g. `let view = element("div", ())`). -pub struct Element { - name: CowStr, - attributes: VecMap, - children: Children, - #[allow(clippy::type_complexity)] - after_update: Option>, -} - -impl Element { - pub fn debug_as_el(&self) -> impl fmt::Debug + '_ { - struct DebugFmt<'a, El, VS>(&'a Element); - impl<'a, El, VS> fmt::Debug for DebugFmt<'a, El, VS> { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "<{}", self.0.name)?; - for (name, value) in &self.0.attributes { - write!(f, " {name}=\"{}\"", value.serialize())?; - } - write!(f, ">") - } - } - DebugFmt(self) - } -} - -/// The state associated with a HTML element `View`. -/// -/// Stores handles to the child elements and any child state. -pub struct ElementState { - child_states: ViewSeqState, - child_elements: Vec, - scratch: Vec, -} - -/// Create a new element view -/// -/// If the element has no children, use the unit type (e.g. `let view = element("div", ())`). -pub fn element(name: impl Into, children: ViewSeq) -> Element { - Element { - name: name.into(), - attributes: Default::default(), - children, - after_update: None, - } -} - -impl Element { - /// Set an attribute on this element. - /// - /// # Panics - /// - /// If the name contains characters that are not valid in an attribute name, - /// then the `View::build`/`View::rebuild` functions will panic for this view. - pub fn attr(mut self, name: impl Into, value: impl IntoAttributeValue) -> Self { - self.set_attr(name, value); - self - } - - /// Set an attribute on this element. - /// - /// # Panics - /// - /// If the name contains characters that are not valid in an attribute name, - /// then the `View::build`/`View::rebuild` functions will panic for this view. - pub fn set_attr(&mut self, name: impl Into, value: impl IntoAttributeValue) { - let name = name.into(); - if let Some(value) = value.into_attribute_value() { - self.attributes.insert(name, value); - } else { - self.remove_attr(&name); - } - } - - pub fn remove_attr(&mut self, name: &str) { - self.attributes.remove(name); - } - - /// Set a function to run after the new view tree has been created. - /// - /// This offers functionality similar to `ref` in React. - /// - /// # Rules for correct use - /// - /// It is important that the structure of the DOM tree is *not* modified using this function. - /// If the DOM tree is modified, then future reconciliation will have undefined and possibly - /// suprising results. - pub fn after_update(mut self, after_update: impl Fn(&El) + 'static) -> Self { - self.after_update = Some(Box::new(after_update)); - self - } -} - -impl ViewMarker for Element {} - -impl View for Element -where - Children: ViewSequence, - El: JsCast + DomElement, -{ - type State = ElementState; - type Element = El; - - fn build(&self, cx: &mut Cx) -> (Id, Self::State, Self::Element) { - let el = cx.create_html_element(&self.name); - for (name, value) in &self.attributes { - el.set_attribute(name, &value.serialize()).unwrap_throw(); - } - - let mut child_elements = vec![]; - let (id, child_states) = cx.with_new_id(|cx| self.children.build(cx, &mut child_elements)); - for child in &child_elements { - el.append_child(child.0.as_node_ref()).unwrap_throw(); - } - - // Set the id used internally to the `data-debugid` attribute. - // This allows the user to see if an element has been re-created or only altered. - #[cfg(debug_assertions)] - el.set_attribute("data-debugid", &id.to_raw().to_string()) - .unwrap_throw(); - - let el = el.dyn_into().unwrap_throw(); - if let Some(after_update) = &self.after_update { - (after_update)(&el); - } - let state = ElementState { - child_states, - child_elements, - scratch: vec![], - }; - (id, state, el) - } - - fn rebuild( - &self, - cx: &mut Cx, - prev: &Self, - id: &mut Id, - state: &mut Self::State, - element: &mut Self::Element, - ) -> ChangeFlags { - let mut changed = ChangeFlags::empty(); - // update tag name - if prev.name != self.name { - // recreate element - let parent = element - .as_element_ref() - .parent_element() - .expect_throw("this element was mounted and so should have a parent"); - parent.remove_child(element.as_node_ref()).unwrap_throw(); - let new_element = cx.create_html_element(&self.name); - // TODO could this be combined with child updates? - while element.as_element_ref().child_element_count() > 0 { - new_element - .append_child(&element.as_element_ref().child_nodes().get(0).unwrap_throw()) - .unwrap_throw(); - } - *element = new_element.dyn_into().unwrap_throw(); - changed |= ChangeFlags::STRUCTURE; - } - - let element = element.as_element_ref(); - - // update attributes - for itm in diff_kv_iterables(&prev.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; - } - } - } - - // update children - let mut splice = VecSplice::new(&mut state.child_elements, &mut state.scratch); - changed |= cx.with_id(*id, |cx| { - self.children - .rebuild(cx, &prev.children, &mut state.child_states, &mut splice) - }); - if changed.contains(ChangeFlags::STRUCTURE) { - // This is crude and will result in more DOM traffic than needed. - // The right thing to do is diff the new state of the children id - // vector against the old, and derive DOM mutations from that. - while let Some(child) = element.first_child() { - element.remove_child(&child).unwrap_throw(); - } - for child in &state.child_elements { - element.append_child(child.0.as_node_ref()).unwrap_throw(); - } - changed.remove(ChangeFlags::STRUCTURE); - } - if let Some(after_update) = &self.after_update { - (after_update)(element.dyn_ref().unwrap_throw()); - changed |= ChangeFlags::OTHER_CHANGE; - } - changed - } - - fn message( - &self, - id_path: &[Id], - state: &mut Self::State, - message: Box, - app_state: &mut T, - ) -> MessageResult { - self.children - .message(id_path, &mut state.child_states, message, app_state) - } -} - -#[cfg(feature = "typed")] -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. - if name == "value" { - let element: &web_sys::HtmlInputElement = element.dyn_ref().unwrap_throw(); - element.set_value(value) - } else if name == "checked" { - let element: &web_sys::HtmlInputElement = element.dyn_ref().unwrap_throw(); - element.set_checked(true) - } else { - element.set_attribute(name, value).unwrap_throw(); - } -} - -#[cfg(not(feature = "typed"))] -fn set_attribute(element: &web_sys::Element, name: &str, value: &str) { - element.set_attribute(name, value).unwrap_throw(); -} - -#[cfg(feature = "typed")] -fn remove_attribute(element: &web_sys::Element, name: &str) { - // we have to special-case `value` because setting the value using `set_attribute` - // doesn't work after the value has been changed. - if name == "checked" { - let element: &web_sys::HtmlInputElement = element.dyn_ref().unwrap_throw(); - element.set_checked(false) - } else { - element.remove_attribute(name).unwrap_throw(); - } -} - -#[cfg(not(feature = "typed"))] -fn remove_attribute(element: &web_sys::Element, name: &str) { - element.remove_attribute(name).unwrap_throw(); -} diff --git a/crates/xilem_html/src/elements.rs b/crates/xilem_html/src/elements.rs new file mode 100644 index 00000000..15bb03d9 --- /dev/null +++ b/crates/xilem_html/src/elements.rs @@ -0,0 +1,405 @@ +use std::marker::PhantomData; + +use wasm_bindgen::{JsCast, UnwrapThrowExt}; +use xilem_core::{Id, MessageResult, VecSplice}; + +use crate::{ + interfaces::sealed::Sealed, vecmap::VecMap, view::DomNode, AttributeValue, ChangeFlags, Cx, + Pod, View, ViewMarker, ViewSequence, HTML_NS, MATHML_NS, SVG_NS, +}; + +use super::interfaces::Element; + +type CowStr = std::borrow::Cow<'static, str>; + +/// The state associated with a HTML element `View`. +/// +/// 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) child_elements: Vec, + pub(crate) scratch: Vec, +} + +// TODO something like the `after_update` of the former `Element` view (likely as a wrapper view instead) + +pub struct CustomElement { + name: CowStr, + children: Children, + #[allow(clippy::type_complexity)] + phantom: PhantomData (T, A)>, +} + +/// Builder function for a custom element view. +pub fn custom_element>( + name: impl Into, + children: Children, +) -> CustomElement { + CustomElement { + name: name.into(), + children, + phantom: PhantomData, + } +} + +impl CustomElement { + fn node_name(&self) -> &str { + &self.name + } +} + +impl ViewMarker for CustomElement {} +impl Sealed for CustomElement {} + +impl View for CustomElement +where + Children: ViewSequence, +{ + type State = ElementState; + + // This is mostly intended for Autonomous custom elements, + // TODO: Custom builtin components need some special handling (`document.createElement("p", { is: "custom-component" })`) + 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 mut child_elements = vec![]; + let (id, children_states) = + cx.with_new_id(|cx| self.children.build(cx, &mut child_elements)); + + for child in &child_elements { + el.append_child(child.0.as_node_ref()).unwrap_throw(); + } + + // Set the id used internally to the `data-debugid` attribute. + // This allows the user to see if an element has been re-created or only altered. + #[cfg(debug_assertions)] + el.set_attribute("data-debugid", &id.to_raw().to_string()) + .unwrap_throw(); + + let el = el.dyn_into().unwrap_throw(); + let state = ElementState { + children_states, + child_elements, + scratch: vec![], + attributes, + }; + (id, state, el) + } + + fn rebuild( + &self, + cx: &mut Cx, + prev: &Self, + id: &mut Id, + state: &mut Self::State, + element: &mut Self::Element, + ) -> ChangeFlags { + let mut changed = ChangeFlags::empty(); + + // update tag name + if prev.name != self.name { + // recreate element + let parent = element + .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; + // TODO could this be combined with child updates? + while element.child_element_count() > 0 { + new_element + .append_child(&element.child_nodes().get(0).unwrap_throw()) + .unwrap_throw(); + } + *element = new_element.dyn_into().unwrap_throw(); + changed |= ChangeFlags::STRUCTURE; + } + + changed |= cx.rebuild_element(element, &mut state.attributes); + + // update children + let mut splice = VecSplice::new(&mut state.child_elements, &mut state.scratch); + changed |= cx.with_id(*id, |cx| { + self.children + .rebuild(cx, &prev.children, &mut state.children_states, &mut splice) + }); + if changed.contains(ChangeFlags::STRUCTURE) { + // This is crude and will result in more DOM traffic than needed. + // The right thing to do is diff the new state of the children id + // vector against the old, and derive DOM mutations from that. + while let Some(child) = element.first_child() { + element.remove_child(&child).unwrap_throw(); + } + for child in &state.child_elements { + element.append_child(child.0.as_node_ref()).unwrap_throw(); + } + changed.remove(ChangeFlags::STRUCTURE); + } + changed + } + + fn message( + &self, + id_path: &[Id], + state: &mut Self::State, + message: Box, + app_state: &mut T, + ) -> MessageResult { + self.children + .message(id_path, &mut state.children_states, message, app_state) + } +} + +impl> Element for CustomElement {} +impl> crate::interfaces::HtmlElement + for CustomElement +{ +} + +macro_rules! generate_dom_interface_impl { + ($dom_interface:ident, ($ty_name:ident, $t:ident, $a:ident, $vs:ident)) => { + impl<$t, $a, $vs> $crate::interfaces::$dom_interface<$t, $a> for $ty_name<$t, $a, $vs> where + $vs: $crate::view::ViewSequence<$t, $a> + { + } + }; +} + +// TODO maybe it's possible to reduce even more in the impl function bodies and put into impl_functions +// (should improve compile times and probably wasm binary size) +macro_rules! define_element { + (($ns:expr, $ty_name:ident, $name:ident, $dom_interface:ident)) => { + define_element!(($ns, $ty_name, $name, $dom_interface, T, A, VS)); + }; + (($ns:expr, $ty_name:ident, $name:ident, $dom_interface:ident, $t:ident, $a: ident, $vs: ident)) => { + pub struct $ty_name<$t, $a = (), $vs = ()>($vs, PhantomData ($t, $a)>); + + impl<$t, $a, $vs> ViewMarker for $ty_name<$t, $a, $vs> {} + impl<$t, $a, $vs> Sealed for $ty_name<$t, $a, $vs> {} + + impl<$t, $a, $vs: ViewSequence<$t, $a>> View<$t, $a> for $ty_name<$t, $a, $vs> { + type State = ElementState<$vs::State>; + type Element = web_sys::$dom_interface; + + fn build(&self, cx: &mut Cx) -> (Id, Self::State, Self::Element) { + let (el, attributes) = cx.build_element($ns, stringify!($name)); + + let mut child_elements = vec![]; + let (id, children_states) = + cx.with_new_id(|cx| self.0.build(cx, &mut child_elements)); + for child in &child_elements { + el.append_child(child.0.as_node_ref()).unwrap_throw(); + } + + // Set the id used internally to the `data-debugid` attribute. + // This allows the user to see if an element has been re-created or only altered. + #[cfg(debug_assertions)] + el.set_attribute("data-debugid", &id.to_raw().to_string()) + .unwrap_throw(); + + let el = el.dyn_into().unwrap_throw(); + let state = ElementState { + children_states, + child_elements, + scratch: vec![], + attributes, + }; + (id, state, el) + } + + fn rebuild( + &self, + cx: &mut Cx, + prev: &Self, + id: &mut Id, + state: &mut Self::State, + element: &mut Self::Element, + ) -> ChangeFlags { + let mut changed = ChangeFlags::empty(); + + changed |= cx.apply_attribute_changes(element, &mut state.attributes); + + // update children + let mut splice = VecSplice::new(&mut state.child_elements, &mut state.scratch); + changed |= cx.with_id(*id, |cx| { + self.0 + .rebuild(cx, &prev.0, &mut state.children_states, &mut splice) + }); + if changed.contains(ChangeFlags::STRUCTURE) { + // This is crude and will result in more DOM traffic than needed. + // The right thing to do is diff the new state of the children id + // vector against the old, and derive DOM mutations from that. + while let Some(child) = element.first_child() { + element.remove_child(&child).unwrap_throw(); + } + for child in &state.child_elements { + element.append_child(child.0.as_node_ref()).unwrap_throw(); + } + changed.remove(ChangeFlags::STRUCTURE); + } + changed + } + + fn message( + &self, + id_path: &[Id], + state: &mut Self::State, + message: Box, + app_state: &mut $t, + ) -> MessageResult<$a> { + self.0 + .message(id_path, &mut state.children_states, message, app_state) + } + } + + /// Builder function for a + #[doc = concat!("`", stringify!($name), "`")] + /// element view. + pub fn $name<$t, $a, $vs: ViewSequence<$t, $a>>(children: $vs) -> $ty_name<$t, $a, $vs> { + $ty_name(children, PhantomData) + } + + generate_dom_interface_impl!($dom_interface, ($ty_name, $t, $a, $vs)); + + paste::paste! { + $crate::interfaces::[]!(generate_dom_interface_impl, ($ty_name, $t, $a, $vs)); + } + }; +} + +macro_rules! define_elements { + ($($element_def:tt,)*) => { + $(define_element!($element_def);)* + }; +} + +define_elements!( + // the order is copied from + // https://developer.mozilla.org/en-US/docs/Web/HTML/Element + // DOM interfaces copied from https://html.spec.whatwg.org/multipage/grouping-content.html and friends + + // TODO include document metadata elements? + + // content sectioning + (HTML_NS, Address, address, HtmlElement), + (HTML_NS, Article, article, HtmlElement), + (HTML_NS, Aside, aside, HtmlElement), + (HTML_NS, Footer, footer, HtmlElement), + (HTML_NS, Header, header, HtmlElement), + (HTML_NS, H1, h1, HtmlHeadingElement), + (HTML_NS, H2, h2, HtmlHeadingElement), + (HTML_NS, H3, h3, HtmlHeadingElement), + (HTML_NS, H4, h4, HtmlHeadingElement), + (HTML_NS, H5, h5, HtmlHeadingElement), + (HTML_NS, H6, h6, HtmlHeadingElement), + (HTML_NS, Hgroup, hgroup, HtmlElement), + (HTML_NS, Main, main, HtmlElement), + (HTML_NS, Nav, nav, HtmlElement), + (HTML_NS, Section, section, HtmlElement), + // text content + (HTML_NS, Blockquote, blockquote, HtmlQuoteElement), + (HTML_NS, Dd, dd, HtmlElement), + (HTML_NS, Div, div, HtmlDivElement), + (HTML_NS, Dl, dl, HtmlDListElement), + (HTML_NS, Dt, dt, HtmlElement), + (HTML_NS, Figcaption, figcaption, HtmlElement), + (HTML_NS, Figure, figure, HtmlElement), + (HTML_NS, Hr, hr, HtmlHrElement), + (HTML_NS, Li, li, HtmlLiElement), + (HTML_NS, Link, link, HtmlLinkElement), + (HTML_NS, Menu, menu, HtmlMenuElement), + (HTML_NS, Ol, ol, HtmlOListElement), + (HTML_NS, P, p, HtmlParagraphElement), + (HTML_NS, Pre, pre, HtmlPreElement), + (HTML_NS, Ul, ul, HtmlUListElement), + // inline text + (HTML_NS, A, a, HtmlAnchorElement, T, A_, VS), + (HTML_NS, Abbr, abbr, HtmlElement), + (HTML_NS, B, b, HtmlElement), + (HTML_NS, Bdi, bdi, HtmlElement), + (HTML_NS, Bdo, bdo, HtmlElement), + (HTML_NS, Br, br, HtmlBrElement), + (HTML_NS, Cite, cite, HtmlElement), + (HTML_NS, Code, code, HtmlElement), + (HTML_NS, Data, data, HtmlDataElement), + (HTML_NS, Dfn, dfn, HtmlElement), + (HTML_NS, Em, em, HtmlElement), + (HTML_NS, I, i, HtmlElement), + (HTML_NS, Kbd, kbd, HtmlElement), + (HTML_NS, Mark, mark, HtmlElement), + (HTML_NS, Q, q, HtmlQuoteElement), + (HTML_NS, Rp, rp, HtmlElement), + (HTML_NS, Rt, rt, HtmlElement), + (HTML_NS, Ruby, ruby, HtmlElement), + (HTML_NS, S, s, HtmlElement), + (HTML_NS, Samp, samp, HtmlElement), + (HTML_NS, Small, small, HtmlElement), + (HTML_NS, Span, span, HtmlSpanElement), + (HTML_NS, Strong, strong, HtmlElement), + (HTML_NS, Sub, sub, HtmlElement), + (HTML_NS, Sup, sup, HtmlElement), + (HTML_NS, Time, time, HtmlTimeElement), + (HTML_NS, U, u, HtmlElement), + (HTML_NS, Var, var, HtmlElement), + (HTML_NS, Wbr, wbr, HtmlElement), + // image and multimedia + (HTML_NS, Area, area, HtmlAreaElement), + (HTML_NS, Audio, audio, HtmlAudioElement), + (HTML_NS, Canvas, canvas, HtmlCanvasElement), + (HTML_NS, Img, img, HtmlImageElement), + (HTML_NS, Map, map, HtmlMapElement), + (HTML_NS, Track, track, HtmlTrackElement), + (HTML_NS, Video, video, HtmlVideoElement), + // embedded content + (HTML_NS, Embed, embed, HtmlEmbedElement), + (HTML_NS, Iframe, iframe, HtmlIFrameElement), + (HTML_NS, Object, object, HtmlObjectElement), + (HTML_NS, Picture, picture, HtmlPictureElement), + (HTML_NS, Portal, portal, HtmlElement), + (HTML_NS, Source, source, HtmlSourceElement), + // scripting + (HTML_NS, Noscript, noscript, HtmlElement), + (HTML_NS, Script, script, HtmlScriptElement), + // demarcating edits + (HTML_NS, Del, del, HtmlModElement), + (HTML_NS, Ins, ins, HtmlModElement), + // tables + (HTML_NS, Caption, caption, HtmlTableCaptionElement), + (HTML_NS, Col, col, HtmlTableColElement), + (HTML_NS, Colgroup, colgroup, HtmlTableColElement), + (HTML_NS, Table, table, HtmlTableElement), + (HTML_NS, Tbody, tbody, HtmlTableSectionElement), + (HTML_NS, Td, td, HtmlTableCellElement), + (HTML_NS, Tfoot, tfoot, HtmlTableSectionElement), + (HTML_NS, Th, th, HtmlTableCellElement), + (HTML_NS, Thead, thead, HtmlTableSectionElement), + (HTML_NS, Tr, tr, HtmlTableRowElement), + // forms + (HTML_NS, Button, button, HtmlButtonElement), + (HTML_NS, Datalist, datalist, HtmlDataListElement), + (HTML_NS, Fieldset, fieldset, HtmlFieldSetElement), + (HTML_NS, Form, form, HtmlFormElement), + (HTML_NS, Input, input, HtmlInputElement), + (HTML_NS, Label, label, HtmlLabelElement), + (HTML_NS, Legend, legend, HtmlLegendElement), + (HTML_NS, Meter, meter, HtmlMeterElement), + (HTML_NS, Optgroup, optgroup, HtmlOptGroupElement), + (HTML_NS, OptionElement, option, HtmlOptionElement), // Avoid cluttering the namespace with `Option` + (HTML_NS, Output, output, HtmlOutputElement), + (HTML_NS, Progress, progress, HtmlProgressElement), + (HTML_NS, Select, select, HtmlSelectElement), + (HTML_NS, Textarea, textarea, HtmlTextAreaElement), + // interactive elements, + (HTML_NS, Details, details, HtmlDetailsElement), + (HTML_NS, Dialog, dialog, HtmlDialogElement), + (HTML_NS, Summary, summary, HtmlElement), + // web components, + (HTML_NS, Slot, slot, HtmlSlotElement), + (HTML_NS, Template, template, HtmlTemplateElement), + // SVG and MathML (TODO, svg and mathml elements) + (SVG_NS, Svg, svg, SvgElement), + (MATHML_NS, Math, math, Element), +); diff --git a/crates/xilem_html/src/event/events.rs b/crates/xilem_html/src/event/events.rs deleted file mode 100644 index 933e25da..00000000 --- a/crates/xilem_html/src/event/events.rs +++ /dev/null @@ -1,273 +0,0 @@ -//! Macros to generate all the different html events -//! -macro_rules! events { - () => {}; - (($ty_name:ident, $builder_name:ident, $name:literal, $web_sys_ty:ty), $($rest:tt)*) => { - event!($ty_name, $builder_name, $name, $web_sys_ty); - events!($($rest)*); - }; -} - -macro_rules! event { - ($ty_name:ident, $builder_name:ident, $name:literal, $web_sys_ty:ty) => { - /// A view that listens for the - #[doc = concat!("`", $name, "`")] - /// event. - pub struct $ty_name - where - V: crate::view::View, - F: Fn(&mut T, &$crate::Event<$web_sys_ty, V::Element>) -> OA, - V::Element: 'static, - OA: $crate::event::OptionalAction, - { - inner: crate::OnEvent<$web_sys_ty, V, F>, - data: std::marker::PhantomData, - action: std::marker::PhantomData, - optional_action: std::marker::PhantomData, - } - - impl $ty_name - where - V: crate::view::View, - F: Fn(&mut T, &$crate::Event<$web_sys_ty, V::Element>) -> OA, - V::Element: 'static, - OA: $crate::event::OptionalAction, - { - /// Whether the event handler should be passive. (default = `true`) - /// - /// Passive event handlers can't prevent the browser's default action from - /// running (otherwise possible with `event.prevent_default()`), which - /// restricts what they can be used for, but reduces overhead. - pub fn passive(mut self, value: bool) -> Self { - self.inner.passive = value; - self - } - } - - /// Builder for the - #[doc = concat!("`", $name, "`")] - /// event listener. - pub fn $builder_name(child: V, callback: F) -> $ty_name - where - V: crate::view::View, - F: Fn(&mut T, &$crate::Event<$web_sys_ty, V::Element>) -> OA, - V::Element: 'static, - OA: $crate::event::OptionalAction, - { - $ty_name { - inner: crate::on_event($name, child, callback), - data: std::marker::PhantomData, - action: std::marker::PhantomData, - optional_action: std::marker::PhantomData, - } - } - - impl crate::view::ViewMarker for $ty_name - where - V: crate::view::View, - F: Fn(&mut T, &$crate::Event<$web_sys_ty, V::Element>) -> OA, - V::Element: 'static, - OA: $crate::event::OptionalAction, - { - } - - impl crate::view::View for $ty_name - where - V: crate::view::View, - F: Fn(&mut T, &$crate::Event<$web_sys_ty, V::Element>) -> OA, - V::Element: 'static, - OA: $crate::event::OptionalAction, - { - type State = crate::event::OnEventState; - type Element = V::Element; - - fn build( - &self, - cx: &mut crate::context::Cx, - ) -> (xilem_core::Id, Self::State, Self::Element) { - self.inner.build(cx) - } - - fn rebuild( - &self, - cx: &mut crate::context::Cx, - prev: &Self, - id: &mut xilem_core::Id, - state: &mut Self::State, - element: &mut Self::Element, - ) -> crate::ChangeFlags { - self.inner.rebuild(cx, &prev.inner, id, state, element) - } - - fn message( - &self, - id_path: &[xilem_core::Id], - state: &mut Self::State, - message: Box, - app_state: &mut T, - ) -> xilem_core::MessageResult { - self.inner.message(id_path, state, message, app_state) - } - } - }; -} - -// event list from -// https://html.spec.whatwg.org/multipage/webappapis.html#idl-definitions -// -// I didn't include the events on the window, since we aren't attaching -// any events to the window in xilem_html - -events!( - (OnAbort, on_abort, "abort", web_sys::Event), - (OnAuxClick, on_auxclick, "auxclick", web_sys::PointerEvent), - ( - OnBeforeInput, - on_beforeinput, - "beforeinput", - web_sys::InputEvent - ), - (OnBeforeMatch, on_beforematch, "beforematch", web_sys::Event), - ( - OnBeforeToggle, - on_beforetoggle, - "beforetoggle", - web_sys::Event - ), - (OnBlur, on_blur, "blur", web_sys::FocusEvent), - (OnCancel, on_cancel, "cancel", web_sys::Event), - (OnCanPlay, on_canplay, "canplay", web_sys::Event), - ( - OnCanPlayThrough, - on_canplaythrough, - "canplaythrough", - web_sys::Event - ), - (OnChange, on_change, "change", web_sys::Event), - (OnClick, on_click, "click", web_sys::MouseEvent), - (OnClose, on_close, "close", web_sys::Event), - (OnContextLost, on_contextlost, "contextlost", web_sys::Event), - ( - OnContextMenu, - on_contextmenu, - "contextmenu", - web_sys::PointerEvent - ), - ( - OnContextRestored, - on_contextrestored, - "contextrestored", - web_sys::Event - ), - (OnCopy, on_copy, "copy", web_sys::Event), - (OnCueChange, on_cuechange, "cuechange", web_sys::Event), - (OnCut, on_cut, "cut", web_sys::Event), - (OnDblClick, on_dblclick, "dblclick", web_sys::MouseEvent), - (OnDrag, on_drag, "drag", web_sys::Event), - (OnDragEnd, on_dragend, "dragend", web_sys::Event), - (OnDragEnter, on_dragenter, "dragenter", web_sys::Event), - (OnDragLeave, on_dragleave, "dragleave", web_sys::Event), - (OnDragOver, on_dragover, "dragover", web_sys::Event), - (OnDragStart, on_dragstart, "dragstart", web_sys::Event), - (OnDrop, on_drop, "drop", web_sys::Event), - ( - OnDurationChange, - on_durationchange, - "durationchange", - web_sys::Event - ), - (OnEmptied, on_emptied, "emptied", web_sys::Event), - (OnEnded, on_ended, "ended", web_sys::Event), - (OnError, on_error, "error", web_sys::Event), - (OnFocus, on_focus, "focus", web_sys::FocusEvent), - (OnFocusIn, on_focusin, "focusin", web_sys::FocusEvent), - (OnFocusOut, on_focusout, "focusout", web_sys::FocusEvent), - (OnFormData, on_formdata, "formdata", web_sys::Event), - (OnInput, on_input, "input", web_sys::InputEvent), - (OnInvalid, on_invalid, "invalid", web_sys::Event), - (OnKeyDown, on_keydown, "keydown", web_sys::KeyboardEvent), - (OnKeyUp, on_keyup, "keyup", web_sys::KeyboardEvent), - (OnLoad, on_load, "load", web_sys::Event), - (OnLoadedData, on_loadeddata, "loadeddata", web_sys::Event), - ( - OnLoadedMetadata, - on_loadedmetadata, - "loadedmetadata", - web_sys::Event - ), - (OnLoadStart, on_loadstart, "loadstart", web_sys::Event), - (OnMouseDown, on_mousedown, "mousedown", web_sys::MouseEvent), - ( - OnMouseEnter, - on_mouseenter, - "mouseenter", - web_sys::MouseEvent - ), - ( - OnMouseLeave, - on_mouseleave, - "mouseleave", - web_sys::MouseEvent - ), - (OnMouseMove, on_mousemove, "mousemove", web_sys::MouseEvent), - (OnMouseOut, on_mouseout, "mouseout", web_sys::MouseEvent), - (OnMouseOver, on_mouseover, "mouseover", web_sys::MouseEvent), - (OnMouseUp, on_mouseup, "mouseup", web_sys::MouseEvent), - (OnPaste, on_paste, "paste", web_sys::Event), - (OnPause, on_pause, "pause", web_sys::Event), - (OnPlay, on_play, "play", web_sys::Event), - (OnPlaying, on_playing, "playing", web_sys::Event), - (OnProgress, on_progress, "progress", web_sys::Event), - (OnRateChange, on_ratechange, "ratechange", web_sys::Event), - (OnReset, on_reset, "reset", web_sys::Event), - (OnResize, on_resize, "resize", web_sys::Event), - (OnScroll, on_scroll, "scroll", web_sys::Event), - (OnScrollEnd, on_scrollend, "scrollend", web_sys::Event), - ( - OnSecurityPolicyViolation, - on_securitypolicyviolation, - "securitypolicyviolation", - web_sys::Event - ), - (OnSeeked, on_seeked, "seeked", web_sys::Event), - (OnSeeking, on_seeking, "seeking", web_sys::Event), - (OnSelect, on_select, "select", web_sys::Event), - (OnSlotChange, on_slotchange, "slotchange", web_sys::Event), - (OnStalled, on_stalled, "stalled", web_sys::Event), - (OnSubmit, on_submit, "submit", web_sys::Event), - (OnSuspend, on_suspend, "suspend", web_sys::Event), - (OnTimeUpdate, on_timeupdate, "timeupdate", web_sys::Event), - (OnToggle, on_toggle, "toggle", web_sys::Event), - ( - OnVolumeChange, - on_volumechange, - "volumechange", - web_sys::Event - ), - (OnWaiting, on_waiting, "waiting", web_sys::Event), - ( - OnWebkitAnimationEnd, - on_webkitanimationend, - "webkitanimationend", - web_sys::Event - ), - ( - OnWebkitAnimationIteration, - on_webkitanimationiteration, - "webkitanimationiteration", - web_sys::Event - ), - ( - OnWebkitAnimationStart, - on_webkitanimationstart, - "webkitanimationstart", - web_sys::Event - ), - ( - OnWebkitTransitionEnd, - on_webkittransitionend, - "webkittransitionend", - web_sys::Event - ), - (OnWheel, on_wheel, "wheel", web_sys::WheelEvent), -); diff --git a/crates/xilem_html/src/event/mod.rs b/crates/xilem_html/src/event/mod.rs deleted file mode 100644 index abe78f27..00000000 --- a/crates/xilem_html/src/event/mod.rs +++ /dev/null @@ -1,211 +0,0 @@ -// Copyright 2023 the Druid Authors. -// SPDX-License-Identifier: Apache-2.0 - -#[cfg(feature = "typed")] -pub mod events; - -use std::{any::Any, marker::PhantomData, ops::Deref}; - -use gloo::events::{EventListener, EventListenerOptions}; -use wasm_bindgen::{JsCast, UnwrapThrowExt}; -use xilem_core::{Id, MessageResult}; - -use crate::{ - context::{ChangeFlags, Cx}, - view::{DomNode, View, ViewMarker}, -}; - -/// Wraps a [`View`] `V` and attaches an event listener. -/// -/// The event type `E` contains both the [`web_sys::Event`] subclass for this event and the -/// [`web_sys::HtmlElement`] subclass that matches `V::Element`. -pub struct OnEvent { - // TODO changing this after creation is unsupported for now, - // please create a new view instead. - event: &'static str, - child: V, - passive: bool, - callback: F, - phantom_event_ty: PhantomData, -} - -impl OnEvent { - fn new(event: &'static str, child: V, callback: F) -> Self { - Self { - event, - child, - callback, - passive: true, - phantom_event_ty: PhantomData, - } - } - - /// Whether the event handler should be passive. (default = `true`) - /// - /// Passive event handlers can't prevent the browser's default action from - /// running (otherwise possible with `event.prevent_default()`), which - /// restricts what they can be used for, but reduces overhead. - pub fn passive(mut self, value: bool) -> Self { - self.passive = value; - self - } -} - -impl ViewMarker for OnEvent {} - -impl View for OnEvent -where - F: Fn(&mut T, &Event) -> OA, - V: View, - E: JsCast + 'static, - V::Element: 'static, - OA: OptionalAction, -{ - type State = OnEventState; - - type Element = V::Element; - - fn build(&self, cx: &mut Cx) -> (Id, Self::State, Self::Element) { - let (id, child_state, element) = self.child.build(cx); - let thunk = cx.with_id(id, |cx| cx.message_thunk()); - let listener = EventListener::new_with_options( - element.as_node_ref(), - self.event, - EventListenerOptions { - passive: self.passive, - ..Default::default() - }, - move |event: &web_sys::Event| { - let event = (*event).clone().dyn_into::().unwrap_throw(); - let event: Event = Event::new(event); - thunk.push_message(EventMsg { event }); - }, - ); - // TODO add `remove_listener_with_callback` to clean up listener? - let state = OnEventState { - listener, - child_state, - }; - (id, state, element) - } - - fn rebuild( - &self, - cx: &mut Cx, - prev: &Self, - id: &mut Id, - state: &mut Self::State, - element: &mut Self::Element, - ) -> ChangeFlags { - // TODO: if the child id changes (as can happen with AnyView), reinstall closure - self.child - .rebuild(cx, &prev.child, id, &mut state.child_state, element) - } - - fn message( - &self, - id_path: &[Id], - state: &mut Self::State, - message: Box, - app_state: &mut T, - ) -> MessageResult { - match message.downcast_ref::>>() { - Some(msg) if id_path.is_empty() => { - match (self.callback)(app_state, &msg.event).action() { - Some(a) => MessageResult::Action(a), - None => MessageResult::Nop, - } - } - _ => self - .child - .message(id_path, &mut state.child_state, message, app_state), - } - } -} - -// Attach an event listener to the child's element -pub fn on_event(name: &'static str, child: V, callback: F) -> OnEvent { - OnEvent::new(name, child, callback) -} - -/// State for the `OnEvent` view. -pub struct OnEventState { - #[allow(unused)] - listener: EventListener, - child_state: S, -} -struct EventMsg { - event: E, -} - -/// Wraps a `web_sys::Event` and provides auto downcasting for both the event and its target. -pub struct Event { - raw: Evt, - el: PhantomData, -} - -impl Event { - fn new(raw: Evt) -> Self { - Self { - raw, - el: PhantomData, - } - } -} - -impl Event -where - Evt: AsRef, - El: JsCast, -{ - /// Get the event target element. - /// - /// Because this type knows its child view's element type, we can downcast to this type here. - pub fn target(&self) -> El { - let evt: &web_sys::Event = self.raw.as_ref(); - evt.target().unwrap_throw().dyn_into().unwrap_throw() - } -} - -impl Deref for Event { - type Target = Evt; - fn deref(&self) -> &Self::Target { - &self.raw - } -} - -/// Implement this trait for types you want to use as actions. -/// -/// The trait exists because otherwise we couldn't provide versions -/// of listeners that take `()`, `A` and `Option`. -pub trait Action {} - -/// Trait that allows callbacks to be polymorphic on return type -/// (`Action`, `Option` or `()`). An implementation detail. -pub trait OptionalAction: sealed::Sealed { - fn action(self) -> Option; -} -mod sealed { - pub trait Sealed {} -} - -impl sealed::Sealed for () {} -impl OptionalAction for () { - fn action(self) -> Option { - None - } -} - -impl sealed::Sealed for A {} -impl OptionalAction for A { - fn action(self) -> Option { - Some(self) - } -} - -impl sealed::Sealed for Option {} -impl OptionalAction for Option { - fn action(self) -> Option { - self - } -} diff --git a/crates/xilem_html/src/events.rs b/crates/xilem_html/src/events.rs new file mode 100644 index 00000000..29f5f5b5 --- /dev/null +++ b/crates/xilem_html/src/events.rs @@ -0,0 +1,381 @@ +use crate::{ + interfaces::{sealed::Sealed, Element}, + view::DomNode, + ChangeFlags, Cx, OptionalAction, View, ViewMarker, +}; +use std::{any::Any, borrow::Cow, marker::PhantomData}; +use wasm_bindgen::{JsCast, UnwrapThrowExt}; +use xilem_core::{Id, MessageResult}; + +pub use gloo::events::EventListenerOptions; + +/// Wraps a [`View`] `V` and attaches an event listener. +/// +/// The event type `E` should inherit from [`web_sys::Event`] +pub struct OnEvent { + pub(crate) element: E, + pub(crate) event: Cow<'static, str>, + pub(crate) options: EventListenerOptions, + pub(crate) handler: C, + #[allow(clippy::type_complexity)] + pub(crate) phantom_event_ty: PhantomData (T, A, Ev)>, +} + +impl OnEvent +where + Ev: JsCast + 'static, +{ + pub fn new(element: E, event: impl Into>, handler: C) -> Self { + OnEvent { + element, + event: event.into(), + options: Default::default(), + handler, + phantom_event_ty: PhantomData, + } + } + + pub fn new_with_options( + element: E, + event: impl Into>, + handler: C, + options: EventListenerOptions, + ) -> Self { + OnEvent { + element, + event: event.into(), + options, + handler, + phantom_event_ty: PhantomData, + } + } + + /// Whether the event handler should be passive. (default = `true`) + /// + /// Passive event handlers can't prevent the browser's default action from + /// running (otherwise possible with `event.prevent_default()`), which + /// restricts what they can be used for, but reduces overhead. + pub fn passive(mut self, value: bool) -> Self { + self.options.passive = value; + self + } +} + +fn create_event_listener( + target: &web_sys::EventTarget, + event: impl Into>, + options: EventListenerOptions, + cx: &Cx, +) -> gloo::events::EventListener { + let thunk = cx.message_thunk(); + gloo::events::EventListener::new_with_options( + target, + event, + options, + move |event: &web_sys::Event| { + let event = (*event).clone().dyn_into::().unwrap_throw(); + thunk.push_message(event); + }, + ) +} + +/// State for the `OnEvent` view. +pub struct OnEventState { + #[allow(unused)] + listener: gloo::events::EventListener, + child_id: Id, + child_state: S, +} + +impl ViewMarker for OnEvent {} +impl Sealed for OnEvent {} + +impl View for OnEvent +where + OA: OptionalAction, + C: Fn(&mut T, Ev) -> OA, + E: Element, + Ev: JsCast + 'static, +{ + type State = OnEventState; + + type Element = E::Element; + + fn build(&self, cx: &mut Cx) -> (Id, Self::State, Self::Element) { + let (id, (element, state)) = cx.with_new_id(|cx| { + let (child_id, child_state, element) = self.element.build(cx); + let listener = create_event_listener::( + element.as_node_ref(), + self.event.clone(), + self.options, + cx, + ); + let state = OnEventState { + child_state, + child_id, + listener, + }; + (element, state) + }); + (id, state, element) + } + + fn rebuild( + &self, + cx: &mut Cx, + prev: &Self, + id: &mut Id, + state: &mut Self::State, + element: &mut Self::Element, + ) -> ChangeFlags { + cx.with_id(*id, |cx| { + let prev_child_id = state.child_id; + let mut changed = self.element.rebuild( + cx, + &prev.element, + &mut state.child_id, + &mut state.child_state, + element, + ); + if state.child_id != prev_child_id { + changed |= ChangeFlags::OTHER_CHANGE; + } + // TODO check equality of prev and current element somehow + if prev.event != self.event || changed.contains(ChangeFlags::STRUCTURE) { + state.listener = create_event_listener::( + element.as_node_ref(), + self.event.clone(), + self.options, + cx, + ); + changed |= ChangeFlags::OTHER_CHANGE; + } + changed + }) + } + + fn message( + &self, + id_path: &[Id], + state: &mut Self::State, + message: Box, + app_state: &mut T, + ) -> MessageResult { + match id_path { + [] if message.downcast_ref::().is_some() => { + let event = message.downcast::().unwrap(); + match (self.handler)(app_state, *event).action() { + Some(a) => MessageResult::Action(a), + None => MessageResult::Nop, + } + } + [element_id, rest_path @ ..] if *element_id == state.child_id => { + self.element + .message(rest_path, &mut state.child_state, message, app_state) + } + _ => MessageResult::Stale(message), + } + } +} + +crate::interfaces::impl_dom_interfaces_for_ty!( + Element, + OnEvent, + vars: , + vars_on_ty: , + bounds: { + Ev: JsCast + 'static, + OA: OptionalAction, + C: Fn(&mut T, Ev) -> OA, + } +); + +macro_rules! event_definitions { + ($(($ty_name:ident, $event_name:literal, $web_sys_ty:ident)),*) => { + $( + $crate::interfaces::impl_dom_interfaces_for_ty!( + Element, + $ty_name, + vars: , + vars_on_ty: , + bounds: { + OA: OptionalAction, + C: Fn(&mut T, web_sys::$web_sys_ty ) -> OA, + } + ); + + pub struct $ty_name { + target: E, + callback: C, + options: EventListenerOptions, + phantom: PhantomData (T, A)>, + } + + impl $ty_name { + pub fn new(target: E, callback: C) -> Self { + Self { + target, + options: Default::default(), + callback, + phantom: PhantomData, + } + } + + /// Whether the event handler should be passive. (default = `true`) + /// + /// Passive event handlers can't prevent the browser's default action from + /// running (otherwise possible with `event.prevent_default()`), which + /// restricts what they can be used for, but reduces overhead. + pub fn passive(mut self, value: bool) -> Self { + self.options.passive = value; + self + } + } + + impl ViewMarker for $ty_name {} + impl Sealed for $ty_name {} + + impl View for $ty_name + where + OA: OptionalAction, + C: Fn(&mut T, web_sys::$web_sys_ty) -> OA, + E: Element, + { + type State = OnEventState; + + type Element = E::Element; + + fn build(&self, cx: &mut Cx) -> (Id, Self::State, Self::Element) { + let (id, (element, state)) = cx.with_new_id(|cx| { + let (child_id, child_state, el) = self.target.build(cx); + let listener = create_event_listener::(el.as_node_ref(), $event_name, self.options, cx); + (el, OnEventState { child_state, child_id, listener }) + }); + (id, state, element) + } + + fn rebuild( + &self, + cx: &mut Cx, + prev: &Self, + id: &mut Id, + state: &mut Self::State, + element: &mut Self::Element, + ) -> ChangeFlags { + cx.with_id(*id, |cx| { + let prev_child_id = state.child_id; + let mut changed = self.target.rebuild(cx, &prev.target, &mut state.child_id, &mut state.child_state, element); + if state.child_id != prev_child_id { + changed |= ChangeFlags::OTHER_CHANGE; + } + // TODO check equality of prev and current element somehow + if changed.contains(ChangeFlags::STRUCTURE) { + state.listener = create_event_listener::(element.as_node_ref(), $event_name, self.options, cx); + changed |= ChangeFlags::OTHER_CHANGE; + } + changed + }) + } + + fn message( + &self, + id_path: &[Id], + state: &mut Self::State, + message: Box, + app_state: &mut T, + ) -> MessageResult { + match id_path { + [] if message.downcast_ref::().is_some() => { + let event = message.downcast::().unwrap(); + match (self.callback)(app_state, *event).action() { + Some(a) => MessageResult::Action(a), + None => MessageResult::Nop, + } + } + [element_id, rest_path @ ..] if *element_id == state.child_id => { + self.target.message(rest_path, &mut state.child_state, message, app_state) + } + _ => MessageResult::Stale(message), + } + } + } + )* + }; +} + +// click/auxclick/contextmenu are still mouse events in either Safari as well as Firefox, +// see: https://stackoverflow.com/questions/70626381/why-chrome-emits-pointerevents-and-firefox-mouseevents-and-which-type-definition/76900433#76900433 +event_definitions!( + (OnAbort, "abort", Event), + (OnAuxClick, "auxclick", MouseEvent), + (OnBeforeInput, "beforeinput", InputEvent), + (OnBeforeMatch, "beforematch", Event), + (OnBeforeToggle, "beforetoggle", Event), + (OnBlur, "blur", FocusEvent), + (OnCancel, "cancel", Event), + (OnCanPlay, "canplay", Event), + (OnCanPlayThrough, "canplaythrough", Event), + (OnChange, "change", Event), + (OnClick, "click", MouseEvent), + (OnClose, "close", Event), + (OnContextLost, "contextlost", Event), + (OnContextMenu, "contextmenu", MouseEvent), + (OnContextRestored, "contextrestored", Event), + (OnCopy, "copy", Event), + (OnCueChange, "cuechange", Event), + (OnCut, "cut", Event), + (OnDblClick, "dblclick", MouseEvent), + (OnDrag, "drag", Event), + (OnDragEnd, "dragend", Event), + (OnDragEnter, "dragenter", Event), + (OnDragLeave, "dragleave", Event), + (OnDragOver, "dragover", Event), + (OnDragStart, "dragstart", Event), + (OnDrop, "drop", Event), + (OnDurationChange, "durationchange", Event), + (OnEmptied, "emptied", Event), + (OnEnded, "ended", Event), + (OnError, "error", Event), + (OnFocus, "focus", FocusEvent), + (OnFocusIn, "focusin", FocusEvent), + (OnFocusOut, "focusout", FocusEvent), + (OnFormData, "formdata", Event), + (OnInput, "input", InputEvent), + (OnInvalid, "invalid", Event), + (OnKeyDown, "keydown", KeyboardEvent), + (OnKeyUp, "keyup", KeyboardEvent), + (OnLoad, "load", Event), + (OnLoadedData, "loadeddata", Event), + (OnLoadedMetadata, "loadedmetadata", Event), + (OnLoadStart, "loadstart", Event), + (OnMouseDown, "mousedown", MouseEvent), + (OnMouseEnter, "mouseenter", MouseEvent), + (OnMouseLeave, "mouseleave", MouseEvent), + (OnMouseMove, "mousemove", MouseEvent), + (OnMouseOut, "mouseout", MouseEvent), + (OnMouseOver, "mouseover", MouseEvent), + (OnMouseUp, "mouseup", MouseEvent), + (OnPaste, "paste", Event), + (OnPause, "pause", Event), + (OnPlay, "play", Event), + (OnPlaying, "playing", Event), + (OnProgress, "progress", Event), + (OnRateChange, "ratechange", Event), + (OnReset, "reset", Event), + (OnResize, "resize", Event), + (OnScroll, "scroll", Event), + (OnScrollEnd, "scrollend", Event), + (OnSecurityPolicyViolation, "securitypolicyviolation", Event), + (OnSeeked, "seeked", Event), + (OnSeeking, "seeking", Event), + (OnSelect, "select", Event), + (OnSlotChange, "slotchange", Event), + (OnStalled, "stalled", Event), + (OnSubmit, "submit", Event), + (OnSuspend, "suspend", Event), + (OnTimeUpdate, "timeupdate", Event), + (OnToggle, "toggle", Event), + (OnVolumeChange, "volumechange", Event), + (OnWaiting, "waiting", Event), + (OnWheel, "wheel", WheelEvent) +); diff --git a/crates/xilem_html/src/interfaces.rs b/crates/xilem_html/src/interfaces.rs new file mode 100644 index 00000000..ab50d579 --- /dev/null +++ b/crates/xilem_html/src/interfaces.rs @@ -0,0 +1,449 @@ +use crate::{View, ViewMarker}; +use std::borrow::Cow; + +use gloo::events::EventListenerOptions; +use wasm_bindgen::JsCast; + +use crate::{ + events::{self, OnEvent}, + Attr, IntoAttributeValue, OptionalAction, +}; + +pub(crate) mod sealed { + pub trait Sealed {} +} + +// TODO should the options be its own function `on_event_with_options`, +// or should that be done via the builder pattern: `el.on_event().passive(false)`? +macro_rules! event_handler_mixin { + ($(($event_ty: ident, $fn_name:ident, $event:expr, $web_sys_event_type:ident),)*) => { + $( + fn $fn_name(self, handler: EH) -> events::$event_ty + where + OA: OptionalAction, + EH: Fn(&mut T, web_sys::$web_sys_event_type) -> OA, + { + $crate::events::$event_ty::new(self, handler) + } + )* + }; +} + +pub trait Element: View + ViewMarker + sealed::Sealed +where + Self: Sized, +{ + fn on( + self, + event: impl Into>, + handler: EH, + ) -> OnEvent + where + E: JsCast + 'static, + OA: OptionalAction, + EH: Fn(&mut T, E) -> OA, + Self: Sized, + { + OnEvent::new(self, event, handler) + } + + fn on_with_options( + self, + event: impl Into>, + handler: EH, + options: EventListenerOptions, + ) -> OnEvent + where + Ev: JsCast + 'static, + OA: OptionalAction, + EH: Fn(&mut T, Ev) -> OA, + Self: Sized, + { + OnEvent::new_with_options(self, event, handler, options) + } + + // TODO should the API be "functional" in the sense, that new attributes are wrappers around the type, + // or should they modify the underlying instance (e.g. via the following methods)? + // The disadvantage that "functional" brings in, is that elements are not modifiable (i.e. attributes can't be simply added etc.) + // fn attrs(&self) -> &Attributes; + // fn attrs_mut(&mut self) -> &mut Attributes; + + /// Set an attribute on this element. + /// + /// # Panics + /// + /// If the name contains characters that are not valid in an attribute name, + /// then the `View::build`/`View::rebuild` functions will panic for this view. + fn attr( + self, + name: impl Into>, + value: impl IntoAttributeValue, + ) -> Attr { + Attr { + element: self, + name: name.into(), + value: value.into_attribute_value(), + phantom: std::marker::PhantomData, + } + } + + // 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()) + } + + // event list from + // https://html.spec.whatwg.org/multipage/webappapis.html#idl-definitions + // + // I didn't include the events on the window, since we aren't attaching + // any events to the window in xilem_html + event_handler_mixin!( + (OnAbort, on_abort, "abort", Event), + (OnAuxClick, on_auxclick, "auxclick", PointerEvent), + (OnBeforeInput, on_beforeinput, "beforeinput", InputEvent), + (OnBeforeMatch, on_beforematch, "beforematch", Event), + (OnBeforeToggle, on_beforetoggle, "beforetoggle", Event), + (OnBlur, on_blur, "blur", FocusEvent), + (OnCancel, on_cancel, "cancel", Event), + (OnCanPlay, on_canplay, "canplay", Event), + (OnCanPlayThrough, on_canplaythrough, "canplaythrough", Event), + (OnChange, on_change, "change", Event), + (OnClick, on_click, "click", MouseEvent), + (OnClose, on_close, "close", Event), + (OnContextLost, on_contextlost, "contextlost", Event), + (OnContextMenu, on_contextmenu, "contextmenu", PointerEvent), + ( + OnContextRestored, + on_contextrestored, + "contextrestored", + Event + ), + (OnCopy, on_copy, "copy", Event), + (OnCueChange, on_cuechange, "cuechange", Event), + (OnCut, on_cut, "cut", Event), + (OnDblClick, on_dblclick, "dblclick", MouseEvent), + (OnDrag, on_drag, "drag", Event), + (OnDragEnd, on_dragend, "dragend", Event), + (OnDragEnter, on_dragenter, "dragenter", Event), + (OnDragLeave, on_dragleave, "dragleave", Event), + (OnDragOver, on_dragover, "dragover", Event), + (OnDragStart, on_dragstart, "dragstart", Event), + (OnDrop, on_drop, "drop", Event), + (OnDurationChange, on_durationchange, "durationchange", Event), + (OnEmptied, on_emptied, "emptied", Event), + (OnEnded, on_ended, "ended", Event), + (OnError, on_error, "error", Event), + (OnFocus, on_focus, "focus", FocusEvent), + (OnFocusIn, on_focusin, "focusin", FocusEvent), + (OnFocusOut, on_focusout, "focusout", FocusEvent), + (OnFormData, on_formdata, "formdata", Event), + (OnInput, on_input, "input", InputEvent), + (OnInvalid, on_invalid, "invalid", Event), + (OnKeyDown, on_keydown, "keydown", KeyboardEvent), + (OnKeyUp, on_keyup, "keyup", KeyboardEvent), + (OnLoad, on_load, "load", Event), + (OnLoadedData, on_loadeddata, "loadeddata", Event), + (OnLoadedMetadata, on_loadedmetadata, "loadedmetadata", Event), + (OnLoadStart, on_loadstart, "loadstart", Event), + (OnMouseDown, on_mousedown, "mousedown", MouseEvent), + (OnMouseEnter, on_mouseenter, "mouseenter", MouseEvent), + (OnMouseLeave, on_mouseleave, "mouseleave", MouseEvent), + (OnMouseMove, on_mousemove, "mousemove", MouseEvent), + (OnMouseOut, on_mouseout, "mouseout", MouseEvent), + (OnMouseOver, on_mouseover, "mouseover", MouseEvent), + (OnMouseUp, on_mouseup, "mouseup", MouseEvent), + (OnPaste, on_paste, "paste", Event), + (OnPause, on_pause, "pause", Event), + (OnPlay, on_play, "play", Event), + (OnPlaying, on_playing, "playing", Event), + (OnProgress, on_progress, "progress", Event), + (OnRateChange, on_ratechange, "ratechange", Event), + (OnReset, on_reset, "reset", Event), + (OnResize, on_resize, "resize", Event), + (OnScroll, on_scroll, "scroll", Event), + (OnScrollEnd, on_scrollend, "scrollend", Event), + ( + OnSecurityPolicyViolation, + on_securitypolicyviolation, + "securitypolicyviolation", + Event + ), + (OnSeeked, on_seeked, "seeked", Event), + (OnSeeking, on_seeking, "seeking", Event), + (OnSelect, on_select, "select", Event), + (OnSlotChange, on_slotchange, "slotchange", Event), + (OnStalled, on_stalled, "stalled", Event), + (OnSubmit, on_submit, "submit", Event), + (OnSuspend, on_suspend, "suspend", Event), + (OnTimeUpdate, on_timeupdate, "timeupdate", Event), + (OnToggle, on_toggle, "toggle", Event), + (OnVolumeChange, on_volumechange, "volumechange", Event), + (OnWaiting, on_waiting, "waiting", Event), + (OnWheel, on_wheel, "wheel", WheelEvent), + ); +} + +// base case for ancestor macros, do nothing, because the body is in all the child interface macros... +#[allow(unused_macros)] +macro_rules! for_all_element_ancestors { + ($($_:tt)*) => {}; +} +#[allow(unused_imports)] +pub(crate) use for_all_element_ancestors; + +macro_rules! dom_interface_macro_and_trait_definitions_impl { + ($interface:ident { + methods: $_methods_body:tt, + child_interfaces: { + $($child_interface:ident { + methods: $child_methods_body:tt, + child_interfaces: $child_interface_body: tt + },)* + } + }) => { + paste::paste! { + $( + pub trait $child_interface: $interface $child_methods_body + + /// Execute $mac which is a macro, that takes $dom_interface:ident () as match arm for all interfaces that + #[doc = concat!("`", stringify!($child_interface), "`")] + /// inherits from + macro_rules! [] { + ($mac:path, $extra_params:tt) => { + $mac!($interface, $extra_params); + $crate::interfaces::[]!($mac, $extra_params); + }; + } + pub(crate) use []; + )* + } + paste::paste! { + /// Execute $mac which is a macro, that takes $dom_interface:ident () as match arm for all interfaces that inherit from + #[doc = concat!("`", stringify!($interface), "`")] + #[allow(unused_macros)] + macro_rules! [] { + ($mac:path, $extra_params:tt) => { + $( + $mac!($child_interface, $extra_params); + $crate::interfaces::[]!($mac, $extra_params); + )* + }; + } + #[allow(unused_imports)] + pub(crate) use []; + } + + $( + $crate::interfaces::dom_interface_macro_and_trait_definitions_impl!( + $child_interface { + methods: $child_methods_body, + child_interfaces: $child_interface_body + } + ); + )* + }; +} + +pub(crate) use dom_interface_macro_and_trait_definitions_impl; + +/// Recursively generates trait and macro definitions for all interfaces, defined below +/// The macros that are defined with this macro are functionally composing a macro which is invoked for all ancestor and descendent interfaces of a given interface +/// For example `for_all_html_video_element_ancestors!($mac, ())` invokes $mac! for the interfaces `HtmlMediaElement`, `HtmlElement` and `Element` +/// And `for_all_html_media_element_descendents` is run for the interfaces `HtmlAudioElement` and `HtmlVideoElement` +macro_rules! dom_interface_macro_and_trait_definitions { + ($($interface:ident $interface_body:tt,)*) => { + $crate::interfaces::dom_interface_macro_and_trait_definitions_impl!( + Element { + methods: {}, + child_interfaces: {$($interface $interface_body,)*} + } + ); + macro_rules! for_all_dom_interfaces { + ($mac:path, $extra_params:tt) => { + $mac!(Element, $extra_params); + $crate::interfaces::for_all_element_descendents!($mac, $extra_params); + }; + } + pub(crate) use for_all_dom_interfaces; + } +} + +macro_rules! impl_dom_interfaces_for_ty_helper { + ($dom_interface:ident, ($ty:ident, <$($additional_generic_var:ident,)*>, <$($additional_generic_var_on_ty:ident,)*>, {$($additional_generic_bounds:tt)*})) => { + $crate::interfaces::impl_dom_interfaces_for_ty_helper!($dom_interface, ($ty, $dom_interface, <$($additional_generic_var,)*>, <$($additional_generic_var_on_ty,)*>, {$($additional_generic_bounds)*})); + }; + ($dom_interface:ident, ($ty:ident, $bound_interface:ident, <$($additional_generic_var:ident,)*>, <$($additional_generic_var_on_ty:ident,)*>, {$($additional_generic_bounds:tt)*})) => { + impl $crate::interfaces::$dom_interface for $ty + where + E: $crate::interfaces::$bound_interface, + $($additional_generic_bounds)* + { + } + }; +} + +pub(crate) use impl_dom_interfaces_for_ty_helper; + +/// Implement DOM interface traits for the given type and all descendent DOM interfaces, +/// such that every possible method defined on the underlying element is accessible via typing +/// The requires the type of signature Type, whereas T is the AppState type, A, is Action, and E is the underlying Element type that is composed +/// It additionally accepts generic vars (vars: ) that is added on the impl>, and vars_on_ty (Type>) and additional generic typebounds +macro_rules! impl_dom_interfaces_for_ty { + ($dom_interface:ident, $ty:ident) => { + $crate::interfaces::impl_dom_interfaces_for_ty!($dom_interface, $ty, vars: <>, vars_on_ty: <>, bounds: {}); + }; + ($dom_interface:ident, $ty:ident, vars: <$($additional_generic_var:ident,)*>, vars_on_ty: <$($additional_generic_var_on_ty:ident,)*>, bounds: {$($additional_generic_bounds:tt)*}) => { + paste::paste! { + $crate::interfaces::[]!( + $crate::interfaces::impl_dom_interfaces_for_ty_helper, + ($ty, $dom_interface, <$($additional_generic_var,)*>, <$($additional_generic_var_on_ty,)*>, {$($additional_generic_bounds)*}) + ); + $crate::interfaces::impl_dom_interfaces_for_ty_helper!($dom_interface, ($ty, $dom_interface, <$($additional_generic_var,)*>, <$($additional_generic_var_on_ty,)*>, {$($additional_generic_bounds)*})); + $crate::interfaces::[]!( + $crate::interfaces::impl_dom_interfaces_for_ty_helper, + ($ty, <$($additional_generic_var,)*>, <$($additional_generic_var_on_ty,)*>, {$($additional_generic_bounds)*}) + ); + } + }; +} + +pub(crate) use impl_dom_interfaces_for_ty; + +dom_interface_macro_and_trait_definitions!( + HtmlElement { + methods: {}, + child_interfaces: { + HtmlAnchorElement { methods: {}, child_interfaces: {} }, + HtmlAreaElement { methods: {}, child_interfaces: {} }, + // HtmlBaseElement { methods: {}, child_interfaces: {} }, TODO include metadata? + // HtmlBodyElement { methods: {}, child_interfaces: {} }, TODO include body element? + HtmlBrElement { methods: {}, child_interfaces: {} }, + HtmlButtonElement { methods: {}, child_interfaces: {} }, + HtmlCanvasElement { + methods: { + fn width(self, value: u32) -> Attr { + self.attr("width", value) + } + fn height(self, value: u32) -> Attr { + self.attr("height", value) + } + }, + child_interfaces: {} + }, + HtmlDataElement { methods: {}, child_interfaces: {} }, + HtmlDataListElement { methods: {}, child_interfaces: {} }, + HtmlDetailsElement { methods: {}, child_interfaces: {} }, + HtmlDialogElement { methods: {}, child_interfaces: {} }, + // HtmlDirectoryElement { methods: {}, child_interfaces: {} }, deprecated + HtmlDivElement { methods: {}, child_interfaces: {} }, + HtmlDListElement { methods: {}, child_interfaces: {} }, + // HtmlUnknownElement { methods: {}, child_interfaces: {} }, useful at all? + HtmlEmbedElement { methods: {}, child_interfaces: {} }, + HtmlFieldSetElement { methods: {}, child_interfaces: {} }, + // HtmlFontElement { methods: {}, child_interfaces: {} }, deprecated + HtmlFormElement { methods: {}, child_interfaces: {} }, + // HtmlFrameElement { methods: {}, child_interfaces: {} }, deprecated + // HtmlFrameSetElement { methods: {}, child_interfaces: {} }, deprecacted + // HtmlHeadElement { methods: {}, child_interfaces: {} }, TODO include metadata? + HtmlHeadingElement { methods: {}, child_interfaces: {} }, + HtmlHrElement { methods: {}, child_interfaces: {} }, + // HtmlHtmlElement { methods: {}, child_interfaces: {} }, TODO include metadata? + HtmlIFrameElement { methods: {}, child_interfaces: {} }, + HtmlImageElement { methods: {}, child_interfaces: {} }, + HtmlInputElement { methods: {}, child_interfaces: {} }, + HtmlLabelElement { methods: {}, child_interfaces: {} }, + HtmlLegendElement { methods: {}, child_interfaces: {} }, + HtmlLiElement { methods: {}, child_interfaces: {} }, + HtmlLinkElement { methods: {}, child_interfaces: {} }, + HtmlMapElement { methods: {}, child_interfaces: {} }, + HtmlMediaElement { + methods: {}, + child_interfaces: { + HtmlAudioElement { methods: {}, child_interfaces: {} }, + HtmlVideoElement { + methods: { + fn width(self, value: u32) -> Attr { + self.attr("width", value) + } + fn height(self, value: u32) -> Attr { + self.attr("height", value) + } + }, + child_interfaces: {} + }, + } + }, + HtmlMenuElement { methods: {}, child_interfaces: {} }, + // HtmlMenuItemElement { methods: {}, child_interfaces: {} }, deprecated + // HtmlMetaElement { methods: {}, child_interfaces: {} }, TODO include metadata? + HtmlMeterElement { methods: {}, child_interfaces: {} }, + HtmlModElement { methods: {}, child_interfaces: {} }, + HtmlObjectElement { methods: {}, child_interfaces: {} }, + HtmlOListElement { methods: {}, child_interfaces: {} }, + HtmlOptGroupElement { methods: {}, child_interfaces: {} }, + HtmlOptionElement { methods: {}, child_interfaces: {} }, + HtmlOutputElement { methods: {}, child_interfaces: {} }, + HtmlParagraphElement { methods: {}, child_interfaces: {} }, + // HtmlParamElement { methods: {}, child_interfaces: {} }, deprecated + HtmlPictureElement { methods: {}, child_interfaces: {} }, + HtmlPreElement { methods: {}, child_interfaces: {} }, + HtmlProgressElement { methods: {}, child_interfaces: {} }, + HtmlQuoteElement { methods: {}, child_interfaces: {} }, + HtmlScriptElement { methods: {}, child_interfaces: {} }, + HtmlSelectElement { methods: {}, child_interfaces: {} }, + HtmlSlotElement { methods: {}, child_interfaces: {} }, + HtmlSourceElement { methods: {}, child_interfaces: {} }, + HtmlSpanElement { methods: {}, child_interfaces: {} }, + // HtmlStyleElement { methods: {}, child_interfaces: {} }, TODO include metadata? + HtmlTableCaptionElement { methods: {}, child_interfaces: {} }, + HtmlTableCellElement { methods: {}, child_interfaces: {} }, + HtmlTableColElement { methods: {}, child_interfaces: {} }, + HtmlTableElement { methods: {}, child_interfaces: {} }, + HtmlTableRowElement { methods: {}, child_interfaces: {} }, + HtmlTableSectionElement { methods: {}, child_interfaces: {} }, + HtmlTemplateElement { methods: {}, child_interfaces: {} }, + HtmlTimeElement { methods: {}, child_interfaces: {} }, + HtmlTextAreaElement { methods: {}, child_interfaces: {} }, + // HtmlTitleElement { methods: {}, child_interfaces: {} }, TODO include metadata? + HtmlTrackElement { methods: {}, child_interfaces: {} }, + HtmlUListElement { methods: {}, child_interfaces: {} }, + } + }, + SvgElement { + methods: {}, + child_interfaces: {} + }, +); + +// Core View implementations + +impl sealed::Sealed + for crate::Adapt +{ +} +impl sealed::Sealed for crate::AdaptState {} + +macro_rules! impl_dom_traits_for_adapt_views { + ($dom_interface:ident, ()) => { + impl $dom_interface + for crate::Adapt + where + V: $dom_interface, + F: Fn( + &mut ParentT, + crate::AdaptThunk, + ) -> xilem_core::MessageResult, + { + } + impl $dom_interface + for crate::AdaptState + where + V: $dom_interface, + F: Fn(&mut ParentT) -> &mut ChildT, + { + } + }; +} +for_all_dom_interfaces!(impl_dom_traits_for_adapt_views, ()); diff --git a/crates/xilem_html/src/lib.rs b/crates/xilem_html/src/lib.rs index 9e6f5078..b21dbf55 100644 --- a/crates/xilem_html/src/lib.rs +++ b/crates/xilem_html/src/lib.rs @@ -8,34 +8,34 @@ use wasm_bindgen::JsCast; mod app; -mod class; +mod attribute; +mod attribute_value; mod context; mod diff; -mod element; -mod event; +pub mod elements; +pub mod events; +pub mod interfaces; mod one_of; +mod optional_action; mod vecmap; mod view; -#[cfg(feature = "typed")] mod view_ext; pub use xilem_core::MessageResult; pub use app::App; -pub use class::class; +pub use attribute::Attr; +pub use attribute_value::{AttributeValue, IntoAttributeValue}; pub use context::{ChangeFlags, Cx}; -#[cfg(feature = "typed")] -pub use element::elements; -pub use element::{element, AttributeValue, Element, ElementState, IntoAttributeValue}; -#[cfg(feature = "typed")] -pub use event::events; -pub use event::{on_event, Action, Event, OnEvent, OnEventState, OptionalAction}; -pub use one_of::{OneOf2, OneOf3, OneOf4, OneOf5, OneOf6, OneOf7, OneOf8}; -pub use view::{ - memoize, s, Adapt, AdaptState, AdaptThunk, AnyView, Memoize, Pod, View, ViewMarker, - ViewSequence, +pub use one_of::{ + OneOf2, OneOf3, OneOf4, OneOf5, OneOf6, OneOf7, OneOf8, OneSeqOf2, OneSeqOf3, OneSeqOf4, + OneSeqOf5, OneSeqOf6, OneSeqOf7, OneSeqOf8, +}; +pub use optional_action::{Action, OptionalAction}; +pub use view::{ + memoize, static_view, Adapt, AdaptState, AdaptThunk, AnyView, Memoize, MemoizeState, Pod, View, + ViewMarker, ViewSequence, }; -#[cfg(feature = "typed")] pub use view_ext::ViewExt; xilem_core::message!(); diff --git a/crates/xilem_html/src/one_of.rs b/crates/xilem_html/src/one_of.rs index ac7bfcc4..7696080d 100644 --- a/crates/xilem_html/src/one_of.rs +++ b/crates/xilem_html/src/one_of.rs @@ -1,8 +1,19 @@ use wasm_bindgen::throw_str; -use crate::{ChangeFlags, Cx, Pod, View, ViewMarker, ViewSequence}; +use crate::{ + interfaces::for_all_element_descendents, ChangeFlags, Cx, Pod, View, ViewMarker, ViewSequence, +}; -macro_rules! one_of { +macro_rules! impl_dom_traits { + ($dom_interface:ident, ($ident:ident: $($vars:ident),+)) => { + impl),+> $crate::interfaces::$dom_interface for $ident<$($vars),+> + where + $($vars: $crate::interfaces::$dom_interface,)+ + {} + }; +} + +macro_rules! one_of_view { ( #[doc = $first_doc_line:literal] $ident:ident { $( $vars:ident ),+ } @@ -14,22 +25,26 @@ macro_rules! one_of { $($vars($vars),)+ } + impl<$($vars),+> crate::interfaces::sealed::Sealed for $ident<$($vars),+> {} + impl_dom_traits!(Element, ($ident: $($vars),+)); + for_all_element_descendents!(impl_dom_traits, ($ident: $($vars),+)); + impl<$($vars),+> AsRef for $ident<$($vars),+> where - $($vars: AsRef,)+ + $($vars: crate::view::DomNode,)+ { fn as_ref(&self) -> &web_sys::Node { match self { - $( $ident::$vars(view) => view.as_ref(), )+ + $( $ident::$vars(view) => view.as_node_ref(), )+ } } } + impl<$($vars),+> ViewMarker for $ident<$($vars),+> {} impl View for $ident<$($vars),+> - where $( - $vars: View + ViewMarker, - $vars::Element: AsRef + 'static, - )+ { + where + $($vars: View,)+ + { type State = $ident<$($vars::State),+>; type Element = $ident<$($vars::Element),+>; @@ -97,7 +112,54 @@ macro_rules! one_of { } } } + }; +} +one_of_view! { + /// This view container can switch between two views. + OneOf2 { A, B } +} +one_of_view! { + /// This view container can switch between three views. + OneOf3 { A, B, C } +} + +one_of_view! { + /// This view container can switch between four views. + OneOf4 { A, B, C, D } +} + +one_of_view! { + /// This view container can switch between five views. + OneOf5 { A, B, C, D, E } +} + +one_of_view! { + /// This view container can switch between six views. + OneOf6 { A, B, C, D, E, F } +} + +one_of_view! { + /// This view container can switch between seven views. + OneOf7 { A, B, C, D, E, F, G } +} + +one_of_view! { + /// This view container can switch between eight views. + OneOf8 { A, B, C, D, E, F, G, H } +} + +macro_rules! one_of_sequence { + ( + #[doc = $first_doc_line:literal] + $ident:ident { $( $vars:ident ),+ } + ) => { + #[doc = $first_doc_line] + /// + /// It is a statically-typed alternative to the type-erased `AnyView`. + pub enum $ident<$($vars),+> { + $($vars($vars),)+ + } impl ViewSequence for $ident<$($vars),+> where $( $vars: ViewSequence, @@ -185,40 +247,39 @@ macro_rules! one_of { } } } - }; } -one_of! { - /// This view container can switch between two views. - OneOf2 { A, B } +one_of_sequence! { + /// This view sequence container can switch between two view sequences. + OneSeqOf2 { A, B } } -one_of! { - /// This view container can switch between three views. - OneOf3 { A, B, C } +one_of_sequence! { + /// This view sequence container can switch between three view sequences. + OneSeqOf3 { A, B, C } } -one_of! { - /// This view container can switch between four views. - OneOf4 { A, B, C, D } +one_of_sequence! { + /// This view sequence container can switch between four view sequences. + OneSeqOf4 { A, B, C, D } } -one_of! { - /// This view container can switch between five views. - OneOf5 { A, B, C, D, E } +one_of_sequence! { + /// This view sequence container can switch between five view sequences. + OneSeqOf5 { A, B, C, D, E } } -one_of! { - /// This view container can switch between six views. - OneOf6 { A, B, C, D, E, F } +one_of_sequence! { + /// This view sequence container can switch between six view sequences. + OneSeqOf6 { A, B, C, D, E, F } } -one_of! { - /// This view container can switch between seven views. - OneOf7 { A, B, C, D, E, F, G } +one_of_sequence! { + /// This view sequence container can switch between seven view sequences. + OneSeqOf7 { A, B, C, D, E, F, G } } -one_of! { - /// This view container can switch between eight views. - OneOf8 { A, B, C, D, E, F, G, H } +one_of_sequence! { + /// This view sequence container can switch between eight view sequences. + OneSeqOf8 { A, B, C, D, E, F, G, H } } diff --git a/crates/xilem_html/src/optional_action.rs b/crates/xilem_html/src/optional_action.rs new file mode 100644 index 00000000..de8021e9 --- /dev/null +++ b/crates/xilem_html/src/optional_action.rs @@ -0,0 +1,35 @@ +/// Implement this trait for types you want to use as actions. +/// +/// The trait exists because otherwise we couldn't provide versions +/// of listeners that take `()`, `A` and `Option`. +pub trait Action {} + +/// Trait that allows callbacks to be polymorphic on return type +/// (`Action`, `Option` or `()`). An implementation detail. +pub trait OptionalAction: sealed::Sealed { + fn action(self) -> Option; +} +mod sealed { + pub trait Sealed {} +} + +impl sealed::Sealed for () {} +impl OptionalAction for () { + fn action(self) -> Option { + None + } +} + +impl sealed::Sealed for A {} +impl OptionalAction for A { + fn action(self) -> Option { + Some(self) + } +} + +impl sealed::Sealed for Option {} +impl OptionalAction for Option { + fn action(self) -> Option { + self + } +} diff --git a/crates/xilem_html/src/vecmap.rs b/crates/xilem_html/src/vecmap.rs index d64fc52b..9f224da8 100644 --- a/crates/xilem_html/src/vecmap.rs +++ b/crates/xilem_html/src/vecmap.rs @@ -38,6 +38,30 @@ impl VecMap { .find_map(|(k, v)| if key.eq(k.borrow()) { Some(v) } else { None }) } + /// Returns `true` if the map contains a value for the specified key. + /// + /// The key may be any borrowed form of the map's key type, but the ordering + /// on the borrowed form *must* match the ordering on the key type. + /// + /// # Examples + /// + /// Basic usage: + /// + /// ```ignore + /// # use crate::vecmap::VecMap; + /// let mut map = VecMap::default(); + /// map.insert(1, "a"); + /// assert!(map.contains_key(&1)); + /// assert!(!map.contains_key(&2)); + /// ``` + pub fn contains_key(&self, key: &Q) -> bool + where + K: Borrow + Ord, + Q: Ord, + { + self.get(key).is_some() + } + /// Returns a mutable reference to the value corresponding to the key. /// /// The key may be any borrowed form of the map's key type, but the ordering @@ -107,6 +131,7 @@ impl VecMap { /// assert_eq!((*first_key, *first_value), (1, "a")); /// ``` pub fn iter(&self) -> impl Iterator { + #[allow(clippy::map_identity)] self.0.iter().map(|(k, v)| (k, v)) } @@ -178,6 +203,10 @@ impl VecMap { } } + pub fn clear(&mut self) { + self.0.clear() + } + /// Returns `true` if the map contains no elements. /// /// # Examples @@ -254,6 +283,14 @@ mod tests { assert_eq!(map.get(&2), None); } + #[test] + fn contains_key() { + let mut map = VecMap::default(); + map.insert(1, "a"); + assert!(map.contains_key(&1)); + assert!(!map.contains_key(&2)); + } + #[test] fn get_mut() { let mut map = VecMap::default(); diff --git a/crates/xilem_html/src/view.rs b/crates/xilem_html/src/view.rs index 361b66e1..4aec2632 100644 --- a/crates/xilem_html/src/view.rs +++ b/crates/xilem_html/src/view.rs @@ -10,7 +10,7 @@ use xilem_core::{Id, MessageResult}; use crate::{context::Cx, ChangeFlags}; -mod sealed { +pub(crate) mod sealed { pub trait Sealed {} } @@ -18,7 +18,7 @@ mod sealed { // for a view element, rather than an associated type with a bound. /// This trait is implemented for types that implement `AsRef`. /// It is an implementation detail. -pub trait DomNode: sealed::Sealed { +pub trait DomNode: sealed::Sealed + 'static { fn into_pod(self) -> Pod; fn as_node_ref(&self) -> &web_sys::Node; } @@ -34,18 +34,6 @@ impl + 'static> DomNode for N { } } -/// This trait is implemented for types that implement `AsRef`. -/// It is an implementation detail. -pub trait DomElement: DomNode { - fn as_element_ref(&self) -> &web_sys::Element; -} - -impl> DomElement for N { - fn as_element_ref(&self) -> &web_sys::Element { - self.as_ref() - } -} - /// A trait for types that can be type-erased and impl `AsRef`. It is an /// implementation detail. pub trait AnyNode: sealed::Sealed { @@ -99,7 +87,7 @@ impl Pod { xilem_core::generate_view_trait! {View, DomNode, Cx, ChangeFlags;} xilem_core::generate_viewsequence_trait! {ViewSequence, View, ViewMarker, DomNode, Cx, ChangeFlags, Pod;} xilem_core::generate_anyview_trait! {AnyView, View, ViewMarker, Cx, ChangeFlags, AnyNode, BoxedView;} -xilem_core::generate_memoize_view! {Memoize, MemoizeState, View, ViewMarker, Cx, ChangeFlags, s, memoize;} +xilem_core::generate_memoize_view! {Memoize, MemoizeState, View, ViewMarker, Cx, ChangeFlags, static_view, memoize;} xilem_core::generate_adapt_view! {View, Cx, ChangeFlags;} xilem_core::generate_adapt_state_view! {View, Cx, ChangeFlags;} diff --git a/crates/xilem_html/src/view_ext.rs b/crates/xilem_html/src/view_ext.rs index f62a46a1..f1ffbcd9 100644 --- a/crates/xilem_html/src/view_ext.rs +++ b/crates/xilem_html/src/view_ext.rs @@ -1,69 +1,10 @@ // Copyright 2023 the Druid Authors. // SPDX-License-Identifier: Apache-2.0 -use std::borrow::Cow; +use crate::{view::View, Adapt, AdaptState, AdaptThunk}; -use crate::{ - class::Class, event::OptionalAction, events, view::View, Adapt, AdaptState, AdaptThunk, Event, -}; - -/// A trait that makes it possible to attach event listeners and more to views -/// in the continuation style. +/// A trait that makes it possible to use core views such as [`Adapt`] in the continuation/builder style. pub trait ViewExt: View + Sized { - /// Add an `onclick` event listener. - fn on_click< - OA: OptionalAction, - F: Fn(&mut T, &Event) -> OA, - >( - self, - f: F, - ) -> events::OnClick { - events::on_click(self, f) - } - - /// Add an `ondblclick` event listener. - fn on_dblclick< - OA: OptionalAction, - F: Fn(&mut T, &Event) -> OA, - >( - self, - f: F, - ) -> events::OnDblClick { - events::on_dblclick(self, f) - } - - /// Add an `oninput` event listener. - fn on_input< - OA: OptionalAction, - F: Fn(&mut T, &Event) -> OA, - >( - self, - f: F, - ) -> events::OnInput { - events::on_input(self, f) - } - - /// Add an `onkeydown` event listener. - fn on_keydown< - OA: OptionalAction, - F: Fn(&mut T, &Event) -> OA, - >( - self, - f: F, - ) -> events::OnKeyDown { - events::on_keydown(self, f) - } - - fn on_blur< - OA: OptionalAction, - F: Fn(&mut T, &Event) -> OA, - >( - self, - f: F, - ) -> events::OnBlur { - events::on_blur(self, f) - } - fn adapt(self, f: F) -> Adapt where F: Fn(&mut ParentT, AdaptThunk) -> xilem_core::MessageResult, @@ -77,11 +18,6 @@ pub trait ViewExt: View + Sized { { AdaptState::new(f, self) } - - /// Apply a CSS class to the child view. - fn class(self, class: impl Into>) -> Class { - crate::class::class(self, class) - } } impl> ViewExt for V {} diff --git a/crates/xilem_html/web_examples/counter/src/main.rs b/crates/xilem_html/web_examples/counter/src/main.rs index baf91904..592db542 100644 --- a/crates/xilem_html/web_examples/counter/src/main.rs +++ b/crates/xilem_html/web_examples/counter/src/main.rs @@ -1,7 +1,7 @@ use xilem_html::{ document_body, elements as el, - events::{self as evt}, - App, Event, View, ViewExt, + interfaces::{Element, HtmlButtonElement}, + App, View, }; #[derive(Default)] @@ -39,13 +39,10 @@ impl AppState { } /// You can create functions that generate views. -fn btn( +fn btn( label: &'static str, - click_fn: F, -) -> evt::OnClick, F, ()> -where - F: Fn(&mut AppState, &Event), -{ + click_fn: impl Fn(&mut AppState, web_sys::MouseEvent), +) -> impl HtmlButtonElement { el::button(label).on_click(click_fn) } diff --git a/crates/xilem_html/web_examples/counter_untyped/Cargo.toml b/crates/xilem_html/web_examples/counter_custom_element/Cargo.toml similarity index 52% rename from crates/xilem_html/web_examples/counter_untyped/Cargo.toml rename to crates/xilem_html/web_examples/counter_custom_element/Cargo.toml index 9a22abad..343df66d 100644 --- a/crates/xilem_html/web_examples/counter_untyped/Cargo.toml +++ b/crates/xilem_html/web_examples/counter_custom_element/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "counter_untyped" +name = "counter_custom_element" version = "0.1.0" publish = false license.workspace = true @@ -8,5 +8,5 @@ edition.workspace = true [dependencies] console_error_panic_hook = "0.1" wasm-bindgen = "0.2.87" -web-sys = { version = "0.3.64", features = ["HtmlButtonElement"] } -xilem_html = { path = "../..", default-features = false } +web-sys = "0.3.64" +xilem_html = { path = "../.." } diff --git a/crates/xilem_html/web_examples/counter_untyped/index.html b/crates/xilem_html/web_examples/counter_custom_element/index.html similarity index 100% rename from crates/xilem_html/web_examples/counter_untyped/index.html rename to crates/xilem_html/web_examples/counter_custom_element/index.html diff --git a/crates/xilem_html/web_examples/counter_untyped/src/main.rs b/crates/xilem_html/web_examples/counter_custom_element/src/main.rs similarity index 56% rename from crates/xilem_html/web_examples/counter_untyped/src/main.rs rename to crates/xilem_html/web_examples/counter_custom_element/src/main.rs index 9e78396d..1e024233 100644 --- a/crates/xilem_html/web_examples/counter_untyped/src/main.rs +++ b/crates/xilem_html/web_examples/counter_custom_element/src/main.rs @@ -1,4 +1,9 @@ -use xilem_html::{document_body, element, on_event, App, Event, View, ViewMarker}; +use xilem_html::{ + document_body, + elements::custom_element, + interfaces::{Element, HtmlElement}, + App, View, +}; #[derive(Default)] struct AppState { @@ -17,24 +22,20 @@ impl AppState { } } -fn btn(label: &'static str, click_fn: F) -> impl View + ViewMarker -where - F: Fn(&mut AppState, &Event), -{ - on_event( - "click", - element("button", label), - move |state: &mut AppState, evt: &Event<_, _>| { - click_fn(state, evt); - }, - ) +fn btn( + label: &'static str, + click_fn: impl Fn(&mut AppState, web_sys::Event), +) -> impl HtmlElement { + custom_element("button", label).on("click", move |state: &mut AppState, evt| { + click_fn(state, evt); + }) } fn app_logic(state: &mut AppState) -> impl View { - element::( + custom_element( "div", ( - element::("span", format!("clicked {} times", state.clicks)), + custom_element("span", format!("clicked {} times", state.clicks)), btn("+1 click", |state, _| AppState::increment(state)), btn("-1 click", |state, _| AppState::decrement(state)), btn("reset clicks", |state, _| AppState::reset(state)), diff --git a/crates/xilem_html/web_examples/todomvc/src/main.rs b/crates/xilem_html/web_examples/todomvc/src/main.rs index 1e877daf..232d6554 100644 --- a/crates/xilem_html/web_examples/todomvc/src/main.rs +++ b/crates/xilem_html/web_examples/todomvc/src/main.rs @@ -2,9 +2,12 @@ mod state; use state::{AppState, Filter, Todo}; +use wasm_bindgen::JsCast; use xilem_html::{ - elements as el, events::on_click, get_element_by_id, Action, Adapt, App, MessageResult, View, - ViewExt, ViewMarker, + elements::{self as el}, + get_element_by_id, + interfaces::*, + Action, Adapt, App, MessageResult, View, }; // All of these actions arise from within a `Todo`, but we need access to the full state to reduce @@ -18,7 +21,7 @@ enum TodoAction { impl Action for TodoAction {} -fn todo_item(todo: &mut Todo, editing: bool) -> impl View + ViewMarker { +fn todo_item(todo: &mut Todo, editing: bool) -> impl Element { let mut class = String::new(); if todo.completed { class.push_str(" completed"); @@ -26,16 +29,16 @@ fn todo_item(todo: &mut Todo, editing: bool) -> impl View + Vi if editing { class.push_str(" editing"); } - let input = el::input(()) + + let checkbox = el::input(()) .attr("class", "toggle") .attr("type", "checkbox") - .attr("checked", todo.completed); + .attr("checked", todo.completed) + .on_click(|state: &mut Todo, _| state.completed = !state.completed); el::li(( el::div(( - input.on_click(|state: &mut Todo, _| { - state.completed = !state.completed; - }), + checkbox, el::label(todo.title.clone()) .on_dblclick(|state: &mut Todo, _| TodoAction::SetEditing(state.id)), el::button(()) @@ -58,17 +61,22 @@ fn todo_item(todo: &mut Todo, editing: bool) -> impl View + Vi } }) .on_input(|state: &mut Todo, evt| { - state.title_editing.clear(); - state.title_editing.push_str(&evt.target().value()); - evt.prevent_default(); + // TODO There could/should be further checks, if this is indeed the right event (same DOM element) + if let Some(element) = evt + .target() + .and_then(|t| t.dyn_into::().ok()) + { + evt.prevent_default(); + state.title_editing = element.value(); + } }) - .passive(false) + .passive(true) .on_blur(|_, _| TodoAction::CancelEditing), )) .attr("class", class) } -fn footer_view(state: &mut AppState, should_display: bool) -> impl View + ViewMarker { +fn footer_view(state: &mut AppState, should_display: bool) -> impl Element { let item_str = if state.todos.len() == 1 { "item" } else { @@ -76,7 +84,7 @@ fn footer_view(state: &mut AppState, should_display: bool) -> impl View 0).then(|| { - on_click( + Element::on_click( el::button("Clear completed").attr("class", "clear-completed"), |state: &mut AppState, _| { state.todos.retain(|todo| !todo.completed); @@ -86,14 +94,14 @@ fn footer_view(state: &mut AppState, should_display: bool) -> impl View impl View impl View impl View impl View + ViewMarker { +fn main_view(state: &mut AppState, should_display: bool) -> impl Element { let editing_id = state.editing_id; let todos: Vec<_> = state .visible_todos() @@ -159,16 +164,14 @@ fn main_view(state: &mut AppState, should_display: bool) -> impl View .attr("class", "toggle-all") .attr("type", "checkbox") .attr("checked", state.are_all_complete()); - let mut section = el::section(( + + 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"), )) - .attr("class", "main"); - if !should_display { - section.set_attr("style", "display:none;"); - } - section + .attr("class", "main") + .attr("style", (!should_display).then_some("display:none;")) } fn app_logic(state: &mut AppState) -> impl View { @@ -191,8 +194,14 @@ fn app_logic(state: &mut AppState) -> impl View { } }) .on_input(|state: &mut AppState, evt| { - state.update_new_todo(&evt.target().value()); - evt.prevent_default(); + // TODO There could/should be further checks, if this is indeed the right event (same DOM element) + if let Some(element) = evt + .target() + .and_then(|t| t.dyn_into::().ok()) + { + state.update_new_todo(&element.value()); + evt.prevent_default(); + } }) .passive(false), ))