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`
This commit is contained in:
Philipp Mildenberger 2023-11-23 17:25:42 +01:00 committed by GitHub
parent 65091c7743
commit d40a94fa8a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 1706 additions and 1286 deletions

3
Cargo.lock generated
View File

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

View File

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

View File

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

View File

@ -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<E, T, A> {
pub(crate) element: E,
pub(crate) name: Cow<'static, str>,
pub(crate) value: Option<AttributeValue>,
pub(crate) phantom: PhantomData<fn() -> (T, A)>,
}
impl<E, T, A> ViewMarker for Attr<E, T, A> {}
impl<E, T, A> Sealed for Attr<E, T, A> {}
impl<E: Element<T, A>, T, A> View<T, A> for Attr<E, T, A> {
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<dyn std::any::Any>,
app_state: &mut T,
) -> MessageResult<A> {
self.element.message(id_path, state, message, app_state)
}
}
crate::interfaces::impl_dom_interfaces_for_ty!(Element, Attr);

View File

@ -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 <input checked>)
I32(i32),

View File

@ -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<V> {
child: V,
// This could reasonably be static Cow also, but keep things simple
class: Cow<'static, str>,
}
pub fn class<V>(child: V, class: impl Into<Cow<'static, str>>) -> Class<V> {
Class {
child,
class: class.into(),
}
}
impl<V> ViewMarker for Class<V> {}
// TODO: make generic over A (probably requires Phantom)
impl<T, V> View<T> for Class<V>
where
V: View<T>,
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<dyn Any>,
app_state: &mut T,
) -> MessageResult<()> {
self.child.message(id_path, state, message, app_state)
}
}

View File

@ -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<CowStr, AttributeValue>,
app_ref: Option<Box<dyn AppRunner>>,
}
@ -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<CowStr, AttributeValue>) {
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<CowStr, AttributeValue>,
) -> 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<AttributeValue>,
) {
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<CowStr, AttributeValue> {
let mut attributes = VecMap::default();
std::mem::swap(&mut attributes, &mut self.current_element_attributes);
for (name, value) in attributes.iter() {
set_attribute(element, name, &value.serialize());
}
attributes
}
pub(crate) fn apply_attribute_changes(
&mut self,
element: &web_sys::Element,
attributes: &mut VecMap<CowStr, AttributeValue>,
) -> ChangeFlags {
let mut changed = ChangeFlags::empty();
// update attributes
for itm in diff_kv_iterables(&*attributes, &self.current_element_attributes) {
match itm {
Diff::Add(name, value) | Diff::Change(name, value) => {
set_attribute(element, name, &value.serialize());
changed |= ChangeFlags::OTHER_CHANGE;
}
Diff::Remove(name) => {
remove_attribute(element, name);
changed |= ChangeFlags::OTHER_CHANGE;
}
}
}
std::mem::swap(attributes, &mut self.current_element_attributes);
self.current_element_attributes.clear();
changed
}
pub fn message_thunk(&self) -> MessageThunk {

View File

@ -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<ViewSeq>(crate::Element<$web_sys_ty, ViewSeq>);
/// Builder function for a
#[doc = concat!("`", stringify!($name), "`")]
/// view.
pub fn $name<ViewSeq>(children: ViewSeq) -> $ty_name<ViewSeq> {
$ty_name(crate::element(stringify!($name), children))
}
impl<ViewSeq> $ty_name<ViewSeq> {
/// 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<std::borrow::Cow<'static, str>>,
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<std::borrow::Cow<'static, str>>,
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<ViewSeq> crate::view::ViewMarker for $ty_name<ViewSeq> {}
impl<T_, A_, ViewSeq> crate::view::View<T_, A_> for $ty_name<ViewSeq>
where
ViewSeq: crate::view::ViewSequence<T_, A_>,
{
type State = crate::ElementState<ViewSeq::State>;
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<dyn std::any::Any>,
app_state: &mut T_,
) -> xilem_core::MessageResult<A_> {
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),
);

View File

@ -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<El, Children = ()> {
name: CowStr,
attributes: VecMap<CowStr, AttributeValue>,
children: Children,
#[allow(clippy::type_complexity)]
after_update: Option<Box<dyn Fn(&El)>>,
}
impl<El, ViewSeq> Element<El, ViewSeq> {
pub fn debug_as_el(&self) -> impl fmt::Debug + '_ {
struct DebugFmt<'a, El, VS>(&'a Element<El, VS>);
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<ViewSeqState> {
child_states: ViewSeqState,
child_elements: Vec<Pod>,
scratch: Vec<Pod>,
}
/// Create a new element view
///
/// If the element has no children, use the unit type (e.g. `let view = element("div", ())`).
pub fn element<El, ViewSeq>(name: impl Into<CowStr>, children: ViewSeq) -> Element<El, ViewSeq> {
Element {
name: name.into(),
attributes: Default::default(),
children,
after_update: None,
}
}
impl<El, ViewSeq> Element<El, ViewSeq> {
/// 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<CowStr>, 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<CowStr>, 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<El, Children> ViewMarker for Element<El, Children> {}
impl<T, A, El, Children> View<T, A> for Element<El, Children>
where
Children: ViewSequence<T, A>,
El: JsCast + DomElement,
{
type State = ElementState<Children::State>;
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<dyn std::any::Any>,
app_state: &mut T,
) -> MessageResult<A> {
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();
}

View File

@ -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<ViewSeqState> {
pub(crate) children_states: ViewSeqState,
pub(crate) attributes: VecMap<CowStr, AttributeValue>,
pub(crate) child_elements: Vec<Pod>,
pub(crate) scratch: Vec<Pod>,
}
// TODO something like the `after_update` of the former `Element` view (likely as a wrapper view instead)
pub struct CustomElement<T, A = (), Children = ()> {
name: CowStr,
children: Children,
#[allow(clippy::type_complexity)]
phantom: PhantomData<fn() -> (T, A)>,
}
/// Builder function for a custom element view.
pub fn custom_element<T, A, Children: ViewSequence<T, A>>(
name: impl Into<CowStr>,
children: Children,
) -> CustomElement<T, A, Children> {
CustomElement {
name: name.into(),
children,
phantom: PhantomData,
}
}
impl<T, A, Children> CustomElement<T, A, Children> {
fn node_name(&self) -> &str {
&self.name
}
}
impl<T, A, Children> ViewMarker for CustomElement<T, A, Children> {}
impl<T, A, Children> Sealed for CustomElement<T, A, Children> {}
impl<T, A, Children> View<T, A> for CustomElement<T, A, Children>
where
Children: ViewSequence<T, A>,
{
type State = ElementState<Children::State>;
// 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<dyn std::any::Any>,
app_state: &mut T,
) -> MessageResult<A> {
self.children
.message(id_path, &mut state.children_states, message, app_state)
}
}
impl<T, A, Children: ViewSequence<T, A>> Element<T, A> for CustomElement<T, A, Children> {}
impl<T, A, Children: ViewSequence<T, A>> crate::interfaces::HtmlElement<T, A>
for CustomElement<T, A, Children>
{
}
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<fn() -> ($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<dyn std::any::Any>,
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::[<for_all_ $dom_interface:snake _ancestors>]!(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),
);

View File

@ -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<T, A, V, F, OA>
where
V: crate::view::View<T, A>,
F: Fn(&mut T, &$crate::Event<$web_sys_ty, V::Element>) -> OA,
V::Element: 'static,
OA: $crate::event::OptionalAction<A>,
{
inner: crate::OnEvent<$web_sys_ty, V, F>,
data: std::marker::PhantomData<T>,
action: std::marker::PhantomData<A>,
optional_action: std::marker::PhantomData<OA>,
}
impl<T, A, V, F, OA> $ty_name<T, A, V, F, OA>
where
V: crate::view::View<T, A>,
F: Fn(&mut T, &$crate::Event<$web_sys_ty, V::Element>) -> OA,
V::Element: 'static,
OA: $crate::event::OptionalAction<A>,
{
/// 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<T, A, V, F, OA>(child: V, callback: F) -> $ty_name<T, A, V, F, OA>
where
V: crate::view::View<T, A>,
F: Fn(&mut T, &$crate::Event<$web_sys_ty, V::Element>) -> OA,
V::Element: 'static,
OA: $crate::event::OptionalAction<A>,
{
$ty_name {
inner: crate::on_event($name, child, callback),
data: std::marker::PhantomData,
action: std::marker::PhantomData,
optional_action: std::marker::PhantomData,
}
}
impl<T, A, V, F, OA> crate::view::ViewMarker for $ty_name<T, A, V, F, OA>
where
V: crate::view::View<T, A>,
F: Fn(&mut T, &$crate::Event<$web_sys_ty, V::Element>) -> OA,
V::Element: 'static,
OA: $crate::event::OptionalAction<A>,
{
}
impl<T, A, V, F, OA> crate::view::View<T, A> for $ty_name<T, A, V, F, OA>
where
V: crate::view::View<T, A>,
F: Fn(&mut T, &$crate::Event<$web_sys_ty, V::Element>) -> OA,
V::Element: 'static,
OA: $crate::event::OptionalAction<A>,
{
type State = crate::event::OnEventState<V::State>;
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<dyn std::any::Any>,
app_state: &mut T,
) -> xilem_core::MessageResult<A> {
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),
);

View File

@ -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<E, V, F> {
// 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<E>,
}
impl<E, V, F> OnEvent<E, V, F> {
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<E, V, F> ViewMarker for OnEvent<E, V, F> {}
impl<T, A, E, F, V, OA> View<T, A> for OnEvent<E, V, F>
where
F: Fn(&mut T, &Event<E, V::Element>) -> OA,
V: View<T, A>,
E: JsCast + 'static,
V::Element: 'static,
OA: OptionalAction<A>,
{
type State = OnEventState<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);
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::<E>().unwrap_throw();
let event: Event<E, V::Element> = 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<dyn Any>,
app_state: &mut T,
) -> MessageResult<A> {
match message.downcast_ref::<EventMsg<Event<E, V::Element>>>() {
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<E, V, F>(name: &'static str, child: V, callback: F) -> OnEvent<E, V, F> {
OnEvent::new(name, child, callback)
}
/// State for the `OnEvent` view.
pub struct OnEventState<S> {
#[allow(unused)]
listener: EventListener,
child_state: S,
}
struct EventMsg<E> {
event: E,
}
/// Wraps a `web_sys::Event` and provides auto downcasting for both the event and its target.
pub struct Event<Evt, El> {
raw: Evt,
el: PhantomData<El>,
}
impl<Evt, El> Event<Evt, El> {
fn new(raw: Evt) -> Self {
Self {
raw,
el: PhantomData,
}
}
}
impl<Evt, El> Event<Evt, El>
where
Evt: AsRef<web_sys::Event>,
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<Evt, El> Deref for Event<Evt, El> {
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<A>`.
pub trait Action {}
/// Trait that allows callbacks to be polymorphic on return type
/// (`Action`, `Option<Action>` or `()`). An implementation detail.
pub trait OptionalAction<A>: sealed::Sealed {
fn action(self) -> Option<A>;
}
mod sealed {
pub trait Sealed {}
}
impl sealed::Sealed for () {}
impl<A> OptionalAction<A> for () {
fn action(self) -> Option<A> {
None
}
}
impl<A: Action> sealed::Sealed for A {}
impl<A: Action> OptionalAction<A> for A {
fn action(self) -> Option<A> {
Some(self)
}
}
impl<A: Action> sealed::Sealed for Option<A> {}
impl<A: Action> OptionalAction<A> for Option<A> {
fn action(self) -> Option<A> {
self
}
}

View File

@ -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<E, T, A, Ev, C> {
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<fn() -> (T, A, Ev)>,
}
impl<E, T, A, Ev, C> OnEvent<E, T, A, Ev, C>
where
Ev: JsCast + 'static,
{
pub fn new(element: E, event: impl Into<Cow<'static, str>>, 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<Cow<'static, str>>,
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<Ev: JsCast + 'static>(
target: &web_sys::EventTarget,
event: impl Into<Cow<'static, str>>,
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::<Ev>().unwrap_throw();
thunk.push_message(event);
},
)
}
/// State for the `OnEvent` view.
pub struct OnEventState<S> {
#[allow(unused)]
listener: gloo::events::EventListener,
child_id: Id,
child_state: S,
}
impl<E, T, A, Ev, C> ViewMarker for OnEvent<E, T, A, Ev, C> {}
impl<E, T, A, Ev, C> Sealed for OnEvent<E, T, A, Ev, C> {}
impl<E, T, A, Ev, C, OA> View<T, A> for OnEvent<E, T, A, Ev, C>
where
OA: OptionalAction<A>,
C: Fn(&mut T, Ev) -> OA,
E: Element<T, A>,
Ev: JsCast + 'static,
{
type State = OnEventState<E::State>;
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::<Ev>(
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::<Ev>(
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<dyn Any>,
app_state: &mut T,
) -> MessageResult<A> {
match id_path {
[] if message.downcast_ref::<Ev>().is_some() => {
let event = message.downcast::<Ev>().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: <Ev, C, OA,>,
vars_on_ty: <Ev, C,>,
bounds: {
Ev: JsCast + 'static,
OA: OptionalAction<A>,
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: <C, OA,>,
vars_on_ty: <C,>,
bounds: {
OA: OptionalAction<A>,
C: Fn(&mut T, web_sys::$web_sys_ty ) -> OA,
}
);
pub struct $ty_name<E, T, A, C> {
target: E,
callback: C,
options: EventListenerOptions,
phantom: PhantomData<fn() -> (T, A)>,
}
impl<E, T, A, C> $ty_name<E, T, A, C> {
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<E, T, A, C> ViewMarker for $ty_name<E, T, A, C> {}
impl<E, T, A, C> Sealed for $ty_name<E, T, A, C> {}
impl<E, T, A, C, OA> View<T, A> for $ty_name<E, T, A, C>
where
OA: OptionalAction<A>,
C: Fn(&mut T, web_sys::$web_sys_ty) -> OA,
E: Element<T, A>,
{
type State = OnEventState<E::State>;
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::<web_sys::$web_sys_ty>(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::<web_sys::$web_sys_ty>(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<dyn Any>,
app_state: &mut T,
) -> MessageResult<A> {
match id_path {
[] if message.downcast_ref::<web_sys::$web_sys_ty>().is_some() => {
let event = message.downcast::<web_sys::$web_sys_ty>().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)
);

View File

@ -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<EH, OA>(self, handler: EH) -> events::$event_ty<Self, T, A, EH>
where
OA: OptionalAction<A>,
EH: Fn(&mut T, web_sys::$web_sys_event_type) -> OA,
{
$crate::events::$event_ty::new(self, handler)
}
)*
};
}
pub trait Element<T, A = ()>: View<T, A> + ViewMarker + sealed::Sealed
where
Self: Sized,
{
fn on<E, EH, OA>(
self,
event: impl Into<Cow<'static, str>>,
handler: EH,
) -> OnEvent<Self, T, A, E, EH>
where
E: JsCast + 'static,
OA: OptionalAction<A>,
EH: Fn(&mut T, E) -> OA,
Self: Sized,
{
OnEvent::new(self, event, handler)
}
fn on_with_options<Ev, EH, OA>(
self,
event: impl Into<Cow<'static, str>>,
handler: EH,
options: EventListenerOptions,
) -> OnEvent<Self, T, A, Ev, EH>
where
Ev: JsCast + 'static,
OA: OptionalAction<A>,
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<Cow<'static, str>>,
value: impl IntoAttributeValue,
) -> Attr<Self, T, A> {
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<Cow<'static, str>>) -> Attr<Self, T, A> {
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<T, A = ()>: $interface<T, A> $child_methods_body
/// Execute $mac which is a macro, that takes $dom_interface:ident (<optional macro parameters>) as match arm for all interfaces that
#[doc = concat!("`", stringify!($child_interface), "`")]
/// inherits from
macro_rules! [<for_all_ $child_interface:snake _ancestors>] {
($mac:path, $extra_params:tt) => {
$mac!($interface, $extra_params);
$crate::interfaces::[<for_all_ $interface:snake _ancestors>]!($mac, $extra_params);
};
}
pub(crate) use [<for_all_ $child_interface:snake _ancestors>];
)*
}
paste::paste! {
/// Execute $mac which is a macro, that takes $dom_interface:ident (<optional macro parameters>) as match arm for all interfaces that inherit from
#[doc = concat!("`", stringify!($interface), "`")]
#[allow(unused_macros)]
macro_rules! [<for_all_ $interface:snake _descendents>] {
($mac:path, $extra_params:tt) => {
$(
$mac!($child_interface, $extra_params);
$crate::interfaces::[<for_all_ $child_interface:snake _ descendents>]!($mac, $extra_params);
)*
};
}
#[allow(unused_imports)]
pub(crate) use [<for_all_ $interface:snake _descendents>];
}
$(
$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<E, T, A, $($additional_generic_var,)*> $crate::interfaces::$dom_interface<T, A> for $ty<E, T, A, $($additional_generic_var_on_ty,)*>
where
E: $crate::interfaces::$bound_interface<T, A>,
$($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<E, T, A>, 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: <vars>) that is added on the impl<E, T, A, <vars>>, and vars_on_ty (Type<E, T, A, <vars_on_ty>>) 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::[<for_all_ $dom_interface:snake _ancestors>]!(
$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::[<for_all_ $dom_interface:snake _descendents>]!(
$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, T, A> {
self.attr("width", value)
}
fn height(self, value: u32) -> Attr<Self, T, A> {
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,T, A> {
self.attr("width", value)
}
fn height(self, value: u32) -> Attr<Self, T, A> {
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<ParentT, ParentA, ChildT, ChildA, V, F> sealed::Sealed
for crate::Adapt<ParentT, ParentA, ChildT, ChildA, V, F>
{
}
impl<ParentT, ChildT, V, F> sealed::Sealed for crate::AdaptState<ParentT, ChildT, V, F> {}
macro_rules! impl_dom_traits_for_adapt_views {
($dom_interface:ident, ()) => {
impl<ParentT, ParentA, ChildT, ChildA, V, F> $dom_interface<ParentT, ParentA>
for crate::Adapt<ParentT, ParentA, ChildT, ChildA, V, F>
where
V: $dom_interface<ChildT, ChildA>,
F: Fn(
&mut ParentT,
crate::AdaptThunk<ChildT, ChildA, V>,
) -> xilem_core::MessageResult<ParentA>,
{
}
impl<ParentT, ChildT, A, V, F> $dom_interface<ParentT, A>
for crate::AdaptState<ParentT, ChildT, V, F>
where
V: $dom_interface<ChildT, A>,
F: Fn(&mut ParentT) -> &mut ChildT,
{
}
};
}
for_all_dom_interfaces!(impl_dom_traits_for_adapt_views, ());

View File

@ -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!();

View File

@ -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<VT, VA, $($vars: $crate::interfaces::$dom_interface<VT, VA>),+> $crate::interfaces::$dom_interface<VT, VA> for $ident<$($vars),+>
where
$($vars: $crate::interfaces::$dom_interface<VT, VA>,)+
{}
};
}
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<web_sys::Node> for $ident<$($vars),+>
where
$($vars: AsRef<web_sys::Node>,)+
$($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<VT, VA, $($vars),+> View<VT, VA> for $ident<$($vars),+>
where $(
$vars: View<VT, VA> + ViewMarker,
$vars::Element: AsRef<web_sys::Node> + 'static,
)+ {
where
$($vars: View<VT, VA>,)+
{
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<VT, VA, $($vars),+> ViewSequence<VT, VA> for $ident<$($vars),+>
where $(
$vars: ViewSequence<VT, VA>,
@ -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 }
}

View File

@ -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<A>`.
pub trait Action {}
/// Trait that allows callbacks to be polymorphic on return type
/// (`Action`, `Option<Action>` or `()`). An implementation detail.
pub trait OptionalAction<A>: sealed::Sealed {
fn action(self) -> Option<A>;
}
mod sealed {
pub trait Sealed {}
}
impl sealed::Sealed for () {}
impl<A> OptionalAction<A> for () {
fn action(self) -> Option<A> {
None
}
}
impl<A: Action> sealed::Sealed for A {}
impl<A: Action> OptionalAction<A> for A {
fn action(self) -> Option<A> {
Some(self)
}
}
impl<A: Action> sealed::Sealed for Option<A> {}
impl<A: Action> OptionalAction<A> for Option<A> {
fn action(self) -> Option<A> {
self
}
}

View File

@ -38,6 +38,30 @@ impl<K, V> VecMap<K, V> {
.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<Q: ?Sized>(&self, key: &Q) -> bool
where
K: Borrow<Q> + 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<K, V> VecMap<K, V> {
/// assert_eq!((*first_key, *first_value), (1, "a"));
/// ```
pub fn iter(&self) -> impl Iterator<Item = (&K, &V)> {
#[allow(clippy::map_identity)]
self.0.iter().map(|(k, v)| (k, v))
}
@ -178,6 +203,10 @@ impl<K, V> VecMap<K, V> {
}
}
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();

View File

@ -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<web_sys::Node>`.
/// 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<N: AsRef<web_sys::Node> + 'static> DomNode for N {
}
}
/// This trait is implemented for types that implement `AsRef<web_sys::Element>`.
/// It is an implementation detail.
pub trait DomElement: DomNode {
fn as_element_ref(&self) -> &web_sys::Element;
}
impl<N: DomNode + AsRef<web_sys::Element>> 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<Node>`. 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;}

View File

@ -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<T, A>: View<T, A> + Sized {
/// Add an `onclick` event listener.
fn on_click<
OA: OptionalAction<A>,
F: Fn(&mut T, &Event<web_sys::MouseEvent, Self::Element>) -> OA,
>(
self,
f: F,
) -> events::OnClick<T, A, Self, F, OA> {
events::on_click(self, f)
}
/// Add an `ondblclick` event listener.
fn on_dblclick<
OA: OptionalAction<A>,
F: Fn(&mut T, &Event<web_sys::MouseEvent, Self::Element>) -> OA,
>(
self,
f: F,
) -> events::OnDblClick<T, A, Self, F, OA> {
events::on_dblclick(self, f)
}
/// Add an `oninput` event listener.
fn on_input<
OA: OptionalAction<A>,
F: Fn(&mut T, &Event<web_sys::InputEvent, Self::Element>) -> OA,
>(
self,
f: F,
) -> events::OnInput<T, A, Self, F, OA> {
events::on_input(self, f)
}
/// Add an `onkeydown` event listener.
fn on_keydown<
OA: OptionalAction<A>,
F: Fn(&mut T, &Event<web_sys::KeyboardEvent, Self::Element>) -> OA,
>(
self,
f: F,
) -> events::OnKeyDown<T, A, Self, F, OA> {
events::on_keydown(self, f)
}
fn on_blur<
OA: OptionalAction<A>,
F: Fn(&mut T, &Event<web_sys::FocusEvent, Self::Element>) -> OA,
>(
self,
f: F,
) -> events::OnBlur<T, A, Self, F, OA> {
events::on_blur(self, f)
}
fn adapt<ParentT, ParentA, F>(self, f: F) -> Adapt<ParentT, ParentA, T, A, Self, F>
where
F: Fn(&mut ParentT, AdaptThunk<T, A, Self>) -> xilem_core::MessageResult<ParentA>,
@ -77,11 +18,6 @@ pub trait ViewExt<T, A>: View<T, A> + Sized {
{
AdaptState::new(f, self)
}
/// Apply a CSS class to the child view.
fn class(self, class: impl Into<Cow<'static, str>>) -> Class<Self> {
crate::class::class(self, class)
}
}
impl<T, A, V: View<T, A>> ViewExt<T, A> for V {}

View File

@ -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<A, F>(
fn btn(
label: &'static str,
click_fn: F,
) -> evt::OnClick<AppState, A, el::Button<&'static str>, F, ()>
where
F: Fn(&mut AppState, &Event<web_sys::MouseEvent, web_sys::HtmlButtonElement>),
{
click_fn: impl Fn(&mut AppState, web_sys::MouseEvent),
) -> impl HtmlButtonElement<AppState> {
el::button(label).on_click(click_fn)
}

View File

@ -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 = "../.." }

View File

@ -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<F>(label: &'static str, click_fn: F) -> impl View<AppState> + ViewMarker
where
F: Fn(&mut AppState, &Event<web_sys::Event, web_sys::HtmlButtonElement>),
{
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<AppState> {
custom_element("button", label).on("click", move |state: &mut AppState, evt| {
click_fn(state, evt);
})
}
fn app_logic(state: &mut AppState) -> impl View<AppState> {
element::<web_sys::HtmlElement, _>(
custom_element(
"div",
(
element::<web_sys::HtmlElement, _>("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)),

View File

@ -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<Todo, TodoAction> + ViewMarker {
fn todo_item(todo: &mut Todo, editing: bool) -> impl Element<Todo, TodoAction> {
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<Todo, TodoAction> + 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<Todo, TodoAction> + 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::<web_sys::HtmlInputElement>().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<AppState> + ViewMarker {
fn footer_view(state: &mut AppState, should_display: bool) -> impl Element<AppState> {
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<AppState
};
let clear_button = (state.todos.iter().filter(|todo| todo.completed).count() > 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<AppState
let filter_class = |filter| (state.filter == filter).then_some("selected");
let mut footer = el::footer((
el::footer((
el::span((
el::strong(state.todos.len().to_string()),
format!(" {} left", item_str),
))
.attr("class", "todo-count"),
el::ul((
el::li(on_click(
el::li(Element::on_click(
el::a("All")
.attr("href", "#/")
.attr("class", filter_class(Filter::All)),
@ -102,7 +110,7 @@ fn footer_view(state: &mut AppState, should_display: bool) -> impl View<AppState
},
)),
" ",
el::li(on_click(
el::li(Element::on_click(
el::a("Active")
.attr("href", "#/active")
.attr("class", filter_class(Filter::Active)),
@ -111,7 +119,7 @@ fn footer_view(state: &mut AppState, should_display: bool) -> impl View<AppState
},
)),
" ",
el::li(on_click(
el::li(Element::on_click(
el::a("Completed")
.attr("href", "#/completed")
.attr("class", filter_class(Filter::Completed)),
@ -123,14 +131,11 @@ fn footer_view(state: &mut AppState, should_display: bool) -> impl View<AppState
.attr("class", "filters"),
clear_button,
))
.attr("class", "footer");
if !should_display {
footer.set_attr("style", "display:none;");
}
footer
.attr("class", "footer")
.attr("style", (!should_display).then_some("display:none;"))
}
fn main_view(state: &mut AppState, should_display: bool) -> impl View<AppState> + ViewMarker {
fn main_view(state: &mut AppState, should_display: bool) -> impl Element<AppState> {
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<AppState>
.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<AppState> {
@ -191,8 +194,14 @@ fn app_logic(state: &mut AppState) -> impl View<AppState> {
}
})
.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::<web_sys::HtmlInputElement>().ok())
{
state.update_new_todo(&element.value());
evt.prevent_default();
}
})
.passive(false),
))