mirror of https://github.com/linebender/xilem
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:
parent
0759de95bd
commit
1fd1ce7885
|
@ -2,3 +2,6 @@
|
|||
target/
|
||||
# We're a library, so ignore Cargo.lock
|
||||
Cargo.lock
|
||||
|
||||
.vscode
|
||||
.cspell
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -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]
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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> {}
|
||||
|
||||
};
|
||||
}
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
dist/
|
|
@ -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',
|
||||
]
|
|
@ -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/
|
|
@ -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())
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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),
|
||||
);
|
|
@ -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();
|
||||
}
|
|
@ -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),
|
||||
);
|
|
@ -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() }
|
||||
}
|
||||
}
|
||||
*/
|
|
@ -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()
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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 = "../.." }
|
|
@ -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>
|
|
@ -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(())
|
||||
}
|
|
@ -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 }
|
|
@ -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>
|
|
@ -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(())
|
||||
}
|
|
@ -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 = "../.." }
|
|
@ -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>
|
|
@ -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(())
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
dist/
|
|
@ -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',
|
||||
]
|
|
@ -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/
|
|
@ -0,0 +1,8 @@
|
|||
<style>
|
||||
rect.red {
|
||||
fill: #e00;
|
||||
}
|
||||
line {
|
||||
stroke: #444;
|
||||
}
|
||||
</style>
|
|
@ -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())
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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(())
|
||||
}
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
Loading…
Reference in New Issue