`, 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