mirror of https://github.com/linebender/xilem
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:
parent
65091c7743
commit
d40a94fa8a
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
]
|
||||
|
|
|
@ -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);
|
|
@ -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),
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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),
|
||||
);
|
|
@ -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();
|
||||
}
|
|
@ -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),
|
||||
);
|
|
@ -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),
|
||||
);
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
);
|
|
@ -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, ());
|
|
@ -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!();
|
||||
|
|
|
@ -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 }
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
|
|
|
@ -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;}
|
||||
|
||||
|
|
|
@ -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 {}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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 = "../.." }
|
|
@ -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)),
|
|
@ -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),
|
||||
))
|
||||
|
|
Loading…
Reference in New Issue