diff --git a/Cargo.lock b/Cargo.lock index 9428387b1..d88740cd8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1258,25 +1258,6 @@ dependencies = [ "xml-rs", ] -[[package]] -name = "gloo" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d15282ece24eaf4bd338d73ef580c6714c8615155c4190c781290ee3fa0fd372" -dependencies = [ - "gloo-events", -] - -[[package]] -name = "gloo-events" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27c26fb45f7c385ba980f5fa87ac677e363949e065a083722697ef1b2cc91e41" -dependencies = [ - "wasm-bindgen", - "web-sys", -] - [[package]] name = "glow" version = "0.13.1" @@ -4021,20 +4002,12 @@ dependencies = [ name = "xilem_web" version = "0.1.0" dependencies = [ - "bitflags 2.5.0", - "gloo", - "log", - "paste", "peniko", "wasm-bindgen", "web-sys", - "xilem_web_core", + "xilem_core", ] -[[package]] -name = "xilem_web_core" -version = "0.1.0" - [[package]] name = "xkbcommon-dl" version = "0.4.2" diff --git a/Cargo.toml b/Cargo.toml index 33a5eb7ff..a213e4bb8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,6 @@ members = [ "masonry", "xilem_web", - "xilem_web/xilem_web_core", "xilem_web/web_examples/counter", "xilem_web/web_examples/counter_custom_element", "xilem_web/web_examples/todomvc", diff --git a/xilem_core/README.md b/xilem_core/README.md index 535d5af87..804bae326 100644 --- a/xilem_core/README.md +++ b/xilem_core/README.md @@ -23,7 +23,7 @@ -Xilem Core provides primitives which are used by [Xilem][] (a cross-platform GUI toolkit). +Xilem Core provides primitives which are used by [Xilem][] (a cross-platform GUI toolkit) and [Xilem Web][] (a web frontend framework). If you are using Xilem, [its documentation][xilem docs] will probably be more helpful for you. Xilem apps will interact with some of the functions from this crate, in particular [`memoize`][]. @@ -73,6 +73,7 @@ Contributions are welcome by pull request. The [Rust code of conduct][] applies. [LICENSE]: LICENSE [Xilem]: https://crates.io/crates/xilem +[Xilem Web]: https://crates.io/crates/xilem_web [xilem docs]: https://docs.rs/xilem/latest/xilem/ [`memoize`]: https://docs.rs/xilem_core/latest/xilem_core/views/memoize/fn.memoize.html [`View`]: https://docs.rs/xilem_core/latest/xilem_core/view/trait.View.html diff --git a/xilem_core/src/message.rs b/xilem_core/src/message.rs index 98e64c30a..6ea793e78 100644 --- a/xilem_core/src/message.rs +++ b/xilem_core/src/message.rs @@ -43,15 +43,11 @@ impl MessageResult { /// A dynamically typed message for the [`View`] trait. /// /// Mostly equivalent to `Box`, but with support for debug printing. -// We can't use intra-doc links here because of +// We can't use intra-doc links here because of rustdoc doesn't understand impls on `dyn Message` /// The primary interface for this type is [`dyn Message::downcast`](trait.Message.html#method.downcast). /// /// These messages must also be [`Send`]. /// This makes using this message type in a multithreaded context easier. -/// If this requirement is causing you issues, feel free to open an issue -/// to discuss. -/// We are aware of potential backwards-compatible workarounds, but -/// are not aware of any tangible need for this. /// /// [`View`]: crate::View pub type DynMessage = Box; diff --git a/xilem_web/Cargo.toml b/xilem_web/Cargo.toml index dccb3292d..cdd1ee0d6 100644 --- a/xilem_web/Cargo.toml +++ b/xilem_web/Cargo.toml @@ -2,7 +2,7 @@ name = "xilem_web" version = "0.1.0" description = "HTML DOM frontend for the Xilem Rust UI framework." -keywords = ["xilem", "html", "dom", "web", "ui"] +keywords = ["xilem", "html", "svg", "dom", "web", "ui"] categories = ["gui", "web-programming"] publish = false # Until it's ready edition.workspace = true @@ -19,155 +19,152 @@ cargo-args = ["-Zunstable-options", "-Zrustdoc-scrape-examples"] workspace = true [dependencies] -xilem_web_core = { workspace = true } +xilem_core = { workspace = true, features = ["kurbo"] } peniko.workspace = true -bitflags.workspace = true wasm-bindgen = "0.2.92" -paste = "1.0.15" -log = "0.4.21" -gloo = { version = "0.11.0", default-features = false, features = ["events"] } [dependencies.web-sys] version = "0.3.69" features = [ - "console", - "CssStyleDeclaration", - "Document", - "DomTokenList", - "Element", - "Event", - "HtmlElement", - "Node", - "NodeList", - "SvgElement", - "SvgaElement", - "SvgAnimateElement", - "SvgAnimateMotionElement", - "SvgAnimateTransformElement", - "SvgCircleElement", - "SvgClipPathElement", - "SvgDefsElement", - "SvgDescElement", - "SvgEllipseElement", - "SvgfeBlendElement", - "SvgfeColorMatrixElement", - "SvgfeComponentTransferElement", - "SvgfeCompositeElement", - "SvgfeConvolveMatrixElement", - "SvgfeDiffuseLightingElement", - "SvgfeDisplacementMapElement", - "SvgfeDistantLightElement", - "SvgfeDropShadowElement", - "SvgfeFloodElement", - "SvgfeFuncAElement", - "SvgfeFuncBElement", - "SvgfeFuncGElement", - "SvgfeFuncRElement", - "SvgfeGaussianBlurElement", - "SvgfeImageElement", - "SvgfeMergeElement", - "SvgfeMergeNodeElement", - "SvgfeMorphologyElement", - "SvgfeOffsetElement", - "SvgfePointLightElement", - "SvgfeSpecularLightingElement", - "SvgfeSpotLightElement", - "SvgfeTileElement", - "SvgfeTurbulenceElement", - "SvgFilterElement", - "SvgForeignObjectElement", - "SvggElement", - # "SvgHatchElement", - # "SvgHatchpathElement", - "SvgImageElement", - "SvgLineElement", - "SvgLinearGradientElement", - "SvgMarkerElement", - "SvgMaskElement", - "SvgMetadataElement", - "SvgmPathElement", - "SvgPathElement", - "SvgPatternElement", - "SvgPolygonElement", - "SvgPolylineElement", - "SvgRadialGradientElement", - "SvgRectElement", - "SvgScriptElement", - "SvgSetElement", - "SvgStopElement", - "SvgStyleElement", - "SvgsvgElement", - "SvgSwitchElement", - "SvgSymbolElement", - "SvgTextElement", - "SvgTextPathElement", - "SvgTitleElement", - "SvgtSpanElement", - "SvgUseElement", - "SvgViewElement", - "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", + "console", + "CssStyleDeclaration", + "Document", + "DomTokenList", + "Element", + "Event", + "AddEventListenerOptions", + "HtmlElement", + "Node", + "NodeList", + "SvgElement", + "SvgaElement", + "SvgAnimateElement", + "SvgAnimateMotionElement", + "SvgAnimateTransformElement", + "SvgCircleElement", + "SvgClipPathElement", + "SvgDefsElement", + "SvgDescElement", + "SvgEllipseElement", + "SvgfeBlendElement", + "SvgfeColorMatrixElement", + "SvgfeComponentTransferElement", + "SvgfeCompositeElement", + "SvgfeConvolveMatrixElement", + "SvgfeDiffuseLightingElement", + "SvgfeDisplacementMapElement", + "SvgfeDistantLightElement", + "SvgfeDropShadowElement", + "SvgfeFloodElement", + "SvgfeFuncAElement", + "SvgfeFuncBElement", + "SvgfeFuncGElement", + "SvgfeFuncRElement", + "SvgfeGaussianBlurElement", + "SvgfeImageElement", + "SvgfeMergeElement", + "SvgfeMergeNodeElement", + "SvgfeMorphologyElement", + "SvgfeOffsetElement", + "SvgfePointLightElement", + "SvgfeSpecularLightingElement", + "SvgfeSpotLightElement", + "SvgfeTileElement", + "SvgfeTurbulenceElement", + "SvgFilterElement", + "SvgForeignObjectElement", + "SvggElement", + # "SvgHatchElement", + # "SvgHatchpathElement", + "SvgImageElement", + "SvgLineElement", + "SvgLinearGradientElement", + "SvgMarkerElement", + "SvgMaskElement", + "SvgMetadataElement", + "SvgmPathElement", + "SvgPathElement", + "SvgPatternElement", + "SvgPolygonElement", + "SvgPolylineElement", + "SvgRadialGradientElement", + "SvgRectElement", + "SvgScriptElement", + "SvgSetElement", + "SvgStopElement", + "SvgStyleElement", + "SvgsvgElement", + "SvgSwitchElement", + "SvgSymbolElement", + "SvgTextElement", + "SvgTextPathElement", + "SvgTitleElement", + "SvgtSpanElement", + "SvgUseElement", + "SvgViewElement", + "Text", + "Window", + "FocusEvent", + "HtmlInputElement", + "InputEvent", + "KeyboardEvent", + "MouseEvent", + "PointerEvent", + "WheelEvent", + "HtmlAnchorElement", + "HtmlAreaElement", + "HtmlAudioElement", + "HtmlBrElement", + "HtmlButtonElement", + "HtmlCanvasElement", + "HtmlDataElement", + "HtmlDataListElement", + "HtmlDetailsElement", + "HtmlDialogElement", + "HtmlDivElement", + "HtmlDListElement", + "HtmlEmbedElement", + "HtmlFieldSetElement", + "HtmlFormElement", + "HtmlHeadingElement", + "HtmlHrElement", + "HtmlIFrameElement", + "HtmlImageElement", + "HtmlInputElement", + "HtmlLabelElement", + "HtmlLegendElement", + "HtmlLiElement", + "HtmlLinkElement", + "HtmlMapElement", + "HtmlMediaElement", + "HtmlMenuElement", + "HtmlMeterElement", + "HtmlModElement", + "HtmlObjectElement", + "HtmlOListElement", + "HtmlOptGroupElement", + "HtmlOptionElement", + "HtmlOutputElement", + "HtmlParagraphElement", + "HtmlPictureElement", + "HtmlPreElement", + "HtmlProgressElement", + "HtmlQuoteElement", + "HtmlScriptElement", + "HtmlSelectElement", + "HtmlSlotElement", + "HtmlSourceElement", + "HtmlSpanElement", + "HtmlTableCaptionElement", + "HtmlTableCellElement", + "HtmlTableColElement", + "HtmlTableElement", + "HtmlTableRowElement", + "HtmlTableSectionElement", + "HtmlTemplateElement", + "HtmlTimeElement", + "HtmlTextAreaElement", + "HtmlTrackElement", + "HtmlUListElement", + "HtmlVideoElement", ] diff --git a/xilem_web/README.md b/xilem_web/README.md index cd5ce9be5..90f01878e 100644 --- a/xilem_web/README.md +++ b/xilem_web/README.md @@ -1,9 +1,77 @@ -# `xilem_web` prototype +
-This is an early prototype of a potential implementation of the Xilem architecture using DOM elements -as Xilem elements (unfortunately the two concepts have the same name). This uses xilem_web_core under the hood, -which is a legacy version of xilem_core. +# Xilem Web -The easiest way to run it is to use [Trunk]. Run `trunk serve`, then navigate the browser to the link provided (usually `http://localhost:8080`). +
+ + + + +
+ +**Experimental implementation of the Xilem architecture for the Web** + +[![Latest published version.](https://img.shields.io/crates/v/xilem_web.svg)](https://crates.io/crates/xilem_web) +[![Documentation build status.](https://img.shields.io/docsrs/xilem_web.svg)](https://docs.rs/xilem_web) +[![Apache 2.0 license.](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](#license) +\ +[![Linebender Zulip chat.](https://img.shields.io/badge/Linebender-%23xilem-blue?logo=Zulip)](https://xi.zulipchat.com/#narrow/stream/354396-xilem) +[![GitHub Actions CI status.](https://img.shields.io/github/actions/workflow/status/linebender/xilem/ci.yml?logo=github&label=CI)](https://github.com/linebender/xilem/actions) +[![Dependency staleness status.](https://deps.rs/crate/xilem_web/latest/status.svg)](https://deps.rs/crate/xilem_web) + +
+ +This is a prototype implementation of the Xilem architecture (through [Xilem Core][]) using DOM elements as Xilem elements (unfortunately the two concepts have the same name). + +## Quickstart + +The easiest way to start, is to use [Trunk] within some of the examples (see the `web_examples/` directory). +Run `trunk serve`, then navigate the browser to the link provided (usually ). + +### Example + +A minimal example to run an application with xilem_web: + +```rust,no_run +use xilem_web::{ + document_body, + elements::html::{button, div, p}, + interfaces::{Element as _, HtmlDivElement}, + App, +}; + +fn app_logic(clicks: &mut u32) -> impl HtmlDivElement { + div(( + button(format!("clicked {clicks} times")).on_click(|clicks: &mut u32, _event| *clicks += 1), + (*clicks >= 5).then_some(p("Huzzah, clicked at least 5 times")), + )) +} + +pub fn main() { + let clicks = 0; + App::new(document_body(), clicks, app_logic).run(); +} +``` + +
+ +## Community + +Discussion of Xilem Core development happens in the [Linebender Zulip](https://xi.zulipchat.com/), specifically in +[#xilem](https://xi.zulipchat.com/#narrow/stream/354396-xilem). +All public content can be read without logging in. + +Contributions are welcome by pull request. The [Rust code of conduct][] applies. + +## License + +- Licensed under the Apache License, Version 2.0 + ([LICENSE] or ) + +
+ +[rust code of conduct]: https://www.rust-lang.org/policies/code-of-conduct [Trunk]: https://trunkrs.dev/ +[LICENSE]: LICENSE +[Xilem Core]: https://crates.io/crates/xilem_core diff --git a/xilem_web/src/app.rs b/xilem_web/src/app.rs index f7c42b1a0..1d6a54318 100644 --- a/xilem_web/src/app.rs +++ b/xilem_web/src/app.rs @@ -1,44 +1,46 @@ // Copyright 2023 the Xilem Authors // SPDX-License-Identifier: Apache-2.0 +use crate::{DomNode, ViewCtx}; use std::{cell::RefCell, rc::Rc}; -use crate::{ - context::Cx, - view::{DomNode, View}, - Message, -}; -use xilem_core::{Id, MessageResult}; +use crate::{DomView, DynMessage, PodMut}; +use xilem_core::{MessageResult, ViewId}; + +pub(crate) struct AppMessage { + pub id_path: Rc<[ViewId]>, + pub body: DynMessage, +} /// The type responsible for running your app. -pub struct App, F: FnMut(&mut T) -> V>(Rc>>); +pub struct App, F: FnMut(&mut T) -> V>(Rc>>); -struct AppInner, F: FnMut(&mut T) -> V> { +struct AppInner, F: FnMut(&mut T) -> V> { data: T, + root: web_sys::Node, app_logic: F, view: Option, - id: Option, - state: Option, + state: Option, element: Option, - cx: Cx, + cx: ViewCtx, } pub(crate) trait AppRunner { - fn handle_message(&self, message: Message); + fn handle_message(&self, message: AppMessage); fn clone_box(&self) -> Box; } -impl + 'static, F: FnMut(&mut T) -> V + 'static> Clone for App { +impl + 'static, F: FnMut(&mut T) -> V + 'static> Clone for App { fn clone(&self) -> Self { App(self.0.clone()) } } -impl + 'static, F: FnMut(&mut T) -> V + 'static> App { +impl + 'static, F: FnMut(&mut T) -> V + 'static> App { /// Create an instance of your app with the given logic and initial state. - pub fn new(data: T, app_logic: F) -> Self { - let inner = AppInner::new(data, app_logic); + pub fn new(root: impl AsRef, data: T, app_logic: F) -> Self { + let inner = AppInner::new(root.as_ref().clone(), data, app_logic); let app = App(Rc::new(RefCell::new(inner))); app.0.borrow_mut().cx.set_runner(app.clone()); app @@ -48,54 +50,59 @@ impl + 'static, F: FnMut(&mut T) -> V + 'static> App, F: FnMut(&mut T) -> V> AppInner { - pub fn new(data: T, app_logic: F) -> Self { - let cx = Cx::new(); +impl, F: FnMut(&mut T) -> V> AppInner { + pub fn new(root: web_sys::Node, data: T, app_logic: F) -> Self { + let cx = ViewCtx::default(); AppInner { data, + root, app_logic, view: None, - id: None, state: None, element: None, cx, } } - fn ensure_app(&mut self, root: &web_sys::HtmlElement) { + fn ensure_app(&mut self) { if self.view.is_none() { let view = (self.app_logic)(&mut self.data); - let (id, state, element) = view.build(&mut self.cx); + let (mut element, state) = view.build(&mut self.cx); + element.node.apply_props(&mut element.props); self.view = Some(view); - self.id = Some(id); self.state = Some(state); - root.append_child(element.as_node_ref()).unwrap(); + // TODO should the element provide a separate method to access reference instead? + let node: &web_sys::Node = element.node.as_ref(); + self.root.append_child(node).unwrap(); self.element = Some(element); } } } -impl + 'static, F: FnMut(&mut T) -> V + 'static> AppRunner for App { +impl + 'static, F: FnMut(&mut T) -> V + 'static> AppRunner + for App +{ // For now we handle the message synchronously, but it would also // make sense to to batch them (for example with requestAnimFrame). - fn handle_message(&self, message: Message) { + fn handle_message(&self, message: AppMessage) { let mut inner_guard = self.0.borrow_mut(); let inner = &mut *inner_guard; if let Some(view) = &mut inner.view { let message_result = view.message( - &message.id_path[1..], inner.state.as_mut().unwrap(), + &message.id_path, message.body, &mut inner.data, ); + match message_result { MessageResult::Nop | MessageResult::Action(_) => { // Nothing to do. @@ -109,15 +116,9 @@ impl + 'static, F: FnMut(&mut T) -> V + 'static> AppRunne } let new_view = (inner.app_logic)(&mut inner.data); - let _changed = new_view.rebuild( - &mut inner.cx, - view, - inner.id.as_mut().unwrap(), - inner.state.as_mut().unwrap(), - inner.element.as_mut().unwrap(), - ); - // Not sure we have to do anything on changed, the rebuild - // traversal should cause the DOM to update. + let el = inner.element.as_mut().unwrap(); + let pod_mut = PodMut::new(&mut el.node, &mut el.props, &inner.root, false); + new_view.rebuild(view, inner.state.as_mut().unwrap(), &mut inner.cx, pod_mut); *view = new_view; } } diff --git a/xilem_web/src/attribute.rs b/xilem_web/src/attribute.rs index 122311511..c131c3a06 100644 --- a/xilem_web/src/attribute.rs +++ b/xilem_web/src/attribute.rs @@ -1,55 +1,302 @@ -// Copyright 2023 the Xilem Authors +// Copyright 2024 the Xilem Authors // SPDX-License-Identifier: Apache-2.0 -use std::borrow::Cow; use std::marker::PhantomData; +use wasm_bindgen::{JsCast, UnwrapThrowExt}; +use xilem_core::{MessageResult, Mut, View, ViewElement, ViewId}; -use xilem_core::{Id, MessageResult}; +use crate::{ + vecmap::VecMap, AttributeValue, DomNode, DynMessage, ElementProps, Pod, PodMut, ViewCtx, +}; -use crate::{interfaces::sealed::Sealed, AttributeValue, ChangeFlags, Cx, View, ViewMarker}; +type CowStr = std::borrow::Cow<'static, str>; -use super::interfaces::Element; +/// This trait enables having attributes DOM [`Element`](`crate::interfaces::Element`)s. It is used within [`View`]s that modify the attributes of an element. +/// +/// Modifications have to be done on the up-traversal of [`View::rebuild`], i.e. after [`View::rebuild`] was invoked for descendent views. +/// See the [`View`] implementation of [`Attr`] for more details how to use it for [`ViewElement`]s that implement this trait. +/// When these methods are used, they have to be used in every reconciliation pass (i.e. [`View::rebuild`]). +pub trait WithAttributes { + /// Needs to be invoked within a [`View::build`] or [`View::rebuild`] before traversing to descendent views, and before any modifications are done + fn start_attribute_modifier(&mut self); + /// Needs to be invoked after any modifications are done + fn end_attribute_modifier(&mut self); + + /// Sets or removes (when value is `None`) an attribute from the underlying element + fn set_attribute(&mut self, name: CowStr, value: Option); + + // TODO first find a use-case for this... + // fn get_attr(&self, name: &str) -> Option<&AttributeValue>; +} + +#[derive(Debug, PartialEq)] +enum AttributeModifier { + Remove(CowStr), + Set(CowStr, AttributeValue), + EndMarker(usize), +} + +/// This contains all the current attributes of an [`Element`](`crate::interfaces::Element`) +#[derive(Debug, Default)] +pub struct Attributes { + attribute_modifiers: Vec, + updated_attributes: VecMap, + idx: usize, // To save some memory, this could be u16 or even u8 (but this is risky) + start_idx: usize, // same here + /// a flag necessary, such that `start_attribute_modifier` doesn't always overwrite the last changes in `View::build` + build_finished: bool, +} + +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. + // TODO not sure, whether this is always a good idea, in case custom or other interfaces such as HtmlOptionElement elements are used that have "value" as an attribute name. + // We likely want to use the DOM attributes instead. + if name == "value" { + if let Some(input_element) = element.dyn_ref::() { + input_element.set_value(value); + } else { + element.set_attribute("value", value).unwrap_throw(); + } + } else if name == "checked" { + if let Some(input_element) = element.dyn_ref::() { + input_element.set_checked(true); + } else { + element.set_attribute("checked", value).unwrap_throw(); + } + } 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" { + if let Some(input_element) = element.dyn_ref::() { + input_element.set_checked(false); + } else { + element.remove_attribute("checked").unwrap_throw(); + } + } else { + element.remove_attribute(name).unwrap_throw(); + } +} + +impl Attributes { + /// applies potential changes of the attributes of an element to the underlying DOM node + pub fn apply_attribute_changes(&mut self, element: &web_sys::Element) { + if !self.updated_attributes.is_empty() { + for modifier in self.attribute_modifiers.iter().rev() { + match modifier { + AttributeModifier::Remove(name) => { + if self.updated_attributes.contains_key(name) { + self.updated_attributes.remove(name); + remove_attribute(element, name); + // element.remove_attribute(name); + } + } + AttributeModifier::Set(name, value) => { + if self.updated_attributes.contains_key(name) { + self.updated_attributes.remove(name); + set_attribute(element, name, &value.serialize()); + // element.set_attribute(name, &value.serialize()); + } + } + AttributeModifier::EndMarker(_) => (), + } + } + debug_assert!(self.updated_attributes.is_empty()); + } + self.build_finished = true; + } +} + +impl WithAttributes for Attributes { + fn set_attribute(&mut self, name: CowStr, value: Option) { + let new_modifier = if let Some(value) = value { + AttributeModifier::Set(name.clone(), value) + } else { + AttributeModifier::Remove(name.clone()) + }; + + if let Some(modifier) = self.attribute_modifiers.get_mut(self.idx) { + if modifier != &new_modifier { + if let AttributeModifier::Remove(previous_name) + | AttributeModifier::Set(previous_name, _) = modifier + { + if &name != previous_name { + self.updated_attributes.insert(previous_name.clone(), ()); + } + } + self.updated_attributes.insert(name, ()); + *modifier = new_modifier; + } + // else remove it out of updated_attributes? (because previous attributes are overwritten) not sure if worth it because potentially worse perf + } else { + self.updated_attributes.insert(name, ()); + self.attribute_modifiers.push(new_modifier); + } + self.idx += 1; + } + + fn start_attribute_modifier(&mut self) { + if self.build_finished { + if self.idx == 0 { + self.start_idx = 0; + } else { + let AttributeModifier::EndMarker(start_idx) = + self.attribute_modifiers[self.idx - 1] + else { + unreachable!("this should not happen, as either `start_attribute_modifier` happens first, or follows an end_attribute_modifier") + }; + self.idx = start_idx; + self.start_idx = start_idx; + } + } + } + + fn end_attribute_modifier(&mut self) { + match self.attribute_modifiers.get_mut(self.idx) { + Some(AttributeModifier::EndMarker(prev_start_idx)) + if *prev_start_idx == self.start_idx => {} // class modifier hasn't changed + Some(modifier) => { + *modifier = AttributeModifier::EndMarker(self.start_idx); + } + None => { + self.attribute_modifiers + .push(AttributeModifier::EndMarker(self.start_idx)); + } + } + self.idx += 1; + self.start_idx = self.idx; + } +} + +impl WithAttributes for ElementProps { + fn start_attribute_modifier(&mut self) { + self.attributes().start_attribute_modifier(); + } + + fn end_attribute_modifier(&mut self) { + self.attributes().end_attribute_modifier(); + } + + fn set_attribute(&mut self, name: CowStr, value: Option) { + self.attributes().set_attribute(name, value); + } +} + +impl, P: WithAttributes> WithAttributes for Pod { + fn start_attribute_modifier(&mut self) { + self.props.start_attribute_modifier(); + } + + fn end_attribute_modifier(&mut self) { + self.props.end_attribute_modifier(); + } + + fn set_attribute(&mut self, name: CowStr, value: Option) { + self.props.set_attribute(name, value); + } +} + +impl, P: WithAttributes> WithAttributes for PodMut<'_, E, P> { + fn start_attribute_modifier(&mut self) { + self.props.start_attribute_modifier(); + } + + fn end_attribute_modifier(&mut self) { + self.props.end_attribute_modifier(); + } + + fn set_attribute(&mut self, name: CowStr, value: Option) { + self.props.set_attribute(name, value); + } +} + +/// Syntax sugar for adding a type bound on the `ViewElement` of a view, such that both, [`ViewElement`] and [`ViewElement::Mut`] are bound to [`WithAttributes`] +pub trait ElementWithAttributes: + for<'a> ViewElement: WithAttributes> + WithAttributes +{ +} + +impl ElementWithAttributes for T +where + T: ViewElement + WithAttributes, + for<'a> T::Mut<'a>: WithAttributes, +{ +} + +/// A view to add or remove an attribute to/from an element, see [`Element::attr`](`crate::interfaces::Element::attr`) for how it's usually used. +#[derive(Clone, Debug)] pub struct Attr { - pub(crate) element: E, - pub(crate) name: Cow<'static, str>, - pub(crate) value: Option, - pub(crate) phantom: PhantomData (T, A)>, + el: E, + name: CowStr, + value: Option, + phantom: PhantomData (T, A)>, } -impl ViewMarker for Attr {} -impl Sealed for Attr {} +impl Attr { + pub fn new(el: E, name: CowStr, value: Option) -> Self { + Attr { + el, + name, + value, + phantom: PhantomData, + } + } +} -impl, T, A> View for Attr { - type State = E::State; +impl View for Attr +where + T: 'static, + A: 'static, + E: View, +{ type Element = E::Element; - fn build(&self, cx: &mut Cx) -> (Id, Self::State, Self::Element) { - cx.add_attr_to_element(&self.name, &self.value); - self.element.build(cx) + type ViewState = E::ViewState; + + fn build(&self, ctx: &mut ViewCtx) -> (Self::Element, Self::ViewState) { + let (mut element, state) = self.el.build(ctx); + element.start_attribute_modifier(); + element.set_attribute(self.name.clone(), self.value.clone()); + element.end_attribute_modifier(); + (element, state) } - fn rebuild( + fn rebuild<'e>( &self, - cx: &mut Cx, prev: &Self, - id: &mut Id, - state: &mut Self::State, - element: &mut Self::Element, - ) -> ChangeFlags { - cx.add_attr_to_element(&self.name, &self.value); - self.element.rebuild(cx, &prev.element, id, state, element) + view_state: &mut Self::ViewState, + ctx: &mut ViewCtx, + mut element: Mut<'e, Self::Element>, + ) -> Mut<'e, Self::Element> { + element.start_attribute_modifier(); + let mut element = self.el.rebuild(&prev.el, view_state, ctx, element); + element.set_attribute(self.name.clone(), self.value.clone()); + element.end_attribute_modifier(); + element + } + + fn teardown( + &self, + view_state: &mut Self::ViewState, + ctx: &mut ViewCtx, + element: Mut<'_, Self::Element>, + ) { + self.el.teardown(view_state, ctx, element); } fn message( &self, - id_path: &[Id], - state: &mut Self::State, - message: Box, + view_state: &mut Self::ViewState, + id_path: &[ViewId], + message: DynMessage, app_state: &mut T, - ) -> MessageResult { - self.element.message(id_path, state, message, app_state) + ) -> MessageResult { + self.el.message(view_state, id_path, message, app_state) } } - -crate::interfaces::impl_dom_interfaces_for_ty!(Element, Attr); diff --git a/xilem_web/src/attribute_value.rs b/xilem_web/src/attribute_value.rs index 2d72b3d76..bb6495310 100644 --- a/xilem_web/src/attribute_value.rs +++ b/xilem_web/src/attribute_value.rs @@ -3,6 +3,9 @@ type CowStr = std::borrow::Cow<'static, str>; +/// Representation of an attribute value. +/// +/// This type is used as optimization, to avoid allocations, as it's copied around a lot #[derive(PartialEq, Clone, Debug, PartialOrd)] pub enum AttributeValue { True, // for the boolean true, this serializes to an empty string (e.g. for ) @@ -26,6 +29,7 @@ impl AttributeValue { } } +/// Types implementing this trait can be used as value in e.g. [`Element::attr`](`crate::interfaces::Element::attr`) pub trait IntoAttributeValue: Sized { fn into_attr_value(self) -> Option; } diff --git a/xilem_web/src/class.rs b/xilem_web/src/class.rs index 02547a0e9..47700f8d3 100644 --- a/xilem_web/src/class.rs +++ b/xilem_web/src/class.rs @@ -1,135 +1,349 @@ // Copyright 2024 the Xilem Authors // SPDX-License-Identifier: Apache-2.0 -use std::{borrow::Cow, marker::PhantomData}; +use std::marker::PhantomData; +use wasm_bindgen::{JsCast, UnwrapThrowExt}; -use xilem_core::{Id, MessageResult}; +use xilem_core::{MessageResult, Mut, View, ViewElement, ViewId}; -use crate::{ - interfaces::{sealed::Sealed, Element}, - ChangeFlags, Cx, View, ViewMarker, -}; +use crate::{vecmap::VecMap, DomNode, DynMessage, ElementProps, Pod, PodMut, ViewCtx}; -/// A trait to make the class adding functions generic over collection type -pub trait IntoClasses { - fn into_classes(self, classes: &mut Vec>); +type CowStr = std::borrow::Cow<'static, str>; + +/// Types implementing this trait can be used in the [`Class`] view, see also [`Element::class`](`crate::interfaces::Element::class`) +pub trait AsClassIter { + fn class_iter(&self) -> impl Iterator; } -impl IntoClasses for String { - fn into_classes(self, classes: &mut Vec>) { - classes.push(self.into()); +impl AsClassIter for Option { + fn class_iter(&self) -> impl Iterator { + self.iter().flat_map(|c| c.class_iter()) } } -impl IntoClasses for &'static str { - fn into_classes(self, classes: &mut Vec>) { - classes.push(self.into()); +impl AsClassIter for String { + fn class_iter(&self) -> impl Iterator { + std::iter::once(self.clone().into()) } } -impl IntoClasses for Cow<'static, str> { - fn into_classes(self, classes: &mut Vec>) { - classes.push(self); +impl AsClassIter for &'static str { + fn class_iter(&self) -> impl Iterator { + std::iter::once(CowStr::from(*self)) } } -impl IntoClasses for Option -where - T: IntoClasses, -{ - fn into_classes(self, classes: &mut Vec>) { - if let Some(t) = self { - t.into_classes(classes); - } +impl AsClassIter for CowStr { + fn class_iter(&self) -> impl Iterator { + std::iter::once(self.clone()) } } -impl IntoClasses for Vec +impl AsClassIter for Vec where - T: IntoClasses, + T: AsClassIter, { - fn into_classes(self, classes: &mut Vec>) { - for itm in self { - itm.into_classes(classes); - } + fn class_iter(&self) -> impl Iterator { + self.iter().flat_map(|c| c.class_iter()) } } -impl IntoClasses for [T; N] { - fn into_classes(self, classes: &mut Vec>) { - for itm in self { - itm.into_classes(classes); +impl AsClassIter for [T; N] { + fn class_iter(&self) -> impl Iterator { + self.iter().flat_map(|c| c.class_iter()) + } +} + +/// This trait enables having classes (via `className`) on DOM [`Element`](`crate::interfaces::Element`)s. It is used within [`View`]s that modify the classes of an element. +/// +/// Modifications have to be done on the up-traversal of [`View::rebuild`], i.e. after [`View::rebuild`] was invoked for descendent views. +/// See the [`View`] implementation of [`Class`] for more details how to use it for [`ViewElement`]s that implement this trait. +/// When these methods are used, they have to be used in every reconciliation pass (i.e. [`View::rebuild`]). +pub trait WithClasses { + /// Needs to be invoked within a [`View::build`] or [`View::rebuild`] before traversing to descendent views, and before any modifications are done + fn start_class_modifier(&mut self); + + /// Needs to be invoked after any modifications are done + fn end_class_modifier(&mut self); + + /// Adds a class to the element + /// + /// It needs to be invoked on the up-traversal, i.e. after [`View::rebuild`] was invoked for descendent views. + fn add_class(&mut self, class_name: CowStr); + + /// Removes a possibly previously added class from the element + /// + /// It needs to be invoked on the up-traversal, i.e. after [`View::rebuild`] was invoked for descendent views. + fn remove_class(&mut self, class_name: CowStr); + + // TODO something like the following, but I'm not yet sure how to support that efficiently (and without much binary bloat) + // The modifiers possibly have to be applied then... + // fn classes(&self) -> impl Iterator; + // maybe also something like: + // fn has_class(&self, class_name: &str) -> bool + // Need to find a use-case for this first though (i.e. a modifier needs to read previously added classes) +} + +#[derive(Debug)] +enum ClassModifier { + Remove(CowStr), + Add(CowStr), + EndMarker(usize), +} + +/// This contains all the current classes of an [`Element`](`crate::interfaces::Element`) +#[derive(Debug, Default)] +pub struct Classes { + // TODO maybe this attribute is redundant and can be formed just from the class_modifiers attribute + classes: VecMap, + class_modifiers: Vec, + class_name: String, + idx: usize, + start_idx: usize, + dirty: bool, + /// a flag necessary, such that `start_class_modifier` doesn't always overwrite the last changes in `View::build` + build_finished: bool, +} + +impl Classes { + pub fn apply_class_changes(&mut self, element: &web_sys::Element) { + if self.dirty { + self.dirty = false; + self.classes.clear(); + for modifier in &self.class_modifiers { + match modifier { + ClassModifier::Remove(class_name) => { + self.classes.remove(class_name); + } + ClassModifier::Add(class_name) => { + self.classes.insert(class_name.clone(), ()); + } + ClassModifier::EndMarker(_) => (), + } + } + // intersperse would be the right way to do this, but avoid extra dependencies just for this (and otherwise it's unstable in std)... + self.class_name.clear(); + let last_idx = self.classes.len().saturating_sub(1); + for (idx, class) in self.classes.keys().enumerate() { + self.class_name += class; + if idx != last_idx { + self.class_name += " "; + } + } + // Svg elements do have issues with className, see https://developer.mozilla.org/en-US/docs/Web/API/Element/className + if element.dyn_ref::().is_some() { + element + .set_attribute("class", &self.class_name) + .unwrap_throw(); + } else { + element.set_class_name(&self.class_name); + } } + self.build_finished = true; } } -macro_rules! impl_tuple_intoclasses { - ($($name:ident : $type:ident),* $(,)?) => { - impl<$($type),*> IntoClasses for ($($type,)*) - where - $($type: IntoClasses),* - { - #[allow(unused_variables)] - fn into_classes(self, classes: &mut Vec>) { - let ($($name,)*) = self; - $( - $name.into_classes(classes); - )* +impl WithClasses for Classes { + fn start_class_modifier(&mut self) { + if self.build_finished { + if self.idx == 0 { + self.start_idx = 0; + } else { + let ClassModifier::EndMarker(start_idx) = self.class_modifiers[self.idx - 1] else { + unreachable!("this should not happen, as either `start_class_modifier` is happens first, or follows an end_class_modifier") + }; + self.idx = start_idx; + self.start_idx = start_idx; + } + } + } + + fn end_class_modifier(&mut self) { + match self.class_modifiers.get_mut(self.idx) { + Some(ClassModifier::EndMarker(_)) if !self.dirty => (), // class modifier hasn't changed + Some(modifier) => { + self.dirty = true; + *modifier = ClassModifier::EndMarker(self.start_idx); + } + None => { + self.dirty = true; + self.class_modifiers + .push(ClassModifier::EndMarker(self.start_idx)); } } - }; + self.idx += 1; + self.start_idx = self.idx; + } + + fn add_class(&mut self, class_name: CowStr) { + match self.class_modifiers.get_mut(self.idx) { + Some(ClassModifier::Add(class)) if class == &class_name => (), // class modifier hasn't changed + Some(modifier) => { + self.dirty = true; + *modifier = ClassModifier::Add(class_name); + } + None => { + self.dirty = true; + self.class_modifiers.push(ClassModifier::Add(class_name)); + } + } + self.idx += 1; + } + + fn remove_class(&mut self, class_name: CowStr) { + // Same code as add_class but with remove... + match self.class_modifiers.get_mut(self.idx) { + Some(ClassModifier::Remove(class)) if class == &class_name => (), // class modifier hasn't changed + Some(modifier) => { + self.dirty = true; + *modifier = ClassModifier::Remove(class_name); + } + None => { + self.dirty = true; + self.class_modifiers.push(ClassModifier::Remove(class_name)); + } + } + self.idx += 1; + } +} + +impl WithClasses for ElementProps { + fn start_class_modifier(&mut self) { + self.classes().start_class_modifier(); + } + + fn end_class_modifier(&mut self) { + self.classes().end_class_modifier(); + } + + fn add_class(&mut self, class_name: CowStr) { + self.classes().add_class(class_name); + } + + fn remove_class(&mut self, class_name: CowStr) { + self.classes().remove_class(class_name); + } } -impl_tuple_intoclasses!(); -impl_tuple_intoclasses!(t1: T1); -impl_tuple_intoclasses!(t1: T1, t2: T2); -impl_tuple_intoclasses!(t1: T1, t2: T2, t3: T3); -impl_tuple_intoclasses!(t1: T1, t2: T2, t3: T3, t4: T4); +impl, P: WithClasses> WithClasses for Pod { + fn start_class_modifier(&mut self) { + self.props.start_class_modifier(); + } + + fn end_class_modifier(&mut self) { + self.props.end_class_modifier(); + } + + fn add_class(&mut self, class_name: CowStr) { + self.props.add_class(class_name); + } -/// Applies a class to the underlying element. -pub struct Class { - pub(crate) element: E, - pub(crate) class_names: Vec>, - pub(crate) phantom: PhantomData (T, A)>, + fn remove_class(&mut self, class_name: CowStr) { + self.props.remove_class(class_name); + } } -impl ViewMarker for Class {} -impl Sealed for Class {} +impl, P: WithClasses> WithClasses for PodMut<'_, E, P> { + fn start_class_modifier(&mut self) { + self.props.start_class_modifier(); + } + + fn end_class_modifier(&mut self) { + self.props.end_class_modifier(); + } + + fn add_class(&mut self, class_name: CowStr) { + self.props.add_class(class_name); + } -impl, T, A> View for Class { - type State = E::State; + fn remove_class(&mut self, class_name: CowStr) { + self.props.remove_class(class_name); + } +} + +/// Syntax sugar for adding a type bound on the `ViewElement` of a view, such that both, [`ViewElement`] and [`ViewElement::Mut`] are bound to [`WithClasses`] +pub trait ElementWithClasses: for<'a> ViewElement: WithClasses> + WithClasses {} + +impl ElementWithClasses for T +where + T: ViewElement + WithClasses, + for<'a> T::Mut<'a>: WithClasses, +{ +} + +/// A view to add classes to elements +#[derive(Clone, Debug)] +pub struct Class { + el: E, + classes: C, + phantom: PhantomData (T, A)>, +} + +impl Class { + pub fn new(el: E, classes: C) -> Self { + Class { + el, + classes, + phantom: PhantomData, + } + } +} + +impl View for Class +where + T: 'static, + A: 'static, + C: AsClassIter + 'static, + E: View, +{ type Element = E::Element; - fn build(&self, cx: &mut Cx) -> (Id, Self::State, Self::Element) { - for class_name in &self.class_names { - cx.add_class_to_element(class_name); + type ViewState = E::ViewState; + + fn build(&self, ctx: &mut ViewCtx) -> (Self::Element, Self::ViewState) { + let (mut e, s) = self.el.build(ctx); + e.start_class_modifier(); + for class in self.classes.class_iter() { + e.add_class(class); } - self.element.build(cx) + e.end_class_modifier(); + (e, s) } - fn rebuild( + fn rebuild<'e>( &self, - cx: &mut Cx, prev: &Self, - id: &mut Id, - state: &mut Self::State, - element: &mut Self::Element, - ) -> ChangeFlags { - for class_name in &self.class_names { - cx.add_class_to_element(class_name); + view_state: &mut Self::ViewState, + ctx: &mut ViewCtx, + mut element: Mut<'e, Self::Element>, + ) -> Mut<'e, Self::Element> { + // This has to happen, before any children are rebuilt, otherwise this state machine breaks... + // The actual modifiers also have to happen after the children are rebuilt, see `add_class` below. + element.start_class_modifier(); + let mut element = self.el.rebuild(&prev.el, view_state, ctx, element); + for class in self.classes.class_iter() { + element.add_class(class); } - self.element.rebuild(cx, &prev.element, id, state, element) + element.end_class_modifier(); + element + } + + fn teardown( + &self, + view_state: &mut Self::ViewState, + ctx: &mut ViewCtx, + element: Mut<'_, Self::Element>, + ) { + self.el.teardown(view_state, ctx, element); } fn message( &self, - id_path: &[Id], - state: &mut Self::State, - message: Box, + view_state: &mut Self::ViewState, + id_path: &[ViewId], + message: DynMessage, app_state: &mut T, - ) -> MessageResult { - self.element.message(id_path, state, message, app_state) + ) -> MessageResult { + self.el.message(view_state, id_path, message, app_state) } } - -crate::interfaces::impl_dom_interfaces_for_ty!(Element, Class); diff --git a/xilem_web/src/context.rs b/xilem_web/src/context.rs index c1fe6d4f9..597827bc9 100644 --- a/xilem_web/src/context.rs +++ b/xilem_web/src/context.rs @@ -1,375 +1,40 @@ -// Copyright 2023 the Xilem Authors +// Copyright 2024 the Xilem Authors // SPDX-License-Identifier: Apache-2.0 -use std::any::Any; - -use bitflags::bitflags; -use wasm_bindgen::{JsCast, UnwrapThrowExt}; -use web_sys::Document; - -use xilem_core::{Id, IdPath}; +use std::rc::Rc; use crate::{ - app::AppRunner, - diff::{diff_kv_iterables, Diff}, - vecmap::VecMap, - view::DomNode, - AttributeValue, Message, Pod, + app::{AppMessage, AppRunner}, + core::{ViewId, ViewPathTracker}, + Message, }; -type CowStr = std::borrow::Cow<'static, str>; - -#[derive(Debug, Default)] -pub struct HtmlProps { - pub(crate) attributes: VecMap, - pub(crate) classes: VecMap, - pub(crate) styles: VecMap, -} - -impl HtmlProps { - fn apply(&mut self, el: &web_sys::Element) -> Self { - let attributes = self.apply_attributes(el); - let classes = self.apply_classes(el); - let styles = self.apply_styles(el); - Self { - attributes, - classes, - styles, - } - } - - fn apply_attributes(&mut self, element: &web_sys::Element) -> VecMap { - let mut attributes = VecMap::default(); - std::mem::swap(&mut attributes, &mut self.attributes); - for (name, value) in attributes.iter() { - set_attribute(element, name, &value.serialize()); - } - attributes - } - - fn apply_classes(&mut self, element: &web_sys::Element) -> VecMap { - let mut classes = VecMap::default(); - std::mem::swap(&mut classes, &mut self.classes); - for (class_name, ()) in classes.iter() { - set_class(element, class_name); - } - classes - } - - fn apply_styles(&mut self, element: &web_sys::Element) -> VecMap { - let mut styles = VecMap::default(); - std::mem::swap(&mut styles, &mut self.styles); - for (name, value) in styles.iter() { - set_style(element, name, value); - } - styles - } - - fn apply_changes(&mut self, element: &web_sys::Element, props: &mut HtmlProps) -> ChangeFlags { - self.apply_attribute_changes(element, &mut props.attributes) - | self.apply_class_changes(element, &mut props.classes) - | self.apply_style_changes(element, &mut props.styles) - } - - pub(crate) fn apply_attribute_changes( - &mut self, - element: &web_sys::Element, - attributes: &mut VecMap, - ) -> ChangeFlags { - let mut changed = ChangeFlags::empty(); - // update attributes - for itm in diff_kv_iterables(&*attributes, &self.attributes) { - match itm { - Diff::Add(name, value) | Diff::Change(name, value) => { - set_attribute(element, name, &value.serialize()); - changed |= ChangeFlags::OTHER_CHANGE; - } - Diff::Remove(name) => { - remove_attribute(element, name); - changed |= ChangeFlags::OTHER_CHANGE; - } - } - } - std::mem::swap(attributes, &mut self.attributes); - self.attributes.clear(); - changed - } - - pub(crate) fn apply_class_changes( - &mut self, - element: &web_sys::Element, - classes: &mut VecMap, - ) -> ChangeFlags { - let mut changed = ChangeFlags::empty(); - // update attributes - for itm in diff_kv_iterables(&*classes, &self.classes) { - match itm { - Diff::Add(class_name, ()) | Diff::Change(class_name, ()) => { - set_class(element, class_name); - changed |= ChangeFlags::OTHER_CHANGE; - } - Diff::Remove(class_name) => { - remove_class(element, class_name); - changed |= ChangeFlags::OTHER_CHANGE; - } - } - } - std::mem::swap(classes, &mut self.classes); - self.classes.clear(); - changed - } - - pub(crate) fn apply_style_changes( - &mut self, - element: &web_sys::Element, - styles: &mut VecMap, - ) -> ChangeFlags { - let mut changed = ChangeFlags::empty(); - // update attributes - for itm in diff_kv_iterables(&*styles, &self.styles) { - match itm { - Diff::Add(name, value) | Diff::Change(name, value) => { - set_style(element, name, value); - changed |= ChangeFlags::OTHER_CHANGE; - } - Diff::Remove(name) => { - remove_style(element, name); - changed |= ChangeFlags::OTHER_CHANGE; - } - } - } - std::mem::swap(styles, &mut self.styles); - self.styles.clear(); - changed - } -} - -fn set_attribute(element: &web_sys::Element, name: &str, value: &str) { - // we have to special-case `value` because setting the value using `set_attribute` - // doesn't work after the value has been changed. - 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(); - } -} - -fn set_class(element: &web_sys::Element, class_name: &str) { - debug_assert!( - !class_name.is_empty(), - "class names cannot be the empty string" - ); - debug_assert!( - !class_name.contains(' '), - "class names cannot contain the ascii space character" - ); - element.class_list().add_1(class_name).unwrap_throw(); -} - -fn remove_class(element: &web_sys::Element, class_name: &str) { - debug_assert!( - !class_name.is_empty(), - "class names cannot be the empty string" - ); - debug_assert!( - !class_name.contains(' '), - "class names cannot contain the ascii space character" - ); - element.class_list().remove_1(class_name).unwrap_throw(); -} - -fn set_style(element: &web_sys::Element, name: &str, value: &str) { - if let Some(el) = element.dyn_ref::() { - el.style().set_property(name, value).unwrap_throw(); - } else if let Some(el) = element.dyn_ref::() { - el.style().set_property(name, value).unwrap_throw(); - } -} - -fn remove_style(element: &web_sys::Element, name: &str) { - if let Some(el) = element.dyn_ref::() { - el.style().remove_property(name).unwrap_throw(); - } else if let Some(el) = element.dyn_ref::() { - el.style().remove_property(name).unwrap_throw(); - } -} - -// Note: xilem has derive Clone here. Not sure. -pub struct Cx { - id_path: IdPath, - document: Document, - // TODO There's likely a cleaner more robust way to propagate the attributes to an element - pub(crate) current_element_props: HtmlProps, - app_ref: Option>, -} - pub struct MessageThunk { - id_path: IdPath, + id_path: Rc<[ViewId]>, app_ref: Box, } -bitflags! { - #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)] - pub struct ChangeFlags: u32 { - const STRUCTURE = 1; - const OTHER_CHANGE = 2; +impl MessageThunk { + pub fn push_message(&self, message_body: impl Message) { + let message = AppMessage { + id_path: Rc::clone(&self.id_path), + body: Box::new(message_body), + }; + self.app_ref.handle_message(message); } } -impl Cx { - pub fn new() -> Self { - Cx { - id_path: Vec::new(), - document: crate::document(), - app_ref: None, - current_element_props: Default::default(), - } - } - - pub fn push(&mut self, id: Id) { - self.id_path.push(id); - } - - pub fn pop(&mut self) { - self.id_path.pop(); - } - - #[allow(unused)] - pub fn id_path(&self) -> &IdPath { - &self.id_path - } - - /// Run some logic with an id added to the id path. - /// - /// This is an ergonomic helper that ensures proper nesting of the id path. - pub fn with_id T>(&mut self, id: Id, f: F) -> T { - self.push(id); - let result = f(self); - self.pop(); - result - } - - /// Allocate a new id and run logic with the new id added to the id path. - /// - /// Also an ergonomic helper. - pub fn with_new_id T>(&mut self, f: F) -> (Id, T) { - let id = Id::next(); - self.push(id); - let result = f(self); - self.pop(); - (id, result) - } - - /// Run some logic within a new Pod context and return the newly created Pod, - /// - /// This logic is usually `View::build` to wrap the returned element into a Pod. - pub fn with_new_pod(&mut self, f: F) -> (Id, S, Pod) - where - E: DomNode, - F: FnOnce(&mut Cx) -> (Id, S, E), - { - let (id, state, element) = f(self); - (id, state, Pod::new(element)) - } - - /// Run some logic within the context of a given Pod, - /// - /// This logic is usually `View::rebuild` - /// - /// # Panics - /// - /// When the element type `E` is not the same type as the inner `DomNode` of the `Pod` - pub fn with_pod(&mut self, pod: &mut Pod, f: F) -> T - where - E: DomNode, - F: FnOnce(&mut E, &mut Cx) -> T, - { - let element = pod - .downcast_mut() - .expect("Element type has changed, this should never happen!"); - f(element, self) - } - - pub fn document(&self) -> &Document { - &self.document - } - - pub(crate) fn build_element(&mut self, ns: &str, name: &str) -> (web_sys::Element, HtmlProps) { - let el = self - .document - .create_element_ns(Some(ns), name) - .expect("could not create element"); - let props = self.current_element_props.apply(&el); - (el, props) - } - - pub(crate) fn rebuild_element( - &mut self, - element: &web_sys::Element, - props: &mut HtmlProps, - ) -> ChangeFlags { - self.current_element_props.apply_changes(element, props) - } - - // TODO Not sure how multiple attribute definitions with the same name should be handled (e.g. `e.attr("class", "a").attr("class", "b")`) - // Currently the outer most (in the example above "b") defines the attribute (when it isn't `None`, in that case the inner attr defines the value) - pub(crate) fn add_attr_to_element(&mut self, name: &CowStr, value: &Option) { - // Panic in dev if "class" is used as an attribute. In production the result is undefined. - debug_assert!( - name != "class", - "classes should be set using the `class` method" - ); - // Panic in dev if "style" is used as an attribute. In production the result is undefined. - debug_assert!( - name != "style", - "styles should be set using the `style` method" - ); - - if let Some(value) = value { - // could be slightly optimized via something like this: `new_attrs.entry(name).or_insert_with(|| value)` - if !self.current_element_props.attributes.contains_key(name) { - self.current_element_props - .attributes - .insert(name.clone(), value.clone()); - } - } - } - - pub(crate) fn add_class_to_element(&mut self, class_name: &CowStr) { - // Don't strictly need this check but I assume its better for perf (might not be though) - if !self.current_element_props.classes.contains_key(class_name) { - self.current_element_props - .classes - .insert(class_name.clone(), ()); - } - } - - pub(crate) fn add_style_to_element(&mut self, name: &CowStr, value: &CowStr) { - if !self.current_element_props.styles.contains_key(name) { - self.current_element_props - .styles - .insert(name.clone(), value.clone()); - } - } +/// The [`View`](`crate::core::View`) `Context` which is used for all [`DomView`](`crate::DomView`)s +#[derive(Default)] +pub struct ViewCtx { + id_path: Vec, + app_ref: Option>, +} +impl ViewCtx { pub fn message_thunk(&self) -> MessageThunk { MessageThunk { - id_path: self.id_path.clone(), + id_path: self.id_path.iter().copied().collect(), app_ref: self.app_ref.as_ref().unwrap().clone_box(), } } @@ -378,24 +43,16 @@ impl Cx { } } -impl Default for Cx { - fn default() -> Self { - Self::new() +impl ViewPathTracker for ViewCtx { + fn push_id(&mut self, id: ViewId) { + self.id_path.push(id); } -} -impl MessageThunk { - pub fn push_message(&self, message_body: impl Any + 'static) { - let message = Message { - id_path: self.id_path.clone(), - body: Box::new(message_body), - }; - self.app_ref.handle_message(message); + fn pop_id(&mut self) { + self.id_path.pop(); } -} -impl ChangeFlags { - pub fn tree_structure() -> Self { - Self::STRUCTURE + fn view_path(&mut self) -> &[ViewId] { + &self.id_path } } diff --git a/xilem_web/src/diff.rs b/xilem_web/src/diff.rs deleted file mode 100644 index 8fac0c1e9..000000000 --- a/xilem_web/src/diff.rs +++ /dev/null @@ -1,164 +0,0 @@ -// Copyright 2023 the Xilem Authors -// SPDX-License-Identifier: Apache-2.0 - -use std::{cmp::Ordering, iter::Peekable}; - -/// Computes the diff between two `Iterators` that have a key value mapping and are ordered by key (e.g. a BTreeMap) -/// -/// # Examples -/// -/// Basic usage: -/// -/// ```ignore -/// use std::collections::BTreeMap; -/// use crate::diff::{Diff, diff_kv_iterables}; -/// -/// let mut old = BTreeMap::default(); -/// old.insert("c", 3); -/// old.insert("b", 2); -/// old.insert("a", 1); -/// -/// let mut new = BTreeMap::default(); -/// new.insert("c", 4); -/// new.insert("d", 2); -/// new.insert("a", 1); -/// -/// let mut diff = diff_kv_iterables(&old, &new); -/// -/// assert!(matches!(diff.next(), Some(Diff::Remove(&"b")))); -/// assert!(matches!(diff.next(), Some(Diff::Change(&"c", 4)))); -/// assert!(matches!(diff.next(), Some(Diff::Add(&"d", 2)))); -/// assert!(diff.next().is_none()); -/// ``` -pub fn diff_kv_iterables<'a, II, K, V>( - prev: II, - next: II, -) -> impl Iterator> + 'a -where - K: Ord + 'a, - V: PartialEq + 'a, - II: IntoIterator + 'a, -{ - DiffMapIterator { - prev: prev.into_iter().peekable(), - next: next.into_iter().peekable(), - } -} - -/// An iterator that compares two ordered maps (like a `BTreeMap`) and outputs a `Diff` for each added, removed or changed key/value pair) -struct DiffMapIterator<'a, K: 'a, V: 'a, I: Iterator> { - prev: Peekable, - next: Peekable, -} - -impl<'a, K: Ord + 'a, V: PartialEq, I: Iterator> Iterator - for DiffMapIterator<'a, K, V, I> -{ - type Item = Diff<&'a K, &'a V>; - fn next(&mut self) -> Option { - loop { - match (self.prev.peek(), self.next.peek()) { - (Some(&(prev_k, prev_v)), Some(&(next_k, next_v))) => match prev_k.cmp(next_k) { - Ordering::Less => { - self.prev.next(); - return Some(Diff::Remove(prev_k)); - } - Ordering::Greater => { - self.next.next(); - return Some(Diff::Add(next_k, next_v)); - } - Ordering::Equal => { - self.prev.next(); - self.next.next(); - if prev_v != next_v { - return Some(Diff::Change(next_k, next_v)); - } - } - }, - (Some(&(prev_k, _)), None) => { - self.prev.next(); - return Some(Diff::Remove(prev_k)); - } - (None, Some(&(next_k, next_v))) => { - self.next.next(); - return Some(Diff::Add(next_k, next_v)); - } - (None, None) => return None, - } - } - } -} - -pub enum Diff { - Add(K, V), - Remove(K), - Change(K, V), -} - -#[cfg(test)] -mod tests { - use super::*; - - macro_rules! tree_map { - (@single $($x:tt)*) => (()); - (@count $($rest:expr),*) => (<[()]>::len(&[$(tree_map!(@single $rest)),*])); - - ($($key:expr => $value:expr,)+) => { tree_map!($($key => $value),+) }; - ($($key:expr => $value:expr),*) => {{ - let mut _map = ::std::collections::BTreeMap::new(); - $( - let _ = _map.insert($key, $value); - )* - _map - }}; - } - - #[test] - fn maps_are_equal() { - let map = tree_map!("an-entry" => 1, "another-entry" => 42); - let map_same = tree_map!("another-entry" => 42, "an-entry" => 1); - assert!(diff_kv_iterables(&map, &map_same).next().is_none()); - } - - #[test] - fn new_map_has_additions() { - let map = tree_map!("an-entry" => 1); - let map_new = tree_map!("an-entry" => 1, "another-entry" => 42); - let mut diff = diff_kv_iterables(&map, &map_new); - assert!(matches!( - diff.next(), - Some(Diff::Add(&"another-entry", &42)) - )); - assert!(diff.next().is_none()); - } - - #[test] - fn new_map_has_removal() { - let map = tree_map!("an-entry" => 1, "another-entry" => 42); - let map_new = tree_map!("an-entry" => 1); - let mut diff = diff_kv_iterables(&map, &map_new); - assert!(matches!(diff.next(), Some(Diff::Remove(&"another-entry")))); - assert!(diff.next().is_none()); - } - - #[test] - fn new_map_has_removal_and_addition() { - let map = tree_map!("an-entry" => 1, "another-entry" => 42); - let map_new = tree_map!("an-entry" => 1, "other-entry" => 2); - let mut diff = diff_kv_iterables(&map, &map_new); - assert!(matches!(diff.next(), Some(Diff::Remove(&"another-entry")))); - assert!(matches!(diff.next(), Some(Diff::Add(&"other-entry", &2)))); - assert!(diff.next().is_none()); - } - - #[test] - fn new_map_changed() { - let map = tree_map!("an-entry" => 1, "another-entry" => 42); - let map_new = tree_map!("an-entry" => 2, "other-entry" => 2); - let mut diff = diff_kv_iterables(&map, &map_new); - assert!(matches!(diff.next(), Some(Diff::Change(&"an-entry", 2)))); - assert!(matches!(diff.next(), Some(Diff::Remove(&"another-entry")))); - assert!(matches!(diff.next(), Some(Diff::Add(&"other-entry", &2)))); - assert!(diff.next().is_none()); - } -} diff --git a/xilem_web/src/element_props.rs b/xilem_web/src/element_props.rs new file mode 100644 index 000000000..e70e61a8c --- /dev/null +++ b/xilem_web/src/element_props.rs @@ -0,0 +1,68 @@ +// Copyright 2024 the Xilem Authors +// SPDX-License-Identifier: Apache-2.0 + +use crate::{attribute::Attributes, class::Classes, document, style::Styles, AnyPod, Pod}; +use wasm_bindgen::UnwrapThrowExt; + +// Lazy access to attributes etc. to avoid allocating unnecessary memory when it isn't needed +// Benchmarks have shown, that this can significantly increase performance and reduce memory usage... +/// This holds all the state for a DOM [`Element`](`crate::interfaces::Element`), it is used for [`DomView::Props`](`crate::DomView::Props`) +pub struct ElementProps { + pub(crate) attributes: Option>, + pub(crate) classes: Option>, + pub(crate) styles: Option>, + pub children: Vec, +} + +impl ElementProps { + // All of this is slightly more complicated than it should be, + // because we want to minimize DOM traffic as much as possible (that's basically the bottleneck) + pub fn update_element(&mut self, element: &web_sys::Element) { + if let Some(attributes) = &mut self.attributes { + attributes.apply_attribute_changes(element); + } + if let Some(classes) = &mut self.classes { + classes.apply_class_changes(element); + } + if let Some(styles) = &mut self.styles { + styles.apply_style_changes(element); + } + } + + pub fn attributes(&mut self) -> &mut Attributes { + // still unstable, but this would even be more concise + // self.attributes.get_or_insert_default() + self.attributes.get_or_insert_with(Default::default) + } + + pub fn styles(&mut self) -> &mut Styles { + self.styles.get_or_insert_with(Default::default) + } + + pub fn classes(&mut self) -> &mut Classes { + self.classes.get_or_insert_with(Default::default) + } +} + +impl Pod { + /// Creates a new Pod with [`web_sys::Element`] as element and `ElementProps` as its [`DomView::Props`](`crate::DomView::Props`) + pub fn new_element(children: Vec, ns: &str, elem_name: &str) -> Self { + let element = document() + .create_element_ns(Some(ns), elem_name) + .unwrap_throw(); + + for child in children.iter() { + let _ = element.append_child(child.node.as_ref()); + } + + Self { + node: element, + props: ElementProps { + attributes: None, + classes: None, + styles: None, + children, + }, + } + } +} diff --git a/xilem_web/src/elements.rs b/xilem_web/src/elements.rs index c1613be3f..94e7c8d5c 100644 --- a/xilem_web/src/elements.rs +++ b/xilem_web/src/elements.rs @@ -1,379 +1,486 @@ -// Copyright 2023 the Xilem Authors +// Copyright 2024 the Xilem Authors // SPDX-License-Identifier: Apache-2.0 -use std::marker::PhantomData; +//! Basic builder functions to create DOM elements, such as [`html::div`] +use std::any::Any; +use std::borrow::Cow; use wasm_bindgen::{JsCast, UnwrapThrowExt}; -use xilem_core::{Id, MessageResult, VecSplice}; use crate::{ - context::HtmlProps, interfaces::sealed::Sealed, view::DomNode, ChangeFlags, Cx, ElementsSplice, - Pod, View, ViewMarker, ViewSequence, HTML_NS, + core::{AppendVec, ElementSplice, MessageResult, Mut, View, ViewId, ViewSequence}, + document, + element_props::ElementProps, + vec_splice::VecSplice, + AnyPod, DomNode, DynMessage, Pod, ViewCtx, HTML_NS, }; -use super::interfaces::Element; - -type CowStr = std::borrow::Cow<'static, str>; - -/// The state associated with a HTML element `View`. -/// -/// Stores handles to the child elements and any child state, as well as attributes and event listeners -pub struct ElementState { - pub(crate) children_states: ViewSeqState, - pub(crate) props: HtmlProps, - pub(crate) child_elements: Vec, - /// This is temporary cache for elements while updating/diffing, - /// after usage it shouldn't contain any elements, - /// and is mainly here to avoid unnecessary allocations - pub(crate) scratch: Vec, +mod sealed { + pub trait Sealed {} } -// TODO something like the `after_update` of the former `Element` view (likely as a wrapper view instead) +// sealed, because this should only cover `ViewSequences` with the blanket impl below +/// This is basically a specialized dynamically dispatchable [`ViewSequence`], It's currently not able to change the underlying type unlike [`AnyDomView`](crate::AnyDomView), so it should not be used as `dyn DomViewSequence`. +/// It's mostly a hack to avoid a completely static view tree, which unfortunately brings rustc (type-checking) down to its knees and results in long compile-times +pub(crate) trait DomViewSequence: + sealed::Sealed + 'static +{ + /// Get an [`Any`] reference to `self`. + fn as_any(&self) -> &dyn Any; -pub struct CustomElement { - name: CowStr, - children: Children, - #[allow(clippy::type_complexity)] - phantom: PhantomData (T, A)>, + /// Build the associated widgets into `elements` and initialize all states. + #[must_use] + fn dyn_seq_build(&self, ctx: &mut ViewCtx, elements: &mut AppendVec) -> Box; + + /// Update the associated widgets. + fn dyn_seq_rebuild( + &self, + prev: &dyn DomViewSequence, + seq_state: &mut Box, + ctx: &mut ViewCtx, + elements: &mut DomChildrenSplice, + ); + + /// Update the associated widgets. + fn dyn_seq_teardown( + &self, + seq_state: &mut Box, + ctx: &mut ViewCtx, + elements: &mut DomChildrenSplice, + ); + + /// Propagate a message. + /// + /// Handle a message, propagating to elements if needed. Here, `id_path` is a slice + /// of ids, where the first item identifiers a child element of this sequence, if necessary. + fn dyn_seq_message( + &self, + seq_state: &mut Box, + id_path: &[ViewId], + message: DynMessage, + app_state: &mut State, + ) -> MessageResult; } -/// Builder function for a custom element view. -pub fn custom_element>( - name: impl Into, - children: Children, -) -> CustomElement { - CustomElement { - name: name.into(), - children, - phantom: PhantomData, - } +impl sealed::Sealed for S +where + State: 'static, + SeqMarker: 'static, + Action: 'static, + S: ViewSequence, +{ } -impl CustomElement { - fn node_name(&self) -> &str { - &self.name +impl DomViewSequence for S +where + State: 'static, + SeqMarker: 'static, + Action: 'static, + S: ViewSequence, +{ + fn as_any(&self) -> &dyn Any { + self + } + + fn dyn_seq_build(&self, ctx: &mut ViewCtx, elements: &mut AppendVec) -> Box { + Box::new(self.seq_build(ctx, elements)) + } + + fn dyn_seq_rebuild( + &self, + prev: &dyn DomViewSequence, + seq_state: &mut Box, + ctx: &mut ViewCtx, + elements: &mut DomChildrenSplice, + ) { + self.seq_rebuild( + prev.as_any().downcast_ref().unwrap_throw(), + seq_state.downcast_mut().unwrap_throw(), + ctx, + elements, + ); + } + + fn dyn_seq_teardown( + &self, + seq_state: &mut Box, + ctx: &mut ViewCtx, + elements: &mut DomChildrenSplice, + ) { + self.seq_teardown(seq_state.downcast_mut().unwrap_throw(), ctx, elements); + } + + fn dyn_seq_message( + &self, + seq_state: &mut Box, + id_path: &[ViewId], + message: DynMessage, + app_state: &mut State, + ) -> MessageResult { + self.seq_message( + seq_state.downcast_mut().unwrap_throw(), + id_path, + message, + app_state, + ) } } -/// An `ElementsSplice` that does DOM updates in place -struct ChildrenSplice<'a, 'b, 'c> { - children: VecSplice<'a, 'b, Pod>, - child_idx: u32, - parent: &'c web_sys::Node, - node_list: Option, - prev_element_count: usize, +// An alternative idea for this would be to track all the changes (via a `Vec`) +// and apply them at once, when this splice is being `Drop`ped, needs some investigation, whether that's better than in place mutations +// TODO maybe we can save some allocations/memory (this needs two extra `Vec`s) +/// This is an [`ElementSplice`] implementation to manage the children of a DOM node in place, it's currently used for updating view sequences +pub struct DomChildrenSplice<'a, 'b, 'c, 'd> { + scratch: &'a mut AppendVec, + children: VecSplice<'b, 'c, AnyPod>, + ix: usize, + parent: &'d web_sys::Node, + parent_was_removed: bool, } -impl<'a, 'b, 'c> ChildrenSplice<'a, 'b, 'c> { - fn new( - children: &'a mut Vec, - scratch: &'b mut Vec, - parent: &'c web_sys::Node, +impl<'a, 'b, 'c, 'd> DomChildrenSplice<'a, 'b, 'c, 'd> { + pub fn new( + scratch: &'a mut AppendVec, + children: &'b mut Vec, + vec_splice_scratch: &'c mut Vec, + parent: &'d web_sys::Node, + parent_was_deleted: bool, ) -> Self { - let prev_element_count = children.len(); Self { - children: VecSplice::new(children, scratch), - child_idx: 0, + scratch, + children: VecSplice::new(children, vec_splice_scratch), + ix: 0, parent, - node_list: None, - prev_element_count, + parent_was_removed: parent_was_deleted, } } } -impl<'a, 'b, 'c> ElementsSplice for ChildrenSplice<'a, 'b, 'c> { - fn push(&mut self, element: Pod, _cx: &mut Cx) { +impl<'a, 'b, 'c, 'd> ElementSplice for DomChildrenSplice<'a, 'b, 'c, 'd> { + fn with_scratch(&mut self, f: impl FnOnce(&mut AppendVec) -> R) -> R { + let ret = f(self.scratch); + for element in self.scratch.drain() { + self.parent + .append_child(element.node.as_ref()) + .unwrap_throw(); + self.children.insert(element); + self.ix += 1; + } + ret + } + + fn insert(&mut self, element: AnyPod) { self.parent - .append_child(element.0.as_node_ref()) + .insert_before( + element.node.as_ref(), + self.children.next_mut().map(|p| p.node.as_ref()), + ) .unwrap_throw(); - self.child_idx += 1; - self.children.push(element); + self.ix += 1; + self.children.insert(element); } - fn mutate(&mut self, _cx: &mut Cx) -> &mut Pod { - self.children.mutate() + fn mutate(&mut self, f: impl FnOnce(Mut<'_, AnyPod>) -> R) -> R { + let child = self.children.mutate(); + let ret = f(child.as_mut(self.parent, self.parent_was_removed)); + self.ix += 1; + ret } - fn delete(&mut self, n: usize, _cx: &mut Cx) { - // Optimization in case all elements are deleted at once - if n == self.prev_element_count { - self.parent.set_text_content(None); - } else { - // lazy NodeList access, in case it's not necessary at all, which is slightly faster when there's no need for the NodeList - let node_list = if let Some(node_list) = &self.node_list { - node_list - } else { - self.node_list = Some(self.parent.child_nodes()); - self.node_list.as_ref().unwrap() - }; - for _ in 0..n { - let child = node_list.get(self.child_idx).unwrap_throw(); - self.parent.remove_child(&child).unwrap_throw(); - } - } - self.children.delete(n); + fn skip(&mut self, n: usize) { + self.children.skip(n); + self.ix += n; } - fn len(&self) -> usize { - self.children.len() + fn delete(&mut self, f: impl FnOnce(Mut<'_, AnyPod>) -> R) -> R { + let mut child = self.children.delete_next(); + let child = child.as_mut(self.parent, true); + // This is an optimization to avoid too much DOM traffic, otherwise first the children would be deleted from that node in an up-traversal + if !self.parent_was_removed { + self.parent.remove_child(child.as_ref()).ok().unwrap_throw(); + } + f(child) } +} - fn mark(&mut self, mut changeflags: ChangeFlags, _cx: &mut Cx) -> ChangeFlags { - if changeflags.contains(ChangeFlags::STRUCTURE) { - let node_list = if let Some(node_list) = &self.node_list { - node_list - } else { - self.node_list = Some(self.parent.child_nodes()); - self.node_list.as_ref().unwrap() - }; - let cur_child = self.children.last_mutated_mut().unwrap_throw(); - let old_child = node_list.get(self.child_idx).unwrap_throw(); - self.parent - .replace_child(cur_child.0.as_node_ref(), &old_child) - .unwrap_throw(); - // TODO(#160) do something else with the structure information? - changeflags.remove(ChangeFlags::STRUCTURE); +/// Used in all the basic DOM elements as [`View::ViewState`] +pub struct ElementState { + seq_state: Box, + append_scratch: AppendVec, + vec_splice_scratch: Vec, +} + +impl ElementState { + pub fn new(seq_state: Box) -> Self { + Self { + seq_state, + append_scratch: Default::default(), + vec_splice_scratch: Default::default(), } - self.child_idx += 1; - changeflags } } -impl ViewMarker for CustomElement {} -impl Sealed for CustomElement {} +// These (boilerplatey) functions are there to reduce the boilerplate created by the macro-expansion below. -impl View for CustomElement +pub(crate) fn build_element( + children: &dyn DomViewSequence, + tag_name: &str, + ns: &str, + ctx: &mut ViewCtx, +) -> (Element, ElementState) where - Children: ViewSequence, + State: 'static, + Action: 'static, + Element: 'static, + SeqMarker: 'static, + Element: From>, { - type State = ElementState; + let mut elements = AppendVec::default(); + let state = ElementState::new(children.dyn_seq_build(ctx, &mut elements)); + ( + Pod::new_element(elements.into_inner(), ns, tag_name).into(), + 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; +pub(crate) fn rebuild_element<'el, State, Action, Element, SeqMarker>( + children: &dyn DomViewSequence, + prev_children: &dyn DomViewSequence, + element: Mut<'el, Pod>, + state: &mut ElementState, + ctx: &mut ViewCtx, +) -> Mut<'el, Pod> +where + State: 'static, + Action: 'static, + Element: 'static, + SeqMarker: 'static, + Element: DomNode, +{ + let mut dom_children_splice = DomChildrenSplice::new( + &mut state.append_scratch, + &mut element.props.children, + &mut state.vec_splice_scratch, + element.node.as_ref(), + element.was_removed, + ); + children.dyn_seq_rebuild( + prev_children, + &mut state.seq_state, + ctx, + &mut dom_children_splice, + ); + element +} - fn build(&self, cx: &mut Cx) -> (Id, Self::State, Self::Element) { - let (el, props) = cx.build_element(HTML_NS, &self.name); +pub(crate) fn teardown_element( + children: &dyn DomViewSequence, + element: Mut<'_, Pod>, + state: &mut ElementState, + ctx: &mut ViewCtx, +) where + State: 'static, + Action: 'static, + Element: 'static, + SeqMarker: 'static, + Element: DomNode, +{ + let mut dom_children_splice = DomChildrenSplice::new( + &mut state.append_scratch, + &mut element.props.children, + &mut state.vec_splice_scratch, + element.node.as_ref(), + true, + ); + children.dyn_seq_teardown(&mut state.seq_state, ctx, &mut dom_children_splice); +} - let mut child_elements = vec![]; - let mut scratch = vec![]; - let mut splice = ChildrenSplice::new(&mut child_elements, &mut scratch, &el); +/// An element that can change its tag, it's useful for autonomous custom elements (i.e. web components) +pub struct CustomElement { + name: Cow<'static, str>, + children: Box>, +} - let (id, children_states) = cx.with_new_id(|cx| self.children.build(cx, &mut splice)); +/// An element that can change its tag, it's useful for autonomous custom elements (i.e. web components) +pub fn custom_element( + name: impl Into>, + children: Children, +) -> CustomElement +where + State: 'static, + Action: 'static, + SeqMarker: 'static, + Children: ViewSequence, +{ + CustomElement { + name: name.into(), + children: Box::new(children), + } +} - debug_assert!(scratch.is_empty()); +impl View + for CustomElement +where + State: 'static, + Action: 'static, + SeqMarker: 'static, +{ + type Element = Pod; - // 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(); + type ViewState = ElementState; - let el = el.dyn_into().unwrap_throw(); - let state = ElementState { - children_states, - child_elements, - scratch, - props, - }; - (id, state, el) + fn build(&self, ctx: &mut ViewCtx) -> (Self::Element, Self::ViewState) { + build_element(&*self.children, &self.name, HTML_NS, ctx) } - fn rebuild( + fn rebuild<'el>( &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 + element_state: &mut Self::ViewState, + ctx: &mut ViewCtx, + element: Mut<'el, Self::Element>, + ) -> Mut<'el, Self::Element> { 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, props) = cx.build_element(HTML_NS, self.node_name()); - state.props = props; - // TODO could this be combined with child updates? - while let Some(child) = element.child_nodes().get(0) { + let new_element = document() + .create_element_ns(Some(HTML_NS), &self.name) + .unwrap_throw(); + + while let Some(child) = element.node.child_nodes().get(0) { new_element.append_child(&child).unwrap_throw(); } - *element = new_element.dyn_into().unwrap_throw(); - changed |= ChangeFlags::STRUCTURE; + element + .parent + .replace_child(&new_element, element.node) + .unwrap_throw(); + *element.node = new_element.dyn_into().unwrap_throw(); } - changed |= cx.rebuild_element(element, &mut state.props); - - // update children - let mut splice = - ChildrenSplice::new(&mut state.child_elements, &mut state.scratch, element); - changed |= cx.with_id(*id, |cx| { - self.children - .rebuild(cx, &prev.children, &mut state.children_states, &mut splice) - }); - debug_assert!(state.scratch.is_empty()); - changed.remove(ChangeFlags::STRUCTURE); - changed + rebuild_element( + &*self.children, + &*prev.children, + element, + element_state, + ctx, + ) + } + + fn teardown( + &self, + element_state: &mut Self::ViewState, + ctx: &mut ViewCtx, + element: Mut<'_, Self::Element>, + ) { + teardown_element(&*self.children, element, element_state, ctx); } fn message( &self, - id_path: &[Id], - state: &mut Self::State, - message: Box, - app_state: &mut T, - ) -> MessageResult { + view_state: &mut Self::ViewState, + id_path: &[ViewId], + message: DynMessage, + app_state: &mut State, + ) -> MessageResult { self.children - .message(id_path, &mut state.children_states, message, app_state) + .dyn_seq_message(&mut view_state.seq_state, id_path, message, app_state) } } -impl> Element for CustomElement {} -impl> crate::interfaces::HtmlElement - for CustomElement -{ -} - -macro_rules! generate_dom_interface_impl { - ($dom_interface:ident, ($ty_name:ident, $t:ident, $a:ident, $vs:ident)) => { - impl<$t, $a, $vs> $crate::interfaces::$dom_interface<$t, $a> for $ty_name<$t, $a, $vs> where - $vs: $crate::view::ViewSequence<$t, $a> - { - } - }; -} - -// TODO maybe it's possible to reduce even more in the impl function bodies and put into impl_functions -// (should improve compile times and probably wasm binary size) macro_rules! define_element { ($ns:expr, ($ty_name:ident, $name:ident, $dom_interface:ident)) => { - define_element!($ns, ( - $ty_name, - $name, - $dom_interface, - stringify!($name), - T, - A, - VS - )); - }; - ($ns:expr, ($ty_name:ident, $name:ident, $dom_interface:ident, $tag_name: expr)) => { - define_element!($ns, ( - $ty_name, - $name, - $dom_interface, - $tag_name, - T, - A, - VS - )); + define_element!($ns, ($ty_name, $name, $dom_interface, stringify!($name))); }; - ($ns:expr, ($ty_name:ident, $name:ident, $dom_interface:ident, $tag_name:expr, $t:ident, $a: ident, $vs: ident)) => { - pub struct $ty_name<$t, $a = (), $vs = ()>($vs, PhantomData ($t, $a)>); - - impl<$t, $a, $vs> ViewMarker for $ty_name<$t, $a, $vs> {} - impl<$t, $a, $vs> Sealed for $ty_name<$t, $a, $vs> {} - - impl<$t, $a, $vs: ViewSequence<$t, $a>> View<$t, $a> for $ty_name<$t, $a, $vs> { - type State = ElementState<$vs::State>; - type Element = web_sys::$dom_interface; - - fn build(&self, cx: &mut Cx) -> (Id, Self::State, Self::Element) { - let (el, props) = cx.build_element($ns, $tag_name); - - let mut child_elements = vec![]; - let mut scratch = vec![]; - let mut splice = ChildrenSplice::new(&mut child_elements, &mut scratch, &el); - - let (id, children_states) = cx.with_new_id(|cx| self.0.build(cx, &mut splice)); - debug_assert!(scratch.is_empty()); - - // 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, - props, - }; - (id, state, el) + ($ns:expr, ($ty_name:ident, $name:ident, $dom_interface:ident, $tag_name:expr)) => { + pub struct $ty_name { + children: Box>, + } + + /// Builder function for a + #[doc = concat!("`", $tag_name, "`")] + /// element view. + pub fn $name< + State: 'static, + Action: 'static, + SeqMarker: 'static, + Children: ViewSequence, + >( + children: Children, + ) -> $ty_name { + $ty_name { + children: Box::new(children), + } + } + + impl View + for $ty_name + where + State: 'static, + Action: 'static, + SeqMarker: 'static, + { + type Element = Pod; + + type ViewState = ElementState; + + fn build(&self, ctx: &mut ViewCtx) -> (Self::Element, Self::ViewState) { + build_element(&*self.children, $tag_name, $ns, ctx) } - fn rebuild( + fn rebuild<'el>( &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.rebuild_element(element, &mut state.props); - - // update children - let mut splice = ChildrenSplice::new(&mut state.child_elements, &mut state.scratch, element); - changed |= cx.with_id(*id, |cx| { - self.0.rebuild(cx, &prev.0, &mut state.children_states, &mut splice) - }); - debug_assert!(state.scratch.is_empty()); - changed.remove(ChangeFlags::STRUCTURE); // this is handled by the ChildrenSplice already - changed + element_state: &mut Self::ViewState, + ctx: &mut ViewCtx, + element: Mut<'el, Self::Element>, + ) -> Mut<'el, Self::Element> { + rebuild_element( + &*self.children, + &*prev.children, + element, + element_state, + ctx, + ) } - fn message( + fn teardown( &self, - id_path: &[Id], - state: &mut Self::State, - message: Box, - app_state: &mut $t, - ) -> MessageResult<$a> { - self.0 - .message(id_path, &mut state.children_states, message, app_state) + element_state: &mut Self::ViewState, + ctx: &mut ViewCtx, + element: Mut<'_, Self::Element>, + ) { + teardown_element(&*self.children, element, element_state, ctx); } - } - - /// Builder function for a - #[doc = concat!("`", $tag_name, "`")] - /// element view. - pub fn $name<$t, $a, $vs: ViewSequence<$t, $a>>(children: $vs) -> $ty_name<$t, $a, $vs> { - $ty_name(children, PhantomData) - } - generate_dom_interface_impl!($dom_interface, ($ty_name, $t, $a, $vs)); - - paste::paste! { - $crate::interfaces::[]!(generate_dom_interface_impl, ($ty_name, $t, $a, $vs)); + fn message( + &self, + view_state: &mut Self::ViewState, + id_path: &[ViewId], + message: DynMessage, + app_state: &mut State, + ) -> MessageResult { + self.children.dyn_seq_message( + &mut view_state.seq_state, + id_path, + message, + app_state, + ) + } } }; } macro_rules! define_elements { ($ns:ident, $($element_def:tt,)*) => { - use std::marker::PhantomData; - use wasm_bindgen::{JsCast, UnwrapThrowExt}; - use xilem_core::{Id, MessageResult}; - use super::{ElementState, ChildrenSplice}; - + use super::{build_element, rebuild_element, teardown_element, DomViewSequence, ElementState}; use crate::{ - interfaces::sealed::Sealed, - ChangeFlags, Cx, View, ViewMarker, ViewSequence, + core::{MessageResult, Mut, ViewId, ViewSequence}, + AnyPod, DynMessage, ElementProps, Pod, View, ViewCtx, }; - $(define_element!(crate::$ns, $element_def);)* }; } pub mod html { + //! HTML elements with the namespace [`HTML_NS`](`crate::HTML_NS`) define_elements!( // the order is copied from // https://developer.mozilla.org/en-US/docs/Web/HTML/Element @@ -414,7 +521,7 @@ pub mod html { (Pre, pre, HtmlPreElement), (Ul, ul, HtmlUListElement), // inline text - (A, a, HtmlAnchorElement, "a", T, A_, VS), + (A, a, HtmlAnchorElement), (Abbr, abbr, HtmlElement), (B, b, HtmlElement), (Bdi, bdi, HtmlElement), @@ -501,6 +608,7 @@ pub mod html { } pub mod mathml { + //! MathML elements with the namespace [`MATHML_NS`](`crate::MATHML_NS`) define_elements!( MATHML_NS, (Math, math, Element), @@ -537,10 +645,11 @@ pub mod mathml { } pub mod svg { + //! SVG elements with the namespace [`SVG_NS`](`crate::SVG_NS`) define_elements!( SVG_NS, (Svg, svg, SvgsvgElement), - (A, a, SvgaElement, "a", T, A_, VS), + (A, a, SvgaElement), (Animate, animate, SvgAnimateElement), ( AnimateMotion, diff --git a/xilem_web/src/events.rs b/xilem_web/src/events.rs index 84679304c..871b940f1 100644 --- a/xilem_web/src/events.rs +++ b/xilem_web/src/events.rs @@ -1,53 +1,37 @@ // Copyright 2023 the Xilem Authors // SPDX-License-Identifier: Apache-2.0 -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}; +use std::{borrow::Cow, marker::PhantomData}; +use wasm_bindgen::{prelude::Closure, throw_str, JsCast, UnwrapThrowExt}; +use web_sys::AddEventListenerOptions; +use xilem_core::{MessageResult, Mut, View, ViewId, ViewPathTracker}; -pub use gloo::events::EventListenerOptions; +use crate::{DynMessage, ElementAsRef, OptionalAction, ViewCtx}; /// Wraps a [`View`] `V` and attaches an event listener. /// -/// The event type `E` should inherit from [`web_sys::Event`] -pub struct OnEvent { - pub(crate) element: E, +/// The event type `Event` should inherit from [`web_sys::Event`] +#[derive(Clone, Debug)] +pub struct OnEvent { + pub(crate) element: V, pub(crate) event: Cow<'static, str>, - pub(crate) options: EventListenerOptions, - pub(crate) handler: C, + pub(crate) capture: bool, + pub(crate) passive: bool, + pub(crate) handler: Callback, #[allow(clippy::type_complexity)] - pub(crate) phantom_event_ty: PhantomData (T, A, Ev)>, + pub(crate) phantom_event_ty: PhantomData (State, Action, Event)>, } -impl OnEvent +impl OnEvent where - Ev: JsCast + 'static, + Event: JsCast + 'static, { - pub fn new(element: E, event: impl Into>, handler: C) -> Self { + pub fn new(element: V, event: impl Into>, handler: Callback) -> Self { OnEvent { element, event: event.into(), - options: Default::default(), - handler, - phantom_event_ty: PhantomData, - } - } - - pub fn new_with_options( - element: E, - event: impl Into>, - handler: C, - options: EventListenerOptions, - ) -> Self { - OnEvent { - element, - event: event.into(), - options, + passive: true, + capture: false, handler, phantom_event_ty: PhantomData, } @@ -59,168 +43,301 @@ where /// 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.passive = value; self } + + /// Whether the event handler should capture the event *before* being dispatched to any EventTarget beneath it in the DOM tree. (default = `false`) + /// + /// Events that are bubbling upward through the tree will not trigger a listener designated to use capture. + /// Event bubbling and capturing are two ways of propagating events that occur in an element that is nested within another element, + /// when both elements have registered a handle for that event. + /// The event propagation mode determines the order in which elements receive the event. + // TODO use similar Nomenclature as gloo (Phase::Bubble/Phase::Capture)? + pub fn capture(mut self, value: bool) -> Self { + self.capture = value; + self + } +} + +fn create_event_listener( + target: &web_sys::EventTarget, + event: &str, + // TODO options + capture: bool, + passive: bool, + ctx: &mut ViewCtx, +) -> Closure { + let thunk = ctx.message_thunk(); + let callback = Closure::new(move |event: web_sys::Event| { + // TODO make this configurable + event.prevent_default(); + event.stop_propagation(); + let event = event.dyn_into::().unwrap_throw(); + thunk.push_message(event); + }); + + let mut options = AddEventListenerOptions::new(); + options.capture(capture); + options.passive(passive); + + target + .add_event_listener_with_callback_and_add_event_listener_options( + event, + callback.as_ref().unchecked_ref(), + &options, + ) + .unwrap_throw(); + callback } -fn create_event_listener( +fn remove_event_listener( target: &web_sys::EventTarget, - event: impl Into>, - options: EventListenerOptions, - cx: &Cx, -) -> gloo::events::EventListener { - let thunk = cx.message_thunk(); - gloo::events::EventListener::new_with_options( - target, - event, - options, - move |event: &web_sys::Event| { - let event = (*event).clone().dyn_into::().unwrap_throw(); - thunk.push_message(event); - }, - ) + event: &str, + callback: &Closure, + is_capture: bool, +) { + target + .remove_event_listener_with_callback_and_bool( + event, + callback.as_ref().unchecked_ref(), + is_capture, + ) + .unwrap_throw(); } /// State for the `OnEvent` view. pub struct OnEventState { #[allow(unused)] - listener: gloo::events::EventListener, - child_id: Id, child_state: S, + callback: Closure, } -impl ViewMarker for OnEvent {} -impl Sealed for OnEvent {} +// These (boilerplatey) functions are there to reduce the boilerplate created by the macro-expansion below. + +fn build_event_listener( + element_view: &V, + event: &str, + capture: bool, + passive: bool, + ctx: &mut ViewCtx, +) -> (V::Element, OnEventState) +where + State: 'static, + Action: 'static, + V: View, + V::Element: ElementAsRef, + Event: JsCast + 'static + crate::Message, +{ + // we use a placeholder id here, the id can never change, so we don't need to store it anywhere + ctx.with_id(ViewId::new(0), |ctx| { + let (element, child_state) = element_view.build(ctx); + let callback = + create_event_listener::(element.as_ref(), event, capture, passive, ctx); + let state = OnEventState { + child_state, + callback, + }; + (element, state) + }) +} + +#[allow(clippy::too_many_arguments)] +fn rebuild_event_listener<'el, State, Action, V, Event>( + element_view: &V, + prev_element_view: &V, + element: Mut<'el, V::Element>, + event: &str, + capture: bool, + passive: bool, + prev_capture: bool, + prev_passive: bool, + state: &mut OnEventState, + ctx: &mut ViewCtx, +) -> Mut<'el, V::Element> +where + State: 'static, + Action: 'static, + V: View, + V::Element: ElementAsRef, + Event: JsCast + 'static + crate::Message, +{ + ctx.with_id(ViewId::new(0), |ctx| { + if prev_capture != capture || prev_passive != passive { + remove_event_listener(element.as_ref(), event, &state.callback, prev_capture); + + state.callback = + create_event_listener::(element.as_ref(), event, capture, passive, ctx); + } + element_view.rebuild(prev_element_view, &mut state.child_state, ctx, element) + }) +} + +fn teardown_event_listener( + element_view: &V, + element: Mut<'_, V::Element>, + _event: &str, + state: &mut OnEventState, + _capture: bool, + ctx: &mut ViewCtx, +) where + State: 'static, + Action: 'static, + V: View, + V::Element: ElementAsRef, +{ + // TODO: is this really needed (as the element will be removed anyway)? + // remove_event_listener(element.as_ref(), event, &state.callback, capture); + ctx.with_id(ViewId::new(0), |ctx| { + element_view.teardown(&mut state.child_state, ctx, element); + }); +} + +fn message_event_listener( + element_view: &V, + state: &mut OnEventState, + id_path: &[ViewId], + message: DynMessage, + app_state: &mut State, + handler: &Callback, +) -> MessageResult +where + State: 'static, + Action: 'static, + V: View, + V::Element: ElementAsRef, + Event: JsCast + 'static + crate::Message, + OA: OptionalAction, + Callback: Fn(&mut State, Event) -> OA + 'static, +{ + let Some((first, remainder)) = id_path.split_first() else { + throw_str("Parent view of `OnEvent` sent outdated and/or incorrect empty view path"); + }; + if first.routing_id() != 0 { + throw_str("Parent view of `OnEvent` sent outdated and/or incorrect empty view path"); + } + if remainder.is_empty() { + let event = message.downcast::().unwrap_throw(); + match (handler)(app_state, *event).action() { + Some(a) => MessageResult::Action(a), + None => MessageResult::Nop, + } + } else { + element_view.message(&mut state.child_state, remainder, message, app_state) + } +} -impl View for OnEvent +impl View + for OnEvent where - OA: OptionalAction, - C: Fn(&mut T, Ev) -> OA, - E: Element, - Ev: JsCast + 'static, + State: 'static, + Action: 'static, + OA: OptionalAction, + Callback: Fn(&mut State, Event) -> OA + 'static, + V: View, + V::Element: ElementAsRef, + Event: JsCast + 'static + crate::Message, { - type State = OnEventState; - - type Element = E::Element; - - fn build(&self, cx: &mut Cx) -> (Id, Self::State, Self::Element) { - let (id, (element, state)) = cx.with_new_id(|cx| { - let (child_id, child_state, element) = self.element.build(cx); - let listener = create_event_listener::( - element.as_node_ref(), - self.event.clone(), - self.options, - cx, - ); - let state = OnEventState { - child_state, - child_id, - listener, - }; - (element, state) - }); - (id, state, element) + type ViewState = OnEventState; + + type Element = V::Element; + + fn build(&self, ctx: &mut ViewCtx) -> (Self::Element, Self::ViewState) { + build_event_listener::<_, _, _, Event>( + &self.element, + &self.event, + self.capture, + self.passive, + ctx, + ) } - fn rebuild( + fn rebuild<'el>( &self, - cx: &mut Cx, prev: &Self, - id: &mut Id, - state: &mut Self::State, - element: &mut Self::Element, - ) -> ChangeFlags { - cx.with_id(*id, |cx| { - let prev_child_id = state.child_id; - let mut changed = self.element.rebuild( - cx, - &prev.element, - &mut state.child_id, - &mut state.child_state, - element, - ); - if state.child_id != prev_child_id { - changed |= ChangeFlags::OTHER_CHANGE; - } - // TODO check equality of prev and current element somehow - if prev.event != self.event || changed.contains(ChangeFlags::STRUCTURE) { - state.listener = create_event_listener::( - element.as_node_ref(), - self.event.clone(), - self.options, - cx, + view_state: &mut Self::ViewState, + ctx: &mut ViewCtx, + element: Mut<'el, Self::Element>, + ) -> Mut<'el, Self::Element> { + // special case, where event name can change, so we can't reuse the rebuild_event_listener function above + ctx.with_id(ViewId::new(0), |ctx| { + if prev.capture != self.capture + || prev.passive != self.passive + || prev.event != self.event + { + remove_event_listener( + element.as_ref(), + &prev.event, + &view_state.callback, + prev.capture, + ); + + view_state.callback = create_event_listener::( + element.as_ref(), + &self.event, + self.capture, + self.passive, + ctx, ); - changed |= ChangeFlags::OTHER_CHANGE; } - changed + self.element + .rebuild(&prev.element, &mut view_state.child_state, ctx, element) }) } - fn message( + fn teardown( &self, - id_path: &[Id], - state: &mut Self::State, - message: Box, - app_state: &mut T, - ) -> MessageResult { - match id_path { - [] if message.downcast_ref::().is_some() => { - let event = message.downcast::().unwrap(); - match (self.handler)(app_state, *event).action() { - Some(a) => MessageResult::Action(a), - None => MessageResult::Nop, - } - } - [element_id, rest_path @ ..] if *element_id == state.child_id => { - self.element - .message(rest_path, &mut state.child_state, message, app_state) - } - _ => MessageResult::Stale(message), - } + view_state: &mut Self::ViewState, + ctx: &mut ViewCtx, + element: Mut<'_, Self::Element>, + ) { + teardown_event_listener( + &self.element, + element, + &self.event, + view_state, + self.capture, + ctx, + ); } -} -crate::interfaces::impl_dom_interfaces_for_ty!( - Element, - OnEvent, - vars: , - vars_on_ty: , - bounds: { - Ev: JsCast + 'static, - OA: OptionalAction, - C: Fn(&mut T, Ev) -> OA, + fn message( + &self, + view_state: &mut Self::ViewState, + id_path: &[ViewId], + message: crate::DynMessage, + app_state: &mut State, + ) -> MessageResult { + message_event_listener( + &self.element, + view_state, + id_path, + message, + app_state, + &self.handler, + ) } -); +} macro_rules! event_definitions { ($(($ty_name:ident, $event_name:literal, $web_sys_ty:ident)),*) => { $( - $crate::interfaces::impl_dom_interfaces_for_ty!( - Element, - $ty_name, - vars: , - vars_on_ty: , - bounds: { - OA: OptionalAction, - C: Fn(&mut T, web_sys::$web_sys_ty ) -> OA, - } - ); - - pub struct $ty_name { - target: E, - callback: C, - options: EventListenerOptions, - phantom: PhantomData (T, A)>, + pub struct $ty_name { + pub(crate) element: V, + pub(crate) capture: bool, + pub(crate) passive: bool, + pub(crate) handler: Callback, + pub(crate) phantom_event_ty: PhantomData (State, Action)>, } - impl $ty_name { - pub fn new(target: E, callback: C) -> Self { + impl $ty_name { + pub fn new(element: V, handler: Callback) -> Self { Self { - target, - options: Default::default(), - callback, - phantom: PhantomData, + element, + passive: true, + capture: false, + handler, + phantom_event_ty: PhantomData, } } @@ -230,76 +347,86 @@ macro_rules! event_definitions { /// 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.passive = value; + self + } + + /// Whether the event handler should capture the event *before* being dispatched to any EventTarget beneath it in the DOM tree. (default = `false`) + /// + /// Events that are bubbling upward through the tree will not trigger a listener designated to use capture. + /// Event bubbling and capturing are two ways of propagating events that occur in an element that is nested within another element, + /// when both elements have registered a handle for that event. + /// The event propagation mode determines the order in which elements receive the event. + // TODO use similar Nomenclature as gloo (Phase::Bubble/Phase::Capture)? + pub fn capture(mut self, value: bool) -> Self { + self.capture = value; self } } - impl ViewMarker for $ty_name {} - impl Sealed for $ty_name {} - impl View for $ty_name + impl View + for $ty_name where - OA: OptionalAction, - C: Fn(&mut T, web_sys::$web_sys_ty) -> OA, - E: Element, + State: 'static, + Action: 'static, + OA: OptionalAction, + Callback: Fn(&mut State, web_sys::$web_sys_ty) -> OA + 'static, + V: View, + V::Element: ElementAsRef, { - type State = OnEventState; + type ViewState = OnEventState; - type Element = E::Element; + type Element = V::Element; - fn build(&self, cx: &mut Cx) -> (Id, Self::State, Self::Element) { - let (id, (element, state)) = cx.with_new_id(|cx| { - let (child_id, child_state, el) = self.target.build(cx); - let listener = create_event_listener::(el.as_node_ref(), $event_name, self.options, cx); - (el, OnEventState { child_state, child_id, listener }) - }); - (id, state, element) + fn build(&self, ctx: &mut ViewCtx) -> (Self::Element, Self::ViewState) { + build_event_listener::<_, _, _, web_sys::$web_sys_ty>( + &self.element, + $event_name, + self.capture, + self.passive, + ctx, + ) } - fn rebuild( + fn rebuild<'el>( &self, - cx: &mut Cx, prev: &Self, - id: &mut Id, - state: &mut Self::State, - element: &mut Self::Element, - ) -> ChangeFlags { - cx.with_id(*id, |cx| { - let prev_child_id = state.child_id; - let mut changed = self.target.rebuild(cx, &prev.target, &mut state.child_id, &mut state.child_state, element); - if state.child_id != prev_child_id { - changed |= ChangeFlags::OTHER_CHANGE; - } - // TODO check equality of prev and current element somehow - if changed.contains(ChangeFlags::STRUCTURE) { - state.listener = create_event_listener::(element.as_node_ref(), $event_name, self.options, cx); - changed |= ChangeFlags::OTHER_CHANGE; - } - changed - }) + view_state: &mut Self::ViewState, + ctx: &mut ViewCtx, + element: Mut<'el, Self::Element>, + ) -> Mut<'el, Self::Element> { + rebuild_event_listener::<_, _, _, web_sys::$web_sys_ty>( + &self.element, + &prev.element, + element, + $event_name, + self.capture, + self.passive, + prev.capture, + prev.passive, + view_state, + ctx, + ) + } + + fn teardown( + &self, + view_state: &mut Self::ViewState, + ctx: &mut ViewCtx, + element: Mut<'_, Self::Element>, + ) { + teardown_event_listener(&self.element, element, $event_name, view_state, self.capture, ctx); } fn message( &self, - id_path: &[Id], - state: &mut Self::State, - message: Box, - app_state: &mut T, - ) -> MessageResult { - match id_path { - [] if message.downcast_ref::().is_some() => { - let event = message.downcast::().unwrap(); - match (self.callback)(app_state, *event).action() { - Some(a) => MessageResult::Action(a), - None => MessageResult::Nop, - } - } - [element_id, rest_path @ ..] if *element_id == state.child_id => { - self.target.message(rest_path, &mut state.child_state, message, app_state) - } - _ => MessageResult::Stale(message), - } + view_state: &mut Self::ViewState, + id_path: &[ViewId], + message: crate::DynMessage, + app_state: &mut State, + ) -> MessageResult { + message_event_listener(&self.element, view_state, id_path, message, app_state, &self.handler) } } )* diff --git a/xilem_web/src/interfaces.rs b/xilem_web/src/interfaces.rs index dd9b32b70..bff9d71bb 100644 --- a/xilem_web/src/interfaces.rs +++ b/xilem_web/src/interfaces.rs @@ -1,116 +1,144 @@ -// Copyright 2023 the Xilem Authors +// Copyright 2024 the Xilem Authors // SPDX-License-Identifier: Apache-2.0 -use crate::{ - class::{Class, IntoClasses}, - style::{IntoStyles, Style}, - Pointer, PointerMsg, View, ViewMarker, -}; -use std::{borrow::Cow, marker::PhantomData}; +//! Opinionated extension traits roughly resembling their equivalently named DOM interfaces. +//! It is used for DOM elements, e.g. created with [`html::span`](`crate::elements::html::span`) to modify the underlying element, such as [`Element::attr`] or [`HtmlElement::style`] +//! +//! These traits can also be used as return type of components to allow modifying the underlying DOM element that is returned. +//! For example: +//! ```ignore +//! fn my_div_element_view() -> impl HtmlDivElement {..} +//! ``` +//! A lot of the possible attributes are not yet added, if you find something missing for you - please open a PR at -use gloo::events::EventListenerOptions; -use wasm_bindgen::JsCast; +use std::borrow::Cow; use crate::{ - events::{self, OnEvent}, - Attr, IntoAttributeValue, OptionalAction, + attribute::{Attr, WithAttributes}, + class::{AsClassIter, Class, WithClasses}, + events, + style::{IntoStyles, Style, WithStyle}, + DomView, IntoAttributeValue, OptionalAction, Pointer, PointerMsg, }; +use wasm_bindgen::JsCast; -pub(crate) mod sealed { - pub trait Sealed {} -} - -// TODO should the options be its own function `on_event_with_options`, -// or should that be done via the builder pattern: `el.on_event().passive(false)`? macro_rules! event_handler_mixin { ($(($event_ty: ident, $fn_name:ident, $event:expr, $web_sys_event_type:ident),)*) => { $( - fn $fn_name(self, handler: EH) -> events::$event_ty + #[doc = concat!("Add an \"", $event, "\" event handler to this [`Element`].")] + /// + /// See [`Element::on`] for more information how to use this. + // TODO: This would be nice, but although all the events are specified in `web_sys` on the `Element` interface, events such as `dragend` or `reset` link to the more relevant sub interface + // We *could* add another parameter to the macro to fix this, or probably even not provide these events directly on the `Element` interface + // /// + // #[doc = concat!("See for more details")] + fn $fn_name( + self, + handler: Callback, + ) -> events::$event_ty where - OA: OptionalAction, - EH: Fn(&mut T, web_sys::$web_sys_event_type) -> OA, + Self: Sized, + Self::Element: AsRef, + OA: OptionalAction, + Callback: Fn(&mut State, web_sys::$web_sys_event_type) -> OA, { - $crate::events::$event_ty::new(self, handler) + events::$event_ty::new(self, handler) } )* }; } -pub trait Element: View + ViewMarker + sealed::Sealed -where - Self: Sized, +pub trait Element: + Sized + + DomView> { - fn on( + /// Set an attribute for an [`Element`] + /// + /// # Examples + /// + /// ``` + /// use xilem_web::{interfaces::Element, elements::html::{a, canvas, div, input}}; + /// # fn component() -> impl Element<()> { + /// div(( + /// a("a link to an anchor").attr("href", "#anchor"), + /// // attribute will only appear if condition is met + /// // previous attribute is overwritten (and removed if condition is false) + /// a("a link to a new anchor - *maybe*") + /// .attr("href", "#anchor") + /// .attr("href", true.then_some("#new-anchor")), + /// input(()).attr("autofocus", true), + /// canvas(()).attr("width", 300) + /// )) + /// # } + /// ``` + fn attr( self, - event: impl Into>, - handler: EH, - ) -> OnEvent - where - E: JsCast + 'static, - OA: OptionalAction, - EH: Fn(&mut T, E) -> OA, - Self: Sized, - { - OnEvent::new(self, event, handler) + name: impl Into>, + value: impl IntoAttributeValue, + ) -> Attr { + Attr::new(self, name.into(), value.into_attr_value()) } - fn on_with_options( + /// Add a class to an [`Element`] + /// + /// # Examples + /// + /// ``` + /// use xilem_web::{interfaces::Element, elements::html::div}; + /// # fn component() -> impl Element<()> { + /// div(()) + /// .class("single-class") + /// .class(["multiple", "classes"]) + /// .class(Some("optional-class")) + /// # } + /// ``` + fn class( + self, + as_classes: AsClasses, + ) -> Class { + Class::new(self, as_classes) + } + + /// Add a generic event handler to this [`Element`]. + /// + /// For builtin events such as `onclick` prefer using the specialized event handlers (e.g. [`Element::on_click`]) + /// + /// # Examples + /// + /// ``` + /// use xilem_web::{interfaces::Element, elements::html::div}; + /// # fn component() -> impl Element<()> { + /// div(()).on("custom-event", |state, event: web_sys::Event| {/* modify `state` */}) + /// # } + /// ``` + fn on( self, event: impl Into>, - handler: EH, - options: EventListenerOptions, - ) -> OnEvent + handler: Callback, + ) -> events::OnEvent where - Ev: JsCast + 'static, - OA: OptionalAction, - EH: Fn(&mut T, Ev) -> OA, + Self::Element: AsRef, + Event: JsCast + 'static, + OA: OptionalAction, + Callback: Fn(&mut State, Event) -> OA, Self: Sized, { - OnEvent::new_with_options(self, event, handler, options) + events::OnEvent::new(self, event, handler) } - fn pointer(self, f: F) -> Pointer { - crate::pointer::pointer(self, f) - } - - // 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( + fn pointer( self, - name: impl Into>, - value: impl IntoAttributeValue, - ) -> Attr { - Attr { - element: self, - name: name.into(), - value: value.into_attr_value(), - phantom: std::marker::PhantomData, - } + handler: Callback, + ) -> Pointer { + crate::pointer::pointer(self, handler) } - /// Add 0 or more classes to the wrapped element. - /// - /// Can pass a string, &'static str, Option, tuple, or vec + /// Defines a unique identifier (ID) which must be unique in the whole document. + /// Its purpose is to identify the element when linking (using a fragment identifier), scripting, or styling (with CSS). /// - /// If multiple classes are added, all will be applied to the element. - fn class(self, class: impl IntoClasses) -> Class { - let mut class_names = vec![]; - class.into_classes(&mut class_names); - Class { - element: self, - class_names, - phantom: PhantomData, - } + /// See for more details + fn id(self, value: impl IntoAttributeValue) -> Attr { + Attr::new(self, Cow::from("id"), value.into_attr_value()) } // event list from @@ -204,449 +232,2100 @@ where ); } -// 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)*) => {}; +impl Element for T +where + T: DomView, + T::Props: WithAttributes + WithClasses, + T::DomNode: AsRef, +{ } -#[allow(unused_imports)] -pub(crate) use for_all_element_ancestors; -macro_rules! dom_interface_macro_and_trait_definitions_impl { - ($interface:ident { - methods: $_methods_body:tt, - child_interfaces: { - $($child_interface:ident { - methods: $child_methods_body:tt, - child_interfaces: $child_interface_body: tt - },)* - } - }) => { - paste::paste! { - $( - pub trait $child_interface: $interface $child_methods_body - - /// Execute $mac which is a macro, that takes $dom_interface:ident () as match arm for all interfaces that - #[doc = concat!("`", stringify!($child_interface), "`")] - /// inherits from - macro_rules! [] { - ($mac:path, $extra_params:tt) => { - $mac!($interface, $extra_params); - $crate::interfaces::[]!($mac, $extra_params); - }; - } - pub(crate) use []; - )* - } - paste::paste! { - /// Execute $mac which is a macro, that takes $dom_interface:ident () as match arm for all interfaces that inherit from - #[doc = concat!("`", stringify!($interface), "`")] - #[allow(unused_macros)] - macro_rules! [] { - ($mac:path, $extra_params:tt) => { - $( - $mac!($child_interface, $extra_params); - $crate::interfaces::[]!($mac, $extra_params); - )* - }; - } - #[allow(unused_imports)] - pub(crate) use []; - } +// #[cfg(feature = "HtmlAnchorElement")] +pub trait HtmlAnchorElement: + HtmlElement> +{ +} - $( - $crate::interfaces::dom_interface_macro_and_trait_definitions_impl!( - $child_interface { - methods: $child_methods_body, - child_interfaces: $child_interface_body - } - ); - )* - }; +// #[cfg(feature = "HtmlAnchorElement")] +impl HtmlAnchorElement for T +where + T: HtmlElement, + T::DomNode: AsRef, +{ } -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; +// #[cfg(feature = "HtmlAreaElement")] +pub trait HtmlAreaElement: + HtmlElement> +{ +} + +// #[cfg(feature = "HtmlAreaElement")] +impl HtmlAreaElement for T +where + T: HtmlElement, + T::DomNode: AsRef, +{ +} + +// #[cfg(feature = "HtmlAudioElement")] +pub trait HtmlAudioElement: + HtmlMediaElement> +{ +} + +// #[cfg(feature = "HtmlAudioElement")] +impl HtmlAudioElement for T +where + T: HtmlMediaElement, + T::DomNode: AsRef, +{ +} + +// #[cfg(feature = "HtmlBaseElement")] +// pub trait HtmlBaseElement: +// HtmlElement> +// { +// } + +// #[cfg(feature = "HtmlBaseElement")] +// impl HtmlBaseElement for T +// where +// T: HtmlElement, +// T::DomNode: AsRef, +// { +// } + +// #[cfg(feature = "HtmlBodyElement")] +// pub trait HtmlBodyElement: +// HtmlElement> +// { +// } + +// #[cfg(feature = "HtmlBodyElement")] +// impl HtmlBodyElement for T +// where +// T: HtmlElement, +// T::DomNode: AsRef, +// { +// } + +// #[cfg(feature = "HtmlBrElement")] +pub trait HtmlBrElement: + HtmlElement> +{ +} + +// #[cfg(feature = "HtmlBrElement")] +impl HtmlBrElement for T +where + T: HtmlElement, + T::DomNode: AsRef, +{ +} + +// #[cfg(feature = "HtmlButtonElement")] +pub trait HtmlButtonElement: + HtmlElement> +{ + /// See for more details + fn disabled(self, disable: bool) -> Attr { + Attr::new(self, "disabled".into(), disable.into_attr_value()) } } -macro_rules! impl_dom_interfaces_for_ty_helper { - ($dom_interface:ident, ($ty:ident, <$($additional_generic_var:ident,)*>, <$($additional_generic_var_on_ty:ident,)*>, {$($additional_generic_bounds:tt)*})) => { - $crate::interfaces::impl_dom_interfaces_for_ty_helper!($dom_interface, ($ty, $dom_interface, <$($additional_generic_var,)*>, <$($additional_generic_var_on_ty,)*>, {$($additional_generic_bounds)*})); - }; - ($dom_interface:ident, ($ty:ident, $bound_interface:ident, <$($additional_generic_var:ident,)*>, <$($additional_generic_var_on_ty:ident,)*>, {$($additional_generic_bounds:tt)*})) => { - impl $crate::interfaces::$dom_interface for $ty - where - E: $crate::interfaces::$bound_interface, - $($additional_generic_bounds)* - { - } - }; +// #[cfg(feature = "HtmlButtonElement")] +impl HtmlButtonElement for T +where + T: HtmlElement, + T::DomNode: AsRef, +{ } -pub(crate) use impl_dom_interfaces_for_ty_helper; +// #[cfg(feature = "HtmlCanvasElement")] +pub trait HtmlCanvasElement: + HtmlElement> +{ + /// See for more details + fn width(self, value: u32) -> Attr { + Attr::new(self, "width".into(), value.into_attr_value()) + } +} -/// Implement DOM interface traits for the given type and all descendent DOM interfaces, -/// such that every possible method defined on the underlying element is accessible via typing -/// The requires the type of signature Type, whereas T is the AppState type, A, is Action, and E is the underlying Element type that is composed -/// It additionally accepts generic vars (vars: ) that is added on the impl>, and vars_on_ty (Type>) and additional generic typebounds -macro_rules! impl_dom_interfaces_for_ty { - ($dom_interface:ident, $ty:ident) => { - $crate::interfaces::impl_dom_interfaces_for_ty!($dom_interface, $ty, vars: <>, vars_on_ty: <>, bounds: {}); - }; - ($dom_interface:ident, $ty:ident, vars: <$($additional_generic_var:ident,)*>, vars_on_ty: <$($additional_generic_var_on_ty:ident,)*>, bounds: {$($additional_generic_bounds:tt)*}) => { - paste::paste! { - $crate::interfaces::[]!( - $crate::interfaces::impl_dom_interfaces_for_ty_helper, - ($ty, $dom_interface, <$($additional_generic_var,)*>, <$($additional_generic_var_on_ty,)*>, {$($additional_generic_bounds)*}) - ); - $crate::interfaces::impl_dom_interfaces_for_ty_helper!($dom_interface, ($ty, $dom_interface, <$($additional_generic_var,)*>, <$($additional_generic_var_on_ty,)*>, {$($additional_generic_bounds)*})); - $crate::interfaces::[]!( - $crate::interfaces::impl_dom_interfaces_for_ty_helper, - ($ty, <$($additional_generic_var,)*>, <$($additional_generic_var_on_ty,)*>, {$($additional_generic_bounds)*}) - ); - } - }; +// #[cfg(feature = "HtmlCanvasElement")] +impl HtmlCanvasElement for T +where + T: HtmlElement, + T::DomNode: AsRef, +{ } -pub(crate) use impl_dom_interfaces_for_ty; - -dom_interface_macro_and_trait_definitions!( - HtmlElement { - methods: { - /// Set a style attribute - fn style(self, style: impl IntoStyles) -> Style { - let mut styles = vec![]; - style.into_styles(&mut styles); - Style { - element: self, - styles, - phantom: PhantomData, - } - } - }, - child_interfaces: { - HtmlAnchorElement { methods: {}, child_interfaces: {} }, - HtmlAreaElement { methods: {}, child_interfaces: {} }, - // HtmlBaseElement { methods: {}, child_interfaces: {} }, TODO include metadata? - // HtmlBodyElement { methods: {}, child_interfaces: {} }, TODO include body element? - HtmlBrElement { methods: {}, child_interfaces: {} }, - HtmlButtonElement { methods: {}, child_interfaces: {} }, - HtmlCanvasElement { - methods: { - fn width(self, value: u32) -> Attr { - self.attr("width", value) - } - fn height(self, value: u32) -> Attr { - self.attr("height", value) - } - }, - child_interfaces: {} - }, - HtmlDataElement { methods: {}, child_interfaces: {} }, - HtmlDataListElement { methods: {}, child_interfaces: {} }, - HtmlDetailsElement { methods: {}, child_interfaces: {} }, - HtmlDialogElement { methods: {}, child_interfaces: {} }, - // HtmlDirectoryElement { methods: {}, child_interfaces: {} }, deprecated - HtmlDivElement { methods: {}, child_interfaces: {} }, - HtmlDListElement { methods: {}, child_interfaces: {} }, - // HtmlUnknownElement { methods: {}, child_interfaces: {} }, useful at all? - HtmlEmbedElement { methods: {}, child_interfaces: {} }, - HtmlFieldSetElement { methods: {}, child_interfaces: {} }, - // HtmlFontElement { methods: {}, child_interfaces: {} }, deprecated - HtmlFormElement { methods: {}, child_interfaces: {} }, - // HtmlFrameElement { methods: {}, child_interfaces: {} }, deprecated - // HtmlFrameSetElement { methods: {}, child_interfaces: {} }, deprecacted - // HtmlHeadElement { methods: {}, child_interfaces: {} }, TODO include metadata? - HtmlHeadingElement { methods: {}, child_interfaces: {} }, - HtmlHrElement { methods: {}, child_interfaces: {} }, - // HtmlHtmlElement { methods: {}, child_interfaces: {} }, TODO include metadata? - HtmlIFrameElement { methods: {}, child_interfaces: {} }, - HtmlImageElement { methods: {}, child_interfaces: {} }, - HtmlInputElement { methods: {}, child_interfaces: {} }, - HtmlLabelElement { methods: {}, child_interfaces: {} }, - HtmlLegendElement { methods: {}, child_interfaces: {} }, - HtmlLiElement { methods: {}, child_interfaces: {} }, - HtmlLinkElement { methods: {}, child_interfaces: {} }, - HtmlMapElement { methods: {}, child_interfaces: {} }, - HtmlMediaElement { - methods: {}, - child_interfaces: { - HtmlAudioElement { methods: {}, child_interfaces: {} }, - HtmlVideoElement { - methods: { - fn width(self, value: u32) -> Attr { - self.attr("width", value) - } - fn height(self, value: u32) -> Attr { - self.attr("height", value) - } - }, - child_interfaces: {} - }, - } - }, - HtmlMenuElement { methods: {}, child_interfaces: {} }, - // HtmlMenuItemElement { methods: {}, child_interfaces: {} }, deprecated - // HtmlMetaElement { methods: {}, child_interfaces: {} }, TODO include metadata? - HtmlMeterElement { methods: {}, child_interfaces: {} }, - HtmlModElement { methods: {}, child_interfaces: {} }, - HtmlObjectElement { methods: {}, child_interfaces: {} }, - HtmlOListElement { methods: {}, child_interfaces: {} }, - HtmlOptGroupElement { methods: {}, child_interfaces: {} }, - HtmlOptionElement { methods: {}, child_interfaces: {} }, - HtmlOutputElement { methods: {}, child_interfaces: {} }, - HtmlParagraphElement { methods: {}, child_interfaces: {} }, - // HtmlParamElement { methods: {}, child_interfaces: {} }, deprecated - HtmlPictureElement { methods: {}, child_interfaces: {} }, - HtmlPreElement { methods: {}, child_interfaces: {} }, - HtmlProgressElement { methods: {}, child_interfaces: {} }, - HtmlQuoteElement { methods: {}, child_interfaces: {} }, - HtmlScriptElement { methods: {}, child_interfaces: {} }, - HtmlSelectElement { methods: {}, child_interfaces: {} }, - HtmlSlotElement { methods: {}, child_interfaces: {} }, - HtmlSourceElement { methods: {}, child_interfaces: {} }, - HtmlSpanElement { methods: {}, child_interfaces: {} }, - // HtmlStyleElement { methods: {}, child_interfaces: {} }, TODO include metadata? - HtmlTableCaptionElement { methods: {}, child_interfaces: {} }, - HtmlTableCellElement { methods: {}, child_interfaces: {} }, - HtmlTableColElement { methods: {}, child_interfaces: {} }, - HtmlTableElement { methods: {}, child_interfaces: {} }, - HtmlTableRowElement { methods: {}, child_interfaces: {} }, - HtmlTableSectionElement { methods: {}, child_interfaces: {} }, - HtmlTemplateElement { methods: {}, child_interfaces: {} }, - HtmlTimeElement { methods: {}, child_interfaces: {} }, - HtmlTextAreaElement { methods: {}, child_interfaces: {} }, - // HtmlTitleElement { methods: {}, child_interfaces: {} }, TODO include metadata? - HtmlTrackElement { methods: {}, child_interfaces: {} }, - HtmlUListElement { methods: {}, child_interfaces: {} }, - } - }, - SvgElement { - methods: { - /// Set a style attribute - fn style(self, style: impl IntoStyles) -> Style { - let mut styles = vec![]; - style.into_styles(&mut styles); - Style { - element: self, - styles, - phantom: PhantomData, - } - } - }, - child_interfaces: { - SvgAnimationElement { - methods: {}, - child_interfaces: { - SvgAnimateElement { methods: {}, child_interfaces: {} }, - SvgAnimateMotionElement { methods: {}, child_interfaces: {} }, - SvgAnimateTransformElement { methods: {}, child_interfaces: {} }, - SvgSetElement { methods: {}, child_interfaces: {} }, - } - }, - SvgClipPathElement { methods: {}, child_interfaces: {} }, - SvgComponentTransferFunctionElement { - methods: {}, - child_interfaces: { - SvgfeFuncAElement { methods: {}, child_interfaces: {} }, - SvgfeFuncBElement { methods: {}, child_interfaces: {} }, - SvgfeFuncGElement { methods: {}, child_interfaces: {} }, - SvgfeFuncRElement { methods: {}, child_interfaces: {} }, - } - }, - SvgDescElement { methods: {}, child_interfaces: {} }, - SvgFilterElement { methods: {}, child_interfaces: {} }, - SvgGradientElement { - methods: {}, - child_interfaces: { - SvgLinearGradientElement { methods: {}, child_interfaces: {} }, - SvgRadialGradientElement { methods: {}, child_interfaces: {} }, - } - }, - SvgGraphicsElement { - methods: {}, - child_interfaces: { - SvgDefsElement { methods: {}, child_interfaces: {} }, - SvgForeignObjectElement { methods: {}, child_interfaces: {} }, - SvgGeometryElement { - methods: { - fn stroke(self, brush: impl Into, style: peniko::kurbo::Stroke) -> crate::svg::Stroke { - crate::svg::stroke(self, brush, style) - } - }, - child_interfaces: { - SvgCircleElement { - methods: { - fn fill(self, brush: impl Into) -> crate::svg::Fill { - crate::svg::fill(self, brush) - } - }, - child_interfaces: {} - }, - SvgEllipseElement { - methods: { - fn fill(self, brush: impl Into) -> crate::svg::Fill { - crate::svg::fill(self, brush) - } - }, - child_interfaces: {} - }, - SvgLineElement { methods: {}, child_interfaces: {} }, - SvgPathElement { - methods: { - fn fill(self, brush: impl Into) -> crate::svg::Fill { - crate::svg::fill(self, brush) - } - }, - child_interfaces: {} - }, - SvgPolygonElement { - methods: { - fn fill(self, brush: impl Into) -> crate::svg::Fill { - crate::svg::fill(self, brush) - } - }, - child_interfaces: {} - }, - SvgPolylineElement { - methods: { - fn fill(self, brush: impl Into) -> crate::svg::Fill { - crate::svg::fill(self, brush) - } - }, - child_interfaces: {} - }, - SvgRectElement { - methods: { - fn fill(self, brush: impl Into) -> crate::svg::Fill { - crate::svg::fill(self, brush) - } - }, - child_interfaces: {} - }, - } - }, - SvgImageElement { methods: {}, child_interfaces: {} }, - SvgSwitchElement { methods: {}, child_interfaces: {} }, - SvgTextContentElement { - methods: { - fn fill(self, brush: impl Into) -> crate::svg::Fill { - crate::svg::fill(self, brush) - } - fn stroke(self, brush: impl Into, style: peniko::kurbo::Stroke) -> crate::svg::Stroke { - crate::svg::stroke(self, brush, style) - } - }, - child_interfaces: { - SvgTextPathElement { methods: {}, child_interfaces: {} }, - SvgTextPositioningElement { - methods: {}, - child_interfaces: { - SvgTextElement { methods: {}, child_interfaces: {} }, - SvgtSpanElement { methods: {}, child_interfaces: {} }, - } - }, - } - }, - SvgUseElement { methods: {}, child_interfaces: {} }, - SvgaElement { methods: {}, child_interfaces: {} }, - SvggElement { - methods: { - fn fill(self, brush: impl Into) -> crate::svg::Fill { - crate::svg::fill(self, brush) - } - fn stroke(self, brush: impl Into, style: peniko::kurbo::Stroke) -> crate::svg::Stroke { - crate::svg::stroke(self, brush, style) - } - }, - child_interfaces: {} - }, - SvgsvgElement { methods: {}, child_interfaces: {} }, - } - }, - SvgMarkerElement { methods: {}, child_interfaces: {} }, - SvgMaskElement { methods: {}, child_interfaces: {} }, - SvgMetadataElement { methods: {}, child_interfaces: {} }, - SvgPatternElement { methods: {}, child_interfaces: {} }, - SvgScriptElement { methods: {}, child_interfaces: {} }, - SvgStopElement { methods: {}, child_interfaces: {} }, - SvgStyleElement { methods: {}, child_interfaces: {} }, - SvgSymbolElement { methods: {}, child_interfaces: {} }, - SvgTitleElement { methods: {}, child_interfaces: {} }, - SvgViewElement { methods: {}, child_interfaces: {} }, - SvgfeBlendElement { methods: {}, child_interfaces: {} }, - SvgfeColorMatrixElement { methods: {}, child_interfaces: {} }, - SvgfeComponentTransferElement { methods: {}, child_interfaces: {} }, - SvgfeCompositeElement { methods: {}, child_interfaces: {} }, - SvgfeConvolveMatrixElement { methods: {}, child_interfaces: {} }, - SvgfeDiffuseLightingElement { methods: {}, child_interfaces: {} }, - SvgfeDisplacementMapElement { methods: {}, child_interfaces: {} }, - SvgfeDistantLightElement { methods: {}, child_interfaces: {} }, - SvgfeDropShadowElement { methods: {}, child_interfaces: {} }, - SvgfeFloodElement { methods: {}, child_interfaces: {} }, - SvgfeGaussianBlurElement { methods: {}, child_interfaces: {} }, - SvgfeImageElement { methods: {}, child_interfaces: {} }, - SvgfeMergeElement { methods: {}, child_interfaces: {} }, - SvgfeMergeNodeElement { methods: {}, child_interfaces: {} }, - SvgfeMorphologyElement { methods: {}, child_interfaces: {} }, - SvgfeOffsetElement { methods: {}, child_interfaces: {} }, - SvgfePointLightElement { methods: {}, child_interfaces: {} }, - SvgfeSpecularLightingElement { methods: {}, child_interfaces: {} }, - SvgfeSpotLightElement { methods: {}, child_interfaces: {} }, - SvgfeTileElement { methods: {}, child_interfaces: {} }, - SvgfeTurbulenceElement { methods: {}, child_interfaces: {} }, - SvgmPathElement { methods: {}, child_interfaces: {} }, - } - }, -); +// #[cfg(feature = "HtmlDataElement")] +pub trait HtmlDataElement: + HtmlElement> +{ +} -// Core View implementations +// #[cfg(feature = "HtmlDataElement")] +impl HtmlDataElement for T +where + T: HtmlElement, + T::DomNode: AsRef, +{ +} -impl sealed::Sealed - for crate::Adapt +// #[cfg(feature = "HtmlDataListElement")] +pub trait HtmlDataListElement: + HtmlElement> { } -impl sealed::Sealed for crate::AdaptState {} -macro_rules! impl_dom_traits_for_adapt_views { - ($dom_interface:ident, ()) => { - impl $dom_interface - for crate::Adapt - where - V: $dom_interface, - F: Fn( - &mut ParentT, - crate::AdaptThunk, - ) -> xilem_core::MessageResult, - { - } - impl $dom_interface - for crate::AdaptState - where - V: $dom_interface, - F: Fn(&mut ParentT) -> &mut ChildT, - { - } - }; +// #[cfg(feature = "HtmlDataListElement")] +impl HtmlDataListElement for T +where + T: HtmlElement, + T::DomNode: AsRef, +{ +} + +// #[cfg(feature = "HtmlDetailsElement")] +pub trait HtmlDetailsElement: + HtmlElement> +{ +} + +// #[cfg(feature = "HtmlDetailsElement")] +impl HtmlDetailsElement for T +where + T: HtmlElement, + T::DomNode: AsRef, +{ +} + +// #[cfg(feature = "HtmlDialogElement")] +pub trait HtmlDialogElement: + HtmlElement> +{ +} + +// #[cfg(feature = "HtmlDialogElement")] +impl HtmlDialogElement for T +where + T: HtmlElement, + T::DomNode: AsRef, +{ +} + +// #[cfg(feature = "HtmlDirectoryElement")] +// pub trait HtmlDirectoryElement: +// HtmlElement> +// { +// } + +// #[cfg(feature = "HtmlDirectoryElement")] +// impl HtmlDirectoryElement for T +// where +// T: HtmlElement, +// T::DomNode: AsRef, +// { +// } + +// #[cfg(feature = "HtmlDivElement")] +pub trait HtmlDivElement: + HtmlElement> +{ +} + +// #[cfg(feature = "HtmlDivElement")] +impl HtmlDivElement for T +where + T: HtmlElement, + T::DomNode: AsRef, +{ +} + +// #[cfg(feature = "HtmlDListElement")] +pub trait HtmlDListElement: + HtmlElement> +{ +} + +// #[cfg(feature = "HtmlDListElement")] +impl HtmlDListElement for T +where + T: HtmlElement, + T::DomNode: AsRef, +{ +} + +// #[cfg(feature = "HtmlElement")] +pub trait HtmlElement: + Element> +{ + /// Set a style attribute + fn style(self, style: impl IntoStyles) -> Style { + let mut styles = vec![]; + style.into_styles(&mut styles); + Style::new(self, styles) + } +} + +// #[cfg(feature = "HtmlElement")] +impl HtmlElement for T +where + T: Element, + T::DomNode: AsRef, + T::Props: WithStyle, +{ +} + +// #[cfg(feature = "HtmlUnknownElement")] +// pub trait HtmlUnknownElement: +// HtmlElement> +// { +// } + +// #[cfg(feature = "HtmlUnknownElement")] +// impl HtmlUnknownElement for T +// where +// T: HtmlElement, +// T::DomNode: AsRef, +// { +// } + +// #[cfg(feature = "HtmlEmbedElement")] +pub trait HtmlEmbedElement: + HtmlElement> +{ +} + +// #[cfg(feature = "HtmlEmbedElement")] +impl HtmlEmbedElement for T +where + T: HtmlElement, + T::DomNode: AsRef, +{ +} + +// #[cfg(feature = "HtmlFieldSetElement")] +pub trait HtmlFieldSetElement: + HtmlElement> +{ +} + +// #[cfg(feature = "HtmlFieldSetElement")] +impl HtmlFieldSetElement for T +where + T: HtmlElement, + T::DomNode: AsRef, +{ +} + +// #[cfg(feature = "HtmlFontElement")] +// pub trait HtmlFontElement: +// HtmlElement> +// { +// } + +// #[cfg(feature = "HtmlFontElement")] +// impl HtmlFontElement for T +// where +// T: HtmlElement, +// T::DomNode: AsRef, +// { +// } + +// #[cfg(feature = "HtmlFormElement")] +pub trait HtmlFormElement: + HtmlElement> +{ +} + +// #[cfg(feature = "HtmlFormElement")] +impl HtmlFormElement for T +where + T: HtmlElement, + T::DomNode: AsRef, +{ +} + +// #[cfg(feature = "HtmlFrameElement")] +// pub trait HtmlFrameElement: +// HtmlElement> +// { +// } + +// #[cfg(feature = "HtmlFrameElement")] +// impl HtmlFrameElement for T +// where +// T: HtmlElement, +// T::DomNode: AsRef, +// { +// } + +// #[cfg(feature = "HtmlFrameSetElement")] +// pub trait HtmlFrameSetElement: +// HtmlElement> +// { +// } + +// #[cfg(feature = "HtmlFrameSetElement")] +// impl HtmlFrameSetElement for T +// where +// T: HtmlElement, +// T::DomNode: AsRef, +// { +// } + +// #[cfg(feature = "HtmlHeadElement")] +// pub trait HtmlHeadElement: +// HtmlElement> +// { +// } + +// #[cfg(feature = "HtmlHeadElement")] +// impl HtmlHeadElement for T +// where +// T: HtmlElement, +// T::DomNode: AsRef, +// { +// } + +// #[cfg(feature = "HtmlHeadingElement")] +pub trait HtmlHeadingElement: + HtmlElement> +{ +} + +// #[cfg(feature = "HtmlHeadingElement")] +impl HtmlHeadingElement for T +where + T: HtmlElement, + T::DomNode: AsRef, +{ +} + +// #[cfg(feature = "HtmlHrElement")] +pub trait HtmlHrElement: + HtmlElement> +{ +} + +// #[cfg(feature = "HtmlHrElement")] +impl HtmlHrElement for T +where + T: HtmlElement, + T::DomNode: AsRef, +{ +} + +// #[cfg(feature = "HtmlHtmlElement")] +// pub trait HtmlHtmlElement: +// HtmlElement> +// { +// } + +// #[cfg(feature = "HtmlHtmlElement")] +// impl HtmlHtmlElement for T +// where +// T: HtmlElement, +// T::DomNode: AsRef, +// { +// } + +// #[cfg(feature = "HtmlIFrameElement")] +pub trait HtmlIFrameElement: + HtmlElement> +{ +} + +// #[cfg(feature = "HtmlIFrameElement")] +impl HtmlIFrameElement for T +where + T: HtmlElement, + T::DomNode: AsRef, +{ +} + +// #[cfg(feature = "HtmlImageElement")] +pub trait HtmlImageElement: + HtmlElement> +{ + /// See for more details + fn src(self, value: impl IntoAttributeValue) -> Attr { + Attr::new(self, Cow::from("src"), value.into_attr_value()) + } +} + +// #[cfg(feature = "HtmlImageElement")] +impl HtmlImageElement for T +where + T: HtmlElement, + T::DomNode: AsRef, +{ +} + +// #[cfg(feature = "HtmlInputElement")] +pub trait HtmlInputElement: + HtmlElement> +{ +} + +// #[cfg(feature = "HtmlInputElement")] +impl HtmlInputElement for T +where + T: HtmlElement, + T::DomNode: AsRef, +{ +} + +// #[cfg(feature = "HtmlLabelElement")] +pub trait HtmlLabelElement: + HtmlElement> +{ + /// The first element in the document with an id attribute matching the value of the for attribute is the labeled control for this label element — if the element with that id is actually a labelable element. + /// If it is not a labelable element, then the for attribute has no effect. + /// If there are other elements that also match the id value, later in the document, they are not considered. + /// + /// See for more details + // TODO different name? + fn for_(self, value: impl IntoAttributeValue) -> Attr { + Attr::new(self, Cow::from("for"), value.into_attr_value()) + } +} + +// #[cfg(feature = "HtmlLabelElement")] +impl HtmlLabelElement for T +where + T: HtmlElement, + T::DomNode: AsRef, +{ +} + +// #[cfg(feature = "HtmlLegendElement")] +pub trait HtmlLegendElement: + HtmlElement> +{ +} + +// #[cfg(feature = "HtmlLegendElement")] +impl HtmlLegendElement for T +where + T: HtmlElement, + T::DomNode: AsRef, +{ +} + +// #[cfg(feature = "HtmlLiElement")] +pub trait HtmlLiElement: + HtmlElement> +{ +} + +// #[cfg(feature = "HtmlLiElement")] +impl HtmlLiElement for T +where + T: HtmlElement, + T::DomNode: AsRef, +{ +} + +// #[cfg(feature = "HtmlLinkElement")] +pub trait HtmlLinkElement: + HtmlElement> +{ +} + +// #[cfg(feature = "HtmlLinkElement")] +impl HtmlLinkElement for T +where + T: HtmlElement, + T::DomNode: AsRef, +{ +} + +// #[cfg(feature = "HtmlMapElement")] +pub trait HtmlMapElement: + HtmlElement> +{ +} + +// #[cfg(feature = "HtmlMapElement")] +impl HtmlMapElement for T +where + T: HtmlElement, + T::DomNode: AsRef, +{ +} + +// #[cfg(feature = "HtmlMediaElement")] +pub trait HtmlMediaElement: + HtmlElement> +{ +} + +// #[cfg(feature = "HtmlMediaElement")] +impl HtmlMediaElement for T +where + T: HtmlElement, + T::DomNode: AsRef, +{ +} + +// #[cfg(feature = "HtmlMenuElement")] +pub trait HtmlMenuElement: + HtmlElement> +{ +} + +// #[cfg(feature = "HtmlMenuElement")] +impl HtmlMenuElement for T +where + T: HtmlElement, + T::DomNode: AsRef, +{ +} + +// #[cfg(feature = "HtmlMenuItemElement")] +// pub trait HtmlMenuItemElement: +// HtmlElement> +// { +// } + +// #[cfg(feature = "HtmlMenuItemElement")] +// impl HtmlMenuItemElement for T +// where +// T: HtmlElement, +// T::DomNode: AsRef, +// { +// } + +// #[cfg(feature = "HtmlMetaElement")] +// pub trait HtmlMetaElement: +// HtmlElement> +// { +// } + +// #[cfg(feature = "HtmlMetaElement")] +// impl HtmlMetaElement for T +// where +// T: HtmlElement, +// T::DomNode: AsRef, +// { +// } + +// #[cfg(feature = "HtmlMeterElement")] +pub trait HtmlMeterElement: + HtmlElement> +{ +} + +// #[cfg(feature = "HtmlMeterElement")] +impl HtmlMeterElement for T +where + T: HtmlElement, + T::DomNode: AsRef, +{ +} + +// #[cfg(feature = "HtmlModElement")] +pub trait HtmlModElement: + HtmlElement> +{ +} + +// #[cfg(feature = "HtmlModElement")] +impl HtmlModElement for T +where + T: HtmlElement, + T::DomNode: AsRef, +{ +} + +// #[cfg(feature = "HtmlObjectElement")] +pub trait HtmlObjectElement: + HtmlElement> +{ +} + +// #[cfg(feature = "HtmlObjectElement")] +impl HtmlObjectElement for T +where + T: HtmlElement, + T::DomNode: AsRef, +{ +} + +// #[cfg(feature = "HtmlOListElement")] +pub trait HtmlOListElement: + HtmlElement> +{ +} + +// #[cfg(feature = "HtmlOListElement")] +impl HtmlOListElement for T +where + T: HtmlElement, + T::DomNode: AsRef, +{ +} + +// #[cfg(feature = "HtmlOptGroupElement")] +pub trait HtmlOptGroupElement: + HtmlElement> +{ +} + +// #[cfg(feature = "HtmlOptGroupElement")] +impl HtmlOptGroupElement for T +where + T: HtmlElement, + T::DomNode: AsRef, +{ +} + +// #[cfg(feature = "HtmlOptionElement")] +pub trait HtmlOptionElement: + HtmlElement> +{ + /// A string representing the value of the HTMLOptionElement, i.e. the value attribute of the equivalent `
+", + include_str!("../README.md"), +)] + +use core::{ + Adapt, AdaptThunk, AnyElement, AnyView, MapAction, MapState, MessageResult, SuperElement, View, + ViewElement, +}; +use std::any::Any; +use wasm_bindgen::UnwrapThrowExt; +use web_sys::wasm_bindgen::JsCast; + +/// The HTML namespace +pub const HTML_NS: &str = "http://www.w3.org/1999/xhtml"; +/// The SVG namespace +pub const SVG_NS: &str = "http://www.w3.org/2000/svg"; +/// The MathML namespace +pub const MATHML_NS: &str = "http://www.w3.org/1998/Math/MathML"; mod app; mod attribute; mod attribute_value; mod class; mod context; -mod diff; -pub mod elements; -pub mod events; -pub mod interfaces; +mod element_props; +mod events; +mod message; mod one_of; mod optional_action; mod pointer; mod style; -pub mod svg; +mod text; +mod vec_splice; mod vecmap; -mod view; -mod view_ext; -extern crate xilem_web_core as xilem_core; - -pub use xilem_core::MessageResult; +pub mod elements; +pub mod interfaces; +pub mod svg; pub use app::App; -pub use attribute::Attr; +pub use attribute::{Attr, Attributes, ElementWithAttributes, WithAttributes}; pub use attribute_value::{AttributeValue, IntoAttributeValue}; -pub use context::{ChangeFlags, Cx}; -pub use one_of::{ - OneOf2, OneOf3, OneOf4, OneOf5, OneOf6, OneOf7, OneOf8, OneSeqOf2, OneSeqOf3, OneSeqOf4, - OneSeqOf5, OneSeqOf6, OneSeqOf7, OneSeqOf8, -}; +pub use class::{AsClassIter, Class, Classes, ElementWithClasses, WithClasses}; +pub use context::ViewCtx; +pub use element_props::ElementProps; +pub use message::{DynMessage, Message}; pub use optional_action::{Action, OptionalAction}; pub use pointer::{Pointer, PointerDetails, PointerMsg}; -pub use style::style; -pub use view::{ - memoize, static_view, Adapt, AdaptState, AdaptThunk, AnyView, BoxedView, ElementsSplice, - Memoize, MemoizeState, Pod, View, ViewMarker, ViewSequence, -}; -pub use view_ext::ViewExt; +pub use style::{style, ElementWithStyle, IntoStyles, Style, Styles, WithStyle}; +pub use xilem_core as core; -xilem_core::message!(); +/// A trait used for type erasure of [`DomNode`]s +/// It is e.g. used in [`AnyPod`] +pub trait AnyNode: AsRef + 'static { + fn as_any_mut(&mut self) -> &mut dyn Any; +} -/// The HTML namespace: `http://www.w3.org/1999/xhtml` -pub const HTML_NS: &str = "http://www.w3.org/1999/xhtml"; -/// The SVG namespace: `http://www.w3.org/2000/svg` -pub const SVG_NS: &str = "http://www.w3.org/2000/svg"; -/// The MathML namespace: `http://www.w3.org/1998/Math/MathML` -pub const MATHML_NS: &str = "http://www.w3.org/1998/Math/MathML"; +impl + Any> AnyNode for N { + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } +} -/// Helper to get the HTML document -pub fn document() -> web_sys::Document { - let window = web_sys::window().expect("no global `window` exists"); - window.document().expect("should have a document on window") +/// A trait to represent DOM nodes, which can optionally have associated `props` that are applied while building/rebuilding the views +pub trait DomNode

: AnyNode + 'static { + fn apply_props(&self, props: &mut P); +} + +/// Syntax sugar for adding a type bound on the `ViewElement` of a view, such that both, [`ViewElement`] and [`ViewElement::Mut`] have the same [`AsRef`] type +pub trait ElementAsRef: for<'a> ViewElement: AsRef> + AsRef {} + +impl ElementAsRef for T +where + T: ViewElement + AsRef, + for<'a> T::Mut<'a>: AsRef, +{ +} + +/// A view which can have any [`DomView`] type, see [`AnyView`] for more details. +pub type AnyDomView = dyn AnyView; + +/// The central [`View`] derived trait to represent DOM nodes in xilem_web, it's the base for all [`View`]s in xilem_web +pub trait DomView: + View> +{ + type DomNode: DomNode; + type Props; + + /// See [`adapt`](`core::adapt`) + fn adapt( + self, + f: ProxyFn, + ) -> Adapt + where + State: 'static, + Action: 'static, + ParentState: 'static, + ParentAction: 'static, + Self: Sized, + ProxyFn: Fn( + &mut ParentState, + AdaptThunk, + ) -> MessageResult + + 'static, + { + core::adapt(self, f) + } + + /// See [`map_state`](`core::map_state`) + fn map_state(self, f: F) -> MapState + where + State: 'static, + ParentState: 'static, + Self: Sized, + F: Fn(&mut ParentState) -> &mut State + 'static, + { + core::map_state(self, f) + } + + /// See [`map_action`](`core::map_action`) + fn map_action(self, f: F) -> MapAction + where + State: 'static, + ParentAction: 'static, + Action: 'static, + Self: Sized, + F: Fn(&mut State, Action) -> ParentAction + 'static, + { + core::map_action(self, f) + } +} + +impl DomView for V +where + V: View>, + W: DomNode

, +{ + type DomNode = W; + type Props = P; +} + +/// A container, which holds the actual DOM node, and associated props, such as attributes or classes. +/// These attributes are not directly set on the DOM node to avoid mutating or reading from the DOM tree unnecessarily, and to have more control over the whole update flow. +pub struct Pod { + pub node: E, + pub props: P, +} + +/// Type-erased [`Pod`], it's used for example as intermediate representation for children of a DOM node +pub type AnyPod = Pod>; + +impl, P: 'static> Pod { + pub fn into_dyn_node(node: E, mut props: P) -> AnyPod { + node.apply_props(&mut props); + Pod { + node: DynNode { + inner: Box::new(node), + }, + props: Box::new(props), + } + } +} + +impl, P: 'static> ViewElement for Pod { + type Mut<'a> = PodMut<'a, E, P>; +} + +impl, P: 'static> SuperElement> for AnyPod { + fn upcast(child: Pod) -> Self { + Pod::into_dyn_node(child.node, child.props) + } + + fn with_downcast_val( + mut this: Self::Mut<'_>, + f: impl FnOnce(PodMut<'_, E, P>) -> R, + ) -> (Self::Mut<'_>, R) { + let downcast = this.downcast(); + let ret = f(downcast); + (this, ret) + } +} + +impl, P: 'static> AnyElement> for AnyPod { + fn replace_inner(mut this: Self::Mut<'_>, child: Pod) -> Self::Mut<'_> { + Pod::replace_inner(&mut this, child); + this + } +} + +impl AnyPod { + pub(crate) fn replace_inner, P: 'static>( + this: &mut PodMut<'_, DynNode, Box>, + node: Pod, + ) { + this.node.inner = Box::new(node.node); + *this.props = Box::new(node.props); + } + + fn as_mut<'a>( + &'a mut self, + parent: &'a web_sys::Node, + was_removed: bool, + ) -> PodMut<'a, DynNode, Box> { + PodMut::new(&mut self.node, &mut self.props, parent, was_removed) + } +} + +/// A type erased DOM node, used in [`AnyPod`] +pub struct DynNode { + inner: Box, +} + +impl AsRef for DynNode { + fn as_ref(&self) -> &web_sys::Node { + (*self.inner).as_ref() + } +} + +impl DomNode> for DynNode { + fn apply_props(&self, _props: &mut Box) { + // TODO this is probably not optimal, as misleading, this is only implemented for concrete (non-type-erased) elements + // I do *think* it's necessary as method on the trait because of the Drop impl (and not having specialization there) + } +} + +/// The mutable representation of [`Pod`]. +/// This is a container which contains info of what has changed and provides mutable access to the underlying element and its props +/// When it's dropped all changes are applied to the underlying DOM node +pub struct PodMut<'a, E: DomNode

, P> { + node: &'a mut E, + props: &'a mut P, + parent: &'a web_sys::Node, + was_removed: bool, +} + +impl<'a, E: DomNode

, P> PodMut<'a, E, P> { + fn new( + node: &'a mut E, + props: &'a mut P, + parent: &'a web_sys::Node, + was_removed: bool, + ) -> PodMut<'a, E, P> { + PodMut { + node, + props, + parent, + was_removed, + } + } +} + +impl PodMut<'_, DynNode, Box> { + fn downcast, P: 'static>(&mut self) -> PodMut<'_, E, P> { + PodMut::new( + self.node.inner.as_any_mut().downcast_mut().unwrap(), + self.props.downcast_mut().unwrap(), + self.parent, + false, + ) + } +} + +impl, P> Drop for PodMut<'_, E, P> { + fn drop(&mut self) { + if !self.was_removed { + self.node.apply_props(self.props); + } + } +} + +impl + DomNode

, P> AsRef for Pod { + fn as_ref(&self) -> &T { + >::as_ref(&self.node) + } +} + +impl + DomNode

, P> AsRef for PodMut<'_, E, P> { + fn as_ref(&self) -> &T { + >::as_ref(self.node) + } +} + +impl DomNode for web_sys::Element { + fn apply_props(&self, props: &mut ElementProps) { + props.update_element(self); + } +} + +impl DomNode<()> for web_sys::Text { + fn apply_props(&self, (): &mut ()) {} } /// Helper to get the HTML document body element @@ -66,6 +311,13 @@ pub fn document_body() -> web_sys::HtmlElement { document().body().expect("HTML document missing body") } +/// Helper to get the HTML document +pub fn document() -> web_sys::Document { + let window = web_sys::window().expect("no global `window` exists"); + window.document().expect("should have a document on window") +} + +/// Helper to get a DOM element by id pub fn get_element_by_id(id: &str) -> web_sys::HtmlElement { document() .get_element_by_id(id) @@ -73,3 +325,169 @@ pub fn get_element_by_id(id: &str) -> web_sys::HtmlElement { .dyn_into() .unwrap() } + +// TODO specialize some of these elements, maybe via features? +macro_rules! impl_dom_node_for_elements { + ($($ty:ident, )*) => {$( + impl DomNode for web_sys::$ty { + fn apply_props(&self, props: &mut ElementProps) { + props.update_element(self); + } + } + + impl From> for Pod { + fn from(value: Pod) -> Self { + Self { + node: value.node.dyn_into().unwrap_throw(), + props: value.props, + } + } + } + )*}; +} + +impl_dom_node_for_elements!( + // Element, + HtmlElement, + HtmlAnchorElement, + HtmlAreaElement, + // HtmlBaseElement, TODO include metadata? + // HtmlBodyElement, TODO include body element? + HtmlBrElement, + HtmlButtonElement, + HtmlCanvasElement, + HtmlDataElement, + HtmlDataListElement, + HtmlDetailsElement, + HtmlDialogElement, + // HtmlDirectoryElement, deprecated + HtmlDivElement, + HtmlDListElement, + // HtmlUnknownElement, useful at all? + HtmlEmbedElement, + HtmlFieldSetElement, + // HtmlFontElement, deprecated + HtmlFormElement, + // HtmlFrameElement, deprecated + // HtmlFrameSetElement, deprecacted + // HtmlHeadElement, TODO include metadata? + HtmlHeadingElement, + HtmlHrElement, + // HtmlHtmlElement, TODO include metadata? + HtmlIFrameElement, + HtmlImageElement, + HtmlInputElement, + HtmlLabelElement, + HtmlLegendElement, + HtmlLiElement, + HtmlLinkElement, + HtmlMapElement, + HtmlMediaElement, + HtmlAudioElement, + HtmlVideoElement, + HtmlMenuElement, + // HtmlMenuItemElement, deprecated + // HtmlMetaElement, TODO include metadata? + HtmlMeterElement, + HtmlModElement, + HtmlObjectElement, + HtmlOListElement, + HtmlOptGroupElement, + HtmlOptionElement, + HtmlOutputElement, + HtmlParagraphElement, + // HtmlParamElement, deprecated + HtmlPictureElement, + HtmlPreElement, + HtmlProgressElement, + HtmlQuoteElement, + HtmlScriptElement, + HtmlSelectElement, + HtmlSlotElement, + HtmlSourceElement, + HtmlSpanElement, + // HtmlStyleElement, TODO include metadata? + HtmlTableCaptionElement, + HtmlTableCellElement, + HtmlTableColElement, + HtmlTableElement, + HtmlTableRowElement, + HtmlTableSectionElement, + HtmlTemplateElement, + HtmlTimeElement, + HtmlTextAreaElement, + // HtmlTitleElement, TODO include metadata? + HtmlTrackElement, + HtmlUListElement, + SvgElement, + SvgAnimationElement, + SvgAnimateElement, + SvgAnimateMotionElement, + SvgAnimateTransformElement, + SvgSetElement, + SvgClipPathElement, + SvgComponentTransferFunctionElement, + SvgfeFuncAElement, + SvgfeFuncBElement, + SvgfeFuncGElement, + SvgfeFuncRElement, + SvgDescElement, + SvgFilterElement, + SvgGradientElement, + SvgLinearGradientElement, + SvgRadialGradientElement, + SvgGraphicsElement, + SvgDefsElement, + SvgForeignObjectElement, + SvgGeometryElement, + SvgCircleElement, + SvgEllipseElement, + SvgLineElement, + SvgPathElement, + SvgPolygonElement, + SvgPolylineElement, + SvgRectElement, + SvgImageElement, + SvgSwitchElement, + SvgTextContentElement, + SvgTextPathElement, + SvgTextPositioningElement, + SvgTextElement, + SvgtSpanElement, + SvgUseElement, + SvgaElement, + SvggElement, + SvgsvgElement, + SvgMarkerElement, + SvgMaskElement, + SvgMetadataElement, + SvgPatternElement, + SvgScriptElement, + SvgStopElement, + SvgStyleElement, + SvgSymbolElement, + SvgTitleElement, + SvgViewElement, + SvgfeBlendElement, + SvgfeColorMatrixElement, + SvgfeComponentTransferElement, + SvgfeCompositeElement, + SvgfeConvolveMatrixElement, + SvgfeDiffuseLightingElement, + SvgfeDisplacementMapElement, + SvgfeDistantLightElement, + SvgfeDropShadowElement, + SvgfeFloodElement, + SvgfeGaussianBlurElement, + SvgfeImageElement, + SvgfeMergeElement, + SvgfeMergeNodeElement, + SvgfeMorphologyElement, + SvgfeOffsetElement, + SvgfePointLightElement, + SvgfeSpecularLightingElement, + SvgfeSpotLightElement, + SvgfeTileElement, + SvgfeTurbulenceElement, + SvgmPathElement, +); diff --git a/xilem_web/src/message.rs b/xilem_web/src/message.rs new file mode 100644 index 000000000..763422517 --- /dev/null +++ b/xilem_web/src/message.rs @@ -0,0 +1,72 @@ +// Copyright 2024 the Xilem Authors +// SPDX-License-Identifier: Apache-2.0 + +use std::{any::Any, fmt::Debug, ops::Deref}; + +/// A dynamically typed message for the [`View`] trait. +/// +/// Mostly equivalent to `Box`, but with support for debug printing. +// We can't use intra-doc links here because of rustdoc doesn't understand impls on `dyn Message` +/// The primary interface for this type is [`dyn Message::downcast`](trait.Message.html#method.downcast). +/// +/// [`View`]: crate::View +pub type DynMessage = Box; +/// Types which can be contained in a [`DynMessage`]. +// The `View` trait could have been made generic over the message type, +// primarily to enable flexibility around Send/Sync and avoid the need +// for allocation. +pub trait Message: 'static { + /// Convert `self` into a [`Box`]. + fn into_any(self: Box) -> Box; + /// Convert `self` into a [`Box`]. + fn as_any(&self) -> &(dyn Any); + /// Gets the debug representation of this message. + fn dyn_debug(&self) -> &dyn Debug; +} + +impl Message for T +where + T: Any + Debug, +{ + fn into_any(self: Box) -> Box { + self + } + fn as_any(&self) -> &dyn Any { + self + } + fn dyn_debug(&self) -> &dyn Debug { + self + } +} + +impl dyn Message { + /// Access the actual type of this [`DynMessage`]. + /// + /// In most cases, this will be unwrapped, as each [`View`](crate::View) will + /// coordinate with their runner and/or element type to only receive messages + /// of a single, expected, underlying type. + /// + /// ## Errors + /// + /// If the message contained within `self` is not of type `T`, returns `self` + /// (so that e.g. a different type can be used) + pub fn downcast(self: Box) -> Result, Box> { + // The panic is unreachable + #![allow(clippy::missing_panics_doc)] + if self.deref().as_any().is::() { + Ok(self + .into_any() + .downcast::() + .expect("`as_any` should correspond with `into_any`")) + } else { + Err(self) + } + } +} + +impl Debug for dyn Message { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + let inner = self.dyn_debug(); + f.debug_tuple("Message").field(&inner).finish() + } +} diff --git a/xilem_web/src/one_of.rs b/xilem_web/src/one_of.rs index 40b22809d..6334c9c89 100644 --- a/xilem_web/src/one_of.rs +++ b/xilem_web/src/one_of.rs @@ -1,288 +1,501 @@ -// Copyright 2023 the Xilem Authors +// Copyright 2024 the Xilem Authors // SPDX-License-Identifier: Apache-2.0 -use wasm_bindgen::throw_str; +use wasm_bindgen::UnwrapThrowExt; +use xilem_core::{ + one_of::{OneOf, OneOfCtx, PhantomElementCtx}, + Mut, +}; use crate::{ - interfaces::for_all_element_descendents, ChangeFlags, Cx, ElementsSplice, View, ViewMarker, - ViewSequence, + attribute::WithAttributes, class::WithClasses, style::WithStyle, AttributeValue, DomNode, Pod, + PodMut, ViewCtx, }; -macro_rules! impl_dom_traits { - ($dom_interface:ident, ($ident:ident: $($vars:ident),+)) => { - impl),+> $crate::interfaces::$dom_interface for $ident<$($vars),+> - where - $($vars: $crate::interfaces::$dom_interface,)+ - {} - }; -} +type CowStr = std::borrow::Cow<'static, str>; -macro_rules! one_of_view { - ( - #[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 + OneOfCtx< + Pod, + Pod, + Pod, + Pod, + Pod, + Pod, + Pod, + Pod, + Pod, + > for ViewCtx +where + P1: 'static, + P2: 'static, + P3: 'static, + P4: 'static, + P5: 'static, + P6: 'static, + P7: 'static, + P8: 'static, + P9: 'static, + N1: DomNode, + N2: DomNode, + N3: DomNode, + N4: DomNode, + N5: DomNode, + N6: DomNode, + N7: DomNode, + N8: DomNode, + N9: DomNode, +{ + type OneOfElement = + Pod, OneOf>; - impl<$($vars),+> crate::interfaces::sealed::Sealed for $ident<$($vars),+> {} - impl_dom_traits!(Element, ($ident: $($vars),+)); - for_all_element_descendents!(impl_dom_traits, ($ident: $($vars),+)); - - impl<$($vars),+> AsRef for $ident<$($vars),+> - where - $($vars: crate::view::DomNode,)+ - { - fn as_ref(&self) -> &web_sys::Node { - match self { - $( $ident::$vars(view) => view.as_node_ref(), )+ - } - } + fn upcast_one_of_element( + elem: OneOf< + Pod, + Pod, + Pod, + Pod, + Pod, + Pod, + Pod, + Pod, + Pod, + >, + ) -> Self::OneOfElement { + match elem { + OneOf::A(e) => Pod { + node: OneOf::A(e.node), + props: OneOf::A(e.props), + }, + OneOf::B(e) => Pod { + node: OneOf::B(e.node), + props: OneOf::B(e.props), + }, + OneOf::C(e) => Pod { + node: OneOf::C(e.node), + props: OneOf::C(e.props), + }, + OneOf::D(e) => Pod { + node: OneOf::D(e.node), + props: OneOf::D(e.props), + }, + OneOf::E(e) => Pod { + node: OneOf::E(e.node), + props: OneOf::E(e.props), + }, + OneOf::F(e) => Pod { + node: OneOf::F(e.node), + props: OneOf::F(e.props), + }, + OneOf::G(e) => Pod { + node: OneOf::G(e.node), + props: OneOf::G(e.props), + }, + OneOf::H(e) => Pod { + node: OneOf::H(e.node), + props: OneOf::H(e.props), + }, + OneOf::I(e) => Pod { + node: OneOf::I(e.node), + props: OneOf::I(e.props), + }, } - impl<$($vars),+> ViewMarker for $ident<$($vars),+> {} - - impl View for $ident<$($vars),+> - where - $($vars: View,)+ - { - type State = $ident<$($vars::State),+>; - type Element = $ident<$($vars::Element),+>; - - fn build(&self, cx: &mut Cx) -> (xilem_core::Id, Self::State, Self::Element) { - match self { - $( - $ident::$vars(view) => { - let (id, state, el) = view.build(cx); - (id, $ident::$vars(state), $ident::$vars(el)) - } - )+ - } - } - - fn rebuild( - &self, - cx: &mut Cx, - prev: &Self, - id: &mut xilem_core::Id, - state: &mut Self::State, - element: &mut Self::Element, - ) -> ChangeFlags { - match (prev, self) { - $( - // Variant is the same as before - ($ident::$vars(prev_view), $ident::$vars(view)) => { - let ($ident::$vars(state), $ident::$vars(element)) = (state, element) - else { - throw_str(concat!( - "invalid state/view in ", stringify!($ident), " (unreachable)", - )); - }; - view.rebuild(cx, prev_view, id, state, element) - } - // Variant has changed - (_, $ident::$vars(view)) => { - let (new_id, new_state, new_element) = view.build(cx); - *id = new_id; - *state = $ident::$vars(new_state); - *element = $ident::$vars(new_element); - ChangeFlags::STRUCTURE - } - )+ - } - } - - fn message( - &self, - id_path: &[xilem_core::Id], - state: &mut Self::State, - message: Box, - app_state: &mut VT, - ) -> xilem_core::MessageResult { - match self { - $( - $ident::$vars(view) => { - let $ident::$vars(state) = state else { - throw_str(concat!( - "invalid state/view in", stringify!($ident), "(unreachable)", - )); - }; - view.message(id_path, state, message, app_state) - } - )+ - } - } + } + + fn update_one_of_element_mut( + elem_mut: &mut Mut<'_, Self::OneOfElement>, + new_elem: OneOf< + Pod, + Pod, + Pod, + Pod, + Pod, + Pod, + Pod, + Pod, + Pod, + >, + ) { + let old_node: &web_sys::Node = elem_mut.node.as_ref(); + let new_node: &web_sys::Node = new_elem.as_ref(); + if old_node != new_node { + elem_mut + .parent + .replace_child(new_node, old_node) + .unwrap_throw(); } - }; -} + (*elem_mut.node, *elem_mut.props) = match new_elem { + OneOf::A(e) => (OneOf::A(e.node), OneOf::A(e.props)), + OneOf::B(e) => (OneOf::B(e.node), OneOf::B(e.props)), + OneOf::C(e) => (OneOf::C(e.node), OneOf::C(e.props)), + OneOf::D(e) => (OneOf::D(e.node), OneOf::D(e.props)), + OneOf::E(e) => (OneOf::E(e.node), OneOf::E(e.props)), + OneOf::F(e) => (OneOf::F(e.node), OneOf::F(e.props)), + OneOf::G(e) => (OneOf::G(e.node), OneOf::G(e.props)), + OneOf::H(e) => (OneOf::H(e.node), OneOf::H(e.props)), + OneOf::I(e) => (OneOf::I(e.node), OneOf::I(e.props)), + }; + } + + fn with_downcast_a( + elem: &mut Mut<'_, Self::OneOfElement>, + f: impl FnOnce(Mut<'_, Pod>), + ) { + let (OneOf::A(node), OneOf::A(props)) = (&mut elem.node, &mut elem.props) else { + unreachable!() + }; + f(PodMut::new(node, props, elem.parent, elem.was_removed)); + } + + fn with_downcast_b( + elem: &mut Mut<'_, Self::OneOfElement>, + f: impl FnOnce(Mut<'_, Pod>), + ) { + let (OneOf::B(node), OneOf::B(props)) = (&mut elem.node, &mut elem.props) else { + unreachable!() + }; + f(PodMut::new(node, props, elem.parent, elem.was_removed)); + } + + fn with_downcast_c( + elem: &mut Mut<'_, Self::OneOfElement>, + f: impl FnOnce(Mut<'_, Pod>), + ) { + let (OneOf::C(node), OneOf::C(props)) = (&mut elem.node, &mut elem.props) else { + unreachable!() + }; + f(PodMut::new(node, props, elem.parent, elem.was_removed)); + } + + fn with_downcast_d( + elem: &mut Mut<'_, Self::OneOfElement>, + f: impl FnOnce(Mut<'_, Pod>), + ) { + let (OneOf::D(node), OneOf::D(props)) = (&mut elem.node, &mut elem.props) else { + unreachable!() + }; + f(PodMut::new(node, props, elem.parent, elem.was_removed)); + } + + fn with_downcast_e( + elem: &mut Mut<'_, Self::OneOfElement>, + f: impl FnOnce(Mut<'_, Pod>), + ) { + let (OneOf::E(node), OneOf::E(props)) = (&mut elem.node, &mut elem.props) else { + unreachable!() + }; + f(PodMut::new(node, props, elem.parent, elem.was_removed)); + } -one_of_view! { - /// This view container can switch between two views. - OneOf2 { A, B } + fn with_downcast_f( + elem: &mut Mut<'_, Self::OneOfElement>, + f: impl FnOnce(Mut<'_, Pod>), + ) { + let (OneOf::F(node), OneOf::F(props)) = (&mut elem.node, &mut elem.props) else { + unreachable!() + }; + f(PodMut::new(node, props, elem.parent, elem.was_removed)); + } + + fn with_downcast_g( + elem: &mut Mut<'_, Self::OneOfElement>, + f: impl FnOnce(Mut<'_, Pod>), + ) { + let (OneOf::G(node), OneOf::G(props)) = (&mut elem.node, &mut elem.props) else { + unreachable!() + }; + f(PodMut::new(node, props, elem.parent, elem.was_removed)); + } + + fn with_downcast_h( + elem: &mut Mut<'_, Self::OneOfElement>, + f: impl FnOnce(Mut<'_, Pod>), + ) { + let (OneOf::H(node), OneOf::H(props)) = (&mut elem.node, &mut elem.props) else { + unreachable!() + }; + f(PodMut::new(node, props, elem.parent, elem.was_removed)); + } + + fn with_downcast_i( + elem: &mut Mut<'_, Self::OneOfElement>, + f: impl FnOnce(Mut<'_, Pod>), + ) { + let (OneOf::I(node), OneOf::I(props)) = (&mut elem.node, &mut elem.props) else { + unreachable!() + }; + f(PodMut::new(node, props, elem.parent, elem.was_removed)); + } } -one_of_view! { - /// This view container can switch between three views. - OneOf3 { A, B, C } + +pub enum Noop {} + +impl PhantomElementCtx for ViewCtx { + type PhantomElement = Pod; } -one_of_view! { - /// This view container can switch between four views. - OneOf4 { A, B, C, D } +impl WithAttributes for Noop { + fn start_attribute_modifier(&mut self) { + unreachable!() + } + + fn end_attribute_modifier(&mut self) { + unreachable!() + } + + fn set_attribute(&mut self, _name: CowStr, _value: Option) { + unreachable!() + } } -one_of_view! { - /// This view container can switch between five views. - OneOf5 { A, B, C, D, E } +impl WithClasses for Noop { + fn start_class_modifier(&mut self) { + unreachable!() + } + + fn add_class(&mut self, _class_name: CowStr) { + unreachable!() + } + + fn remove_class(&mut self, _class_name: CowStr) { + unreachable!() + } + + fn end_class_modifier(&mut self) { + unreachable!() + } } -one_of_view! { - /// This view container can switch between six views. - OneOf6 { A, B, C, D, E, F } +impl WithStyle for Noop { + fn start_style_modifier(&mut self) { + unreachable!() + } + + fn set_style(&mut self, _name: CowStr, _value: Option) { + unreachable!() + } + + fn end_style_modifier(&mut self) { + unreachable!() + } } -one_of_view! { - /// This view container can switch between seven views. - OneOf7 { A, B, C, D, E, F, G } +impl AsRef for Noop { + fn as_ref(&self) -> &T { + unreachable!() + } } -one_of_view! { - /// This view container can switch between eight views. - OneOf8 { A, B, C, D, E, F, G, H } +impl

DomNode

for Noop { + fn apply_props(&self, _props: &mut P) { + unreachable!() + } } -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< + E1: WithAttributes, + E2: WithAttributes, + E3: WithAttributes, + E4: WithAttributes, + E5: WithAttributes, + E6: WithAttributes, + E7: WithAttributes, + E8: WithAttributes, + E9: WithAttributes, + > WithAttributes for OneOf +{ + fn start_attribute_modifier(&mut self) { + match self { + OneOf::A(e) => e.start_attribute_modifier(), + OneOf::B(e) => e.start_attribute_modifier(), + OneOf::C(e) => e.start_attribute_modifier(), + OneOf::D(e) => e.start_attribute_modifier(), + OneOf::E(e) => e.start_attribute_modifier(), + OneOf::F(e) => e.start_attribute_modifier(), + OneOf::G(e) => e.start_attribute_modifier(), + OneOf::H(e) => e.start_attribute_modifier(), + OneOf::I(e) => e.start_attribute_modifier(), } - impl ViewSequence for $ident<$($vars),+> - where $( - $vars: ViewSequence, - )+ { - type State = $ident<$($vars::State),+>; - - fn build(&self, cx: &mut Cx, elements: &mut dyn ElementsSplice) -> Self::State { - match self { - $( - $ident::$vars(view_sequence) => { - $ident::$vars(view_sequence.build(cx, elements)) - } - )+ - } - } - - fn rebuild( - &self, - cx: &mut Cx, - prev: &Self, - state: &mut Self::State, - elements: &mut dyn ElementsSplice, - ) -> ChangeFlags { - match (prev, self) { - $( - // Variant is the same as before - ($ident::$vars(prev_view), $ident::$vars(view_sequence)) => { - let $ident::$vars(state) = state else { - throw_str(concat!( - "invalid state/view_sequence in ", - stringify!($ident), - " (unreachable)", - )); - }; - view_sequence.rebuild(cx, prev_view, state, elements) - } - // Variant has changed - (_, $ident::$vars(view_sequence)) => { - let new_state = view_sequence.build(cx, elements); - *state = $ident::$vars(new_state); - ChangeFlags::STRUCTURE - } - )+ - } - } - - fn message( - &self, - id_path: &[xilem_core::Id], - state: &mut Self::State, - message: Box, - app_state: &mut VT, - ) -> xilem_core::MessageResult { - match self { - $( - $ident::$vars(view_sequence) => { - let $ident::$vars(state) = state else { - throw_str(concat!( - "invalid state/view_sequence in ", - stringify!($ident), - " (unreachable)", - )); - }; - view_sequence.message(id_path, state, message, app_state) - } - )+ - } - } - - fn count(&self, state: &Self::State) -> usize { - match self { - $( - $ident::$vars(view_sequence) => { - let $ident::$vars(state) = state else { - throw_str(concat!( - "invalid state/view_sequence in ", - stringify!($ident), - " (unreachable)", - )); - }; - view_sequence.count(state) - } - )+ - } - } + } + + fn end_attribute_modifier(&mut self) { + match self { + OneOf::A(e) => e.end_attribute_modifier(), + OneOf::B(e) => e.end_attribute_modifier(), + OneOf::C(e) => e.end_attribute_modifier(), + OneOf::D(e) => e.end_attribute_modifier(), + OneOf::E(e) => e.end_attribute_modifier(), + OneOf::F(e) => e.end_attribute_modifier(), + OneOf::G(e) => e.end_attribute_modifier(), + OneOf::H(e) => e.end_attribute_modifier(), + OneOf::I(e) => e.end_attribute_modifier(), } - }; -} + } -one_of_sequence! { - /// This view sequence container can switch between two view sequences. - OneSeqOf2 { A, B } -} -one_of_sequence! { - /// This view sequence container can switch between three view sequences. - OneSeqOf3 { A, B, C } + fn set_attribute(&mut self, name: CowStr, value: Option) { + match self { + OneOf::A(e) => e.set_attribute(name, value), + OneOf::B(e) => e.set_attribute(name, value), + OneOf::C(e) => e.set_attribute(name, value), + OneOf::D(e) => e.set_attribute(name, value), + OneOf::E(e) => e.set_attribute(name, value), + OneOf::F(e) => e.set_attribute(name, value), + OneOf::G(e) => e.set_attribute(name, value), + OneOf::H(e) => e.set_attribute(name, value), + OneOf::I(e) => e.set_attribute(name, value), + } + } } -one_of_sequence! { - /// This view sequence container can switch between four view sequences. - OneSeqOf4 { A, B, C, D } -} +impl< + E1: WithClasses, + E2: WithClasses, + E3: WithClasses, + E4: WithClasses, + E5: WithClasses, + E6: WithClasses, + E7: WithClasses, + E8: WithClasses, + E9: WithClasses, + > WithClasses for OneOf +{ + fn start_class_modifier(&mut self) { + match self { + OneOf::A(e) => e.start_class_modifier(), + OneOf::B(e) => e.start_class_modifier(), + OneOf::C(e) => e.start_class_modifier(), + OneOf::D(e) => e.start_class_modifier(), + OneOf::E(e) => e.start_class_modifier(), + OneOf::F(e) => e.start_class_modifier(), + OneOf::G(e) => e.start_class_modifier(), + OneOf::H(e) => e.start_class_modifier(), + OneOf::I(e) => e.start_class_modifier(), + } + } -one_of_sequence! { - /// This view sequence container can switch between five view sequences. - OneSeqOf5 { A, B, C, D, E } -} + fn add_class(&mut self, class_name: CowStr) { + match self { + OneOf::A(e) => e.add_class(class_name), + OneOf::B(e) => e.add_class(class_name), + OneOf::C(e) => e.add_class(class_name), + OneOf::D(e) => e.add_class(class_name), + OneOf::E(e) => e.add_class(class_name), + OneOf::F(e) => e.add_class(class_name), + OneOf::G(e) => e.add_class(class_name), + OneOf::H(e) => e.add_class(class_name), + OneOf::I(e) => e.add_class(class_name), + } + } -one_of_sequence! { - /// This view sequence container can switch between six view sequences. - OneSeqOf6 { A, B, C, D, E, F } + fn remove_class(&mut self, class_name: CowStr) { + match self { + OneOf::A(e) => e.remove_class(class_name), + OneOf::B(e) => e.remove_class(class_name), + OneOf::C(e) => e.remove_class(class_name), + OneOf::D(e) => e.remove_class(class_name), + OneOf::E(e) => e.remove_class(class_name), + OneOf::F(e) => e.remove_class(class_name), + OneOf::G(e) => e.remove_class(class_name), + OneOf::H(e) => e.remove_class(class_name), + OneOf::I(e) => e.remove_class(class_name), + } + } + + fn end_class_modifier(&mut self) { + match self { + OneOf::A(e) => e.end_class_modifier(), + OneOf::B(e) => e.end_class_modifier(), + OneOf::C(e) => e.end_class_modifier(), + OneOf::D(e) => e.end_class_modifier(), + OneOf::E(e) => e.end_class_modifier(), + OneOf::F(e) => e.end_class_modifier(), + OneOf::G(e) => e.end_class_modifier(), + OneOf::H(e) => e.end_class_modifier(), + OneOf::I(e) => e.end_class_modifier(), + } + } } -one_of_sequence! { - /// This view sequence container can switch between seven view sequences. - OneSeqOf7 { A, B, C, D, E, F, G } +impl< + E1: WithStyle, + E2: WithStyle, + E3: WithStyle, + E4: WithStyle, + E5: WithStyle, + E6: WithStyle, + E7: WithStyle, + E8: WithStyle, + E9: WithStyle, + > WithStyle for OneOf +{ + fn start_style_modifier(&mut self) { + match self { + OneOf::A(e) => e.start_style_modifier(), + OneOf::B(e) => e.start_style_modifier(), + OneOf::C(e) => e.start_style_modifier(), + OneOf::D(e) => e.start_style_modifier(), + OneOf::E(e) => e.start_style_modifier(), + OneOf::F(e) => e.start_style_modifier(), + OneOf::G(e) => e.start_style_modifier(), + OneOf::H(e) => e.start_style_modifier(), + OneOf::I(e) => e.start_style_modifier(), + } + } + + fn set_style(&mut self, name: CowStr, value: Option) { + match self { + OneOf::A(e) => e.set_style(name, value), + OneOf::B(e) => e.set_style(name, value), + OneOf::C(e) => e.set_style(name, value), + OneOf::D(e) => e.set_style(name, value), + OneOf::E(e) => e.set_style(name, value), + OneOf::F(e) => e.set_style(name, value), + OneOf::G(e) => e.set_style(name, value), + OneOf::H(e) => e.set_style(name, value), + OneOf::I(e) => e.set_style(name, value), + } + } + + fn end_style_modifier(&mut self) { + match self { + OneOf::A(e) => e.end_style_modifier(), + OneOf::B(e) => e.end_style_modifier(), + OneOf::C(e) => e.end_style_modifier(), + OneOf::D(e) => e.end_style_modifier(), + OneOf::E(e) => e.end_style_modifier(), + OneOf::F(e) => e.end_style_modifier(), + OneOf::G(e) => e.end_style_modifier(), + OneOf::H(e) => e.end_style_modifier(), + OneOf::I(e) => e.end_style_modifier(), + } + } } -one_of_sequence! { - /// This view sequence container can switch between eight view sequences. - OneSeqOf8 { A, B, C, D, E, F, G, H } +impl + DomNode> for OneOf +where + E1: DomNode, + E2: DomNode, + E3: DomNode, + E4: DomNode, + E5: DomNode, + E6: DomNode, + E7: DomNode, + E8: DomNode, + E9: DomNode, +{ + fn apply_props(&self, props: &mut OneOf) { + match (self, props) { + (OneOf::A(el), OneOf::A(props)) => el.apply_props(props), + (OneOf::B(el), OneOf::B(props)) => el.apply_props(props), + (OneOf::C(el), OneOf::C(props)) => el.apply_props(props), + (OneOf::D(el), OneOf::D(props)) => el.apply_props(props), + (OneOf::E(el), OneOf::E(props)) => el.apply_props(props), + (OneOf::F(el), OneOf::F(props)) => el.apply_props(props), + (OneOf::G(el), OneOf::G(props)) => el.apply_props(props), + (OneOf::H(el), OneOf::H(props)) => el.apply_props(props), + (OneOf::I(el), OneOf::I(props)) => el.apply_props(props), + _ => unreachable!(), + } + } } diff --git a/xilem_web/src/pointer.rs b/xilem_web/src/pointer.rs index 527485642..357d9ba6e 100644 --- a/xilem_web/src/pointer.rs +++ b/xilem_web/src/pointer.rs @@ -3,19 +3,16 @@ //! Interactivity with pointer events. -use std::{any::Any, marker::PhantomData}; +use std::marker::PhantomData; -use wasm_bindgen::{prelude::Closure, JsCast}; +use wasm_bindgen::{prelude::Closure, throw_str, JsCast, UnwrapThrowExt}; use web_sys::PointerEvent; -use xilem_core::{Id, MessageResult}; +use xilem_core::{MessageResult, Mut, View, ViewId, ViewPathTracker}; -use crate::{ - context::{ChangeFlags, Cx}, - interfaces::Element, - view::{DomNode, View, ViewMarker}, -}; +use crate::{interfaces::Element, DynMessage, ElementAsRef, ViewCtx}; +/// A view that allows stateful handling of PointerEvents with [`PointerMsg`] pub struct Pointer { child: V, callback: F, @@ -72,86 +69,107 @@ pub fn pointer>( } } -crate::interfaces::impl_dom_interfaces_for_ty!( - Element, - Pointer, - vars: , - vars_on_ty: , - bounds: { - F: Fn(&mut T, PointerMsg) -> A, - } -); - -impl ViewMarker for Pointer {} -impl crate::interfaces::sealed::Sealed for Pointer {} - -impl A, V: View> View for Pointer { - type State = PointerState; +impl View + for Pointer +where + State: 'static, + Action: 'static, + Callback: Fn(&mut State, PointerMsg) -> Action + 'static, + V: View, + V::Element: ElementAsRef, +{ + type ViewState = PointerState; 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 el = element.as_node_ref().dyn_ref::().unwrap(); - let el_clone = el.clone(); - let down_closure = Closure::new(move |e: PointerEvent| { - thunk.push_message(PointerMsg::Down(PointerDetails::from_pointer_event(&e))); - el_clone.set_pointer_capture(e.pointer_id()).unwrap(); - e.prevent_default(); - e.stop_propagation(); - }); - el.add_event_listener_with_callback("pointerdown", down_closure.as_ref().unchecked_ref()) + fn build(&self, ctx: &mut ViewCtx) -> (Self::Element, Self::ViewState) { + ctx.with_id(ViewId::new(0), |ctx| { + let (element, child_state) = self.child.build(ctx); + let thunk = ctx.message_thunk(); + let el = element.as_ref().dyn_ref::().unwrap(); + let el_clone = el.clone(); + let down_closure = Closure::new(move |e: PointerEvent| { + thunk.push_message(PointerMsg::Down(PointerDetails::from_pointer_event(&e))); + el_clone.set_pointer_capture(e.pointer_id()).unwrap(); + e.prevent_default(); + e.stop_propagation(); + }); + el.add_event_listener_with_callback( + "pointerdown", + down_closure.as_ref().unchecked_ref(), + ) .unwrap(); - let thunk = cx.with_id(id, |cx| cx.message_thunk()); - let move_closure = Closure::new(move |e: PointerEvent| { - thunk.push_message(PointerMsg::Move(PointerDetails::from_pointer_event(&e))); - e.prevent_default(); - e.stop_propagation(); - }); - el.add_event_listener_with_callback("pointermove", move_closure.as_ref().unchecked_ref()) + let thunk = ctx.message_thunk(); + let move_closure = Closure::new(move |e: PointerEvent| { + thunk.push_message(PointerMsg::Move(PointerDetails::from_pointer_event(&e))); + e.prevent_default(); + e.stop_propagation(); + }); + el.add_event_listener_with_callback( + "pointermove", + move_closure.as_ref().unchecked_ref(), + ) .unwrap(); - let thunk = cx.with_id(id, |cx| cx.message_thunk()); - let up_closure = Closure::new(move |e: PointerEvent| { - thunk.push_message(PointerMsg::Up(PointerDetails::from_pointer_event(&e))); - e.prevent_default(); - e.stop_propagation(); - }); - el.add_event_listener_with_callback("pointerup", up_closure.as_ref().unchecked_ref()) - .unwrap(); - let state = PointerState { - down_closure, - move_closure, - up_closure, - child_state, - }; - (id, state, element) + let thunk = ctx.message_thunk(); + let up_closure = Closure::new(move |e: PointerEvent| { + thunk.push_message(PointerMsg::Up(PointerDetails::from_pointer_event(&e))); + e.prevent_default(); + e.stop_propagation(); + }); + el.add_event_listener_with_callback("pointerup", up_closure.as_ref().unchecked_ref()) + .unwrap(); + let state = PointerState { + down_closure, + move_closure, + up_closure, + child_state, + }; + (element, state) + }) } - fn rebuild( + fn rebuild<'el>( &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 + view_state: &mut Self::ViewState, + ctx: &mut ViewCtx, + element: Mut<'el, Self::Element>, + ) -> Mut<'el, Self::Element> { + ctx.with_id(ViewId::new(0), |ctx| { + self.child + .rebuild(&prev.child, &mut view_state.child_state, ctx, element) + }) + } + + fn teardown( + &self, + view_state: &mut Self::ViewState, + ctx: &mut ViewCtx, + element: Mut<'_, Self::Element>, + ) { + // TODO remove event listeners from child or is this not necessary? self.child - .rebuild(cx, &prev.child, id, &mut state.child_state, element) + .teardown(&mut view_state.child_state, ctx, element); } fn message( &self, - id_path: &[Id], - state: &mut Self::State, - message: Box, - app_state: &mut T, - ) -> MessageResult { - match message.downcast() { - Ok(msg) => MessageResult::Action((self.callback)(app_state, *msg)), - Err(message) => self - .child - .message(id_path, &mut state.child_state, message, app_state), + view_state: &mut Self::ViewState, + id_path: &[ViewId], + message: DynMessage, + app_state: &mut State, + ) -> MessageResult { + let Some((first, remainder)) = id_path.split_first() else { + throw_str("Parent view of `Pointer` sent outdated and/or incorrect empty view path"); + }; + if first.routing_id() != 0 { + throw_str("Parent view of `Pointer` sent outdated and/or incorrect empty view path"); + } + if remainder.is_empty() { + let msg = message.downcast().unwrap_throw(); + MessageResult::Action((self.callback)(app_state, *msg)) + } else { + self.child + .message(&mut view_state.child_state, remainder, message, app_state) } } } diff --git a/xilem_web/src/style.rs b/xilem_web/src/style.rs index deccdf67c..7f792e413 100644 --- a/xilem_web/src/style.rs +++ b/xilem_web/src/style.rs @@ -1,19 +1,20 @@ // Copyright 2024 the Xilem Authors // SPDX-License-Identifier: Apache-2.0 -use std::collections::BTreeMap; -use std::marker::PhantomData; -use std::{borrow::Cow, collections::HashMap}; +use std::{ + collections::{BTreeMap, HashMap}, + marker::PhantomData, +}; +use wasm_bindgen::{JsCast, UnwrapThrowExt}; +use xilem_core::{MessageResult, Mut, View, ViewElement, ViewId}; -use xilem_core::{Id, MessageResult}; +use crate::{vecmap::VecMap, DomNode, DynMessage, ElementProps, Pod, PodMut, ViewCtx}; -use crate::{interfaces::sealed::Sealed, ChangeFlags, Cx, View, ViewMarker}; - -use super::interfaces::Element; +type CowStr = std::borrow::Cow<'static, str>; /// A trait to make the class adding functions generic over collection type pub trait IntoStyles { - fn into_styles(self, styles: &mut Vec<(Cow<'static, str>, Cow<'static, str>)>); + fn into_styles(self, styles: &mut Vec<(CowStr, CowStr)>); } struct StyleTuple(T1, T2); @@ -21,18 +22,18 @@ struct StyleTuple(T1, T2); /// Create a style from a style name and its value. pub fn style(name: T1, value: T2) -> impl IntoStyles where - T1: Into>, - T2: Into>, + T1: Into, + T2: Into, { StyleTuple(name, value) } impl IntoStyles for StyleTuple where - T1: Into>, - T2: Into>, + T1: Into, + T2: Into, { - fn into_styles(self, styles: &mut Vec<(Cow<'static, str>, Cow<'static, str>)>) { + fn into_styles(self, styles: &mut Vec<(CowStr, CowStr)>) { let StyleTuple(key, value) = self; styles.push((key.into(), value.into())); } @@ -42,7 +43,7 @@ impl IntoStyles for Option where T: IntoStyles, { - fn into_styles(self, styles: &mut Vec<(Cow<'static, str>, Cow<'static, str>)>) { + fn into_styles(self, styles: &mut Vec<(CowStr, CowStr)>) { if let Some(t) = self { t.into_styles(styles); } @@ -53,7 +54,7 @@ impl IntoStyles for Vec where T: IntoStyles, { - fn into_styles(self, styles: &mut Vec<(Cow<'static, str>, Cow<'static, str>)>) { + fn into_styles(self, styles: &mut Vec<(CowStr, CowStr)>) { for itm in self { itm.into_styles(styles); } @@ -61,7 +62,7 @@ where } impl IntoStyles for [T; N] { - fn into_styles(self, styles: &mut Vec<(Cow<'static, str>, Cow<'static, str>)>) { + fn into_styles(self, styles: &mut Vec<(CowStr, CowStr)>) { for itm in self { itm.into_styles(styles); } @@ -70,10 +71,10 @@ impl IntoStyles for [T; N] { impl IntoStyles for HashMap where - T1: Into>, - T2: Into>, + T1: Into, + T2: Into, { - fn into_styles(self, styles: &mut Vec<(Cow<'static, str>, Cow<'static, str>)>) { + fn into_styles(self, styles: &mut Vec<(CowStr, CowStr)>) { for (key, value) in self { styles.push((key.into(), value.into())); } @@ -82,82 +83,274 @@ where impl IntoStyles for BTreeMap where - T1: Into>, - T2: Into>, + T1: Into, + T2: Into, +{ + fn into_styles(self, styles: &mut Vec<(CowStr, CowStr)>) { + for (key, value) in self { + styles.push((key.into(), value.into())); + } + } +} + +impl IntoStyles for VecMap +where + T1: Into, + T2: Into, { - fn into_styles(self, styles: &mut Vec<(Cow<'static, str>, Cow<'static, str>)>) { + fn into_styles(self, styles: &mut Vec<(CowStr, CowStr)>) { for (key, value) in self { styles.push((key.into(), value.into())); } } } -macro_rules! impl_tuple_intostyles { - ($($name:ident : $type:ident),* $(,)?) => { - impl<$($type),*> IntoStyles for ($($type,)*) - where - $($type: IntoStyles),* - { - #[allow(unused_variables)] - fn into_styles(self, styles: &mut Vec<(Cow<'static, str>, Cow<'static, str>)>) { - let ($($name,)*) = self; - $( - $name.into_styles(styles); - )* +pub trait WithStyle { + fn start_style_modifier(&mut self); + fn end_style_modifier(&mut self); + fn set_style(&mut self, name: CowStr, value: Option); + // TODO first find a use-case for this... + // fn get_attr(&self, name: &str) -> Option<&CowStr>; +} + +#[derive(Debug, PartialEq)] +enum StyleModifier { + Remove(CowStr), + Set(CowStr, CowStr), + EndMarker(usize), +} + +#[derive(Debug, Default)] +pub struct Styles { + style_modifiers: Vec, + updated_styles: VecMap, + idx: usize, // To save some memory, this could be u16 or even u8 (but this is risky) + start_idx: usize, // same here + /// a flag necessary, such that `start_style_modifier` doesn't always overwrite the last changes in `View::build` + build_finished: bool, +} + +fn set_style(element: &web_sys::Element, name: &str, value: &str) { + if let Some(el) = element.dyn_ref::() { + el.style().set_property(name, value).unwrap_throw(); + } else if let Some(el) = element.dyn_ref::() { + el.style().set_property(name, value).unwrap_throw(); + } +} + +fn remove_style(element: &web_sys::Element, name: &str) { + if let Some(el) = element.dyn_ref::() { + el.style().remove_property(name).unwrap_throw(); + } else if let Some(el) = element.dyn_ref::() { + el.style().remove_property(name).unwrap_throw(); + } +} + +impl Styles { + pub fn apply_style_changes(&mut self, element: &web_sys::Element) { + if !self.updated_styles.is_empty() { + for modifier in self.style_modifiers.iter().rev() { + match modifier { + StyleModifier::Remove(name) => { + if self.updated_styles.contains_key(name) { + self.updated_styles.remove(name); + remove_style(element, name); + } + } + StyleModifier::Set(name, value) => { + if self.updated_styles.contains_key(name) { + self.updated_styles.remove(name); + set_style(element, name, value); + } + } + StyleModifier::EndMarker(_) => (), + } + } + debug_assert!(self.updated_styles.is_empty()); + } + self.build_finished = true; + } +} + +impl WithStyle for Styles { + fn set_style(&mut self, name: CowStr, value: Option) { + let new_modifier = if let Some(value) = value { + StyleModifier::Set(name.clone(), value) + } else { + StyleModifier::Remove(name.clone()) + }; + + if let Some(modifier) = self.style_modifiers.get_mut(self.idx) { + if modifier != &new_modifier { + if let StyleModifier::Remove(previous_name) | StyleModifier::Set(previous_name, _) = + modifier + { + if &name != previous_name { + self.updated_styles.insert(previous_name.clone(), ()); + } + } + self.updated_styles.insert(name, ()); + *modifier = new_modifier; + } + // else remove it out of updated_styles? (because previous styles are overwritten) not sure if worth it because potentially worse perf + } else { + self.updated_styles.insert(name, ()); + self.style_modifiers.push(new_modifier); + } + self.idx += 1; + } + + fn start_style_modifier(&mut self) { + if self.build_finished { + if self.idx == 0 { + self.start_idx = 0; + } else { + let StyleModifier::EndMarker(start_idx) = self.style_modifiers[self.idx - 1] else { + unreachable!("this should not happen, as either `start_style_modifier` happens first, or follows an end_style_modifier") + }; + self.idx = start_idx; + self.start_idx = start_idx; + } + } + } + + fn end_style_modifier(&mut self) { + match self.style_modifiers.get_mut(self.idx) { + Some(StyleModifier::EndMarker(prev_start_idx)) if *prev_start_idx == self.start_idx => { + } // class modifier hasn't changed + Some(modifier) => { + *modifier = StyleModifier::EndMarker(self.start_idx); + } + None => { + self.style_modifiers + .push(StyleModifier::EndMarker(self.start_idx)); } } - }; + self.idx += 1; + self.start_idx = self.idx; + } +} + +impl WithStyle for ElementProps { + fn start_style_modifier(&mut self) { + self.styles().start_style_modifier(); + } + + fn end_style_modifier(&mut self) { + self.styles().end_style_modifier(); + } + + fn set_style(&mut self, name: CowStr, value: Option) { + self.styles().set_style(name, value); + } +} + +impl, P: WithStyle> WithStyle for Pod { + fn start_style_modifier(&mut self) { + self.props.start_style_modifier(); + } + + fn end_style_modifier(&mut self) { + self.props.end_style_modifier(); + } + + fn set_style(&mut self, name: CowStr, value: Option) { + self.props.set_style(name, value); + } +} + +impl, P: WithStyle> WithStyle for PodMut<'_, E, P> { + fn start_style_modifier(&mut self) { + self.props.start_style_modifier(); + } + + fn end_style_modifier(&mut self) { + self.props.end_style_modifier(); + } + + fn set_style(&mut self, name: CowStr, value: Option) { + self.props.set_style(name, value); + } } -impl_tuple_intostyles!(); -impl_tuple_intostyles!(t1: T1); -impl_tuple_intostyles!(t1: T1, t2: T2); -impl_tuple_intostyles!(t1: T1, t2: T2, t3: T3); -impl_tuple_intostyles!(t1: T1, t2: T2, t3: T3, t4: T4); +pub trait ElementWithStyle: for<'a> ViewElement: WithStyle> + WithStyle {} +impl ElementWithStyle for T +where + T: ViewElement + WithStyle, + for<'a> T::Mut<'a>: WithStyle, +{ +} + +#[derive(Clone, Debug)] pub struct Style { - pub(crate) element: E, - pub(crate) styles: Vec<(Cow<'static, str>, Cow<'static, str>)>, - pub(crate) phantom: PhantomData (T, A)>, + el: E, + styles: Vec<(CowStr, CowStr)>, + phantom: PhantomData (T, A)>, } -impl ViewMarker for Style {} -impl Sealed for Style {} +impl Style { + pub fn new(el: E, styles: Vec<(CowStr, CowStr)>) -> Self { + Style { + el, + styles, + phantom: PhantomData, + } + } +} -impl, T, A> View for Style { - type State = E::State; +impl View for Style +where + T: 'static, + A: 'static, + E: View, +{ type Element = E::Element; - fn build(&self, cx: &mut Cx) -> (Id, Self::State, Self::Element) { + type ViewState = E::ViewState; + + fn build(&self, ctx: &mut ViewCtx) -> (Self::Element, Self::ViewState) { + let (mut element, state) = self.el.build(ctx); + element.start_style_modifier(); for (key, value) in &self.styles { - cx.add_style_to_element(key, value); + element.set_style(key.clone(), Some(value.clone())); } - self.element.build(cx) + element.end_style_modifier(); + (element, state) } - fn rebuild( + fn rebuild<'e>( &self, - cx: &mut Cx, prev: &Self, - id: &mut Id, - state: &mut Self::State, - element: &mut Self::Element, - ) -> ChangeFlags { + view_state: &mut Self::ViewState, + ctx: &mut ViewCtx, + mut element: Mut<'e, Self::Element>, + ) -> Mut<'e, Self::Element> { + element.start_style_modifier(); + let mut element = self.el.rebuild(&prev.el, view_state, ctx, element); for (key, value) in &self.styles { - cx.add_style_to_element(key, value); + element.set_style(key.clone(), Some(value.clone())); } - self.element.rebuild(cx, &prev.element, id, state, element) + element.end_style_modifier(); + element + } + + fn teardown( + &self, + view_state: &mut Self::ViewState, + ctx: &mut ViewCtx, + element: Mut<'_, Self::Element>, + ) { + self.el.teardown(view_state, ctx, element); } fn message( &self, - id_path: &[Id], - state: &mut Self::State, - message: Box, + view_state: &mut Self::ViewState, + id_path: &[ViewId], + message: DynMessage, app_state: &mut T, - ) -> MessageResult { - self.element.message(id_path, state, message, app_state) + ) -> MessageResult { + self.el.message(view_state, id_path, message, app_state) } } - -crate::interfaces::impl_dom_interfaces_for_ty!(Element, Style); diff --git a/xilem_web/src/svg/common_attrs.rs b/xilem_web/src/svg/common_attrs.rs index ccd12dc31..67d6e580c 100644 --- a/xilem_web/src/svg/common_attrs.rs +++ b/xilem_web/src/svg/common_attrs.rs @@ -2,37 +2,32 @@ // SPDX-License-Identifier: Apache-2.0 use std::borrow::Cow; -use std::{any::Any, marker::PhantomData}; +use std::marker::PhantomData; use peniko::Brush; -use xilem_core::{Id, MessageResult}; +use xilem_core::{MessageResult, Mut, View, ViewId}; use crate::{ - interfaces::{ - Element, SvgCircleElement, SvgElement, SvgEllipseElement, SvgGeometryElement, - SvgGraphicsElement, SvgLineElement, SvgPathElement, SvgPolygonElement, SvgPolylineElement, - SvgRectElement, SvgTextContentElement, SvgTextElement, SvgTextPathElement, - SvgTextPositioningElement, SvggElement, SvgtSpanElement, - }, - ChangeFlags, Cx, IntoAttributeValue, View, ViewMarker, + attribute::{ElementWithAttributes, WithAttributes}, + DynMessage, IntoAttributeValue, ViewCtx, }; -pub struct Fill { +pub struct Fill { child: V, // This could reasonably be static Cow also, but keep things simple brush: Brush, - phantom: PhantomData (T, A)>, + phantom: PhantomData (State, Action)>, } -pub struct Stroke { +pub struct Stroke { child: V, // This could reasonably be static Cow also, but keep things simple brush: Brush, style: peniko::kurbo::Stroke, - phantom: PhantomData (T, A)>, + phantom: PhantomData (State, Action)>, } -pub fn fill(child: V, brush: impl Into) -> Fill { +pub fn fill(child: V, brush: impl Into) -> Fill { Fill { child, brush: brush.into(), @@ -40,11 +35,11 @@ pub fn fill(child: V, brush: impl Into) -> Fill { } } -pub fn stroke( +pub fn stroke( child: V, brush: impl Into, style: peniko::kurbo::Stroke, -) -> Stroke { +) -> Stroke { Stroke { child, brush: brush.into(), @@ -66,127 +61,114 @@ fn brush_to_string(brush: &Brush) -> String { } } -// manually implement interfaces, because multiple independent DOM interfaces use the View -impl> Element for Fill {} -impl> SvgElement for Fill {} -impl> SvgGraphicsElement for Fill {} -impl> SvggElement for Fill {} -// descendants of SvgGeometryElement (with the exception of SvgLineElement) -impl> SvgGeometryElement for Fill {} -impl> SvgCircleElement for Fill {} -impl> SvgEllipseElement for Fill {} -impl> SvgPathElement for Fill {} -impl> SvgPolygonElement for Fill {} -impl> SvgPolylineElement for Fill {} -impl> SvgRectElement for Fill {} -// descendants of SvgTextContentElement -impl> SvgTextContentElement for Fill {} -impl> SvgTextPathElement for Fill {} -impl> SvgTextPositioningElement for Fill {} -impl> SvgTextElement for Fill {} -impl> SvgtSpanElement for Fill {} - -impl ViewMarker for Fill {} -impl crate::interfaces::sealed::Sealed for Fill {} - -impl> View for Fill { - type State = (Cow<'static, str>, V::State); +impl View for Fill +where + State: 'static, + Action: 'static, + V: View, +{ + type ViewState = (Cow<'static, str>, V::ViewState); type Element = V::Element; - fn build(&self, cx: &mut Cx) -> (Id, Self::State, Self::Element) { + fn build(&self, ctx: &mut ViewCtx) -> (Self::Element, Self::ViewState) { + let (mut element, child_state) = self.child.build(ctx); let brush_svg_repr = Cow::from(brush_to_string(&self.brush)); - cx.add_attr_to_element(&"fill".into(), &brush_svg_repr.clone().into_attr_value()); - let (id, child_state, element) = self.child.build(cx); - (id, (brush_svg_repr, child_state), element) + element.start_attribute_modifier(); + element.set_attribute("fill".into(), brush_svg_repr.clone().into_attr_value()); + element.end_attribute_modifier(); + (element, (brush_svg_repr, child_state)) } - fn rebuild( + fn rebuild<'el>( &self, - cx: &mut Cx, prev: &Self, - id: &mut Id, - (brush_svg_repr, child_state): &mut Self::State, - element: &mut V::Element, - ) -> ChangeFlags { + (brush_svg_repr, child_state): &mut Self::ViewState, + ctx: &mut ViewCtx, + mut element: Mut<'el, Self::Element>, + ) -> Mut<'el, Self::Element> { + element.start_attribute_modifier(); + let mut element = self.child.rebuild(&prev.child, child_state, ctx, element); if self.brush != prev.brush { *brush_svg_repr = Cow::from(brush_to_string(&self.brush)); } - cx.add_attr_to_element(&"fill".into(), &brush_svg_repr.clone().into_attr_value()); - self.child - .rebuild(cx, &prev.child, id, child_state, element) + element.set_attribute("fill".into(), brush_svg_repr.clone().into_attr_value()); + element.end_attribute_modifier(); + element + } + + fn teardown( + &self, + view_state: &mut Self::ViewState, + ctx: &mut ViewCtx, + element: Mut<'_, Self::Element>, + ) { + self.child.teardown(&mut view_state.1, ctx, element); } fn message( &self, - id_path: &[Id], - (_, child_state): &mut Self::State, - message: Box, - app_state: &mut T, - ) -> MessageResult { - self.child.message(id_path, child_state, message, app_state) + (_, child_state): &mut Self::ViewState, + id_path: &[ViewId], + message: DynMessage, + app_state: &mut State, + ) -> MessageResult { + self.child.message(child_state, id_path, message, app_state) } } -// manually implement interfaces, because multiple independent DOM interfaces use the View -impl> Element for Stroke {} -impl> SvgElement for Stroke {} -impl> SvgGraphicsElement for Stroke {} -impl> SvggElement for Stroke {} -// descendants of SvgGeometryElement -impl> SvgGeometryElement for Stroke {} -impl> SvgCircleElement for Stroke {} -impl> SvgEllipseElement for Stroke {} -impl> SvgLineElement for Stroke {} -impl> SvgPathElement for Stroke {} -impl> SvgPolygonElement for Stroke {} -impl> SvgPolylineElement for Stroke {} -impl> SvgRectElement for Stroke {} -// descendants of SvgTextContentElement -impl> SvgTextContentElement for Stroke {} -impl> SvgTextPathElement for Stroke {} -impl> SvgTextPositioningElement for Stroke {} -impl> SvgTextElement for Stroke {} -impl> SvgtSpanElement for Stroke {} - -impl ViewMarker for Stroke {} -impl crate::interfaces::sealed::Sealed for Stroke {} - -impl> View for Stroke { - type State = (Cow<'static, str>, V::State); +impl View for Stroke +where + State: 'static, + Action: 'static, + V: View, +{ + type ViewState = (Cow<'static, str>, V::ViewState); type Element = V::Element; - fn build(&self, cx: &mut Cx) -> (Id, Self::State, Self::Element) { + fn build(&self, ctx: &mut ViewCtx) -> (Self::Element, Self::ViewState) { + let (mut element, child_state) = self.child.build(ctx); let brush_svg_repr = Cow::from(brush_to_string(&self.brush)); - cx.add_attr_to_element(&"stroke".into(), &brush_svg_repr.clone().into_attr_value()); - cx.add_attr_to_element(&"stroke-width".into(), &self.style.width.into_attr_value()); - let (id, child_state, element) = self.child.build(cx); - (id, (brush_svg_repr, child_state), element) + element.start_attribute_modifier(); + element.set_attribute("stroke".into(), brush_svg_repr.clone().into_attr_value()); + element.set_attribute("stroke-width".into(), self.style.width.into_attr_value()); + element.end_attribute_modifier(); + (element, (brush_svg_repr, child_state)) } - fn rebuild( + fn rebuild<'el>( &self, - cx: &mut Cx, prev: &Self, - id: &mut Id, - (brush_svg_repr, child_state): &mut Self::State, - element: &mut V::Element, - ) -> ChangeFlags { + (brush_svg_repr, child_state): &mut Self::ViewState, + ctx: &mut ViewCtx, + mut element: Mut<'el, Self::Element>, + ) -> Mut<'el, Self::Element> { + element.start_attribute_modifier(); + let mut element = self.child.rebuild(&prev.child, child_state, ctx, element); if self.brush != prev.brush { *brush_svg_repr = Cow::from(brush_to_string(&self.brush)); } - cx.add_attr_to_element(&"stroke".into(), &brush_svg_repr.clone().into_attr_value()); - cx.add_attr_to_element(&"stroke-width".into(), &self.style.width.into_attr_value()); - self.child - .rebuild(cx, &prev.child, id, child_state, element) + element.set_attribute("stroke".into(), brush_svg_repr.clone().into_attr_value()); + element.set_attribute("stroke-width".into(), self.style.width.into_attr_value()); + element.end_attribute_modifier(); + element + } + + fn teardown( + &self, + view_state: &mut Self::ViewState, + ctx: &mut ViewCtx, + element: Mut<'_, Self::Element>, + ) { + self.child.teardown(&mut view_state.1, ctx, element); } fn message( &self, - id_path: &[Id], - (_, child_state): &mut Self::State, - message: Box, - app_state: &mut T, - ) -> MessageResult { - self.child.message(id_path, child_state, message, app_state) + (_, child_state): &mut Self::ViewState, + id_path: &[ViewId], + message: DynMessage, + app_state: &mut State, + ) -> MessageResult { + self.child.message(child_state, id_path, message, app_state) } } diff --git a/xilem_web/src/svg/kurbo_shape.rs b/xilem_web/src/svg/kurbo_shape.rs index ac45147c8..19d6fa14d 100644 --- a/xilem_web/src/svg/kurbo_shape.rs +++ b/xilem_web/src/svg/kurbo_shape.rs @@ -6,200 +6,219 @@ use peniko::kurbo::{BezPath, Circle, Line, Rect}; use std::borrow::Cow; -use xilem_core::{Id, MessageResult}; +use xilem_core::{MessageResult, Mut, OrphanView}; use crate::{ - context::{ChangeFlags, Cx, HtmlProps}, - interfaces::sealed::Sealed, - view::{View, ViewMarker}, - IntoAttributeValue, SVG_NS, + attribute::WithAttributes, element_props::ElementProps, DynMessage, IntoAttributeValue, Pod, + ViewCtx, SVG_NS, }; -macro_rules! generate_dom_interface_impl { - ($dom_interface:ident, ($ty_name:ident)) => { - impl $crate::interfaces::$dom_interface for $ty_name {} - }; -} +impl OrphanView for ViewCtx { + type OrphanViewState = (); + type OrphanElement = Pod; + + fn orphan_build( + view: &Line, + _ctx: &mut ViewCtx, + ) -> (Self::OrphanElement, Self::OrphanViewState) { + let mut element: Self::OrphanElement = Pod::new_element(Vec::new(), SVG_NS, "line").into(); + element.start_attribute_modifier(); + element.set_attribute("x1".into(), view.p0.x.into_attr_value()); + element.set_attribute("y1".into(), view.p0.y.into_attr_value()); + element.set_attribute("x2".into(), view.p1.x.into_attr_value()); + element.set_attribute("y2".into(), view.p1.y.into_attr_value()); + element.end_attribute_modifier(); + (element, ()) + } -generate_dom_interface_impl!(SvgLineElement, (Line)); -crate::interfaces::for_all_svg_line_element_ancestors!(generate_dom_interface_impl, (Line)); - -impl ViewMarker for Line {} -impl Sealed for Line {} - -impl View for Line { - type State = HtmlProps; - type Element = web_sys::Element; - - fn build(&self, cx: &mut Cx) -> (Id, Self::State, Self::Element) { - cx.add_attr_to_element(&"x1".into(), &self.p0.x.into_attr_value()); - cx.add_attr_to_element(&"y1".into(), &self.p0.y.into_attr_value()); - cx.add_attr_to_element(&"x2".into(), &self.p1.x.into_attr_value()); - cx.add_attr_to_element(&"y2".into(), &self.p1.y.into_attr_value()); - let (el, props) = cx.build_element(SVG_NS, "line"); - let id = Id::next(); - (id, props, el) - } - - fn rebuild( - &self, - cx: &mut Cx, - _prev: &Self, - _id: &mut Id, - props: &mut Self::State, - element: &mut Self::Element, - ) -> ChangeFlags { - cx.add_attr_to_element(&"x1".into(), &self.p0.x.into_attr_value()); - cx.add_attr_to_element(&"y1".into(), &self.p0.y.into_attr_value()); - cx.add_attr_to_element(&"x2".into(), &self.p1.x.into_attr_value()); - cx.add_attr_to_element(&"y2".into(), &self.p1.y.into_attr_value()); - cx.rebuild_element(element, props) - } - - fn message( - &self, - _id_path: &[Id], - _state: &mut Self::State, - message: Box, - _app_state: &mut T, - ) -> MessageResult { - MessageResult::Stale(message) + fn orphan_rebuild<'el>( + new: &Line, + _prev: &Line, + (): &mut Self::OrphanViewState, + _ctx: &mut ViewCtx, + mut element: Mut<'el, Self::OrphanElement>, + ) -> Mut<'el, Self::OrphanElement> { + element.start_attribute_modifier(); + element.set_attribute("x1".into(), new.p0.x.into_attr_value()); + element.set_attribute("y1".into(), new.p0.y.into_attr_value()); + element.set_attribute("x2".into(), new.p1.x.into_attr_value()); + element.set_attribute("y2".into(), new.p1.y.into_attr_value()); + element.end_attribute_modifier(); + element + } + + fn orphan_teardown( + _view: &Line, + (): &mut Self::OrphanViewState, + _ctx: &mut ViewCtx, + _element: Mut<'_, Self::OrphanElement>, + ) { } -} -generate_dom_interface_impl!(SvgRectElement, (Rect)); -crate::interfaces::for_all_svg_rect_element_ancestors!(generate_dom_interface_impl, (Rect)); - -impl ViewMarker for Rect {} -impl Sealed for Rect {} - -impl View for Rect { - type State = HtmlProps; - type Element = web_sys::Element; - - fn build(&self, cx: &mut Cx) -> (Id, Self::State, Self::Element) { - cx.add_attr_to_element(&"x".into(), &self.x0.into_attr_value()); - cx.add_attr_to_element(&"y".into(), &self.y0.into_attr_value()); - let size = self.size(); - cx.add_attr_to_element(&"width".into(), &size.width.into_attr_value()); - cx.add_attr_to_element(&"height".into(), &size.height.into_attr_value()); - let (el, props) = cx.build_element(SVG_NS, "rect"); - let id = Id::next(); - (id, props, el) - } - - fn rebuild( - &self, - cx: &mut Cx, - _prev: &Self, - _id: &mut Id, - props: &mut Self::State, - element: &mut Self::Element, - ) -> ChangeFlags { - cx.add_attr_to_element(&"x".into(), &self.x0.into_attr_value()); - cx.add_attr_to_element(&"y".into(), &self.y0.into_attr_value()); - let size = self.size(); - cx.add_attr_to_element(&"width".into(), &size.width.into_attr_value()); - cx.add_attr_to_element(&"height".into(), &size.height.into_attr_value()); - cx.rebuild_element(element, props) - } - - fn message( - &self, - _id_path: &[Id], - _state: &mut Self::State, - message: Box, - _app_state: &mut T, - ) -> MessageResult { + fn orphan_message( + _view: &Line, + (): &mut Self::OrphanViewState, + _id_path: &[xilem_core::ViewId], + message: DynMessage, + _app_state: &mut State, + ) -> MessageResult { MessageResult::Stale(message) } } -generate_dom_interface_impl!(SvgCircleElement, (Circle)); -crate::interfaces::for_all_svg_circle_element_ancestors!(generate_dom_interface_impl, (Circle)); - -impl ViewMarker for Circle {} -impl Sealed for Circle {} - -impl View for Circle { - type State = HtmlProps; - type Element = web_sys::Element; - - fn build(&self, cx: &mut Cx) -> (Id, Self::State, Self::Element) { - cx.add_attr_to_element(&"cx".into(), &self.center.x.into_attr_value()); - cx.add_attr_to_element(&"cy".into(), &self.center.y.into_attr_value()); - cx.add_attr_to_element(&"r".into(), &self.radius.into_attr_value()); - let (el, props) = cx.build_element(SVG_NS, "circle"); - let id = Id::next(); - (id, props, el) - } - - fn rebuild( - &self, - cx: &mut Cx, - _prev: &Self, - _id: &mut Id, - props: &mut Self::State, - element: &mut Self::Element, - ) -> ChangeFlags { - cx.add_attr_to_element(&"cx".into(), &self.center.x.into_attr_value()); - cx.add_attr_to_element(&"cy".into(), &self.center.y.into_attr_value()); - cx.add_attr_to_element(&"r".into(), &self.radius.into_attr_value()); - cx.rebuild_element(element, props) - } - - fn message( - &self, - _id_path: &[Id], - _state: &mut Self::State, - message: Box, - _app_state: &mut T, - ) -> MessageResult { +impl OrphanView for ViewCtx { + type OrphanViewState = (); + type OrphanElement = Pod; + + fn orphan_build( + view: &Rect, + _ctx: &mut ViewCtx, + ) -> (Self::OrphanElement, Self::OrphanViewState) { + let mut element: Self::OrphanElement = Pod::new_element(Vec::new(), SVG_NS, "rect").into(); + element.start_attribute_modifier(); + element.set_attribute("x".into(), view.x0.into_attr_value()); + element.set_attribute("y".into(), view.y0.into_attr_value()); + element.set_attribute("width".into(), view.width().into_attr_value()); + element.set_attribute("height".into(), view.height().into_attr_value()); + element.end_attribute_modifier(); + (element, ()) + } + + fn orphan_rebuild<'el>( + new: &Rect, + _prev: &Rect, + (): &mut Self::OrphanViewState, + _ctx: &mut ViewCtx, + mut element: Mut<'el, Self::OrphanElement>, + ) -> Mut<'el, Self::OrphanElement> { + element.start_attribute_modifier(); + element.set_attribute("x".into(), new.x0.into_attr_value()); + element.set_attribute("y".into(), new.y0.into_attr_value()); + element.set_attribute("width".into(), new.width().into_attr_value()); + element.set_attribute("height".into(), new.height().into_attr_value()); + element.end_attribute_modifier(); + element + } + + fn orphan_teardown( + _view: &Rect, + (): &mut Self::OrphanViewState, + _ctx: &mut ViewCtx, + _element: Mut<'_, Self::OrphanElement>, + ) { + } + + fn orphan_message( + _view: &Rect, + (): &mut Self::OrphanViewState, + _id_path: &[xilem_core::ViewId], + message: DynMessage, + _app_state: &mut State, + ) -> MessageResult { MessageResult::Stale(message) } } -generate_dom_interface_impl!(SvgPathElement, (BezPath)); -crate::interfaces::for_all_svg_path_element_ancestors!(generate_dom_interface_impl, (BezPath)); +impl OrphanView for ViewCtx { + type OrphanViewState = (); + type OrphanElement = Pod; + + fn orphan_build( + view: &Circle, + _ctx: &mut ViewCtx, + ) -> (Self::OrphanElement, Self::OrphanViewState) { + let mut element: Self::OrphanElement = + Pod::new_element(Vec::new(), SVG_NS, "circle").into(); + element.start_attribute_modifier(); + element.set_attribute("cx".into(), view.center.x.into_attr_value()); + element.set_attribute("cy".into(), view.center.y.into_attr_value()); + element.set_attribute("r".into(), view.radius.into_attr_value()); + element.end_attribute_modifier(); + (element, ()) + } -impl ViewMarker for BezPath {} -impl Sealed for BezPath {} + fn orphan_rebuild<'el>( + new: &Circle, + _prev: &Circle, + (): &mut Self::OrphanViewState, + _ctx: &mut ViewCtx, + mut element: Mut<'el, Self::OrphanElement>, + ) -> Mut<'el, Self::OrphanElement> { + element.start_attribute_modifier(); + element.set_attribute("cx".into(), new.center.x.into_attr_value()); + element.set_attribute("cy".into(), new.center.y.into_attr_value()); + element.set_attribute("r".into(), new.radius.into_attr_value()); + element.end_attribute_modifier(); + element + } + + fn orphan_teardown( + _view: &Circle, + (): &mut Self::OrphanViewState, + _ctx: &mut ViewCtx, + _element: Mut<'_, Self::OrphanElement>, + ) { + } -impl View for BezPath { - type State = (Cow<'static, str>, HtmlProps); - type Element = web_sys::Element; + fn orphan_message( + _view: &Circle, + (): &mut Self::OrphanViewState, + _id_path: &[xilem_core::ViewId], + message: DynMessage, + _app_state: &mut State, + ) -> MessageResult { + MessageResult::Stale(message) + } +} - fn build(&self, cx: &mut Cx) -> (Id, Self::State, Self::Element) { - let svg_repr = Cow::from(self.to_svg()); - cx.add_attr_to_element(&"d".into(), &svg_repr.clone().into_attr_value()); - let (el, props) = cx.build_element(SVG_NS, "path"); - let id = Id::next(); - (id, (svg_repr, props), el) +impl OrphanView for ViewCtx { + type OrphanViewState = Cow<'static, str>; + type OrphanElement = Pod; + + fn orphan_build( + view: &BezPath, + _ctx: &mut ViewCtx, + ) -> (Self::OrphanElement, Self::OrphanViewState) { + let mut element: Self::OrphanElement = Pod::new_element(Vec::new(), SVG_NS, "path").into(); + let svg_repr = Cow::from(view.to_svg()); + element.start_attribute_modifier(); + element.set_attribute("d".into(), svg_repr.clone().into_attr_value()); + element.end_attribute_modifier(); + (element, svg_repr) } - fn rebuild( - &self, - cx: &mut Cx, - prev: &Self, - _id: &mut Id, - (svg_repr, props): &mut Self::State, - element: &mut Self::Element, - ) -> ChangeFlags { + fn orphan_rebuild<'el>( + new: &BezPath, + prev: &BezPath, + svg_repr: &mut Self::OrphanViewState, + _ctx: &mut ViewCtx, + mut element: Mut<'el, Self::OrphanElement>, + ) -> Mut<'el, Self::OrphanElement> { // slight optimization to avoid serialization/allocation - if self != prev { - *svg_repr = Cow::from(self.to_svg()); + if new != prev { + *svg_repr = Cow::from(new.to_svg()); } - cx.add_attr_to_element(&"d".into(), &svg_repr.clone().into_attr_value()); - cx.rebuild_element(element, props) + element.start_attribute_modifier(); + element.set_attribute("d".into(), svg_repr.clone().into_attr_value()); + element.end_attribute_modifier(); + element + } + + fn orphan_teardown( + _view: &BezPath, + _view_state: &mut Self::OrphanViewState, + _ctx: &mut ViewCtx, + _element: Mut<'_, Self::OrphanElement>, + ) { } - fn message( - &self, - _id_path: &[Id], - _state: &mut Self::State, - message: Box, - _app_state: &mut T, - ) -> MessageResult { + fn orphan_message( + _view: &BezPath, + _view_state: &mut Self::OrphanViewState, + _id_path: &[xilem_core::ViewId], + message: DynMessage, + _app_state: &mut State, + ) -> MessageResult { MessageResult::Stale(message) } } diff --git a/xilem_web/src/svg/mod.rs b/xilem_web/src/svg/mod.rs index 0b972ff99..c8d54bb9d 100644 --- a/xilem_web/src/svg/mod.rs +++ b/xilem_web/src/svg/mod.rs @@ -1,6 +1,8 @@ // Copyright 2023 the Xilem Authors // SPDX-License-Identifier: Apache-2.0 +//! Contains opinionated views such as [`kurbo`] shapes which can be used in an svg context + pub(crate) mod common_attrs; pub(crate) mod kurbo_shape; diff --git a/xilem_web/src/text.rs b/xilem_web/src/text.rs new file mode 100644 index 000000000..91ce7cbef --- /dev/null +++ b/xilem_web/src/text.rs @@ -0,0 +1,130 @@ +// Copyright 2024 the Xilem Authors +// SPDX-License-Identifier: Apache-2.0 + +use xilem_core::{Mut, OrphanView}; + +use crate::{DynMessage, Pod, ViewCtx}; + +// strings -> text nodes +macro_rules! impl_string_view { + ($ty:ty) => { + impl OrphanView<$ty, State, Action, DynMessage> for ViewCtx { + type OrphanElement = Pod; + + type OrphanViewState = (); + + fn orphan_build( + view: &$ty, + _ctx: &mut ViewCtx, + ) -> (Self::OrphanElement, Self::OrphanViewState) { + let pod = Pod { + node: web_sys::Text::new_with_data(view).unwrap(), + props: (), + }; + (pod, ()) + } + + fn orphan_rebuild<'a>( + new: &$ty, + prev: &$ty, + (): &mut Self::OrphanViewState, + _ctx: &mut ViewCtx, + element: Mut<'a, Self::OrphanElement>, + ) -> Mut<'a, Self::OrphanElement> { + if prev != new { + element.node.set_data(new); + } + element + } + + fn orphan_teardown( + _view: &$ty, + _view_state: &mut Self::OrphanViewState, + _ctx: &mut ViewCtx, + _element: Mut<'_, Pod>, + ) { + } + + fn orphan_message( + _view: &$ty, + _view_state: &mut Self::OrphanViewState, + _id_path: &[xilem_core::ViewId], + message: DynMessage, + _app_state: &mut State, + ) -> xilem_core::MessageResult { + xilem_core::MessageResult::Stale(message) + } + } + }; +} + +impl_string_view!(&'static str); +impl_string_view!(String); +impl_string_view!(std::borrow::Cow<'static, str>); + +macro_rules! impl_to_string_view { + ($ty:ty) => { + impl OrphanView<$ty, State, Action, DynMessage> for ViewCtx { + type OrphanElement = Pod; + + type OrphanViewState = (); + + fn orphan_build( + view: &$ty, + _ctx: &mut ViewCtx, + ) -> (Self::OrphanElement, Self::OrphanViewState) { + let pod = Pod { + node: web_sys::Text::new_with_data(&view.to_string()).unwrap(), + props: (), + }; + (pod, ()) + } + + fn orphan_rebuild<'a>( + new: &$ty, + prev: &$ty, + (): &mut Self::OrphanViewState, + _ctx: &mut ViewCtx, + element: Mut<'a, Self::OrphanElement>, + ) -> Mut<'a, Self::OrphanElement> { + if prev != new { + element.node.set_data(&new.to_string()); + } + element + } + + fn orphan_teardown( + _view: &$ty, + _view_state: &mut Self::OrphanViewState, + _ctx: &mut ViewCtx, + _element: Mut<'_, Pod>, + ) { + } + + fn orphan_message( + _view: &$ty, + _view_state: &mut Self::OrphanViewState, + _id_path: &[xilem_core::ViewId], + message: DynMessage, + _app_state: &mut State, + ) -> xilem_core::MessageResult { + xilem_core::MessageResult::Stale(message) + } + } + }; +} + +// Allow numbers to be used directly as a view +impl_to_string_view!(f32); +impl_to_string_view!(f64); +impl_to_string_view!(i8); +impl_to_string_view!(u8); +impl_to_string_view!(i16); +impl_to_string_view!(u16); +impl_to_string_view!(i32); +impl_to_string_view!(u32); +impl_to_string_view!(i64); +impl_to_string_view!(u64); +impl_to_string_view!(u128); +impl_to_string_view!(isize); +impl_to_string_view!(usize); diff --git a/xilem_web/src/vec_splice.rs b/xilem_web/src/vec_splice.rs new file mode 100644 index 000000000..bf3d5a3e1 --- /dev/null +++ b/xilem_web/src/vec_splice.rs @@ -0,0 +1,64 @@ +// Copyright 2023 the Xilem Authors +// SPDX-License-Identifier: Apache-2.0 + +pub(crate) struct VecSplice<'v, 's, T> { + v: &'v mut Vec, + scratch: &'s mut Vec, + ix: usize, +} + +impl<'v, 's, T> VecSplice<'v, 's, T> { + pub fn new(v: &'v mut Vec, scratch: &'s mut Vec) -> Self { + Self { v, scratch, ix: 0 } + } + + pub fn skip(&mut self, n: usize) { + let v_len = self.v.len(); + let new_ix = self.ix + n; + // 2 < 3 + // ix: 0, n = 3 + if v_len < new_ix { + let s_len = self.scratch.len(); + if v_len + s_len < new_ix { + unreachable!("This is a bug, please report an issue about `ElementSplice::skip`"); + } + let new_scratch_len = s_len - (new_ix - v_len); + self.v.extend(self.scratch.splice(new_scratch_len.., [])); + } + self.ix += n; + } + + pub fn delete_next(&mut self) -> T { + self.clear_tail(); + self.scratch + .pop() + .expect("This is a bug, please report an issue about `ElementSplice::delete`") + } + + pub fn insert(&mut self, value: T) { + self.clear_tail(); + self.v.push(value); + self.ix += 1; + } + + pub fn next_mut(&mut self) -> Option<&mut T> { + self.v + .get_mut(self.ix + 1) + .or_else(|| self.scratch.last_mut()) + } + + pub fn mutate(&mut self) -> &mut T { + if self.v.len() == self.ix { + self.v.push(self.scratch.pop().unwrap()); + } + let ix = self.ix; + self.ix += 1; + &mut self.v[ix] + } + + fn clear_tail(&mut self) { + if self.v.len() > self.ix { + self.scratch.extend(self.v.splice(self.ix.., []).rev()); + } + } +} diff --git a/xilem_web/src/vecmap.rs b/xilem_web/src/vecmap.rs index eecc44ff2..bf832002b 100644 --- a/xilem_web/src/vecmap.rs +++ b/xilem_web/src/vecmap.rs @@ -279,6 +279,16 @@ impl<'a, K, V> IntoIterator for &'a VecMap { } } +impl IntoIterator for VecMap { + type Item = (K, V); + + type IntoIter = std::vec::IntoIter<(K, V)>; + + fn into_iter(self) -> Self::IntoIter { + self.0.into_iter() + } +} + // Basically all the doc tests from the rustdoc examples above, to avoid having to expose this module (pub) #[cfg(test)] mod tests { diff --git a/xilem_web/src/view.rs b/xilem_web/src/view.rs deleted file mode 100644 index fedfe46c2..000000000 --- a/xilem_web/src/view.rs +++ /dev/null @@ -1,199 +0,0 @@ -// Copyright 2023 the Xilem Authors -// SPDX-License-Identifier: Apache-2.0 - -//! Integration with xilem_core. This instantiates the View and related -//! traits for DOM node generation. - -use std::{any::Any, borrow::Cow, ops::Deref}; - -use xilem_core::{Id, MessageResult}; - -use crate::{context::Cx, ChangeFlags}; - -pub(crate) mod sealed { - pub trait Sealed {} -} - -// A possible refinement of xilem_core is to allow a single concrete type -// for a view element, rather than an associated type with a bound. -/// This trait is implemented for types that implement `AsRef`. -/// It is an implementation detail. -pub trait DomNode: sealed::Sealed + 'static { - fn into_pod(self) -> Pod; - fn as_node_ref(&self) -> &web_sys::Node; -} - -impl + 'static> sealed::Sealed for N {} -impl + 'static> DomNode for N { - fn into_pod(self) -> Pod { - Pod(Box::new(self)) - } - - fn as_node_ref(&self) -> &web_sys::Node { - self.as_ref() - } -} - -/// A trait for types that can be type-erased and impl `AsRef`. It is an -/// implementation detail. -pub trait AnyNode: sealed::Sealed { - fn as_any_mut(&mut self) -> &mut dyn Any; - - fn as_node_ref(&self) -> &web_sys::Node; -} - -impl + Any> AnyNode for N { - fn as_any_mut(&mut self) -> &mut dyn Any { - self - } - - fn as_node_ref(&self) -> &web_sys::Node { - self.as_ref() - } -} - -impl sealed::Sealed for Box {} -impl DomNode for Box { - fn into_pod(self) -> Pod { - Pod(self) - } - - fn as_node_ref(&self) -> &web_sys::Node { - self.deref().as_node_ref() - } -} - -/// A container that holds a DOM element. -/// -/// This implementation may be overkill (it's possibly enough that everything is -/// just a `web_sys::Element`), but does allow element types that contain other -/// data, if needed. -pub struct Pod(pub Box); - -impl Pod { - pub(crate) fn new(node: impl DomNode) -> Self { - node.into_pod() - } - - pub(crate) fn downcast_mut(&mut self) -> Option<&mut T> { - self.0.as_any_mut().downcast_mut() - } - - pub(crate) fn mark(&mut self, flags: ChangeFlags) -> ChangeFlags { - flags - } -} - -xilem_core::generate_view_trait! {View, DomNode, Cx, ChangeFlags;} -xilem_core::generate_viewsequence_trait! {ViewSequence, View, ViewMarker, ElementsSplice, 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, static_view, memoize;} -xilem_core::generate_adapt_view! {View, Cx, ChangeFlags;} -xilem_core::generate_adapt_state_view! {View, Cx, ChangeFlags;} - -// strings -> text nodes - -macro_rules! impl_string_view { - ($ty:ty) => { - impl ViewMarker for $ty {} - impl View for $ty { - type State = (); - type Element = web_sys::Text; - - fn build(&self, _cx: &mut Cx) -> (Id, Self::State, Self::Element) { - (Id::next(), (), new_text(self)) - } - - fn rebuild( - &self, - _cx: &mut Cx, - prev: &Self, - _id: &mut Id, - _state: &mut Self::State, - element: &mut Self::Element, - ) -> ChangeFlags { - if prev != self { - element.set_data(self); - ChangeFlags::OTHER_CHANGE - } else { - ChangeFlags::empty() - } - } - - fn message( - &self, - _id_path: &[Id], - _state: &mut Self::State, - message: Box, - _app_state: &mut T, - ) -> MessageResult { - MessageResult::Stale(message) - } - } - }; -} - -impl_string_view!(String); -impl_string_view!(&'static str); -impl_string_view!(Cow<'static, str>); - -// Specialization would probably avoid manual implementation, -// but it's probably a good idea to have more control than via a blanket impl -macro_rules! impl_to_string_view { - ($ty:ty) => { - impl ViewMarker for $ty {} - impl View for $ty { - type State = (); - type Element = web_sys::Text; - - fn build(&self, _cx: &mut Cx) -> (Id, Self::State, Self::Element) { - (Id::next(), (), new_text(&self.to_string())) - } - - fn rebuild( - &self, - _cx: &mut Cx, - prev: &Self, - _id: &mut Id, - _state: &mut Self::State, - element: &mut Self::Element, - ) -> ChangeFlags { - if prev != self { - element.set_data(&self.to_string()); - ChangeFlags::OTHER_CHANGE - } else { - ChangeFlags::empty() - } - } - - fn message( - &self, - _id_path: &[Id], - _state: &mut Self::State, - message: Box, - _app_state: &mut T, - ) -> MessageResult { - MessageResult::Stale(message) - } - } - }; -} - -// Allow numbers to be used directly as a view -impl_to_string_view!(f32); -impl_to_string_view!(f64); -impl_to_string_view!(i8); -impl_to_string_view!(u8); -impl_to_string_view!(i16); -impl_to_string_view!(u16); -impl_to_string_view!(i32); -impl_to_string_view!(u32); -impl_to_string_view!(i64); -impl_to_string_view!(u64); -impl_to_string_view!(u128); -impl_to_string_view!(isize); -impl_to_string_view!(usize); - -fn new_text(text: &str) -> web_sys::Text { - web_sys::Text::new_with_data(text).unwrap() -} diff --git a/xilem_web/src/view_ext.rs b/xilem_web/src/view_ext.rs deleted file mode 100644 index 4a8aa79bd..000000000 --- a/xilem_web/src/view_ext.rs +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright 2023 the Xilem Authors -// SPDX-License-Identifier: Apache-2.0 - -use crate::{view::View, Adapt, AdaptState, AdaptThunk}; - -/// A trait that makes it possible to use core views such as [`Adapt`] in the continuation/builder style. -pub trait ViewExt: View + Sized { - fn adapt(self, f: F) -> Adapt - where - F: Fn(&mut ParentT, AdaptThunk) -> xilem_core::MessageResult, - { - Adapt::new(f, self) - } - - fn adapt_state(self, f: F) -> AdaptState - where - F: Fn(&mut ParentT) -> &mut T + Send, - { - AdaptState::new(f, self) - } -} - -impl> ViewExt for V {} diff --git a/xilem_web/web_examples/counter/src/main.rs b/xilem_web/web_examples/counter/src/main.rs index e202d22e6..a205923e5 100644 --- a/xilem_web/web_examples/counter/src/main.rs +++ b/xilem_web/web_examples/counter/src/main.rs @@ -4,8 +4,8 @@ use xilem_web::{ document_body, elements::html as el, - interfaces::{Element, HtmlButtonElement}, - App, View, + interfaces::{Element, HtmlButtonElement, HtmlDivElement}, + App, }; #[derive(Default)] @@ -45,12 +45,12 @@ impl AppState { /// You can create functions that generate views. fn btn( label: &'static str, - click_fn: impl Fn(&mut AppState, web_sys::MouseEvent), + click_fn: impl Fn(&mut AppState, web_sys::MouseEvent) + 'static, ) -> impl HtmlButtonElement { el::button(label).on_click(click_fn) } -fn app_logic(state: &mut AppState) -> impl View { +fn app_logic(state: &mut AppState) -> impl HtmlDivElement { el::div(( el::span(format!("clicked {} times", state.clicks)).class(state.class), el::br(()), @@ -66,6 +66,5 @@ fn app_logic(state: &mut AppState) -> impl View { pub fn main() { console_error_panic_hook::set_once(); - let app = App::new(AppState::default(), app_logic); - app.run(&document_body()); + App::new(document_body(), AppState::default(), app_logic).run(); } diff --git a/xilem_web/web_examples/counter_custom_element/src/main.rs b/xilem_web/web_examples/counter_custom_element/src/main.rs index 3547bc152..a09cea4e6 100644 --- a/xilem_web/web_examples/counter_custom_element/src/main.rs +++ b/xilem_web/web_examples/counter_custom_element/src/main.rs @@ -5,7 +5,7 @@ use xilem_web::{ document_body, elements::custom_element, interfaces::{Element, HtmlElement}, - App, View, + App, DomView, }; #[derive(Default)] @@ -27,14 +27,14 @@ impl AppState { fn btn( label: &'static str, - click_fn: impl Fn(&mut AppState, web_sys::Event), + click_fn: impl Fn(&mut AppState, web_sys::Event) + 'static, ) -> impl HtmlElement { custom_element("button", label).on("click", move |state: &mut AppState, evt| { click_fn(state, evt); }) } -fn app_logic(state: &mut AppState) -> impl View { +fn app_logic(state: &mut AppState) -> impl DomView { custom_element( "div", ( @@ -48,6 +48,5 @@ fn app_logic(state: &mut AppState) -> impl View { pub fn main() { console_error_panic_hook::set_once(); - let app = App::new(AppState::default(), app_logic); - app.run(&document_body()); + App::new(document_body(), AppState::default(), app_logic).run(); } diff --git a/xilem_web/web_examples/mathml_svg/src/main.rs b/xilem_web/web_examples/mathml_svg/src/main.rs index 049fd91a0..3d8514f5e 100644 --- a/xilem_web/web_examples/mathml_svg/src/main.rs +++ b/xilem_web/web_examples/mathml_svg/src/main.rs @@ -50,7 +50,7 @@ fn slider( pub fn main() { console_error_panic_hook::set_once(); - App::new(Triangle { a: 200, b: 100 }, |t| { + App::new(document_body(), Triangle { a: 200, b: 100 }, |t| { let x1 = 390; let y1 = 30; let x2 = x1; @@ -91,5 +91,5 @@ pub fn main() { ))), )) }) - .run(&document_body()); + .run(); } diff --git a/xilem_web/web_examples/svgtoy/src/main.rs b/xilem_web/web_examples/svgtoy/src/main.rs index 88ceaf43e..6b744651c 100644 --- a/xilem_web/web_examples/svgtoy/src/main.rs +++ b/xilem_web/web_examples/svgtoy/src/main.rs @@ -9,7 +9,7 @@ use xilem_web::{ kurbo::{self, Rect}, peniko::Color, }, - App, PointerMsg, View, + App, DomView, PointerMsg, }; #[derive(Default)] @@ -53,7 +53,7 @@ impl GrabState { } } -fn app_logic(state: &mut AppState) -> impl View { +fn app_logic(state: &mut AppState) -> impl DomView { let v = (0..10) .map(|i| Rect::from_origin_size((10.0 * i as f64, 150.0), (8.0, 8.0))) .collect::>(); @@ -82,6 +82,5 @@ fn app_logic(state: &mut AppState) -> impl View { pub fn main() { console_error_panic_hook::set_once(); - let app = App::new(AppState::default(), app_logic); - app.run(&document_body()); + App::new(document_body(), AppState::default(), app_logic).run(); } diff --git a/xilem_web/web_examples/todomvc/src/main.rs b/xilem_web/web_examples/todomvc/src/main.rs index 2c180f223..fe18459d7 100644 --- a/xilem_web/web_examples/todomvc/src/main.rs +++ b/xilem_web/web_examples/todomvc/src/main.rs @@ -7,8 +7,11 @@ use state::{AppState, Filter, Todo}; use wasm_bindgen::JsCast; use xilem_web::{ - elements::html as el, get_element_by_id, interfaces::*, style as s, Action, Adapt, App, - MessageResult, View, + core::{adapt, MessageResult}, + elements::html as el, + get_element_by_id, + interfaces::*, + style as s, Action, App, DomView, }; // All of these actions arise from within a `Todo`, but we need access to the full state to reduce @@ -78,12 +81,11 @@ fn footer_view(state: &mut AppState, should_display: bool) -> impl Element 0).then(|| { - Element::on_click( - el::button("Clear completed").class("clear-completed"), - |state: &mut AppState, _| { + el::button("Clear completed") + .class("clear-completed") + .on_click(|state: &mut AppState, _| { state.todos.retain(|todo| !todo.completed); - }, - ) + }) }); let filter_class = |filter| (state.filter == filter).then_some("selected"); @@ -134,7 +136,8 @@ fn main_view(state: &mut AppState, should_display: bool) -> impl Element = state .visible_todos() .map(|(idx, todo)| { - Adapt::new( + adapt( + todo_item(todo, editing_id == Some(todo.id)), move |data: &mut AppState, thunk| { if let MessageResult::Action(action) = thunk.call(&mut data.todos[idx]) { match action { @@ -149,7 +152,6 @@ fn main_view(state: &mut AppState, should_display: bool) -> impl Element impl Element impl View { +fn app_logic(state: &mut AppState) -> impl DomView { tracing::debug!("render: {state:?}"); let some_todos = !state.todos.is_empty(); let main = main_view(state, some_todos); @@ -208,5 +210,5 @@ fn app_logic(state: &mut AppState) -> impl View { pub fn main() { console_error_panic_hook::set_once(); tracing_wasm::set_as_global_default(); - App::new(AppState::load(), app_logic).run(&get_element_by_id("todoapp")); + App::new(get_element_by_id("todoapp"), AppState::load(), app_logic).run(); } diff --git a/xilem_web/xilem_web_core/Cargo.toml b/xilem_web/xilem_web_core/Cargo.toml deleted file mode 100644 index 12c09d224..000000000 --- a/xilem_web/xilem_web_core/Cargo.toml +++ /dev/null @@ -1,21 +0,0 @@ -[package] -name = "xilem_web_core" -version = "0.1.0" -description = "Common core of the Xilem Rust UI framework." -keywords = ["xilem", "ui", "reactive", "performance"] -categories = ["gui"] -publish = false # Until it's ready -edition.workspace = true -license.workspace = true -repository.workspace = true -homepage.workspace = true - -[package.metadata.docs.rs] -all-features = true -# rustdoc-scrape-examples tracking issue https://github.com/rust-lang/rust/issues/88791 -cargo-args = ["-Zunstable-options", "-Zrustdoc-scrape-examples"] - -[lints] -workspace = true - -[dependencies] diff --git a/xilem_web/xilem_web_core/LICENSE b/xilem_web/xilem_web_core/LICENSE deleted file mode 100644 index d64569567..000000000 --- a/xilem_web/xilem_web_core/LICENSE +++ /dev/null @@ -1,202 +0,0 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/xilem_web/xilem_web_core/src/any_view.rs b/xilem_web/xilem_web_core/src/any_view.rs deleted file mode 100644 index 172f1aa41..000000000 --- a/xilem_web/xilem_web_core/src/any_view.rs +++ /dev/null @@ -1,137 +0,0 @@ -// Copyright 2023 the Xilem Authors -// SPDX-License-Identifier: Apache-2.0 - -#[macro_export] -macro_rules! generate_anyview_trait { - ($anyview:ident, $viewtrait:ident, $viewmarker:ty, $cx:ty, $changeflags:ty, $anywidget:ident, $boxedview:ident; $($ss:tt)*) => { - /// A trait enabling type erasure of views. - pub trait $anyview { - fn as_any(&self) -> &dyn std::any::Any; - - fn dyn_build( - &self, - cx: &mut $cx, - ) -> ($crate::Id, Box, Box); - - fn dyn_rebuild( - &self, - cx: &mut $cx, - prev: &dyn $anyview, - id: &mut $crate::Id, - state: &mut Box, - element: &mut Box, - ) -> $changeflags; - - fn dyn_message( - &self, - id_path: &[$crate::Id], - state: &mut dyn std::any::Any, - message: Box, - app_state: &mut T, - ) -> $crate::MessageResult; - } - - impl + 'static> $anyview for V - where - V::State: 'static, - V::Element: $anywidget + 'static, - { - fn as_any(&self) -> &dyn std::any::Any { - self - } - - fn dyn_build( - &self, - cx: &mut $cx, - ) -> ($crate::Id, Box, Box) { - let (id, state, element) = self.build(cx); - (id, Box::new(state), Box::new(element)) - } - - fn dyn_rebuild( - &self, - cx: &mut $cx, - prev: &dyn $anyview, - id: &mut $crate::Id, - state: &mut Box, - element: &mut Box, - ) -> ChangeFlags { - use std::ops::DerefMut; - if let Some(prev) = prev.as_any().downcast_ref() { - if let Some(state) = state.downcast_mut() { - if let Some(element) = element.deref_mut().as_any_mut().downcast_mut() { - self.rebuild(cx, prev, id, state, element) - } else { - eprintln!("downcast of element failed in dyn_rebuild"); - <$changeflags>::default() - } - } else { - eprintln!("downcast of state failed in dyn_rebuild"); - <$changeflags>::default() - } - } else { - let (new_id, new_state, new_element) = self.build(cx); - *id = new_id; - *state = Box::new(new_state); - *element = Box::new(new_element); - <$changeflags>::tree_structure() - } - } - - fn dyn_message( - &self, - id_path: &[$crate::Id], - state: &mut dyn std::any::Any, - message: Box, - app_state: &mut T, - ) -> $crate::MessageResult { - if let Some(state) = state.downcast_mut() { - self.message(id_path, state, message, app_state) - } else { - // Possibly softer failure? - panic!("downcast error in dyn_event"); - } - } - } - - pub type $boxedview = Box $( $ss )* >; - - impl $viewmarker for $boxedview {} - - impl $viewtrait for $boxedview { - type State = Box; - - type Element = Box; - - fn build(&self, cx: &mut $cx) -> ($crate::Id, Self::State, Self::Element) { - use std::ops::Deref; - self.deref().dyn_build(cx) - } - - fn rebuild( - &self, - cx: &mut $cx, - prev: &Self, - id: &mut $crate::Id, - state: &mut Self::State, - element: &mut Self::Element, - ) -> $changeflags { - use std::ops::Deref; - self.deref() - .dyn_rebuild(cx, prev.deref(), id, state, element) - } - - fn message( - &self, - id_path: &[$crate::Id], - state: &mut Self::State, - message: Box, - app_state: &mut T, - ) -> $crate::MessageResult { - use std::ops::{Deref, DerefMut}; - self.deref() - .dyn_message(id_path, state.deref_mut(), message, app_state) - } - } - }; -} diff --git a/xilem_web/xilem_web_core/src/id.rs b/xilem_web/xilem_web_core/src/id.rs deleted file mode 100644 index 18590cf66..000000000 --- a/xilem_web/xilem_web_core/src/id.rs +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright 2022 the Xilem Authors and the Druid Authors -// SPDX-License-Identifier: Apache-2.0 - -use std::{ - num::NonZeroU64, - sync::atomic::{AtomicU64, Ordering}, -}; - -#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Hash)] -/// A stable identifier for an element. -pub struct Id(NonZeroU64); - -pub type IdPath = Vec; - -impl Id { - /// Allocate a new, unique `Id`. - pub fn next() -> Id { - static ID_COUNTER: AtomicU64 = AtomicU64::new(1); - // Note: we can make the safety argument for the unchecked version. - Id(NonZeroU64::new(ID_COUNTER.fetch_add(1, Ordering::Relaxed)).unwrap()) - } - - #[allow(unused)] - pub fn to_raw(self) -> u64 { - self.0.into() - } - - pub fn to_nonzero_raw(self) -> NonZeroU64 { - self.0 - } - - /* - /// Turns an `accesskit::NodeId` id into an `Id`. - /// - /// This method will only return `Some` for `accesskit::NodeId` values which were created from - /// `Id`'s. - /// - // TODO: Maybe we should not use AccessKit Ids at all in Widget implementation and do the - // mapping in the `App`. - pub fn try_from_accesskit(id: accesskit::NodeId) -> Option { - id.0.try_into().ok().map(|id| Id(id)) - } - */ -} - -// Discussion question: do we need AccessKit integration for id's at the view level, or is -// that primarily a widget concern? If the former, then we should probably have a feature -// that enables these conversions. - -/* -impl From for accesskit::NodeId { - fn from(id: Id) -> accesskit::NodeId { - id.to_nonzero_raw().into() - } -} -*/ diff --git a/xilem_web/xilem_web_core/src/lib.rs b/xilem_web/xilem_web_core/src/lib.rs deleted file mode 100644 index afe57a0d2..000000000 --- a/xilem_web/xilem_web_core/src/lib.rs +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright 2022 the Xilem Authors -// SPDX-License-Identifier: Apache-2.0 - -//! Generic implementation of Xilem view traits. -//! -//! This crate has a few basic types needed to support views, and also -//! a set of macros used to instantiate the main view traits. The client -//! will need to supply a bound on elements, a "pod" type which -//! supports dynamic dispatching and marking of change flags, and a -//! context. -//! -//! All this is still experimental. This crate is where more of the core -//! Xilem architecture will land (some of which was implemented in the -//! original prototype but not yet ported): adapt, memoize, use_state, -//! and possibly some async logic. Likely most of env will also land -//! here, but that also requires coordination with the context. - -mod any_view; -mod id; -mod message; -mod sequence; -mod vec_splice; -mod view; - -pub use id::{Id, IdPath}; -pub use message::{AsyncWake, MessageResult}; -pub use vec_splice::VecSplice; diff --git a/xilem_web/xilem_web_core/src/message.rs b/xilem_web/xilem_web_core/src/message.rs deleted file mode 100644 index ac6bdb5aa..000000000 --- a/xilem_web/xilem_web_core/src/message.rs +++ /dev/null @@ -1,74 +0,0 @@ -// Copyright 2022 the Xilem Authors -// SPDX-License-Identifier: Apache-2.0 - -use std::any::Any; - -#[macro_export] -macro_rules! message { - ($($bounds:tt)*) => { - pub struct Message { - pub id_path: xilem_core::IdPath, - pub body: Box, - } - - impl Message { - pub fn new(id_path: xilem_core::IdPath, event: impl std::any::Any + $($bounds)*) -> Message { - Message { - id_path, - body: Box::new(event), - } - } - } - }; -} - -/// A result wrapper type for event handlers. -#[derive(Default)] -pub enum MessageResult { - /// The event handler was invoked and returned an action. - /// - /// Use this return type if your widgets should respond to events by passing - /// a value up the tree, rather than changing their internal state. - Action(A), - /// The event handler received a change request that requests a rebuild. - /// - /// Note: A rebuild will always occur if there was a state change. This return - /// type can be used to indicate that a full rebuild is necessary even if the - /// state remained the same. It is expected that this type won't be used very - /// often. - #[allow(unused)] - RequestRebuild, - /// The event handler discarded the event. - /// - /// This is the variant that you **almost always want** when you're not returning - /// an action. - #[allow(unused)] - #[default] - Nop, - /// The event was addressed to an id path no longer in the tree. - /// - /// This is a normal outcome for async operation when the tree is changing - /// dynamically, but otherwise indicates a logic error. - Stale(Box), -} - -// TODO: does this belong in core? -pub struct AsyncWake; - -impl MessageResult { - pub fn map(self, f: impl FnOnce(A) -> B) -> MessageResult { - match self { - MessageResult::Action(a) => MessageResult::Action(f(a)), - MessageResult::RequestRebuild => MessageResult::RequestRebuild, - MessageResult::Stale(event) => MessageResult::Stale(event), - MessageResult::Nop => MessageResult::Nop, - } - } - - pub fn or(self, f: impl FnOnce(Box) -> Self) -> Self { - match self { - MessageResult::Stale(event) => f(event), - _ => self, - } - } -} diff --git a/xilem_web/xilem_web_core/src/sequence.rs b/xilem_web/xilem_web_core/src/sequence.rs deleted file mode 100644 index 5773fcca4..000000000 --- a/xilem_web/xilem_web_core/src/sequence.rs +++ /dev/null @@ -1,363 +0,0 @@ -// Copyright 2023 the Xilem Authors -// SPDX-License-Identifier: Apache-2.0 - -#[doc(hidden)] -#[macro_export] -macro_rules! impl_view_tuple { - ( $viewseq:ident, $elements_splice: ident, $pod:ty, $cx:ty, $changeflags:ty, $( $t:ident),* ; $( $i:tt ),* ) => { - impl ),* > $viewseq for ( $( $t, )* ) { - type State = ( $( $t::State, )*); - - fn build(&self, cx: &mut $cx, elements: &mut dyn $elements_splice) -> Self::State { - let b = ( $( self.$i.build(cx, elements), )* ); - let state = ( $( b.$i, )*); - state - } - - fn rebuild( - &self, - cx: &mut $cx, - prev: &Self, - state: &mut Self::State, - els: &mut dyn $elements_splice, - ) -> ChangeFlags { - let mut changed = <$changeflags>::default(); - $( - let el_changed = self.$i.rebuild(cx, &prev.$i, &mut state.$i, els); - changed |= el_changed; - )* - changed - } - - fn message( - &self, - id_path: &[$crate::Id], - state: &mut Self::State, - message: Box, - app_state: &mut T, - ) -> $crate::MessageResult { - $crate::MessageResult::Stale(message) - $( - .or(|message|{ - self.$i.message(id_path, &mut state.$i, message, app_state) - }) - )* - } - - fn count(&self, state: &Self::State) -> usize { - 0 - $( - + self.$i.count(&state.$i) - )* - } - } - } -} -#[macro_export] -macro_rules! generate_viewsequence_trait { - ($viewseq:ident, $view:ident, $viewmarker: ident, $elements_splice: ident, $bound:ident, $cx:ty, $changeflags:ty, $pod:ty; $( $ss:tt )* ) => { - - /// A temporary "splice" to add, update, delete and monitor elements in a sequence of elements. - /// It is mainly intended for view sequences - /// - /// Usually it's backed by a collection (e.g. `Vec`) that holds all the (existing) elements. - /// It sweeps over the element collection and does updates in place. - /// Internally it works by having a pointer/index to the current/old element (0 at the beginning), - /// and the pointer is incremented by basically all methods that mutate that sequence. - pub trait $elements_splice { - /// Insert a new element at the current index in the resulting collection (and increment the index by 1) - fn push(&mut self, element: $pod, cx: &mut $cx); - /// Mutate the next existing element, and add it to the resulting collection (and increment the index by 1) - fn mutate(&mut self, cx: &mut $cx) -> &mut $pod; - // TODO(#160) this could also track view id changes (old_id, new_id) - /// Mark any changes done by `mutate` on the current element (this doesn't change the index) - fn mark(&mut self, changeflags: $changeflags, cx: &mut $cx) -> $changeflags; - /// Delete the next n existing elements (this doesn't change the index) - fn delete(&mut self, n: usize, cx: &mut $cx); - /// Current length of the elements collection - fn len(&self) -> usize; - // TODO(#160) add a skip method when it is necessary (e.g. relevant for immutable ViewSequences like ropes) - } - - impl<'a, 'b> $elements_splice for $crate::VecSplice<'a, 'b, $pod> { - fn push(&mut self, element: $pod, _cx: &mut $cx) { - self.push(element); - } - - fn mutate(&mut self, _cx: &mut $cx) -> &mut $pod - { - self.mutate() - } - - fn mark(&mut self, changeflags: $changeflags, _cx: &mut $cx) -> $changeflags - { - self.last_mutated_mut().map(|pod| pod.mark(changeflags)).unwrap_or_default() - } - - fn delete(&mut self, n: usize, _cx: &mut $cx) { - self.delete(n) - } - - fn len(&self) -> usize { - self.len() - } - } - - /// This trait represents a (possibly empty) sequence of views. - /// - /// It is up to the parent view how to lay out and display them. - pub trait $viewseq $( $ss )* { - /// Associated states for the views. - type State $( $ss )*; - - /// Build the associated widgets and initialize all states. - /// - /// To be able to monitor changes (e.g. tree-structure tracking) rather than just adding elements, - /// this takes an element splice as well (when it could be just a `Vec` otherwise) - fn build(&self, cx: &mut $cx, elements: &mut dyn $elements_splice) -> Self::State; - - /// Update the associated widget. - /// - /// Returns `true` when anything has changed. - fn rebuild( - &self, - cx: &mut $cx, - prev: &Self, - state: &mut Self::State, - elements: &mut dyn $elements_splice, - ) -> $changeflags; - - /// Propagate a message. - /// - /// Handle a message, propagating to elements if needed. Here, `id_path` is a slice - /// of ids beginning at an element of this view_sequence. - fn message( - &self, - id_path: &[$crate::Id], - state: &mut Self::State, - message: Box, - app_state: &mut T, - ) -> $crate::MessageResult; - - /// Returns the current amount of widgets built by this sequence. - fn count(&self, state: &Self::State) -> usize; - } - - impl + $viewmarker> $viewseq for V - where - V::Element: $bound + 'static, - { - type State = (>::State, $crate::Id); - - fn build(&self, cx: &mut $cx, elements: &mut dyn $elements_splice) -> Self::State { - let (id, state, pod) = cx.with_new_pod(|cx| >::build(self, cx)); - elements.push(pod, cx); - (state, id) - } - - fn rebuild( - &self, - cx: &mut $cx, - prev: &Self, - state: &mut Self::State, - elements: &mut dyn $elements_splice, - ) -> $changeflags { - let pod = elements.mutate(cx); - let flags = cx.with_pod(pod, |el, cx| { - >::rebuild( - self, - cx, - prev, - &mut state.1, - &mut state.0, - el, - ) - }); - elements.mark(flags, cx) - } - - fn message( - &self, - id_path: &[$crate::Id], - state: &mut Self::State, - message: Box, - app_state: &mut T, - ) -> $crate::MessageResult { - if let Some((first, rest_path)) = id_path.split_first() { - if first == &state.1 { - return >::message( - self, - rest_path, - &mut state.0, - message, - app_state, - ); - } - } - $crate::MessageResult::Stale(message) - } - - fn count(&self, _state: &Self::State) -> usize { - 1 - } - } - - impl> $viewseq for Option { - type State = Option; - - fn build(&self, cx: &mut $cx, elements: &mut dyn $elements_splice) -> Self::State { - match self { - None => None, - Some(vt) => { - let state = vt.build(cx, elements); - Some(state) - } - } - } - - fn rebuild( - &self, - cx: &mut $cx, - prev: &Self, - state: &mut Self::State, - elements: &mut dyn $elements_splice, - ) -> $changeflags { - match (self, &mut *state, prev) { - (Some(this), Some(state), Some(prev)) => this.rebuild(cx, prev, state, elements), - (None, Some(seq_state), Some(prev)) => { - let count = prev.count(&seq_state); - elements.delete(count, cx); - *state = None; - - <$changeflags>::tree_structure() - } - (Some(this), None, None) => { - *state = Some(this.build(cx, elements)); - - <$changeflags>::tree_structure() - } - (None, None, None) => <$changeflags>::empty(), - _ => panic!("non matching state and prev value"), - } - } - - fn message( - &self, - id_path: &[$crate::Id], - state: &mut Self::State, - message: Box, - app_state: &mut T, - ) -> $crate::MessageResult { - match (self, state) { - (Some(vt), Some(state)) => vt.message(id_path, state, message, app_state), - (None, None) => $crate::MessageResult::Stale(message), - _ => panic!("non matching state and prev value"), - } - } - - fn count(&self, state: &Self::State) -> usize { - match (self, state) { - (Some(vt), Some(state)) => vt.count(state), - (None, None) => 0, - _ => panic!("non matching state and prev value"), - } - } - } - - impl> $viewseq for Vec { - type State = Vec; - - fn build(&self, cx: &mut $cx, elements: &mut dyn $elements_splice) -> Self::State { - self.iter().map(|child| child.build(cx, elements)).collect() - } - - fn rebuild( - &self, - cx: &mut $cx, - prev: &Self, - state: &mut Self::State, - elements: &mut dyn $elements_splice, - ) -> $changeflags { - let mut changed = <$changeflags>::default(); - for ((child, child_prev), child_state) in self.iter().zip(prev).zip(state.iter_mut()) { - let el_changed = child.rebuild(cx, child_prev, child_state, elements); - changed |= el_changed; - } - let n = self.len(); - if n < prev.len() { - let n_delete = state - .splice(n.., []) - .enumerate() - .map(|(i, state)| prev[n + i].count(&state)) - .sum(); - elements.delete(n_delete, cx); - changed |= <$changeflags>::tree_structure(); - } else if n > prev.len() { - for i in prev.len()..n { - state.push(self[i].build(cx, elements)); - } - changed |= <$changeflags>::tree_structure(); - } - changed - } - - fn count(&self, state: &Self::State) -> usize { - self.iter().zip(state).map(|(child, child_state)| - child.count(child_state)) - .sum() - } - - fn message( - &self, - id_path: &[$crate::Id], - state: &mut Self::State, - message: Box, - app_state: &mut T, - ) -> $crate::MessageResult { - let mut result = $crate::MessageResult::Stale(message); - for (child, child_state) in self.iter().zip(state) { - if let $crate::MessageResult::Stale(message) = result { - result = child.message(id_path, child_state, message, app_state); - } else { - break; - } - } - result - } - } - - /// This trait marks a type a - #[doc = concat!(stringify!($view), ".")] - /// - /// This trait is a workaround for Rust's orphan rules. It serves as a switch between - /// default and custom - #[doc = concat!("`", stringify!($viewseq), "`")] - /// implementations. You can't implement - #[doc = concat!("`", stringify!($viewseq), "`")] - /// for types which also implement - #[doc = concat!("`", stringify!($viewmarker), "`.")] - pub trait $viewmarker {} - - $crate::impl_view_tuple!($viewseq, $elements_splice, $pod, $cx, $changeflags, ;); - $crate::impl_view_tuple!($viewseq, $elements_splice, $pod, $cx, $changeflags, - V0; 0); - $crate::impl_view_tuple!($viewseq, $elements_splice, $pod, $cx, $changeflags, - V0, V1; 0, 1); - $crate::impl_view_tuple!($viewseq, $elements_splice, $pod, $cx, $changeflags, - V0, V1, V2; 0, 1, 2); - $crate::impl_view_tuple!($viewseq, $elements_splice, $pod, $cx, $changeflags, - V0, V1, V2, V3; 0, 1, 2, 3); - $crate::impl_view_tuple!($viewseq, $elements_splice, $pod, $cx, $changeflags, - V0, V1, V2, V3, V4; 0, 1, 2, 3, 4); - $crate::impl_view_tuple!($viewseq, $elements_splice, $pod, $cx, $changeflags, - V0, V1, V2, V3, V4, V5; 0, 1, 2, 3, 4, 5); - $crate::impl_view_tuple!($viewseq, $elements_splice, $pod, $cx, $changeflags, - V0, V1, V2, V3, V4, V5, V6; 0, 1, 2, 3, 4, 5, 6); - $crate::impl_view_tuple!($viewseq, $elements_splice, $pod, $cx, $changeflags, - V0, V1, V2, V3, V4, V5, V6, V7; 0, 1, 2, 3, 4, 5, 6, 7); - $crate::impl_view_tuple!($viewseq, $elements_splice, $pod, $cx, $changeflags, - V0, V1, V2, V3, V4, V5, V6, V7, V8; 0, 1, 2, 3, 4, 5, 6, 7, 8); - $crate::impl_view_tuple!($viewseq, $elements_splice, $pod, $cx, $changeflags, - V0, V1, V2, V3, V4, V5, V6, V7, V8, V9; 0, 1, 2, 3, 4, 5, 6, 7, 8, 9); - }; -} diff --git a/xilem_web/xilem_web_core/src/vec_splice.rs b/xilem_web/xilem_web_core/src/vec_splice.rs deleted file mode 100644 index b2aed303e..000000000 --- a/xilem_web/xilem_web_core/src/vec_splice.rs +++ /dev/null @@ -1,91 +0,0 @@ -// Copyright 2023 the Xilem Authors -// SPDX-License-Identifier: Apache-2.0 - -pub struct VecSplice<'a, 'b, T> { - v: &'a mut Vec, - scratch: &'b mut Vec, - ix: usize, -} - -impl<'a, 'b, T> VecSplice<'a, 'b, T> { - pub fn new(v: &'a mut Vec, scratch: &'b mut Vec) -> Self { - let ix = 0; - VecSplice { v, scratch, ix } - } - - pub fn skip(&mut self, n: usize) { - if self.v.len() < self.ix + n { - let l = self.scratch.len(); - self.v.extend(self.scratch.splice(l - n.., [])); - self.v[self.ix..].reverse(); - } - self.ix += n; - } - - pub fn delete(&mut self, n: usize) { - if self.v.len() < self.ix + n { - self.scratch.truncate(self.scratch.len() - n); - } else { - if self.v.len() > self.ix + n { - let l = self.scratch.len(); - self.scratch.extend(self.v.splice(self.ix + n.., [])); - self.scratch[l..].reverse(); - } - self.v.truncate(self.ix); - } - } - - pub fn push(&mut self, value: T) { - self.clear_tail(); - self.v.push(value); - self.ix += 1; - } - - pub fn mutate(&mut self) -> &mut T { - if self.v.len() == self.ix { - self.v.push(self.scratch.pop().unwrap()); - } - let ix = self.ix; - self.ix += 1; - &mut self.v[ix] - } - - pub fn last_mutated(&self) -> Option<&T> { - if self.ix == 0 { - None - } else { - self.v.get(self.ix - 1) - } - } - - pub fn last_mutated_mut(&mut self) -> Option<&mut T> { - if self.ix == 0 { - None - } else { - self.v.get_mut(self.ix - 1) - } - } - - pub fn len(&self) -> usize { - self.ix - } - - pub fn is_empty(&self) -> bool { - self.len() == 0 - } - - pub fn as_vec) -> R>(&mut self, f: F) -> R { - self.clear_tail(); - let ret = f(self.v); - self.ix = self.v.len(); - ret - } - - fn clear_tail(&mut self) { - if self.v.len() > self.ix { - let l = self.scratch.len(); - self.scratch.extend(self.v.splice(self.ix.., [])); - self.scratch[l..].reverse(); - } - } -} diff --git a/xilem_web/xilem_web_core/src/view/adapt.rs b/xilem_web/xilem_web_core/src/view/adapt.rs deleted file mode 100644 index 98dc88122..000000000 --- a/xilem_web/xilem_web_core/src/view/adapt.rs +++ /dev/null @@ -1,220 +0,0 @@ -// Copyright 2023 the Xilem Authors -// SPDX-License-Identifier: Apache-2.0 - -#[macro_export] -macro_rules! generate_adapt_view { - ($viewtrait:ident, $cx:ty, $changeflags:ty; $($ss:tt)*) => { - /// A view that wraps a child view and modifies the state that callbacks have access to. - /// - /// # Examples - /// - /// Suppose you have an outer type that looks like - /// - /// ```ignore - /// struct State { - /// todos: Vec - /// } - /// ``` - /// - /// and an inner type/view that looks like - /// - /// ```ignore - /// struct Todo { - /// label: String - /// } - /// - /// struct TodoView { - /// label: String - /// } - /// - /// enum TodoAction { - /// Delete - /// } - /// - /// impl View for TodoView { - /// // ... - /// } - /// ``` - /// - /// then your top-level action (`()`) and state type (`State`) don't match `TodoView`'s. - /// You can use the `Adapt` view to mediate between them: - /// - /// ```ignore - /// state - /// .todos - /// .enumerate() - /// .map(|(idx, todo)| { - /// Adapt::new( - /// move |data: &mut AppState, thunk| { - /// if let MessageResult::Action(action) = thunk.call(&mut data.todos[idx]) { - /// match action { - /// TodoAction::Delete => data.todos.remove(idx), - /// } - /// } - /// MessageResult::Nop - /// }, - /// TodoView { label: todo.label } - /// ) - /// }) - /// ``` - pub struct Adapt< - ParentT, - ParentA, - ChildT, - ChildA, - V, - F = fn(&mut ParentT, AdaptThunk) -> $crate::MessageResult, - > { - f: F, - child: V, - phantom: std::marker::PhantomData (ParentT, ParentA, ChildT, ChildA)>, - } - - /// A "thunk" which dispatches an message to an adapt node's child. - /// - /// The closure passed to [`Adapt`][crate::Adapt] should call this thunk with the child's - /// app state. - pub struct AdaptThunk<'a, ChildT, ChildA, V: $viewtrait> { - child: &'a V, - state: &'a mut V::State, - id_path: &'a [$crate::Id], - message: Box, - } - - impl Adapt - where - V: $viewtrait, - F: Fn(&mut ParentT, AdaptThunk) -> $crate::MessageResult $( $ss )*, - { - pub fn new(f: F, child: V) -> Self { - Adapt { - f, - child, - phantom: Default::default(), - } - } - } - - impl<'a, ChildT, ChildA, V: $viewtrait> AdaptThunk<'a, ChildT, ChildA, V> { - pub fn call(self, app_state: &mut ChildT) -> $crate::MessageResult { - self.child - .message(self.id_path, self.state, self.message, app_state) - } - } - - impl $viewtrait - for Adapt - where - V: $viewtrait, - F: Fn(&mut ParentT, AdaptThunk) -> $crate::MessageResult $( $ss )*, - { - type State = V::State; - - type Element = V::Element; - - fn build(&self, cx: &mut $cx) -> ($crate::Id, Self::State, Self::Element) { - self.child.build(cx) - } - - fn rebuild( - &self, - cx: &mut $cx, - prev: &Self, - id: &mut $crate::Id, - state: &mut Self::State, - element: &mut Self::Element, - ) -> $changeflags { - self.child.rebuild(cx, &prev.child, id, state, element) - } - - fn message( - &self, - id_path: &[$crate::Id], - state: &mut Self::State, - message: Box, - app_state: &mut ParentT, - ) -> $crate::MessageResult { - let thunk = AdaptThunk { - child: &self.child, - state, - id_path, - message, - }; - (self.f)(app_state, thunk) - } - } - - impl ViewMarker - for Adapt - where - V: $viewtrait, - F: Fn(&mut ParentT, AdaptThunk) -> $crate::MessageResult $( $ss )*, - { - } - }; -} - -#[macro_export] -macro_rules! generate_adapt_state_view { - ($viewtrait:ident, $cx:ty, $changeflags:ty; $($ss:tt)*) => { - /// A view that wraps a child view and modifies the state that callbacks have access to. - pub struct AdaptState &mut ChildT> { - f: F, - child: V, - phantom: std::marker::PhantomData (ParentT, ChildT)>, - } - - impl AdaptState - where - F: Fn(&mut ParentT) -> &mut ChildT $( $ss )*, - { - pub fn new(f: F, child: V) -> Self { - Self { - f, - child, - phantom: Default::default(), - } - } - } - - impl $viewtrait for AdaptState - where - V: $viewtrait, - F: Fn(&mut ParentT) -> &mut ChildT $( $ss )*, - { - type State = V::State; - type Element = V::Element; - - fn build(&self, cx: &mut $cx) -> ($crate::Id, Self::State, Self::Element) { - self.child.build(cx) - } - - fn rebuild( - &self, - cx: &mut $cx, - prev: &Self, - id: &mut $crate::Id, - state: &mut Self::State, - element: &mut Self::Element, - ) -> $changeflags { - self.child.rebuild(cx, &prev.child, id, state, element) - } - - fn message( - &self, - id_path: &[$crate::Id], - state: &mut Self::State, - message: Box, - app_state: &mut ParentT, - ) -> $crate::MessageResult { - self.child - .message(id_path, state, message, (self.f)(app_state)) - } - } - - impl ViewMarker for AdaptState where - F: Fn(&mut ParentT) -> &mut ChildT $( $ss )* - { - } - }; -} diff --git a/xilem_web/xilem_web_core/src/view/memoize.rs b/xilem_web/xilem_web_core/src/view/memoize.rs deleted file mode 100644 index f935c3194..000000000 --- a/xilem_web/xilem_web_core/src/view/memoize.rs +++ /dev/null @@ -1,110 +0,0 @@ -// Copyright 2023 the Xilem Authors -// SPDX-License-Identifier: Apache-2.0 - -#[macro_export] -macro_rules! generate_memoize_view { - ($memoizeview:ident, - $memoizestate:ident, - $viewtrait:ident, - $viewmarker:ty, - $cx:ty, - $changeflags:ty, - $staticviewfunction:ident, - $memoizeviewfunction:ident; - $($ss:tt)* - ) => { - pub struct $memoizeview { - data: D, - child_cb: F, - } - - pub struct $memoizestate> { - view: V, - view_state: V::State, - dirty: bool, - } - - impl $memoizeview - where - F: Fn(&D) -> V, - { - pub fn new(data: D, child_cb: F) -> Self { - $memoizeview { data, child_cb } - } - } - - impl $viewmarker for $memoizeview {} - - impl $viewtrait for $memoizeview - where - D: PartialEq $( $ss )* + 'static, - V: $viewtrait, - F: Fn(&D) -> V $( $ss )*, - { - type State = $memoizestate; - - type Element = V::Element; - - fn build(&self, cx: &mut $cx) -> ($crate::Id, Self::State, Self::Element) { - let view = (self.child_cb)(&self.data); - let (id, view_state, element) = view.build(cx); - let memoize_state = $memoizestate { - view, - view_state, - dirty: false, - }; - (id, memoize_state, element) - } - - fn rebuild( - &self, - cx: &mut $cx, - prev: &Self, - id: &mut $crate::Id, - state: &mut Self::State, - element: &mut Self::Element, - ) -> $changeflags { - if std::mem::take(&mut state.dirty) || prev.data != self.data { - let view = (self.child_cb)(&self.data); - let changed = view.rebuild(cx, &state.view, id, &mut state.view_state, element); - state.view = view; - changed - } else { - <$changeflags>::empty() - } - } - - fn message( - &self, - id_path: &[$crate::Id], - state: &mut Self::State, - event: Box, - app_state: &mut T, - ) -> $crate::MessageResult { - let r = state - .view - .message(id_path, &mut state.view_state, event, app_state); - if matches!(r, $crate::MessageResult::RequestRebuild) { - state.dirty = true; - } - r - } - } - - /// A static view, all of the content of the `view` should be constant, as this function is only run once - pub fn $staticviewfunction(view: F) -> $memoizeview<(), impl Fn(&()) -> V> - where - F: Fn() -> V $( $ss )* + 'static, - { - $memoizeview::new((), move |_: &()| view()) - } - - /// Memoize the view, until the `data` changes (in which case `view` is called again) - pub fn $memoizeviewfunction(data: D, view: F) -> $memoizeview - where - F: Fn(&D) -> V $( $ss )*, - { - $memoizeview::new(data, view) - } - }; -} diff --git a/xilem_web/xilem_web_core/src/view/mod.rs b/xilem_web/xilem_web_core/src/view/mod.rs deleted file mode 100644 index e9e39a781..000000000 --- a/xilem_web/xilem_web_core/src/view/mod.rs +++ /dev/null @@ -1,74 +0,0 @@ -// Copyright 2023 the Xilem Authors -// SPDX-License-Identifier: Apache-2.0 - -mod adapt; -mod memoize; - -/// Create the `View` trait for a particular xilem context (e.g. html, native, ...). -/// -/// Arguments are -/// -/// - `$viewtrait` - The name of the view trait we want to generate. -/// - `$bound` - A bound on all element types that will be used. -/// - `$cx` - The name of text context type that will be passed to the `build`/`rebuild` -/// methods, and be responsible for managing element creation & deletion. -/// - `$changeflags` - The type that reports down/up the tree. Can be used to avoid -/// doing work when we can prove nothing needs doing. -/// - `$ss` - (optional) parent traits to this trait (e.g. `:Send`). Also applied to -/// the state type requirements -#[macro_export] -macro_rules! generate_view_trait { - ($viewtrait:ident, $bound:ident, $cx:ty, $changeflags:ty; $($ss:tt)*) => { - /// A view object representing a node in the UI. - /// - /// This is a central trait for representing UI. An app will generate a tree of - /// these objects (the view tree) as the primary interface for expressing UI. - /// The view tree is transitory and is retained only long enough to dispatch - /// messages and then serve as a reference for diffing for the next view tree. - /// - /// The framework will then run methods on these views to create the associated - /// state tree and element tree, as well as incremental updates and message - /// propagation. - /// - /// The - #[doc = concat!("`", stringify!($viewtrait), "`")] - // trait is parameterized by `T`, which is known as the "app state", - /// and also a type for actions which are passed up the tree in message - /// propagation. During message handling, mutable access to the app state is - /// given to view nodes, which in turn can expose it to callbacks. - pub trait $viewtrait $( $ss )* { - /// Associated state for the view. - type State $( $ss )*; - - /// The associated element for the view. - type Element: $bound; - - /// Build the associated widget and initialize state. - fn build(&self, cx: &mut $cx) -> ($crate::Id, Self::State, Self::Element); - - /// Update the associated element. - /// - /// Returns an indication of what, if anything, has changed. - fn rebuild( - &self, - cx: &mut $cx, - prev: &Self, - id: &mut $crate::Id, - state: &mut Self::State, - element: &mut Self::Element, - ) -> $changeflags; - - /// Propagate a message. - /// - /// Handle a message, propagating to children if needed. Here, `id_path` is a slice - /// of ids beginning at a child of this view. - fn message( - &self, - id_path: &[$crate::Id], - state: &mut Self::State, - message: Box, - app_state: &mut T, - ) -> $crate::MessageResult; - } - }; -}