Add wasm DOM implementation for xilem.

I'm doing all the work in a single commit so it's easier to rebase.

Below is a list of the individual changes I did

 - Squash prev commits
 - Remove top-level action handler (use Adapt instead).
 - Type events in the same way as elements (could also do attributes the
   same)
 - Allow users to avoid compiling the typed html/attributes/events
 - Use more specific types for mouse events from web_sys
 - change "click" from PointerEvent to MouseEvent (PointerEvent caused a
   panic on `dyn_into().unwrap()`)
This commit is contained in:
Raph Levien 2023-03-20 17:19:22 -07:00 committed by Richard Dodd
parent 0759de95bd
commit 1fd1ce7885
48 changed files with 4427 additions and 387 deletions

3
.gitignore vendored
View File

@ -2,3 +2,6 @@
target/
# We're a library, so ignore Cargo.lock
Cargo.lock
.vscode
.cspell

793
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,12 @@
[workspace]
members = [
"crates/xilem_core",
"crates/xilem_svg",
"crates/xilem_html",
"crates/xilem_html/web_examples/counter",
"crates/xilem_html/web_examples/counter_untyped",
"crates/xilem_html/web_examples/todomvc",
".",
]
[package]

View File

@ -23,5 +23,5 @@ mod vec_splice;
mod view;
pub use id::{Id, IdPath};
pub use message::{AsyncWake, Message, MessageResult};
pub use message::{AsyncWake, MessageResult};
pub use vec_splice::VecSplice;

View File

@ -3,21 +3,60 @@
use std::any::Any;
use crate::id::IdPath;
#[macro_export]
macro_rules! message {
() => {
pub struct Message {
pub id_path: IdPath,
pub body: Box<dyn Any + Send>,
pub id_path: xilem_core::IdPath,
pub body: Box<dyn std::any::Any>,
}
impl Message {
pub fn new(id_path: xilem_core::IdPath, event: impl std::any::Any) -> Message {
Message {
id_path,
body: Box::new(event),
}
}
}
};
($($bounds:tt)*) => {
pub struct Message {
pub id_path: xilem_core::IdPath,
pub body: Box<dyn std::any::Any + $($bounds)*>,
}
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.
pub enum MessageResult<A> {
/// 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)]
Nop,
/// The event was addressed to an id path no longer in the tree.
@ -27,6 +66,12 @@ pub enum MessageResult<A> {
Stale(Box<dyn Any>),
}
impl<A> Default for MessageResult<A> {
fn default() -> Self {
MessageResult::Nop
}
}
// TODO: does this belong in core?
pub struct AsyncWake;
@ -47,12 +92,3 @@ impl<A> MessageResult<A> {
}
}
}
impl Message {
pub fn new(id_path: IdPath, event: impl Any + Send) -> Message {
Message {
id_path,
body: Box::new(event),
}
}
}

View File

@ -5,7 +5,7 @@
#[macro_export]
macro_rules! impl_view_tuple {
( $viewseq:ident, $pod:ty, $cx:ty, $changeflags:ty, $( $t:ident),* ; $( $i:tt ),* ) => {
impl<T, A, $( $t: ViewSequence<T, A> ),* > ViewSequence<T, A> for ( $( $t, )* ) {
impl<T, A, $( $t: $viewseq<T, A> ),* > $viewseq<T, A> for ( $( $t, )* ) {
type State = ( $( $t::State, )*);
fn build(&self, cx: &mut $cx, elements: &mut Vec<$pod>) -> Self::State {
@ -292,6 +292,7 @@ macro_rules! generate_viewsequence_trait {
#[doc = concat!("`", stringify!($viewmarker), "`.")]
pub trait $viewmarker {}
$crate::impl_view_tuple!($viewseq, $pod, $cx, $changeflags, ;);
$crate::impl_view_tuple!($viewseq, $pod, $cx, $changeflags,
V0; 0);
$crate::impl_view_tuple!($viewseq, $pod, $cx, $changeflags,

View File

@ -1,6 +1,18 @@
// Copyright 2023 the Druid Authors.
// SPDX-License-Identifier: Apache-2.0
/// 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)*) => {
@ -9,17 +21,17 @@ macro_rules! generate_view_trait {
/// 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
/// events and then serve as a reference for diffing for the next view tree.
/// 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 event
/// 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 event
/// propagation. During event handling, mutable access to the app state is
/// 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<T, A = ()> $( $ss )* {
/// Associated state for the view.
@ -55,5 +67,84 @@ macro_rules! generate_view_trait {
app_state: &mut T,
) -> $crate::MessageResult<A>;
}
pub struct Adapt<OutData, OutMsg, InData, InMsg, F: Fn(&mut OutData, AdaptThunk<InData, InMsg, V>) -> $crate::MessageResult<OutMsg>, V: View<InData, InMsg>> {
f: F,
child: V,
phantom: std::marker::PhantomData<fn() -> (OutData, OutMsg, InData, InMsg)>,
}
/// 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, InData, InMsg, V: View<InData, InMsg>> {
child: &'a V,
state: &'a mut V::State,
id_path: &'a [$crate::Id],
message: Box<dyn std::any::Any>,
}
impl<OutData, OutMsg, InData, InMsg, F: Fn(&mut OutData, AdaptThunk<InData, InMsg, V>) -> $crate::MessageResult<OutMsg>, V: View<InData, InMsg>>
Adapt<OutData, OutMsg, InData, InMsg, F, V>
{
pub fn new(f: F, child: V) -> Self {
Adapt {
f,
child,
phantom: Default::default(),
}
}
}
impl<'a, InData, InMsg, V: View<InData, InMsg>> AdaptThunk<'a, InData, InMsg, V> {
pub fn call(self, app_state: &mut InData) -> $crate::MessageResult<InMsg> {
self.child
.message(self.id_path, self.state, self.message, app_state)
}
}
impl<OutData, OutMsg, InData, InMsg, F: Fn(&mut OutData, AdaptThunk<InData, InMsg, V>) -> $crate::MessageResult<OutMsg> + Send, V: View<InData, InMsg>>
View<OutData, OutMsg> for Adapt<OutData, OutMsg, InData, InMsg, F, V>
{
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<dyn std::any::Any>,
app_state: &mut OutData,
) -> $crate::MessageResult<OutMsg> {
let thunk = AdaptThunk {
child: &self.child,
state,
id_path,
message,
};
(self.f)(app_state, thunk)
}
}
impl<OutData, OutMsg, InData, InMsg, F: Fn(&mut OutData, AdaptThunk<InData, InMsg, V>) -> $crate::MessageResult<OutMsg>, V: View<InData, InMsg>>
ViewMarker for Adapt<OutData, OutMsg, InData, InMsg, F, V> {}
};
}

1
crates/xilem_html/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
dist/

View File

@ -0,0 +1,92 @@
[package]
name = "xilem_html"
version = "0.1.0"
license = "Apache-2.0"
edition = "2021"
[features]
default = ["typed"]
typed = [
'web-sys/FocusEvent',
'web-sys/HtmlAnchorElement',
'web-sys/HtmlAreaElement',
'web-sys/HtmlAudioElement',
'web-sys/HtmlBrElement',
'web-sys/HtmlButtonElement',
'web-sys/HtmlCanvasElement',
'web-sys/HtmlDataElement',
'web-sys/HtmlDataListElement',
'web-sys/HtmlDetailsElement',
'web-sys/HtmlDialogElement',
'web-sys/HtmlDivElement',
'web-sys/HtmlDListElement',
'web-sys/HtmlEmbedElement',
'web-sys/HtmlFieldSetElement',
'web-sys/HtmlFormElement',
'web-sys/HtmlHeadingElement',
'web-sys/HtmlHrElement',
'web-sys/HtmlIFrameElement',
'web-sys/HtmlImageElement',
'web-sys/HtmlInputElement',
'web-sys/HtmlLabelElement',
'web-sys/HtmlLegendElement',
'web-sys/HtmlLiElement',
'web-sys/HtmlMapElement',
'web-sys/HtmlMenuElement',
'web-sys/HtmlMeterElement',
'web-sys/HtmlModElement',
'web-sys/HtmlObjectElement',
'web-sys/HtmlOListElement',
'web-sys/HtmlOptGroupElement',
'web-sys/HtmlOptionElement',
'web-sys/HtmlOutputElement',
'web-sys/HtmlParagraphElement',
'web-sys/HtmlPictureElement',
'web-sys/HtmlPreElement',
'web-sys/HtmlProgressElement',
'web-sys/HtmlQuoteElement',
'web-sys/HtmlScriptElement',
'web-sys/HtmlSelectElement',
'web-sys/HtmlSlotElement',
'web-sys/HtmlSourceElement',
'web-sys/HtmlSpanElement',
'web-sys/HtmlTableElement',
'web-sys/HtmlTableCellElement',
'web-sys/HtmlTableColElement',
'web-sys/HtmlTableCaptionElement',
'web-sys/HtmlTableRowElement',
'web-sys/HtmlTableSectionElement',
'web-sys/HtmlTemplateElement',
'web-sys/HtmlTextAreaElement',
'web-sys/HtmlTimeElement',
'web-sys/HtmlTrackElement',
'web-sys/HtmlUListElement',
'web-sys/HtmlVideoElement',
'web-sys/InputEvent',
'web-sys/KeyboardEvent',
'web-sys/MouseEvent',
'web-sys/PointerEvent',
'web-sys/WheelEvent',
]
[dependencies]
bitflags = "1.3.2"
wasm-bindgen = "0.2.87"
kurbo = "0.9.1"
xilem_core = { path = "../xilem_core" }
log = "0.4.19"
[dependencies.web-sys]
version = "0.3.4"
features = [
'console',
'Document',
'Element',
'Event',
'HtmlElement',
'Node',
'NodeList',
'SvgElement',
'Text',
'Window',
]

View File

@ -0,0 +1,7 @@
# Xilemsvg prototype
This is a proof of concept showing how to use `xilem_core` to render interactive vector graphics into SVG DOM nodes, running in a browser. A next step would be to factor it into a library so that applications can depend on it, but at the moment the test scene is baked in.
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`).
[Trunk]: https://trunkrs.dev/

View File

@ -0,0 +1,122 @@
// Copyright 2023 the Druid Authors.
// SPDX-License-Identifier: Apache-2.0
use std::{cell::RefCell, rc::Rc};
use crate::{
context::Cx,
view::{DomNode, View},
Message,
};
use xilem_core::{Id, MessageResult};
pub struct App<T, V: View<T>, F: FnMut(&mut T) -> V>(Rc<RefCell<AppInner<T, V, F>>>);
struct AppInner<T, V: View<T>, F: FnMut(&mut T) -> V> {
data: T,
app_logic: F,
view: Option<V>,
id: Option<Id>,
state: Option<V::State>,
element: Option<V::Element>,
cx: Cx,
}
pub(crate) trait AppRunner {
fn handle_message(&self, message: Message);
fn clone_box(&self) -> Box<dyn AppRunner>;
}
impl<T: 'static, V: View<T> + 'static, F: FnMut(&mut T) -> V + 'static> Clone for App<T, V, F> {
fn clone(&self) -> Self {
App(self.0.clone())
}
}
impl<T: 'static, V: View<T> + 'static, F: FnMut(&mut T) -> V + 'static> App<T, V, F> {
pub fn new(data: T, app_logic: F) -> Self {
let inner = AppInner::new(data, app_logic);
let app = App(Rc::new(RefCell::new(inner)));
app.0.borrow_mut().cx.set_runner(app.clone());
app
}
pub fn run(self, root: &web_sys::HtmlElement) {
self.0.borrow_mut().ensure_app(root);
// Latter may not be necessary, we have an rc loop.
std::mem::forget(self)
}
}
impl<T, V: View<T>, F: FnMut(&mut T) -> V> AppInner<T, V, F> {
pub fn new(data: T, app_logic: F) -> Self {
let cx = Cx::new();
AppInner {
data,
app_logic,
view: None,
id: None,
state: None,
element: None,
cx,
}
}
fn ensure_app(&mut self, root: &web_sys::HtmlElement) {
if self.view.is_none() {
let view = (self.app_logic)(&mut self.data);
let (id, state, element) = view.build(&mut self.cx);
self.view = Some(view);
self.id = Some(id);
self.state = Some(state);
root.append_child(element.as_node_ref()).unwrap();
self.element = Some(element);
}
}
}
impl<T: 'static, V: View<T> + 'static, F: FnMut(&mut T) -> V + 'static> AppRunner for App<T, V, F> {
// 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) {
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.body,
&mut inner.data,
);
match message_result {
MessageResult::Nop | MessageResult::Action(_) => {
// Nothing to do.
}
MessageResult::RequestRebuild => {
// TODO force a rebuild?
}
MessageResult::Stale(_) => {
// TODO perhaps inform the user that a stale request bubbled to the top?
}
}
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.
*view = new_view;
}
}
fn clone_box(&self) -> Box<dyn AppRunner> {
Box::new(self.clone())
}
}

View File

@ -0,0 +1,75 @@
// Copyright 2023 the Druid Authors.
// SPDX-License-Identifier: Apache-2.0
use std::{any::Any, borrow::Cow};
use xilem_core::{Id, MessageResult};
use crate::{
context::{ChangeFlags, Cx},
view::{DomElement, View, ViewMarker},
};
pub struct Class<V> {
child: V,
// This could reasonably be static Cow also, but keep things simple
class: Cow<'static, str>,
}
pub fn class<V>(child: V, class: impl Into<Cow<'static, str>>) -> Class<V> {
Class {
child,
class: class.into(),
}
}
impl<V> ViewMarker for Class<V> {}
// TODO: make generic over A (probably requires Phantom)
impl<T, V> View<T> for Class<V>
where
V: View<T>,
V::Element: DomElement,
{
type State = V::State;
type Element = V::Element;
fn build(&self, cx: &mut Cx) -> (Id, Self::State, Self::Element) {
let (id, child_state, element) = self.child.build(cx);
element
.as_element_ref()
.set_attribute("class", &self.class)
.unwrap();
(id, child_state, element)
}
fn rebuild(
&self,
cx: &mut Cx,
prev: &Self,
id: &mut Id,
state: &mut Self::State,
element: &mut V::Element,
) -> ChangeFlags {
let prev_id = *id;
let mut changed = self.child.rebuild(cx, &prev.child, id, state, element);
if self.class != prev.class || prev_id != *id {
element
.as_element_ref()
.set_attribute("class", &self.class)
.unwrap();
changed.insert(ChangeFlags::OTHER_CHANGE);
}
changed
}
fn message(
&self,
id_path: &[Id],
state: &mut Self::State,
message: Box<dyn Any>,
app_state: &mut T,
) -> MessageResult<()> {
self.child.message(id_path, state, message, app_state)
}
}

View File

@ -0,0 +1,117 @@
use std::any::Any;
use bitflags::bitflags;
use wasm_bindgen::JsCast;
use web_sys::Document;
use xilem_core::{Id, IdPath};
use crate::{app::AppRunner, Message, HTML_NS, SVG_NS};
// Note: xilem has derive Clone here. Not sure.
pub struct Cx {
id_path: IdPath,
document: Document,
app_ref: Option<Box<dyn AppRunner>>,
}
pub struct MessageThunk {
id_path: IdPath,
app_ref: Box<dyn AppRunner>,
}
bitflags! {
#[derive(Default)]
pub struct ChangeFlags: u32 {
const STRUCTURE = 1;
const OTHER_CHANGE = 2;
}
}
impl Cx {
pub fn new() -> Self {
Cx {
id_path: Vec::new(),
document: crate::document(),
app_ref: None,
}
}
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, F: FnOnce(&mut Cx) -> 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, F: FnOnce(&mut Cx) -> T>(&mut self, f: F) -> (Id, T) {
let id = Id::next();
self.push(id);
let result = f(self);
self.pop();
(id, result)
}
pub fn document(&self) -> &Document {
&self.document
}
pub fn create_element(&self, ns: &str, name: &str) -> web_sys::Element {
self.document
.create_element_ns(Some(ns), name)
.expect("could not create element")
}
pub fn create_html_element(&self, name: &str) -> web_sys::HtmlElement {
self.create_element(HTML_NS, name).unchecked_into()
}
pub fn create_svg_element(&self, name: &str) -> web_sys::SvgElement {
self.create_element(SVG_NS, name).unchecked_into()
}
pub fn message_thunk(&self) -> MessageThunk {
MessageThunk {
id_path: self.id_path.clone(),
app_ref: self.app_ref.as_ref().unwrap().clone_box(),
}
}
pub(crate) fn set_runner(&mut self, runner: impl AppRunner + 'static) {
self.app_ref = Some(Box::new(runner));
}
}
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);
}
}
impl ChangeFlags {
pub fn tree_structure() -> Self {
Self::STRUCTURE
}
}

View File

@ -0,0 +1,226 @@
//! Macros to generate all the different html elements
//!
macro_rules! elements {
() => {};
(($ty_name:ident, $builder_name:ident, $name:literal, $web_sys_ty:ty), $($rest:tt)*) => {
element!($ty_name, $builder_name, $name, $web_sys_ty);
elements!($($rest)*);
};
}
macro_rules! element {
($ty_name:ident, $builder_name:ident, $name:literal, $web_sys_ty:ty) => {
pub struct $ty_name<ViewSeq>(crate::Element<$web_sys_ty, ViewSeq>);
pub fn $builder_name<ViewSeq>(children: ViewSeq) -> $ty_name<ViewSeq> {
$ty_name(crate::element($name, children))
}
impl<ViewSeq> $ty_name<ViewSeq> {
/// Set an attribute on this element.
///
/// # Panics
///
/// If the name contains characters that are not valid in an attribute name,
/// then the `View::build`/`View::rebuild` functions will panic for this view.
pub fn attr(
mut self,
name: impl Into<std::borrow::Cow<'static, str>>,
value: impl Into<std::borrow::Cow<'static, str>>,
) -> Self {
self.0.set_attr(name, value);
self
}
/// Set an attribute on this element.
///
/// # Panics
///
/// If the name contains characters that are not valid in an attribute name,
/// then the `View::build`/`View::rebuild` functions will panic for this view.
pub fn set_attr(
&mut self,
name: impl Into<std::borrow::Cow<'static, str>>,
value: impl Into<std::borrow::Cow<'static, str>>,
) -> &mut Self {
self.0.set_attr(name, value);
self
}
}
impl<ViewSeq> crate::view::ViewMarker for $ty_name<ViewSeq> {}
impl<T_, A_, ViewSeq> crate::view::View<T_, A_> for $ty_name<ViewSeq>
where
ViewSeq: crate::view::ViewSequence<T_, A_>,
{
type State = crate::ElementState<ViewSeq::State>;
type Element = $web_sys_ty;
fn build(
&self,
cx: &mut crate::context::Cx,
) -> (xilem_core::Id, Self::State, Self::Element) {
self.0.build(cx)
}
fn rebuild(
&self,
cx: &mut crate::context::Cx,
prev: &Self,
id: &mut xilem_core::Id,
state: &mut Self::State,
element: &mut Self::Element,
) -> crate::ChangeFlags {
self.0.rebuild(cx, &prev.0, id, state, element)
}
fn message(
&self,
id_path: &[xilem_core::Id],
state: &mut Self::State,
message: Box<dyn std::any::Any>,
app_state: &mut T_,
) -> xilem_core::MessageResult<A_> {
self.0.message(id_path, state, message, app_state)
}
}
};
}
// void elements (those without children) are `area`, `base`, `br`, `col`,
// `embed`, `hr`, `img`, `input`, `link`, `meta`, `source`, `track`, `wbr`
elements!(
// the order is copied from
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element
// DOM interfaces copied from https://html.spec.whatwg.org/multipage/grouping-content.html and friends
// content sectioning
(Address, address, "address", web_sys::HtmlElement),
(Article, article, "article", web_sys::HtmlElement),
(Aside, aside, "aside", web_sys::HtmlElement),
(Footer, footer, "footer", web_sys::HtmlElement),
(Header, header, "header", web_sys::HtmlElement),
(H1, h1, "h1", web_sys::HtmlHeadingElement),
(H2, h2, "h2", web_sys::HtmlHeadingElement),
(H3, h3, "h3", web_sys::HtmlHeadingElement),
(H4, h4, "h4", web_sys::HtmlHeadingElement),
(H5, h5, "h5", web_sys::HtmlHeadingElement),
(H6, h6, "h6", web_sys::HtmlHeadingElement),
(Hgroup, hgroup, "hgroup", web_sys::HtmlElement),
(Main, main, "main", web_sys::HtmlElement),
(Nav, nav, "nav", web_sys::HtmlElement),
(Section, section, "section", web_sys::HtmlElement),
// text content
(
Blockquote,
blockquote,
"blockquote",
web_sys::HtmlQuoteElement
),
(Dd, dd, "dd", web_sys::HtmlElement),
(Div, div, "div", web_sys::HtmlDivElement),
(Dl, dl, "dl", web_sys::HtmlDListElement),
(Dt, dt, "dt", web_sys::HtmlElement),
(Figcaption, figcaption, "figcaption", web_sys::HtmlElement),
(Figure, figure, "figure", web_sys::HtmlElement),
(Hr, hr, "hr", web_sys::HtmlHrElement),
(Li, li, "li", web_sys::HtmlLiElement),
(Menu, menu, "menu", web_sys::HtmlMenuElement),
(Ol, ol, "ol", web_sys::HtmlOListElement),
(P, p, "p", web_sys::HtmlParagraphElement),
(Pre, pre, "pre", web_sys::HtmlPreElement),
(Ul, ul, "ul", web_sys::HtmlUListElement),
// inline text
(A, a, "a", web_sys::HtmlAnchorElement),
(Abbr, abbr, "abbr", web_sys::HtmlElement),
(B, b, "b", web_sys::HtmlElement),
(Bdi, bdi, "bdi", web_sys::HtmlElement),
(Bdo, bdo, "bdo", web_sys::HtmlElement),
(Br, br, "br", web_sys::HtmlBrElement),
(Cite, cite, "cite", web_sys::HtmlElement),
(Code, code, "code", web_sys::HtmlElement),
(Data, data, "data", web_sys::HtmlDataElement),
(Dfn, dfn, "dfn", web_sys::HtmlElement),
(Em, em, "em", web_sys::HtmlElement),
(I, i, "i", web_sys::HtmlElement),
(Kbd, kbd, "kbd", web_sys::HtmlElement),
(Mark, mark, "mark", web_sys::HtmlElement),
(Q, q, "q", web_sys::HtmlQuoteElement),
(Rp, rp, "rp", web_sys::HtmlElement),
(Rt, rt, "rt", web_sys::HtmlElement),
(Ruby, ruby, "ruby", web_sys::HtmlElement),
(S, s, "s", web_sys::HtmlElement),
(Samp, samp, "samp", web_sys::HtmlElement),
(Small, small, "small", web_sys::HtmlElement),
(Span, span, "span", web_sys::HtmlSpanElement),
(Strong, strong, "strong", web_sys::HtmlElement),
(Sub, sub, "sub", web_sys::HtmlElement),
(Sup, sup, "sup", web_sys::HtmlElement),
(Time, time, "time", web_sys::HtmlTimeElement),
(U, u, "u", web_sys::HtmlElement),
(Var, var, "var", web_sys::HtmlElement),
(Wbr, wbr, "wbr", web_sys::HtmlElement),
// image and multimedia
(Area, area, "area", web_sys::HtmlAreaElement),
(Audio, audio, "audio", web_sys::HtmlAudioElement),
(Img, img, "img", web_sys::HtmlImageElement),
(Map, map, "map", web_sys::HtmlMapElement),
(Track, track, "track", web_sys::HtmlTrackElement),
(Video, video, "video", web_sys::HtmlVideoElement),
// embedded content
(Embed, embed, "embed", web_sys::HtmlEmbedElement),
(Iframe, iframe, "iframe", web_sys::HtmlIFrameElement),
(Object, object, "object", web_sys::HtmlObjectElement),
(Picture, picture, "picture", web_sys::HtmlPictureElement),
(Portal, portal, "portal", web_sys::HtmlElement),
(Source, source, "source", web_sys::HtmlSourceElement),
// SVG and MathML (TODO, svg and mathml elements)
(Svg, svg, "svg", web_sys::HtmlElement),
(Math, math, "math", web_sys::HtmlElement),
// scripting
(Canvas, canvas, "canvas", web_sys::HtmlCanvasElement),
(Noscript, noscript, "noscript", web_sys::HtmlElement),
(Script, script, "script", web_sys::HtmlScriptElement),
// demarcating edits
(Del, del, "del", web_sys::HtmlModElement),
(Ins, ins, "ins", web_sys::HtmlModElement),
// tables
(
Caption,
caption,
"caption",
web_sys::HtmlTableCaptionElement
),
(Col, col, "col", web_sys::HtmlTableColElement),
(Colgroup, colgroup, "colgroup", web_sys::HtmlTableColElement),
(Table, table, "table", web_sys::HtmlTableSectionElement),
(Tbody, tbody, "tbody", web_sys::HtmlTableSectionElement),
(Td, td, "td", web_sys::HtmlTableCellElement),
(Tfoot, tfoot, "tfoot", web_sys::HtmlTableSectionElement),
(Th, th, "th", web_sys::HtmlTableCellElement),
(Thead, thead, "thead", web_sys::HtmlTableSectionElement),
(Tr, tr, "tr", web_sys::HtmlTableRowElement),
// forms
(Button, button, "button", web_sys::HtmlButtonElement),
(Datalist, datalist, "datalist", web_sys::HtmlDataListElement),
(Fieldset, fieldset, "fieldset", web_sys::HtmlFieldSetElement),
(Form, form, "form", web_sys::HtmlFormElement),
(Input, input, "input", web_sys::HtmlInputElement),
(Label, label, "label", web_sys::HtmlLabelElement),
(Legend, legend, "legend", web_sys::HtmlLegendElement),
(Meter, meter, "meter", web_sys::HtmlMeterElement),
(Optgroup, optgroup, "optgroup", web_sys::HtmlOptGroupElement),
(Option, option, "option", web_sys::HtmlOptionElement),
(Output, output, "output", web_sys::HtmlOutputElement),
(Progress, progress, "progress", web_sys::HtmlProgressElement),
(Select, select, "select", web_sys::HtmlSelectElement),
(Textarea, textarea, "textarea", web_sys::HtmlTextAreaElement),
// interactive elements,
(Details, details, "details", web_sys::HtmlDetailsElement),
(Dialog, dialog, "dialog", web_sys::HtmlDialogElement),
(Summary, summary, "summary", web_sys::HtmlElement),
// web components,
(Slot, slot, "slot", web_sys::HtmlSlotElement),
(Template, template, "template", web_sys::HtmlTemplateElement),
);

View File

@ -0,0 +1,256 @@
//! The HTML element view and associated types/functions.
//!
//! If you are writing your own views, we recommend adding
//! `use xilem_html::elements as el` or similar to the top of your file.
use crate::{
context::{ChangeFlags, Cx},
view::{DomElement, Pod, View, ViewMarker, ViewSequence},
};
use std::{borrow::Cow, cmp::Ordering, collections::BTreeMap, fmt, marker::PhantomData};
use wasm_bindgen::{JsCast, UnwrapThrowExt};
use xilem_core::{Id, MessageResult, VecSplice};
#[cfg(feature = "typed")]
pub mod elements;
/// A view representing a HTML element.
pub struct Element<El, Children = ()> {
name: Cow<'static, str>,
attributes: BTreeMap<Cow<'static, str>, Cow<'static, str>>,
children: Children,
ty: PhantomData<El>,
}
impl<E, ViewSeq> Element<E, ViewSeq> {
pub fn debug_as_el(&self) -> impl fmt::Debug + '_ {
struct DebugFmt<'a, E, VS>(&'a Element<E, VS>);
impl<'a, E, VS> fmt::Debug for DebugFmt<'a, E, VS> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "<{}", self.0.name)?;
for (name, value) in &self.0.attributes {
write!(f, " {name}=\"{value}\"")?;
}
write!(f, ">")
}
}
DebugFmt(self)
}
}
/// The state associated with a HTML element `View`.
///
/// Stores handles to the child elements and any child state.
pub struct ElementState<ViewSeqState> {
child_states: ViewSeqState,
child_elements: Vec<Pod>,
}
/// Create a new element
pub fn element<E, ViewSeq>(
name: impl Into<Cow<'static, str>>,
children: ViewSeq,
) -> Element<E, ViewSeq> {
Element {
name: name.into(),
attributes: BTreeMap::new(),
children,
ty: PhantomData,
}
}
impl<E, ViewSeq> Element<E, ViewSeq> {
/// Set an attribute on this element.
///
/// # Panics
///
/// If the name contains characters that are not valid in an attribute name,
/// then the `View::build`/`View::rebuild` functions will panic for this view.
pub fn attr(
mut self,
name: impl Into<Cow<'static, str>>,
value: impl Into<Cow<'static, str>>,
) -> Self {
self.attributes.insert(name.into(), value.into());
self
}
pub fn set_attr(
&mut self,
name: impl Into<Cow<'static, str>>,
value: impl Into<Cow<'static, str>>,
) {
self.attributes.insert(name.into(), value.into());
}
}
impl<El, Children> ViewMarker for Element<El, Children> {}
impl<T, A, El, Children> View<T, A> for Element<El, Children>
where
Children: ViewSequence<T, A>,
// In addition, the `E` parameter is expected to be a child of `web_sys::Node`
El: JsCast + DomElement,
{
type State = ElementState<Children::State>;
type Element = El;
fn build(&self, cx: &mut Cx) -> (Id, Self::State, El) {
let el = cx.create_html_element(&self.name);
for (name, value) in &self.attributes {
el.set_attribute(name, value).unwrap();
}
let mut child_elements = vec![];
let (id, child_states) = cx.with_new_id(|cx| self.children.build(cx, &mut child_elements));
for child in &child_elements {
el.append_child(child.0.as_node_ref()).unwrap();
}
let state = ElementState {
child_states,
child_elements,
};
(id, state, el.dyn_into().unwrap_throw())
}
fn rebuild(
&self,
cx: &mut Cx,
prev: &Self,
id: &mut Id,
state: &mut Self::State,
element: &mut El,
) -> ChangeFlags {
let mut changed = ChangeFlags::empty();
// update tag name
if prev.name != self.name {
// recreate element
let parent = element
.as_element_ref()
.parent_element()
.expect_throw("this element was mounted and so should have a parent");
parent.remove_child(element.as_node_ref()).unwrap();
let new_element = cx.create_html_element(&self.name);
// TODO could this be combined with child updates?
while element.as_element_ref().child_element_count() > 0 {
new_element
.append_child(&element.as_element_ref().child_nodes().get(0).unwrap_throw())
.unwrap_throw();
}
*element = new_element.dyn_into().unwrap_throw();
changed |= ChangeFlags::STRUCTURE;
}
let element = element.as_element_ref();
// update attributes
// TODO can I use VecSplice for this?
let mut prev_attrs = prev.attributes.iter().peekable();
let mut self_attrs = self.attributes.iter().peekable();
while let (Some((prev_name, prev_value)), Some((self_name, self_value))) =
(prev_attrs.peek(), self_attrs.peek())
{
match prev_name.cmp(self_name) {
Ordering::Less => {
// attribute from prev is disappeared
remove_attribute(element, prev_name);
changed |= ChangeFlags::OTHER_CHANGE;
prev_attrs.next();
}
Ordering::Greater => {
// new attribute has appeared
set_attribute(element, self_name, self_value);
changed |= ChangeFlags::OTHER_CHANGE;
self_attrs.next();
}
Ordering::Equal => {
// attribute may has changed
if prev_value != self_value {
set_attribute(element, self_name, self_value);
changed |= ChangeFlags::OTHER_CHANGE;
}
prev_attrs.next();
self_attrs.next();
}
}
}
// Only max 1 of these loops will run
while let Some((name, _)) = prev_attrs.next() {
remove_attribute(element, name);
changed |= ChangeFlags::OTHER_CHANGE;
}
while let Some((name, value)) = self_attrs.next() {
set_attribute(element, name, value);
changed |= ChangeFlags::OTHER_CHANGE;
}
// update children
// TODO avoid reallocation every render?
let mut scratch = vec![];
let mut splice = VecSplice::new(&mut state.child_elements, &mut scratch);
changed |= cx.with_id(*id, |cx| {
self.children
.rebuild(cx, &prev.children, &mut state.child_states, &mut splice)
});
if changed.contains(ChangeFlags::STRUCTURE) {
// This is crude and will result in more DOM traffic than needed.
// The right thing to do is diff the new state of the children id
// vector against the old, and derive DOM mutations from that.
while let Some(child) = element.first_child() {
element.remove_child(&child).unwrap();
}
for child in &state.child_elements {
element.append_child(child.0.as_node_ref()).unwrap();
}
changed.remove(ChangeFlags::STRUCTURE);
}
changed
}
fn message(
&self,
id_path: &[Id],
state: &mut Self::State,
message: Box<dyn std::any::Any>,
app_state: &mut T,
) -> MessageResult<A> {
self.children
.message(id_path, &mut state.child_states, message, app_state)
}
}
#[cfg(feature = "typed")]
fn set_attribute(element: &web_sys::Element, name: &str, value: &str) {
// we have to special-case `value` because setting the value using `set_attribute`
// doesn't work after the value has been changed.
if name == "value" {
let element: &web_sys::HtmlInputElement = element.dyn_ref().unwrap_throw();
element.set_value(value)
} else if name == "checked" {
let element: &web_sys::HtmlInputElement = element.dyn_ref().unwrap_throw();
element.set_checked(true)
} else {
element.set_attribute(name, value).unwrap_throw();
}
}
#[cfg(not(feature = "typed"))]
fn set_attribute(element: &web_sys::Element, name: &str, value: &str) {
element.set_attribute(name, value).unwrap_throw();
}
#[cfg(feature = "typed")]
fn remove_attribute(element: &web_sys::Element, name: &str) {
// we have to special-case `value` because setting the value using `set_attribute`
// doesn't work after the value has been changed.
if name == "checked" {
let element: &web_sys::HtmlInputElement = element.dyn_ref().unwrap_throw();
element.set_checked(false)
} else {
element.remove_attribute(name).unwrap_throw();
}
}
#[cfg(not(feature = "typed"))]
fn remove_attribute(element: &web_sys::Element, name: &str) {
element.remove_attribute(name).unwrap_throw();
}

View File

@ -0,0 +1,219 @@
//! Macros to generate all the different html events
//!
macro_rules! events {
() => {};
(($ty_name:ident, $builder_name:ident, $name:literal, $web_sys_ty:ty), $($rest:tt)*) => {
event!($ty_name, $builder_name, $name, $web_sys_ty);
events!($($rest)*);
};
}
macro_rules! event {
($ty_name:ident, $builder_name:ident, $name:literal, $web_sys_ty:ty) => {
pub struct $ty_name<V, F>(crate::OnEvent<$web_sys_ty, V, F>);
pub fn $builder_name<V, F>(child: V, callback: F) -> $ty_name<V, F> {
$ty_name(crate::on_event($name, child, callback))
}
impl<V, F> crate::view::ViewMarker for $ty_name<V, F> {}
impl<T, A, V, F> crate::view::View<T, A> for $ty_name<V, F>
where
V: crate::view::View<T, A>,
F: Fn(&mut T, &$crate::Event<$web_sys_ty, V::Element>) -> $crate::MessageResult<A>,
V::Element: 'static,
{
type State = crate::event::OnEventState<V::State>;
type Element = V::Element;
fn build(
&self,
cx: &mut crate::context::Cx,
) -> (xilem_core::Id, Self::State, Self::Element) {
self.0.build(cx)
}
fn rebuild(
&self,
cx: &mut crate::context::Cx,
prev: &Self,
id: &mut xilem_core::Id,
state: &mut Self::State,
element: &mut Self::Element,
) -> crate::ChangeFlags {
self.0.rebuild(cx, &prev.0, id, state, element)
}
fn message(
&self,
id_path: &[xilem_core::Id],
state: &mut Self::State,
message: Box<dyn std::any::Any>,
app_state: &mut T,
) -> xilem_core::MessageResult<A> {
self.0.message(id_path, state, message, app_state)
}
}
};
}
// event list from
// https://html.spec.whatwg.org/multipage/webappapis.html#idl-definitions
//
// I didn't include the events on the window, since we aren't attaching
// any events to the window in xilem_html
events!(
(OnAbort, on_abort, "abort", web_sys::Event),
(OnAuxClick, on_auxclick, "auxclick", web_sys::PointerEvent),
(
OnBeforeInput,
on_beforeinput,
"beforeinput",
web_sys::InputEvent
),
(OnBeforeMatch, on_beforematch, "beforematch", web_sys::Event),
(
OnBeforeToggle,
on_beforetoggle,
"beforetoggle",
web_sys::Event
),
(OnBlur, on_blur, "blur", web_sys::FocusEvent),
(OnCancel, on_cancel, "cancel", web_sys::Event),
(OnCanPlay, on_canplay, "canplay", web_sys::Event),
(
OnCanPlayThrough,
on_canplaythrough,
"canplaythrough",
web_sys::Event
),
(OnChange, on_change, "change", web_sys::Event),
(OnClick, on_click, "click", web_sys::MouseEvent),
(OnClose, on_close, "close", web_sys::Event),
(OnContextLost, on_contextlost, "contextlost", web_sys::Event),
(
OnContextMenu,
on_contextmenu,
"contextmenu",
web_sys::PointerEvent
),
(
OnContextRestored,
on_contextrestored,
"contextrestored",
web_sys::Event
),
(OnCopy, on_copy, "copy", web_sys::Event),
(OnCueChange, on_cuechange, "cuechange", web_sys::Event),
(OnCut, on_cut, "cut", web_sys::Event),
(OnDblClick, on_dblclick, "dblclick", web_sys::MouseEvent),
(OnDrag, on_drag, "drag", web_sys::Event),
(OnDragEnd, on_dragend, "dragend", web_sys::Event),
(OnDragEnter, on_dragenter, "dragenter", web_sys::Event),
(OnDragLeave, on_dragleave, "dragleave", web_sys::Event),
(OnDragOver, on_dragover, "dragover", web_sys::Event),
(OnDragStart, on_dragstart, "dragstart", web_sys::Event),
(OnDrop, on_drop, "drop", web_sys::Event),
(
OnDurationChange,
on_durationchange,
"durationchange",
web_sys::Event
),
(OnEmptied, on_emptied, "emptied", web_sys::Event),
(OnEnded, on_ended, "ended", web_sys::Event),
(OnError, on_error, "error", web_sys::Event),
(OnFocus, on_focus, "focus", web_sys::FocusEvent),
(OnFocusIn, on_focusin, "focusin", web_sys::FocusEvent),
(OnFocusOut, on_focusout, "focusout", web_sys::FocusEvent),
(OnFormData, on_formdata, "formdata", web_sys::Event),
(OnInput, on_input, "input", web_sys::InputEvent),
(OnInvalid, on_invalid, "invalid", web_sys::Event),
(OnKeyDown, on_keydown, "keydown", web_sys::KeyboardEvent),
(OnKeyUp, on_keyup, "keyup", web_sys::KeyboardEvent),
(OnLoad, on_load, "load", web_sys::Event),
(OnLoadedData, on_loadeddata, "loadeddata", web_sys::Event),
(
OnLoadedMetadata,
on_loadedmetadata,
"loadedmetadata",
web_sys::Event
),
(OnLoadStart, on_loadstart, "loadstart", web_sys::Event),
(OnMouseDown, on_mousedown, "mousedown", web_sys::MouseEvent),
(
OnMouseEnter,
on_mouseenter,
"mouseenter",
web_sys::MouseEvent
),
(
OnMouseLeave,
on_mouseleave,
"mouseleave",
web_sys::MouseEvent
),
(OnMouseMove, on_mousemove, "mousemove", web_sys::MouseEvent),
(OnMouseOut, on_mouseout, "mouseout", web_sys::MouseEvent),
(OnMouseOver, on_mouseover, "mouseover", web_sys::MouseEvent),
(OnMouseUp, on_mouseup, "mouseup", web_sys::MouseEvent),
(OnPaste, on_paste, "paste", web_sys::Event),
(OnPause, on_pause, "pause", web_sys::Event),
(OnPlay, on_play, "play", web_sys::Event),
(OnPlaying, on_playing, "playing", web_sys::Event),
(OnProgress, on_progress, "progress", web_sys::Event),
(OnRateChange, on_ratechange, "ratechange", web_sys::Event),
(OnReset, on_reset, "reset", web_sys::Event),
(OnResize, on_resize, "resize", web_sys::Event),
(OnScroll, on_scroll, "scroll", web_sys::Event),
(OnScrollEnd, on_scrollend, "scrollend", web_sys::Event),
(
OnSecurityPolicyViolation,
on_securitypolicyviolation,
"securitypolicyviolation",
web_sys::Event
),
(OnSeeked, on_seeked, "seeked", web_sys::Event),
(OnSeeking, on_seeking, "seeking", web_sys::Event),
(OnSelect, on_select, "select", web_sys::Event),
(OnSlotChange, on_slotchange, "slotchange", web_sys::Event),
(OnStalled, on_stalled, "stalled", web_sys::Event),
(OnSubmit, on_submit, "submit", web_sys::Event),
(OnSuspend, on_suspend, "suspend", web_sys::Event),
(OnTimeUpdate, on_timeupdate, "timeupdate", web_sys::Event),
(OnToggle, on_toggle, "toggle", web_sys::Event),
(
OnVolumeChange,
on_volumechange,
"volumechange",
web_sys::Event
),
(OnWaiting, on_waiting, "waiting", web_sys::Event),
(
OnWebkitAnimationEnd,
on_webkitanimationend,
"webkitanimationend",
web_sys::Event
),
(
OnWebkitAnimationIteration,
on_webkitanimationiteration,
"webkitanimationiteration",
web_sys::Event
),
(
OnWebkitAnimationStart,
on_webkitanimationstart,
"webkitanimationstart",
web_sys::Event
),
(
OnWebkitTransitionEnd,
on_webkittransitionend,
"webkittransitionend",
web_sys::Event
),
(OnWheel, on_wheel, "wheel", web_sys::WheelEvent),
);

View File

@ -0,0 +1,236 @@
// Copyright 2023 the Druid Authors.
// SPDX-License-Identifier: Apache-2.0
#[cfg(feature = "typed")]
pub mod events;
use std::{any::Any, marker::PhantomData, ops::Deref};
use wasm_bindgen::{prelude::Closure, JsCast, UnwrapThrowExt};
use xilem_core::{Id, MessageResult};
use crate::{
context::{ChangeFlags, Cx},
view::{DomNode, View, ViewMarker},
};
pub struct OnEvent<E, V, F> {
// TODO changing this after creation is unsupported for now,
// please create a new view instead.
event: &'static str,
child: V,
callback: F,
phantom_event_ty: PhantomData<E>,
}
impl<E, V, F> OnEvent<E, V, F> {
fn new(event: &'static str, child: V, callback: F) -> Self {
Self {
event,
child,
callback,
phantom_event_ty: PhantomData,
}
}
}
impl<E, V, F> ViewMarker for OnEvent<E, V, F> {}
impl<T, A, E, F, V> View<T, A> for OnEvent<E, V, F>
where
F: Fn(&mut T, &Event<E, V::Element>) -> MessageResult<A>,
V: View<T, A>,
E: JsCast + 'static,
V::Element: 'static,
{
type State = OnEventState<V::State>;
type Element = V::Element;
fn build(&self, cx: &mut Cx) -> (Id, Self::State, Self::Element) {
let (id, child_state, element) = self.child.build(cx);
let thunk = cx.with_id(id, |cx| cx.message_thunk());
let closure = Closure::wrap(Box::new(move |event: web_sys::Event| {
let event = event.dyn_into::<E>().unwrap_throw();
let event: Event<E, V::Element> = Event::new(event);
thunk.push_message(EventMsg { event });
}) as Box<dyn FnMut(web_sys::Event)>);
element
.as_node_ref()
.add_event_listener_with_callback(self.event, closure.as_ref().unchecked_ref())
.unwrap_throw();
// TODO add `remove_listener_with_callback` to clean up listener?
let state = OnEventState {
closure,
child_state,
};
(id, state, element)
}
fn rebuild(
&self,
cx: &mut Cx,
prev: &Self,
id: &mut Id,
state: &mut Self::State,
element: &mut Self::Element,
) -> ChangeFlags {
// TODO: if the child id changes (as can happen with AnyView), reinstall closure
self.child
.rebuild(cx, &prev.child, id, &mut state.child_state, element)
}
fn message(
&self,
id_path: &[Id],
state: &mut Self::State,
message: Box<dyn Any>,
app_state: &mut T,
) -> MessageResult<A> {
if let Some(msg) = message.downcast_ref::<EventMsg<Event<E, V::Element>>>() {
(self.callback)(app_state, &msg.event)
} else {
self.child
.message(id_path, &mut state.child_state, message, app_state)
}
}
}
// Attach an event listener to the child's element
pub fn on_event<E, V, F>(name: &'static str, child: V, callback: F) -> OnEvent<E, V, F> {
OnEvent::new(name, child, callback)
}
pub struct OnEventState<S> {
#[allow(unused)]
closure: Closure<dyn FnMut(web_sys::Event)>,
child_state: S,
}
struct EventMsg<E> {
event: E,
}
/*
// on input
pub fn on_input<T, A, F: Fn(&mut T, &web_sys::InputEvent) -> MessageResult<A>, V: View<T, A>>(
child: V,
callback: F,
) -> OnEvent<web_sys::InputEvent, V, F> {
OnEvent::new("input", child, callback)
}
// on click
pub fn on_click<T, A, F: Fn(&mut T, &web_sys::Event) -> MessageResult<A>, V: View<T, A>>(
child: V,
callback: F,
) -> OnEvent<web_sys::Event, V, F> {
OnEvent::new("click", child, callback)
}
// on click
pub fn on_dblclick<T, A, F: Fn(&mut T, &web_sys::Event) -> MessageResult<A>, V: View<T, A>>(
child: V,
callback: F,
) -> OnEvent<web_sys::Event, V, F> {
OnEvent::new("dblclick", child, callback)
}
// on keydown
pub fn on_keydown<
T,
A,
F: Fn(&mut T, &web_sys::KeyboardEvent) -> MessageResult<A>,
V: View<T, A>,
>(
child: V,
callback: F,
) -> OnEvent<web_sys::KeyboardEvent, V, F> {
OnEvent::new("keydown", child, callback)
}
*/
pub struct Event<Evt, El> {
raw: Evt,
el: PhantomData<El>,
}
impl<Evt, El> Event<Evt, El> {
fn new(raw: Evt) -> Self {
Self {
raw,
el: PhantomData,
}
}
}
impl<Evt, El> Event<Evt, El>
where
Evt: AsRef<web_sys::Event>,
El: JsCast,
{
pub fn target(&self) -> El {
let evt: &web_sys::Event = self.raw.as_ref();
evt.target().unwrap_throw().dyn_into().unwrap_throw()
}
}
impl<Evt, El> Deref for Event<Evt, El> {
type Target = Evt;
fn deref(&self) -> &Self::Target {
&self.raw
}
}
/*
/// Types that can be created from a `web_sys::Event`.
///
/// Implementations may make the assumption that the event
/// is a particular subtype (e.g. `InputEvent`) and panic
/// when this is not the case (although it's preferred to use
/// `throw_str` and friends).
pub trait FromEvent: 'static {
/// Convert the given event into `self`, or panic.
fn from_event(event: &web_sys::Event) -> Self;
}
#[derive(Debug)]
pub struct InputEvent {
pub data: Option<String>,
/// The value of `event.target.value`.
pub value: String,
}
impl FromEvent for InputEvent {
fn from_event(event: &web_sys::Event) -> Self {
let event: &web_sys::InputEvent = event.dyn_ref().unwrap_throw();
Self {
data: event.data(),
value: event
.target()
.unwrap_throw()
.dyn_into::<web_sys::HtmlInputElement>()
.unwrap_throw()
.value(),
}
}
}
pub struct Event {}
impl FromEvent for Event {
fn from_event(_event: &web_sys::Event) -> Self {
Self {}
}
}
pub struct KeyboardEvent {
pub key: String,
}
impl FromEvent for KeyboardEvent {
fn from_event(event: &web_sys::Event) -> Self {
let event: &web_sys::KeyboardEvent = event.dyn_ref().unwrap();
Self { key: event.key() }
}
}
*/

View File

@ -0,0 +1,65 @@
// Copyright 2023 the Druid Authors.
// SPDX-License-Identifier: Apache-2.0
//! A test program to exercise using xilem_core to generate SVG nodes that
//! render in a browser.
//!
//! Run using `trunk serve`.
use wasm_bindgen::JsCast;
mod app;
//mod button;
mod class;
mod context;
mod event;
//mod div;
mod element;
mod text;
mod view;
#[cfg(feature = "typed")]
mod view_ext;
pub use xilem_core::MessageResult;
pub use app::App;
pub use class::class;
pub use context::ChangeFlags;
#[cfg(feature = "typed")]
pub use element::elements;
pub use element::{element, Element, ElementState};
#[cfg(feature = "typed")]
pub use event::events;
pub use event::{on_event, Event, OnEvent, OnEventState};
pub use text::{text, Text};
pub use view::{Adapt, AdaptThunk, Pod, View, ViewMarker, ViewSequence};
#[cfg(feature = "typed")]
pub use view_ext::ViewExt;
xilem_core::message!();
/// 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";
/// 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 the HTML document body element
pub fn document_body() -> web_sys::HtmlElement {
document().body().expect("HTML document missing body")
}
pub fn get_element_by_id(id: &str) -> web_sys::HtmlElement {
document()
.get_element_by_id(id)
.unwrap()
.dyn_into()
.unwrap()
}

View File

@ -0,0 +1,61 @@
use std::borrow::Cow;
use wasm_bindgen::JsCast;
use xilem_core::{Id, MessageResult};
use crate::{
context::{ChangeFlags, Cx},
view::{View, ViewMarker},
};
pub struct Text {
text: Cow<'static, str>,
}
/// Create a text node
pub fn text(text: impl Into<Cow<'static, str>>) -> Text {
Text { text: text.into() }
}
impl ViewMarker for Text {}
impl<T, A> View<T, A> for Text {
type State = ();
type Element = web_sys::Text;
fn build(&self, _cx: &mut Cx) -> (Id, Self::State, Self::Element) {
let el = new_text(&self.text);
let id = Id::next();
(id, (), el.unchecked_into())
}
fn rebuild(
&self,
_cx: &mut Cx,
prev: &Self,
_id: &mut Id,
_state: &mut Self::State,
element: &mut Self::Element,
) -> ChangeFlags {
let mut is_changed = ChangeFlags::empty();
if prev.text != self.text {
element.set_data(&self.text);
is_changed |= ChangeFlags::OTHER_CHANGE;
}
is_changed
}
fn message(
&self,
_id_path: &[Id],
_state: &mut Self::State,
_message: Box<dyn std::any::Any>,
_app_state: &mut T,
) -> MessageResult<A> {
MessageResult::Nop
}
}
fn new_text(text: &str) -> web_sys::Text {
web_sys::Text::new_with_data(text).unwrap()
}

View File

@ -0,0 +1,100 @@
// Copyright 2023 the Druid 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, ops::Deref};
use crate::{context::Cx, ChangeFlags};
// 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.
pub trait DomNode {
fn into_pod(self) -> Pod;
fn as_node_ref(&self) -> &web_sys::Node;
}
impl<N: AsRef<web_sys::Node> + 'static> DomNode for N {
fn into_pod(self) -> Pod {
Pod(Box::new(self))
}
fn as_node_ref(&self) -> &web_sys::Node {
self.as_ref()
}
}
pub trait DomElement: DomNode {
fn as_element_ref(&self) -> &web_sys::Element;
}
impl<N: DomNode + AsRef<web_sys::Element>> DomElement for N {
fn as_element_ref(&self) -> &web_sys::Element {
self.as_ref()
}
}
pub trait AnyNode {
fn as_any_mut(&mut self) -> &mut dyn Any;
fn as_node_ref(&self) -> &web_sys::Node;
}
impl<N: AsRef<web_sys::Node> + 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 DomNode for Box<dyn AnyNode> {
fn into_pod(self) -> Pod {
Pod(self)
}
fn as_node_ref(&self) -> &web_sys::Node {
self.deref().as_node_ref()
}
}
struct Void;
// Dummy implementation that should never be used.
impl DomNode for Void {
fn into_pod(self) -> Pod {
unreachable!()
}
fn as_node_ref(&self) -> &web_sys::Node {
unreachable!()
}
}
/// 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<dyn AnyNode>);
impl Pod {
fn new(node: impl DomNode) -> Self {
node.into_pod()
}
fn downcast_mut<'a, T: 'static>(&'a mut self) -> Option<&'a mut T> {
self.0.as_any_mut().downcast_mut()
}
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, DomNode, Cx, ChangeFlags, Pod;}
xilem_core::generate_anyview_trait! {View, Cx, ChangeFlags, AnyNode}

View File

@ -0,0 +1,63 @@
// Copyright 2023 the Druid Authors.
// SPDX-License-Identifier: Apache-2.0
use std::borrow::Cow;
use xilem_core::MessageResult;
use crate::{class::Class, events as e, view::View, Event};
pub trait ViewExt<T, A>: View<T, A> + Sized {
fn on_click<F: Fn(&mut T, &Event<web_sys::MouseEvent, Self::Element>) -> MessageResult<A>>(
self,
f: F,
) -> e::OnClick<Self, F>;
fn on_dblclick<F: Fn(&mut T, &Event<web_sys::MouseEvent, Self::Element>) -> MessageResult<A>>(
self,
f: F,
) -> e::OnDblClick<Self, F>;
fn on_input<F: Fn(&mut T, &Event<web_sys::InputEvent, Self::Element>) -> MessageResult<A>>(
self,
f: F,
) -> e::OnInput<Self, F>;
fn on_keydown<
F: Fn(&mut T, &Event<web_sys::KeyboardEvent, Self::Element>) -> MessageResult<A>,
>(
self,
f: F,
) -> e::OnKeyDown<Self, F>;
fn class(self, class: impl Into<Cow<'static, str>>) -> Class<Self> {
crate::class::class(self, class)
}
}
impl<T, A, V: View<T, A>> ViewExt<T, A> for V {
fn on_click<F: Fn(&mut T, &Event<web_sys::MouseEvent, Self::Element>) -> MessageResult<A>>(
self,
f: F,
) -> e::OnClick<Self, F> {
e::on_click(self, f)
}
fn on_dblclick<
F: Fn(&mut T, &Event<web_sys::MouseEvent, Self::Element>) -> MessageResult<A>,
>(
self,
f: F,
) -> e::OnDblClick<Self, F> {
e::on_dblclick(self, f)
}
fn on_input<F: Fn(&mut T, &Event<web_sys::InputEvent, Self::Element>) -> MessageResult<A>>(
self,
f: F,
) -> e::OnInput<Self, F> {
crate::events::on_input(self, f)
}
fn on_keydown<
F: Fn(&mut T, &Event<web_sys::KeyboardEvent, Self::Element>) -> MessageResult<A>,
>(
self,
f: F,
) -> e::OnKeyDown<Self, F> {
crate::events::on_keydown(self, f)
}
}

View File

@ -0,0 +1,13 @@
[package]
name = "counter"
version = "0.1.0"
license = "Apache-2.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
wasm-bindgen = "0.2.87"
web-sys = "0.3.64"
xilem_html = { path = "../.." }

View File

@ -0,0 +1,19 @@
<!DOCTYPE html>
<head>
<style>
.gray {
background-color: lightgrey;
}
.green {
background-color: lightgreen;
}
rect.red {
fill: #e00;
}
line {
stroke: #444;
}
</style>
</head>
<body>
</body>

View File

@ -0,0 +1,80 @@
use wasm_bindgen::{prelude::*, JsValue};
use xilem_html::{
document_body, elements as el, events as evt, text, App, Event, MessageResult, Text, View,
ViewExt,
};
#[derive(Default)]
struct AppState {
clicks: i32,
class: &'static str,
text: String,
}
impl AppState {
fn increment(&mut self) -> MessageResult<()> {
self.clicks += 1;
MessageResult::Nop
}
fn decrement(&mut self) -> MessageResult<()> {
self.clicks -= 1;
MessageResult::Nop
}
fn reset(&mut self) -> MessageResult<()> {
self.clicks = 0;
MessageResult::Nop
}
fn change_class(&mut self) -> MessageResult<()> {
if self.class == "gray" {
self.class = "green";
} else {
self.class = "gray";
}
MessageResult::Nop
}
fn change_text(&mut self) -> MessageResult<()> {
if self.text == "test" {
self.text = "test2".into();
} else {
self.text = "test".into();
}
MessageResult::Nop
}
}
/// You can create functions that generate views.
fn btn<F>(label: &'static str, click_fn: F) -> evt::OnClick<el::Button<Text>, F>
where
F: Fn(
&mut AppState,
&Event<web_sys::MouseEvent, web_sys::HtmlButtonElement>,
) -> MessageResult<()>,
{
el::button(text(label)).on_click(click_fn)
}
fn app_logic(state: &mut AppState) -> impl View<AppState> {
el::div((
el::span(text(format!("clicked {} times", state.clicks))).attr("class", state.class),
el::br(()),
btn("+1 click", |state, _| AppState::increment(state)),
btn("-1 click", |state, _| AppState::decrement(state)),
btn("reset clicks", |state, _| AppState::reset(state)),
btn("a different class", |state, _| {
AppState::change_class(state)
}),
btn("change text", |state, _| AppState::change_text(state)),
el::br(()),
text(state.text.clone()),
))
}
// Called by our JS entry point to run the example
#[wasm_bindgen(start)]
pub fn run() -> Result<(), JsValue> {
let app = App::new(AppState::default(), app_logic);
app.run(&document_body());
Ok(())
}

View File

@ -0,0 +1,13 @@
[package]
name = "counter_untyped"
version = "0.1.0"
license = "Apache-2.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
wasm-bindgen = "0.2.87"
web-sys = { version = "0.3.64", features = ["HtmlButtonElement"] }
xilem_html = { path = "../..", default-features = false }

View File

@ -0,0 +1,21 @@
<!DOCTYPE html>
<head>
<style>
.gray {
background-color: lightgrey;
}
.green {
background-color: lightgreen;
}
rect.red {
fill: #e00;
}
line {
stroke: #444;
}
</style>
</head>
<body>
<p>This is like the <code>counter</code> example, but does not use the typed
elements/events/attrs in <code>xilem_html</code>, instead using strings</p>
</body>

View File

@ -0,0 +1,52 @@
use wasm_bindgen::{prelude::*, JsValue};
use xilem_html::{
document_body, element as el, on_event, text, App, Event, MessageResult, View, ViewMarker,
};
#[derive(Default)]
struct AppState {
clicks: i32,
}
impl AppState {
fn increment(&mut self) -> MessageResult<()> {
self.clicks += 1;
MessageResult::Nop
}
fn decrement(&mut self) -> MessageResult<()> {
self.clicks -= 1;
MessageResult::Nop
}
fn reset(&mut self) -> MessageResult<()> {
self.clicks = 0;
MessageResult::Nop
}
}
fn btn<F>(label: &'static str, click_fn: F) -> impl View<AppState> + ViewMarker
where
F: Fn(&mut AppState, &Event<web_sys::Event, web_sys::HtmlButtonElement>) -> MessageResult<()>,
{
on_event("click", el("button", text(label)), click_fn)
}
fn app_logic(state: &mut AppState) -> impl View<AppState> {
el::<web_sys::HtmlElement, _>(
"div",
(
el::<web_sys::HtmlElement, _>("span", text(format!("clicked {} times", state.clicks))),
btn("+1 click", |state, _| AppState::increment(state)),
btn("-1 click", |state, _| AppState::decrement(state)),
btn("reset clicks", |state, _| AppState::reset(state)),
),
)
}
// Called by our JS entry point to run the example
#[wasm_bindgen(start)]
pub fn run() -> Result<(), JsValue> {
let app = App::new(AppState::default(), app_logic);
app.run(&document_body());
Ok(())
}

View File

@ -0,0 +1,16 @@
[package]
name = "todomvc"
version = "0.1.0"
license = "Apache-2.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
console_error_panic_hook = "0.1.7"
console_log = { version = "1.0.0", features = ["color"] }
log = "0.4.19"
wasm-bindgen = "0.2.87"
web-sys = "0.3.64"
xilem_html = { path = "../.." }

View File

@ -0,0 +1,537 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>xilem_html • TodoMVC</title>
<style>
/* base.css */
hr {
margin: 20px 0;
border: 0;
border-top: 1px dashed #c5c5c5;
border-bottom: 1px dashed #f7f7f7;
}
.learn a {
font-weight: normal;
text-decoration: none;
color: #b83f45;
}
.learn a:hover {
text-decoration: underline;
color: #787e7e;
}
.learn h3,
.learn h4,
.learn h5 {
margin: 10px 0;
font-weight: 500;
line-height: 1.2;
color: #000;
}
.learn h3 {
font-size: 24px;
}
.learn h4 {
font-size: 18px;
}
.learn h5 {
margin-bottom: 0;
font-size: 14px;
}
.learn ul {
padding: 0;
margin: 0 0 30px 25px;
}
.learn li {
line-height: 20px;
}
.learn p {
font-size: 15px;
font-weight: 300;
line-height: 1.3;
margin-top: 0;
margin-bottom: 0;
}
#issue-count {
display: none;
}
.quote {
border: none;
margin: 20px 0 60px 0;
}
.quote p {
font-style: italic;
}
.quote p:before {
content: '“';
font-size: 50px;
opacity: .15;
position: absolute;
top: -20px;
left: 3px;
}
.quote p:after {
content: '”';
font-size: 50px;
opacity: .15;
position: absolute;
bottom: -42px;
right: 3px;
}
.quote footer {
position: absolute;
bottom: -40px;
right: 0;
}
.quote footer img {
border-radius: 3px;
}
.quote footer a {
margin-left: 5px;
vertical-align: middle;
}
.speech-bubble {
position: relative;
padding: 10px;
background: rgba(0, 0, 0, .04);
border-radius: 5px;
}
.speech-bubble:after {
content: '';
position: absolute;
top: 100%;
right: 30px;
border: 13px solid transparent;
border-top-color: rgba(0, 0, 0, .04);
}
.learn-bar > .learn {
position: absolute;
width: 272px;
top: 8px;
left: -300px;
padding: 10px;
border-radius: 5px;
background-color: rgba(255, 255, 255, .6);
transition-property: left;
transition-duration: 500ms;
}
@media (min-width: 899px) {
.learn-bar {
width: auto;
padding-left: 300px;
}
.learn-bar > .learn {
left: 8px;
}
}
/* index.css */
html,
body {
margin: 0;
padding: 0;
}
button {
margin: 0;
padding: 0;
border: 0;
background: none;
font-size: 100%;
vertical-align: baseline;
font-family: inherit;
font-weight: inherit;
color: inherit;
-webkit-appearance: none;
appearance: none;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;
line-height: 1.4em;
background: #f5f5f5;
color: #4d4d4d;
min-width: 230px;
max-width: 550px;
margin: 0 auto;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
font-weight: 300;
}
:focus {
outline: 0;
}
.hidden {
display: none;
}
.todoapp {
background: #fff;
margin: 130px 0 40px 0;
position: relative;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2),
0 25px 50px 0 rgba(0, 0, 0, 0.1);
}
.todoapp input::-webkit-input-placeholder {
font-style: italic;
font-weight: 300;
color: #e6e6e6;
}
.todoapp input::-moz-placeholder {
font-style: italic;
font-weight: 300;
color: #e6e6e6;
}
.todoapp input::input-placeholder {
font-style: italic;
font-weight: 300;
color: #e6e6e6;
}
.todoapp h1 {
position: absolute;
top: -155px;
width: 100%;
font-size: 100px;
font-weight: 100;
text-align: center;
color: rgba(175, 47, 47, 0.15);
-webkit-text-rendering: optimizeLegibility;
-moz-text-rendering: optimizeLegibility;
text-rendering: optimizeLegibility;
}
.new-todo,
.edit {
position: relative;
margin: 0;
width: 100%;
font-size: 24px;
font-family: inherit;
font-weight: inherit;
line-height: 1.4em;
border: 0;
color: inherit;
padding: 6px;
border: 1px solid #999;
box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);
box-sizing: border-box;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.new-todo {
padding: 16px 16px 16px 60px;
border: none;
background: rgba(0, 0, 0, 0.003);
box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03);
}
.main {
position: relative;
z-index: 2;
border-top: 1px solid #e6e6e6;
}
.toggle-all {
text-align: center;
border: none; /* Mobile Safari */
opacity: 0;
position: absolute;
}
.toggle-all + label {
width: 60px;
height: 34px;
font-size: 0;
position: absolute;
top: -52px;
left: -13px;
-webkit-transform: rotate(90deg);
transform: rotate(90deg);
}
.toggle-all + label:before {
content: '';
font-size: 22px;
color: #e6e6e6;
padding: 10px 27px 10px 27px;
}
.toggle-all:checked + label:before {
color: #737373;
}
.todo-list {
margin: 0;
padding: 0;
list-style: none;
}
.todo-list li {
position: relative;
font-size: 24px;
border-bottom: 1px solid #ededed;
}
.todo-list li:last-child {
border-bottom: none;
}
.todo-list li.editing {
border-bottom: none;
padding: 0;
}
.todo-list li.editing .edit {
display: block;
width: 506px;
padding: 12px 16px;
margin: 0 0 0 43px;
}
.todo-list li.editing .view {
display: none;
}
.todo-list li .toggle {
text-align: center;
width: 40px;
/* auto, since non-WebKit browsers doesn't support input styling */
height: auto;
position: absolute;
top: 0;
bottom: 0;
margin: auto 0;
border: none; /* Mobile Safari */
-webkit-appearance: none;
appearance: none;
}
.todo-list li .toggle {
opacity: 0;
}
.todo-list li .toggle + label {
/*
Firefox requires `#` to be escaped - https://bugzilla.mozilla.org/show_bug.cgi?id=922433
IE and Edge requires *everything* to be escaped to render, so we do that instead of just the `#` - https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/7157459/
*/
background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23ededed%22%20stroke-width%3D%223%22/%3E%3C/svg%3E');
background-repeat: no-repeat;
background-position: center left;
}
.todo-list li .toggle:checked + label {
background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23bddad5%22%20stroke-width%3D%223%22/%3E%3Cpath%20fill%3D%22%235dc2af%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22/%3E%3C/svg%3E');
}
.todo-list li label {
word-break: break-all;
padding: 15px 15px 15px 60px;
display: block;
line-height: 1.2;
transition: color 0.4s;
}
.todo-list li.completed label {
color: #d9d9d9;
text-decoration: line-through;
}
.todo-list li .destroy {
display: none;
position: absolute;
top: 0;
right: 10px;
bottom: 0;
width: 40px;
height: 40px;
margin: auto 0;
font-size: 30px;
color: #cc9a9a;
margin-bottom: 11px;
transition: color 0.2s ease-out;
}
.todo-list li .destroy:hover {
color: #af5b5e;
}
.todo-list li .destroy:after {
content: '×';
}
.todo-list li:hover .destroy {
display: block;
}
.todo-list li .edit {
display: none;
}
.todo-list li.editing:last-child {
margin-bottom: -1px;
}
.footer {
color: #777;
padding: 10px 15px;
height: 20px;
text-align: center;
border-top: 1px solid #e6e6e6;
}
.footer:before {
content: '';
position: absolute;
right: 0;
bottom: 0;
left: 0;
height: 50px;
overflow: hidden;
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2),
0 8px 0 -3px #f6f6f6,
0 9px 1px -3px rgba(0, 0, 0, 0.2),
0 16px 0 -6px #f6f6f6,
0 17px 2px -6px rgba(0, 0, 0, 0.2);
}
.todo-count {
float: left;
text-align: left;
}
.todo-count strong {
font-weight: 300;
}
.filters {
margin: 0;
padding: 0;
list-style: none;
position: absolute;
right: 0;
left: 0;
}
.filters li {
display: inline;
}
.filters li a {
color: inherit;
margin: 3px;
padding: 3px 7px;
text-decoration: none;
border: 1px solid transparent;
border-radius: 3px;
}
.filters li a:hover {
border-color: rgba(175, 47, 47, 0.1);
}
.filters li a.selected {
border-color: rgba(175, 47, 47, 0.2);
}
.clear-completed,
html .clear-completed:active {
float: right;
position: relative;
line-height: 20px;
text-decoration: none;
cursor: pointer;
}
.clear-completed:hover {
text-decoration: underline;
}
.info {
margin: 65px auto 0;
color: #bfbfbf;
font-size: 10px;
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
text-align: center;
}
.info p {
line-height: 1;
}
.info a {
color: inherit;
text-decoration: none;
font-weight: 400;
}
.info a:hover {
text-decoration: underline;
}
/*
Hack to remove background from Mobile Safari.
Can't use it globally since it destroys checkboxes in Firefox
*/
@media screen and (-webkit-min-device-pixel-ratio:0) {
.toggle-all,
.todo-list li .toggle {
background: none;
}
.todo-list li .toggle {
height: 40px;
}
}
@media (max-width: 430px) {
.footer {
height: 50px;
}
.filters {
bottom: 10px;
}
}
</style>
</head>
<body>
<section id="todoapp" class="todoapp"></section>
<footer class="info">
<p>Double-click to edit a todo</p>
<p>Created by <a href="http://github.com/petehunt/">petehunt</a></p>
<p>Forked from <a href="http://todomvc.com">TodoMVC</a></p>
</footer>
</body>
</html>

View File

@ -0,0 +1,212 @@
use std::panic;
mod state;
use state::{AppState, Filter, Todo};
use wasm_bindgen::{prelude::*, JsValue};
use xilem_html::{
elements as el, get_element_by_id, text, Adapt, App, MessageResult, View, ViewExt, ViewMarker,
};
// All of these actions arise from within a `Todo`, but we need access to the full state to reduce
// them.
enum TodoAction {
SetEditing(u64),
CancelEditing,
Destroy(u64),
}
fn todo_item(todo: &mut Todo, editing: bool) -> impl View<Todo, TodoAction> + ViewMarker {
let mut class = String::new();
if todo.completed {
class.push_str(" completed");
}
if editing {
class.push_str(" editing");
}
let mut input = el::input(())
.attr("class", "toggle")
.attr("type", "checkbox");
if todo.completed {
input.set_attr("checked", "checked");
};
el::li((
el::div((
input.on_click(|state: &mut Todo, _| {
state.completed = !state.completed;
MessageResult::RequestRebuild
}),
el::label(text(todo.title.clone())).on_dblclick(|state: &mut Todo, _| {
MessageResult::Action(TodoAction::SetEditing(state.id))
}),
el::button(())
.attr("class", "destroy")
.on_click(|state: &mut Todo, _| {
MessageResult::Action(TodoAction::Destroy(state.id))
}),
))
.attr("class", "view"),
el::input(())
.attr("value", todo.title_editing.clone())
.attr("class", "edit")
.on_keydown(|state: &mut Todo, evt| {
let key = evt.key();
if key == "Enter" {
state.save_editing();
MessageResult::Action(TodoAction::CancelEditing)
} else if key == "Escape" {
MessageResult::Action(TodoAction::CancelEditing)
} else {
MessageResult::Nop
}
})
.on_input(|state: &mut Todo, evt| {
state.title_editing.clear();
state.title_editing.push_str(&evt.target().value());
evt.prevent_default();
MessageResult::Nop
}),
))
.attr("class", class)
}
fn footer_view(state: &mut AppState) -> impl View<AppState> + ViewMarker {
let item_str = if state.todos.len() == 1 {
"item"
} else {
"items"
};
let clear_button = (state.todos.iter().filter(|todo| todo.completed).count() > 0).then(|| {
el::button(text("Clear completed"))
.attr("class", "clear-completed")
.on_click(|state: &mut AppState, _| {
state.todos.retain(|todo| !todo.completed);
MessageResult::RequestRebuild
})
});
let filter_class = |filter| {
if state.filter == filter {
"selected"
} else {
""
}
};
el::footer((
el::span((
el::strong(text(state.todos.len().to_string())),
text(format!(" {} left", item_str)),
))
.attr("class", "todo-count"),
el::ul((
el::li(
el::a(text("All"))
.attr("href", "#/")
.attr("class", filter_class(Filter::All))
.on_click(|state: &mut AppState, _| {
state.filter = Filter::All;
MessageResult::RequestRebuild
}),
),
text(" "),
el::li(
el::a(text("Active"))
.attr("href", "#/active")
.attr("class", filter_class(Filter::Active))
.on_click(|state: &mut AppState, _| {
state.filter = Filter::Active;
MessageResult::RequestRebuild
}),
),
text(" "),
el::li(
el::a(text("Completed"))
.attr("href", "#/completed")
.attr("class", filter_class(Filter::Completed))
.on_click(|state: &mut AppState, _| {
state.filter = Filter::Completed;
MessageResult::RequestRebuild
}),
),
))
.attr("class", "filters"),
clear_button,
))
.attr("class", "footer")
}
fn main_view(state: &mut AppState) -> impl View<AppState> + ViewMarker {
let editing_id = state.editing_id;
let todos: Vec<_> = state
.visible_todos()
.map(|(idx, todo)| {
Adapt::new(
move |data: &mut AppState, thunk| {
if let MessageResult::Action(action) = thunk.call(&mut data.todos[idx]) {
match action {
TodoAction::SetEditing(id) => data.start_editing(id),
TodoAction::CancelEditing => data.editing_id = None,
TodoAction::Destroy(id) => data.todos.retain(|todo| todo.id != id),
}
}
MessageResult::Nop
},
todo_item(todo, editing_id == Some(todo.id)),
)
})
.collect();
el::section((
el::input(())
.attr("id", "toggle-all")
.attr("class", "toggle-all")
.attr("type", "checkbox")
.attr("checked", "true"),
el::label(()).attr("for", "toggle-all"),
el::ul(todos).attr("class", "todo-list"),
))
.attr("class", "main")
}
fn app_logic(state: &mut AppState) -> impl View<AppState> {
log::debug!("render: {state:?}");
let main = (!state.todos.is_empty()).then(|| main_view(state));
let footer = (!state.todos.is_empty()).then(|| footer_view(state));
el::div((
el::header((
el::h1(text("TODOs")),
el::input(())
.attr("class", "new-todo")
.attr("placeholder", "What needs to be done?")
.attr("value", state.new_todo.clone())
.attr("autofocus", "true")
.on_keydown(|state: &mut AppState, evt| {
if evt.key() == "Enter" {
state.create_todo();
}
MessageResult::RequestRebuild
})
.on_input(|state: &mut AppState, evt| {
state.update_new_todo(&evt.target().value());
evt.prevent_default();
MessageResult::RequestRebuild
}),
))
.attr("class", "header"),
main,
footer,
))
}
// Called by our JS entry point to run the example
#[wasm_bindgen(start)]
pub fn run() -> Result<(), JsValue> {
panic::set_hook(Box::new(console_error_panic_hook::hook));
console_log::init_with_level(log::Level::Debug).unwrap();
App::new(AppState::default(), app_logic).run(&get_element_by_id("todoapp"));
Ok(())
}

View File

@ -0,0 +1,87 @@
use std::sync::atomic::{AtomicU64, Ordering};
fn next_id() -> u64 {
static ID_GEN: AtomicU64 = AtomicU64::new(1);
ID_GEN.fetch_add(1, Ordering::Relaxed)
}
#[derive(Default, Debug)]
pub struct AppState {
pub new_todo: String,
pub todos: Vec<Todo>,
pub filter: Filter,
pub editing_id: Option<u64>,
}
impl AppState {
pub fn create_todo(&mut self) {
if self.new_todo.is_empty() {
return;
}
let title = self.new_todo.trim().to_string();
self.new_todo.clear();
self.todos.push(Todo::new(title));
}
pub fn visible_todos(&mut self) -> impl Iterator<Item = (usize, &mut Todo)> {
self.todos
.iter_mut()
.enumerate()
.filter(|(_, todo)| match self.filter {
Filter::All => true,
Filter::Active => !todo.completed,
Filter::Completed => todo.completed,
})
}
pub fn update_new_todo(&mut self, new_text: &str) {
self.new_todo.clear();
self.new_todo.push_str(new_text);
}
pub fn start_editing(&mut self, id: u64) {
if let Some(ref mut todo) = self.todos.iter_mut().filter(|todo| todo.id == id).next() {
todo.title_editing.clear();
todo.title_editing.push_str(&todo.title);
self.editing_id = Some(id)
}
}
}
#[derive(Debug)]
pub struct Todo {
pub id: u64,
pub title: String,
pub title_editing: String,
pub completed: bool,
}
impl Todo {
pub fn new(title: String) -> Self {
let title_editing = title.clone();
Self {
id: next_id(),
title,
title_editing,
completed: false,
}
}
pub fn save_editing(&mut self) {
self.title.clear();
self.title.push_str(&self.title_editing);
}
}
#[derive(Debug, PartialEq, Copy, Clone)]
pub enum Filter {
All,
Active,
Completed,
}
impl Default for Filter {
fn default() -> Self {
Self::All
}
}

1
crates/xilem_svg/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
dist/

View File

@ -0,0 +1,27 @@
[package]
name = "xilemsvg"
version = "0.1.0"
license = "Apache-2.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
bitflags = "1.3.2"
wasm-bindgen = "0.2.84"
kurbo = "0.9.1"
xilem_core = { path = "../xilem_core" }
[dependencies.web-sys]
version = "0.3.4"
features = [
'console',
'Document',
'Element',
'HtmlElement',
'Node',
'PointerEvent',
'SvgElement',
'Window',
]

View File

@ -0,0 +1,7 @@
# Xilemsvg prototype
This is a proof of concept showing how to use `xilem_core` to render interactive vector graphics into SVG DOM nodes, running in a browser. A next step would be to factor it into a library so that applications can depend on it, but at the moment the test scene is baked in.
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`).
[Trunk]: https://trunkrs.dev/

View File

@ -0,0 +1,8 @@
<style>
rect.red {
fill: #e00;
}
line {
stroke: #444;
}
</style>

119
crates/xilem_svg/src/app.rs Normal file
View File

@ -0,0 +1,119 @@
// Copyright 2023 the Druid Authors.
// SPDX-License-Identifier: Apache-2.0
use std::{cell::RefCell, rc::Rc};
use crate::{
context::Cx,
view::{DomElement, View},
Message,
};
use xilem_core::Id;
pub struct App<T, V: View<T>, F: FnMut(&mut T) -> V>(Rc<RefCell<AppInner<T, V, F>>>);
struct AppInner<T, V: View<T>, F: FnMut(&mut T) -> V> {
data: T,
app_logic: F,
view: Option<V>,
id: Option<Id>,
state: Option<V::State>,
element: Option<V::Element>,
cx: Cx,
}
pub(crate) trait AppRunner {
fn handle_message(&self, message: Message);
fn clone_box(&self) -> Box<dyn AppRunner>;
}
impl<T: 'static, V: View<T> + 'static, F: FnMut(&mut T) -> V + 'static> Clone for App<T, V, F> {
fn clone(&self) -> Self {
App(self.0.clone())
}
}
impl<T: 'static, V: View<T> + 'static, F: FnMut(&mut T) -> V + 'static> App<T, V, F> {
pub fn new(data: T, app_logic: F) -> Self {
let inner = AppInner::new(data, app_logic);
let app = App(Rc::new(RefCell::new(inner)));
app.0.borrow_mut().cx.set_runner(app.clone());
app
}
pub fn run(self) {
self.0.borrow_mut().ensure_app();
// Latter may not be necessary, we have an rc loop.
std::mem::forget(self)
}
}
impl<T, V: View<T>, F: FnMut(&mut T) -> V> AppInner<T, V, F> {
pub fn new(data: T, app_logic: F) -> Self {
let cx = Cx::new();
AppInner {
data,
app_logic,
view: None,
id: None,
state: None,
element: None,
cx,
}
}
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);
self.view = Some(view);
self.id = Some(id);
self.state = Some(state);
let body = self.cx.document().body().unwrap();
let svg = self
.cx
.document()
.create_element_ns(Some("http://www.w3.org/2000/svg"), "svg")
.unwrap();
svg.set_attribute("width", "800").unwrap();
svg.set_attribute("height", "600").unwrap();
body.append_child(&svg).unwrap();
svg.append_child(element.as_element_ref()).unwrap();
self.element = Some(element);
}
}
}
impl<T: 'static, V: View<T> + 'static, F: FnMut(&mut T) -> V + 'static> AppRunner for App<T, V, F> {
// 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) {
let mut inner_guard = self.0.borrow_mut();
let inner = &mut *inner_guard;
if let Some(view) = &mut inner.view {
view.message(
&message.id_path[1..],
inner.state.as_mut().unwrap(),
message.body,
&mut inner.data,
);
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.
*view = new_view;
}
}
fn clone_box(&self) -> Box<dyn AppRunner> {
Box::new(self.clone())
}
}

View File

@ -0,0 +1,71 @@
// Copyright 2023 the Druid Authors.
// SPDX-License-Identifier: Apache-2.0
use std::any::Any;
use xilem_core::{Id, MessageResult};
use crate::{
context::{ChangeFlags, Cx},
view::{DomElement, View, ViewMarker},
};
pub struct Class<V> {
child: V,
// This could reasonably be static Cow also, but keep things simple
class: String,
}
pub fn class<V>(child: V, class: impl Into<String>) -> Class<V> {
Class {
child,
class: class.into(),
}
}
impl<V> ViewMarker for Class<V> {}
// TODO: make generic over A (probably requires Phantom)
impl<T, V: View<T>> View<T> for Class<V> {
type State = V::State;
type Element = V::Element;
fn build(&self, cx: &mut Cx) -> (Id, Self::State, Self::Element) {
let (id, child_state, element) = self.child.build(cx);
element
.as_element_ref()
.set_attribute("class", &self.class)
.unwrap();
(id, child_state, element)
}
fn rebuild(
&self,
cx: &mut Cx,
prev: &Self,
id: &mut Id,
state: &mut Self::State,
element: &mut V::Element,
) -> ChangeFlags {
let prev_id = *id;
let mut changed = self.child.rebuild(cx, &prev.child, id, state, element);
if self.class != prev.class || prev_id != *id {
element
.as_element_ref()
.set_attribute("class", &self.class)
.unwrap();
changed.insert(ChangeFlags::OTHER_CHANGE);
}
changed
}
fn message(
&self,
id_path: &[Id],
state: &mut Self::State,
message: Box<dyn Any>,
app_state: &mut T,
) -> MessageResult<()> {
self.child.message(id_path, state, message, app_state)
}
}

View File

@ -0,0 +1,86 @@
// Copyright 2023 the Druid Authors.
// SPDX-License-Identifier: Apache-2.0
use std::any::Any;
use wasm_bindgen::{prelude::Closure, JsCast};
use web_sys::SvgElement;
use xilem_core::{Id, MessageResult};
use crate::{
context::{ChangeFlags, Cx},
view::{DomElement, View, ViewMarker},
};
pub struct Clicked<V, F> {
child: V,
callback: F,
}
pub struct ClickedState<S> {
// Closure is retained so it can be called by environment
#[allow(unused)]
closure: Closure<dyn FnMut()>,
child_state: S,
}
struct ClickedMsg;
pub fn clicked<T, F: Fn(&mut T), V: View<T>>(child: V, callback: F) -> Clicked<V, F> {
Clicked { child, callback }
}
impl<V, F> ViewMarker for Clicked<V, F> {}
impl<T, F: Fn(&mut T) + Send, V: View<T>> View<T> for Clicked<V, F> {
type State = ClickedState<V::State>;
type Element = V::Element;
fn build(&self, cx: &mut Cx) -> (Id, Self::State, Self::Element) {
let (id, child_state, element) = self.child.build(cx);
let thunk = cx.with_id(id, |cx| cx.message_thunk());
let closure =
Closure::wrap(Box::new(move || thunk.push_message(ClickedMsg)) as Box<dyn FnMut()>);
element
.as_element_ref()
.dyn_ref::<SvgElement>()
.expect("not an svg element")
.set_onclick(Some(closure.as_ref().unchecked_ref()));
let state = ClickedState {
closure,
child_state,
};
(id, state, element)
}
fn rebuild(
&self,
cx: &mut Cx,
prev: &Self,
id: &mut Id,
state: &mut Self::State,
element: &mut Self::Element,
) -> ChangeFlags {
// TODO: if the child id changes (as can happen with AnyView), reinstall closure
self.child
.rebuild(cx, &prev.child, id, &mut state.child_state, element)
}
fn message(
&self,
id_path: &[Id],
state: &mut Self::State,
message: Box<dyn Any>,
app_state: &mut T,
) -> MessageResult<()> {
if message.downcast_ref::<ClickedMsg>().is_some() {
(self.callback)(app_state);
MessageResult::Action(())
} else {
self.child
.message(id_path, &mut state.child_state, message, app_state)
}
}
}

View File

@ -0,0 +1,104 @@
use std::any::Any;
use bitflags::bitflags;
use web_sys::Document;
use xilem_core::{Id, IdPath};
use crate::{app::AppRunner, Message};
// Note: xilem has derive Clone here. Not sure.
pub struct Cx {
id_path: IdPath,
document: Document,
app_ref: Option<Box<dyn AppRunner>>,
}
pub struct MessageThunk {
id_path: IdPath,
app_ref: Box<dyn AppRunner>,
}
bitflags! {
#[derive(Default)]
pub struct ChangeFlags: u32 {
const STRUCTURE = 1;
const OTHER_CHANGE = 2;
}
}
impl ChangeFlags {
pub fn tree_structure() -> Self {
ChangeFlags::STRUCTURE
}
}
impl Cx {
pub fn new() -> Self {
let window = web_sys::window().expect("no global `window` exists");
let document = window.document().expect("should have a document on window");
Cx {
id_path: Vec::new(),
document,
app_ref: None,
}
}
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, F: FnOnce(&mut Cx) -> 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, F: FnOnce(&mut Cx) -> T>(&mut self, f: F) -> (Id, T) {
let id = Id::next();
self.push(id);
let result = f(self);
self.pop();
(id, result)
}
pub fn document(&self) -> &Document {
&self.document
}
pub fn message_thunk(&self) -> MessageThunk {
MessageThunk {
id_path: self.id_path.clone(),
app_ref: self.app_ref.as_ref().unwrap().clone_box(),
}
}
pub(crate) fn set_runner(&mut self, runner: impl AppRunner + 'static) {
self.app_ref = Some(Box::new(runner));
}
}
impl MessageThunk {
pub fn push_message(&self, message_body: impl Any + Send + 'static) {
let message = Message {
id_path: self.id_path.clone(),
body: Box::new(message_body),
};
self.app_ref.handle_message(message);
}
}

View File

@ -0,0 +1,91 @@
// Copyright 2023 the Druid Authors.
// SPDX-License-Identifier: Apache-2.0
//! Group
use web_sys::Element;
use xilem_core::{Id, MessageResult, VecSplice};
use crate::{
context::{ChangeFlags, Cx},
view::{Pod, View, ViewMarker, ViewSequence},
};
pub struct Group<VS> {
children: VS,
}
pub struct GroupState<S> {
state: S,
elements: Vec<Pod>,
}
pub fn group<VS>(children: VS) -> Group<VS> {
Group { children }
}
impl<VS> ViewMarker for Group<VS> {}
impl<T, A, VS> View<T, A> for Group<VS>
where
VS: ViewSequence<T, A>,
{
type State = GroupState<VS::State>;
type Element = web_sys::Element;
fn build(&self, cx: &mut Cx) -> (Id, Self::State, Element) {
let el = cx
.document()
.create_element_ns(Some("http://www.w3.org/2000/svg"), "g")
.unwrap();
let mut elements = vec![];
let (id, state) = cx.with_new_id(|cx| self.children.build(cx, &mut elements));
for child in &elements {
el.append_child(child.0.as_element_ref()).unwrap();
}
let group_state = GroupState { state, elements };
(id, group_state, el)
}
fn rebuild(
&self,
cx: &mut Cx,
prev: &Self,
id: &mut Id,
state: &mut Self::State,
element: &mut Element,
) -> ChangeFlags {
let mut scratch = vec![];
let mut splice = VecSplice::new(&mut state.elements, &mut scratch);
let mut changed = cx.with_id(*id, |cx| {
self.children
.rebuild(cx, &prev.children, &mut state.state, &mut splice)
});
if changed.contains(ChangeFlags::STRUCTURE) {
// This is crude and will result in more DOM traffic than needed.
// The right thing to do is diff the new state of the children id
// vector against the old, and derive DOM mutations from that.
while let Some(child) = element.first_child() {
_ = element.remove_child(&child);
}
for child in &state.elements {
_ = element.append_child(child.0.as_element_ref());
}
// TODO: we may want to propagate that something changed
changed.remove(ChangeFlags::STRUCTURE);
}
changed
}
fn message(
&self,
id_path: &[Id],
state: &mut Self::State,
message: Box<dyn std::any::Any>,
app_state: &mut T,
) -> MessageResult<A> {
self.children
.message(id_path, &mut state.state, message, app_state)
}
}

View File

@ -0,0 +1,282 @@
// Copyright 2023 the Druid Authors.
// SPDX-License-Identifier: Apache-2.0
//! Implementation of the View trait for various kurbo shapes.
use kurbo::{BezPath, Circle, Line, Rect};
use web_sys::Element;
use xilem_core::{Id, MessageResult};
use crate::{
context::{ChangeFlags, Cx},
pointer::PointerMsg,
view::{View, ViewMarker},
};
pub trait KurboShape: Sized {
fn class(self, class: impl Into<String>) -> crate::class::Class<Self> {
crate::class::class(self, class)
}
fn clicked<T, F: Fn(&mut T)>(self, f: F) -> crate::clicked::Clicked<Self, F>
where
Self: View<T>,
{
crate::clicked::clicked(self, f)
}
fn pointer<T, F: Fn(&mut T, PointerMsg)>(self, f: F) -> crate::pointer::Pointer<Self, F>
where
Self: View<T>,
{
crate::pointer::pointer(self, f)
}
}
impl KurboShape for Line {}
impl KurboShape for Rect {}
impl KurboShape for Circle {}
impl KurboShape for BezPath {}
impl ViewMarker for Line {}
impl<T> View<T> for Line {
type State = ();
type Element = Element;
fn build(&self, cx: &mut Cx) -> (Id, Self::State, Element) {
let el = cx
.document()
.create_element_ns(Some("http://www.w3.org/2000/svg"), "line")
.unwrap();
el.set_attribute("x1", &format!("{}", self.p0.x)).unwrap();
el.set_attribute("y1", &format!("{}", self.p0.y)).unwrap();
el.set_attribute("x2", &format!("{}", self.p1.x)).unwrap();
el.set_attribute("y2", &format!("{}", self.p1.y)).unwrap();
let id = Id::next();
(id, (), el)
}
fn rebuild(
&self,
_cx: &mut Cx,
prev: &Self,
_id: &mut Id,
_state: &mut Self::State,
element: &mut Element,
) -> ChangeFlags {
let mut is_changed = ChangeFlags::default();
if self.p0.x != prev.p0.x {
element
.set_attribute("x1", &format!("{}", self.p0.x))
.unwrap();
is_changed |= ChangeFlags::OTHER_CHANGE;
}
if self.p0.y != prev.p0.y {
element
.set_attribute("y1", &format!("{}", self.p0.y))
.unwrap();
is_changed |= ChangeFlags::OTHER_CHANGE;
}
if self.p1.x != prev.p1.x {
element
.set_attribute("x2", &format!("{}", self.p1.x))
.unwrap();
is_changed |= ChangeFlags::OTHER_CHANGE;
}
if self.p1.y != prev.p1.y {
element
.set_attribute("y2", &format!("{}", self.p1.y))
.unwrap();
is_changed |= ChangeFlags::OTHER_CHANGE;
}
is_changed
}
fn message(
&self,
_id_path: &[Id],
_state: &mut Self::State,
message: Box<dyn std::any::Any>,
_app_state: &mut T,
) -> MessageResult<()> {
MessageResult::Stale(message)
}
}
impl ViewMarker for Rect {}
impl<T> View<T> for Rect {
type State = ();
type Element = Element;
fn build(&self, cx: &mut Cx) -> (Id, Self::State, Element) {
let el = cx
.document()
.create_element_ns(Some("http://www.w3.org/2000/svg"), "rect")
.unwrap();
el.set_attribute("x", &format!("{}", self.x0)).unwrap();
el.set_attribute("y", &format!("{}", self.y0)).unwrap();
let size = self.size();
el.set_attribute("width", &format!("{}", size.width))
.unwrap();
el.set_attribute("height", &format!("{}", size.height))
.unwrap();
let id = Id::next();
(id, (), el)
}
fn rebuild(
&self,
_cx: &mut Cx,
prev: &Self,
_id: &mut Id,
_state: &mut Self::State,
element: &mut Element,
) -> ChangeFlags {
let mut is_changed = ChangeFlags::default();
if self.x0 != prev.x0 {
element.set_attribute("x", &format!("{}", self.x0)).unwrap();
is_changed |= ChangeFlags::OTHER_CHANGE;
}
if self.y0 != prev.y0 {
element.set_attribute("y", &format!("{}", self.y0)).unwrap();
is_changed |= ChangeFlags::OTHER_CHANGE;
}
let size = self.size();
let prev_size = prev.size();
if size.width != prev_size.width {
element
.set_attribute("width", &format!("{}", size.width))
.unwrap();
is_changed |= ChangeFlags::OTHER_CHANGE;
}
if size.height != prev_size.height {
element
.set_attribute("height", &format!("{}", size.height))
.unwrap();
is_changed |= ChangeFlags::OTHER_CHANGE;
}
is_changed
}
fn message(
&self,
_id_path: &[Id],
_state: &mut Self::State,
message: Box<dyn std::any::Any>,
_app_state: &mut T,
) -> MessageResult<()> {
MessageResult::Stale(message)
}
}
impl ViewMarker for Circle {}
impl<T> View<T> for Circle {
type State = ();
type Element = Element;
fn build(&self, cx: &mut Cx) -> (Id, Self::State, Element) {
let el = cx
.document()
.create_element_ns(Some("http://www.w3.org/2000/svg"), "circle")
.unwrap();
el.set_attribute("cx", &format!("{}", self.center.x))
.unwrap();
el.set_attribute("cy", &format!("{}", self.center.y))
.unwrap();
el.set_attribute("r", &format!("{}", self.radius)).unwrap();
let id = Id::next();
(id, (), el)
}
fn rebuild(
&self,
_cx: &mut Cx,
prev: &Self,
_id: &mut Id,
_state: &mut Self::State,
element: &mut Element,
) -> ChangeFlags {
let mut is_changed = ChangeFlags::default();
if self.center.x != prev.center.x {
element
.set_attribute("cx", &format!("{}", self.center.x))
.unwrap();
is_changed |= ChangeFlags::OTHER_CHANGE;
}
if self.center.y != prev.center.y {
element
.set_attribute("cy", &format!("{}", self.center.y))
.unwrap();
is_changed |= ChangeFlags::OTHER_CHANGE;
}
if self.radius != prev.radius {
element
.set_attribute("r", &format!("{}", self.radius))
.unwrap();
is_changed |= ChangeFlags::OTHER_CHANGE;
}
is_changed
}
fn message(
&self,
_id_path: &[Id],
_state: &mut Self::State,
message: Box<dyn std::any::Any>,
_app_state: &mut T,
) -> MessageResult<()> {
MessageResult::Stale(message)
}
}
impl ViewMarker for BezPath {}
impl<T> View<T> for BezPath {
type State = ();
type Element = Element;
fn build(&self, cx: &mut Cx) -> (Id, Self::State, Element) {
let el = cx
.document()
.create_element_ns(Some("http://www.w3.org/2000/svg"), "path")
.unwrap();
el.set_attribute("d", &format!("{}", self.to_svg()))
.unwrap();
let id = Id::next();
(id, (), el)
}
fn rebuild(
&self,
_d: &mut Cx,
prev: &Self,
_id: &mut Id,
_state: &mut Self::State,
element: &mut Element,
) -> ChangeFlags {
let mut is_changed = ChangeFlags::default();
if self != prev {
element
.set_attribute("d", &format!("{}", self.to_svg()))
.unwrap();
is_changed |= ChangeFlags::OTHER_CHANGE;
}
is_changed
}
fn message(
&self,
_id_path: &[Id],
_state: &mut Self::State,
message: Box<dyn std::any::Any>,
_app_state: &mut T,
) -> MessageResult<()> {
MessageResult::Stale(message)
}
}
// TODO: RoundedRect

103
crates/xilem_svg/src/lib.rs Normal file
View File

@ -0,0 +1,103 @@
// Copyright 2023 the Druid Authors.
// SPDX-License-Identifier: Apache-2.0
//! A test program to exercise using xilem_core to generate SVG nodes that
//! render in a browser.
//!
//! Run using `trunk serve`.
mod app;
mod class;
mod clicked;
mod context;
mod group;
mod kurbo_shape;
mod pointer;
mod view;
mod view_ext;
use app::App;
use group::group;
use kurbo::Rect;
use kurbo_shape::KurboShape;
use pointer::PointerMsg;
use view::View;
use wasm_bindgen::prelude::*;
pub use context::ChangeFlags;
xilem_core::message!(Send);
#[derive(Default)]
struct AppState {
x: f64,
y: f64,
grab: GrabState,
}
#[derive(Default)]
struct GrabState {
is_down: bool,
id: i32,
dx: f64,
dy: f64,
}
impl GrabState {
fn handle(&mut self, x: &mut f64, y: &mut f64, p: &PointerMsg) {
match p {
PointerMsg::Down(e) => {
if e.button == 0 {
self.dx = *x - e.x;
self.dy = *y - e.y;
self.id = e.id;
self.is_down = true;
}
}
PointerMsg::Move(e) => {
if self.is_down && self.id == e.id {
*x = self.dx + e.x;
*y = self.dy + e.y;
}
}
PointerMsg::Up(e) => {
if self.id == e.id {
self.is_down = false;
}
}
}
}
}
fn app_logic(state: &mut AppState) -> impl View<AppState> {
let v = (0..10)
.map(|i| Rect::from_origin_size((10.0 * i as f64, 150.0), (8.0, 8.0)))
.collect::<Vec<_>>();
group((
Rect::new(100.0, 100.0, 200.0, 200.0).clicked(|_| {
web_sys::console::log_1(&"app logic clicked".into());
}),
Rect::new(210.0, 100.0, 310.0, 200.0),
Rect::new(320.0, 100.0, 420.0, 200.0).class("red"),
Rect::new(state.x, state.y, state.x + 100., state.y + 100.)
.pointer(|s: &mut AppState, msg| s.grab.handle(&mut s.x, &mut s.y, &msg)),
group(v),
Rect::new(210.0, 210.0, 310.0, 310.0).pointer(|_, e| {
web_sys::console::log_1(&format!("pointer event {e:?}").into());
}),
kurbo::Line::new((310.0, 210.0), (410.0, 310.0)),
kurbo::Circle::new((460.0, 260.0), 45.0).clicked(|_| {
web_sys::console::log_1(&"circle clicked".into());
}),
))
//button(format!("Count {}", count), |count| *count += 1)
}
// Called by our JS entry point to run the example
#[wasm_bindgen(start)]
pub fn run() -> Result<(), JsValue> {
let app = App::new(AppState::default(), app_logic);
app.run();
Ok(())
}

View File

@ -0,0 +1,143 @@
// Copyright 2023 the Druid Authors.
// SPDX-License-Identifier: Apache-2.0
//! Interactivity with pointer events.
use std::any::Any;
use wasm_bindgen::{prelude::Closure, JsCast};
use web_sys::PointerEvent;
use xilem_core::{Id, MessageResult};
use crate::{
context::{ChangeFlags, Cx},
view::{DomElement, View, ViewMarker},
};
pub struct Pointer<V, F> {
child: V,
callback: F,
}
pub struct PointerState<S> {
// Closures are retained so they can be called by environment
#[allow(unused)]
down_closure: Closure<dyn FnMut(PointerEvent)>,
#[allow(unused)]
move_closure: Closure<dyn FnMut(PointerEvent)>,
#[allow(unused)]
up_closure: Closure<dyn FnMut(PointerEvent)>,
child_state: S,
}
#[derive(Debug)]
pub enum PointerMsg {
Down(PointerDetails),
Move(PointerDetails),
Up(PointerDetails),
}
#[derive(Debug)]
pub struct PointerDetails {
pub id: i32,
pub button: i16,
pub x: f64,
pub y: f64,
}
impl PointerDetails {
fn from_pointer_event(e: &PointerEvent) -> Self {
PointerDetails {
id: e.pointer_id(),
button: e.button(),
x: e.client_x() as f64,
y: e.client_y() as f64,
}
}
}
pub fn pointer<T, F: Fn(&mut T, PointerMsg), V: View<T>>(child: V, callback: F) -> Pointer<V, F> {
Pointer { child, callback }
}
impl<V, F> ViewMarker for Pointer<V, F> {}
impl<T, F: Fn(&mut T, PointerMsg) + Send, V: View<T>> View<T> for Pointer<V, F> {
type State = PointerState<V::State>;
type Element = V::Element;
fn build(&self, cx: &mut Cx) -> (Id, Self::State, Self::Element) {
let (id, child_state, element) = self.child.build(cx);
let thunk = cx.with_id(id, |cx| cx.message_thunk());
let el_clone = element.as_element_ref().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();
});
element
.as_element_ref()
.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();
});
element
.as_element_ref()
.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();
});
element
.as_element_ref()
.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)
}
fn rebuild(
&self,
cx: &mut Cx,
prev: &Self,
id: &mut Id,
state: &mut Self::State,
element: &mut Self::Element,
) -> ChangeFlags {
// TODO: if the child id changes (as can happen with AnyView), reinstall closure
self.child
.rebuild(cx, &prev.child, id, &mut state.child_state, element)
}
fn message(
&self,
id_path: &[Id],
state: &mut Self::State,
message: Box<dyn Any>,
app_state: &mut T,
) -> MessageResult<()> {
match message.downcast() {
Ok(msg) => {
(self.callback)(app_state, *msg);
MessageResult::Action(())
}
Err(message) => self
.child
.message(id_path, &mut state.child_state, message, app_state),
}
}
}

View File

@ -0,0 +1,77 @@
// Copyright 2023 the Druid Authors.
// SPDX-License-Identifier: Apache-2.0
//! Integration with xilem_core. This instantiates the View and related
//! traits for DOM node generation.
use std::ops::Deref;
use crate::{context::Cx, ChangeFlags};
// 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.
pub trait DomElement {
fn into_pod(self) -> Pod;
fn as_element_ref(&self) -> &web_sys::Element;
}
pub trait AnyElement {
fn as_any_mut(&mut self) -> &mut dyn std::any::Any;
fn as_element_ref(&self) -> &web_sys::Element;
}
impl AnyElement for web_sys::Element {
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
self
}
fn as_element_ref(&self) -> &web_sys::Element {
self
}
}
impl DomElement for web_sys::Element {
fn into_pod(self) -> Pod {
Pod(Box::new(self))
}
fn as_element_ref(&self) -> &web_sys::Element {
self
}
}
impl DomElement for Box<dyn AnyElement> {
fn into_pod(self) -> Pod {
Pod(self)
}
fn as_element_ref(&self) -> &web_sys::Element {
self.deref().as_element_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<dyn AnyElement>);
impl Pod {
fn new(node: impl DomElement) -> Self {
node.into_pod()
}
fn downcast_mut<'a, T: 'static>(&'a mut self) -> Option<&'a mut T> {
self.0.as_any_mut().downcast_mut()
}
fn mark(&mut self, flags: ChangeFlags) -> ChangeFlags {
flags
}
}
xilem_core::generate_view_trait! {View, DomElement, Cx, ChangeFlags;}
xilem_core::generate_viewsequence_trait! {ViewSequence, View, ViewMarker, DomElement, Cx, ChangeFlags, Pod;}
xilem_core::generate_anyview_trait! {View, Cx, ChangeFlags, AnyElement}

View File

@ -0,0 +1,25 @@
// Copyright 2023 the Druid Authors.
// SPDX-License-Identifier: Apache-2.0
use crate::{
class::Class,
clicked::Clicked,
pointer::{Pointer, PointerMsg},
view::View,
};
pub trait ViewExt<T>: View<T> + Sized {
fn clicked<F: Fn(&mut T)>(self, f: F) -> Clicked<Self, F>;
fn pointer<F: Fn(&mut T, PointerMsg)>(self, f: F) -> Pointer<Self, F> {
crate::pointer::pointer(self, f)
}
fn class(self, class: impl Into<String>) -> Class<Self> {
crate::class::class(self, class)
}
}
impl<T, V: View<T>> ViewExt<T> for V {
fn clicked<F: Fn(&mut T)>(self, f: F) -> Clicked<Self, F> {
crate::clicked::clicked(self, f)
}
}

View File

@ -23,17 +23,17 @@ use parley::FontContext;
use tokio::runtime::Runtime;
use vello::kurbo::{Point, Rect};
use vello::SceneFragment;
use xilem_core::{AsyncWake, Message, MessageResult};
use xilem_core::{AsyncWake, MessageResult};
use crate::widget::{
AccessCx, BoxConstraints, CxState, EventCx, LayoutCx, LifeCycle, LifeCycleCx, PaintCx, Pod,
PodFlags, UpdateCx, ViewContext, WidgetState,
};
use crate::IdPath;
use crate::{
view::{Cx, Id, View},
widget::Event,
};
use crate::{IdPath, Message};
/// App is the native backend implementation of Xilem. It contains the code interacting with glazier
/// and vello.

View File

@ -10,7 +10,9 @@ mod text;
pub mod view;
pub mod widget;
pub use xilem_core::{IdPath, Message, MessageResult};
xilem_core::message!(Send);
pub use xilem_core::{IdPath, MessageResult};
pub use app::App;
pub use app_main::AppLauncher;

View File

@ -24,9 +24,9 @@ use glazier::{
WindowHandle,
};
use parley::FontContext;
use xilem_core::Message;
use super::{PodFlags, WidgetState};
use crate::Message;
// These contexts loosely follow Druid.