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/
|
target/
|
||||||
# We're a library, so ignore Cargo.lock
|
# We're a library, so ignore Cargo.lock
|
||||||
Cargo.lock
|
Cargo.lock
|
||||||
|
|
||||||
|
.vscode
|
||||||
|
.cspell
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,6 +1,12 @@
|
||||||
[workspace]
|
[workspace]
|
||||||
members = [
|
members = [
|
||||||
"crates/xilem_core",
|
"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]
|
[package]
|
||||||
|
|
|
@ -23,5 +23,5 @@ mod vec_splice;
|
||||||
mod view;
|
mod view;
|
||||||
|
|
||||||
pub use id::{Id, IdPath};
|
pub use id::{Id, IdPath};
|
||||||
pub use message::{AsyncWake, Message, MessageResult};
|
pub use message::{AsyncWake, MessageResult};
|
||||||
pub use vec_splice::VecSplice;
|
pub use vec_splice::VecSplice;
|
||||||
|
|
|
@ -3,21 +3,60 @@
|
||||||
|
|
||||||
use std::any::Any;
|
use std::any::Any;
|
||||||
|
|
||||||
use crate::id::IdPath;
|
#[macro_export]
|
||||||
|
macro_rules! message {
|
||||||
|
() => {
|
||||||
pub struct Message {
|
pub struct Message {
|
||||||
pub id_path: IdPath,
|
pub id_path: xilem_core::IdPath,
|
||||||
pub body: Box<dyn Any + Send>,
|
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.
|
/// A result wrapper type for event handlers.
|
||||||
pub enum MessageResult<A> {
|
pub enum MessageResult<A> {
|
||||||
/// The event handler was invoked and returned an action.
|
/// 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),
|
Action(A),
|
||||||
/// The event handler received a change request that requests a rebuild.
|
/// 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)]
|
#[allow(unused)]
|
||||||
RequestRebuild,
|
RequestRebuild,
|
||||||
/// The event handler discarded the event.
|
/// The event handler discarded the event.
|
||||||
|
///
|
||||||
|
/// This is the variant that you **almost always want** when you're not returning
|
||||||
|
/// an action.
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
Nop,
|
Nop,
|
||||||
/// The event was addressed to an id path no longer in the tree.
|
/// 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>),
|
Stale(Box<dyn Any>),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<A> Default for MessageResult<A> {
|
||||||
|
fn default() -> Self {
|
||||||
|
MessageResult::Nop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: does this belong in core?
|
// TODO: does this belong in core?
|
||||||
pub struct AsyncWake;
|
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_export]
|
||||||
macro_rules! impl_view_tuple {
|
macro_rules! impl_view_tuple {
|
||||||
( $viewseq:ident, $pod:ty, $cx:ty, $changeflags:ty, $( $t:ident),* ; $( $i:tt ),* ) => {
|
( $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, )*);
|
type State = ( $( $t::State, )*);
|
||||||
|
|
||||||
fn build(&self, cx: &mut $cx, elements: &mut Vec<$pod>) -> Self::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), "`.")]
|
#[doc = concat!("`", stringify!($viewmarker), "`.")]
|
||||||
pub trait $viewmarker {}
|
pub trait $viewmarker {}
|
||||||
|
|
||||||
|
$crate::impl_view_tuple!($viewseq, $pod, $cx, $changeflags, ;);
|
||||||
$crate::impl_view_tuple!($viewseq, $pod, $cx, $changeflags,
|
$crate::impl_view_tuple!($viewseq, $pod, $cx, $changeflags,
|
||||||
V0; 0);
|
V0; 0);
|
||||||
$crate::impl_view_tuple!($viewseq, $pod, $cx, $changeflags,
|
$crate::impl_view_tuple!($viewseq, $pod, $cx, $changeflags,
|
||||||
|
|
|
@ -1,6 +1,18 @@
|
||||||
// Copyright 2023 the Druid Authors.
|
// Copyright 2023 the Druid Authors.
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
// 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_export]
|
||||||
macro_rules! generate_view_trait {
|
macro_rules! generate_view_trait {
|
||||||
($viewtrait:ident, $bound:ident, $cx:ty, $changeflags:ty; $($ss:tt)*) => {
|
($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
|
/// 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.
|
/// 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
|
/// 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
|
/// 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.
|
/// propagation.
|
||||||
///
|
///
|
||||||
/// The
|
/// The
|
||||||
#[doc = concat!("`", stringify!($viewtrait), "`")]
|
#[doc = concat!("`", stringify!($viewtrait), "`")]
|
||||||
// trait is parameterized by `T`, which is known as the "app state",
|
// 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
|
/// and also a type for actions which are passed up the tree in message
|
||||||
/// propagation. During event handling, mutable access to the app state is
|
/// propagation. During message handling, mutable access to the app state is
|
||||||
/// given to view nodes, which in turn can expose it to callbacks.
|
/// given to view nodes, which in turn can expose it to callbacks.
|
||||||
pub trait $viewtrait<T, A = ()> $( $ss )* {
|
pub trait $viewtrait<T, A = ()> $( $ss )* {
|
||||||
/// Associated state for the view.
|
/// Associated state for the view.
|
||||||
|
@ -55,5 +67,84 @@ macro_rules! generate_view_trait {
|
||||||
app_state: &mut T,
|
app_state: &mut T,
|
||||||
) -> $crate::MessageResult<A>;
|
) -> $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 tokio::runtime::Runtime;
|
||||||
use vello::kurbo::{Point, Rect};
|
use vello::kurbo::{Point, Rect};
|
||||||
use vello::SceneFragment;
|
use vello::SceneFragment;
|
||||||
use xilem_core::{AsyncWake, Message, MessageResult};
|
use xilem_core::{AsyncWake, MessageResult};
|
||||||
|
|
||||||
use crate::widget::{
|
use crate::widget::{
|
||||||
AccessCx, BoxConstraints, CxState, EventCx, LayoutCx, LifeCycle, LifeCycleCx, PaintCx, Pod,
|
AccessCx, BoxConstraints, CxState, EventCx, LayoutCx, LifeCycle, LifeCycleCx, PaintCx, Pod,
|
||||||
PodFlags, UpdateCx, ViewContext, WidgetState,
|
PodFlags, UpdateCx, ViewContext, WidgetState,
|
||||||
};
|
};
|
||||||
use crate::IdPath;
|
|
||||||
use crate::{
|
use crate::{
|
||||||
view::{Cx, Id, View},
|
view::{Cx, Id, View},
|
||||||
widget::Event,
|
widget::Event,
|
||||||
};
|
};
|
||||||
|
use crate::{IdPath, Message};
|
||||||
|
|
||||||
/// App is the native backend implementation of Xilem. It contains the code interacting with glazier
|
/// App is the native backend implementation of Xilem. It contains the code interacting with glazier
|
||||||
/// and vello.
|
/// and vello.
|
||||||
|
|
|
@ -10,7 +10,9 @@ mod text;
|
||||||
pub mod view;
|
pub mod view;
|
||||||
pub mod widget;
|
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::App;
|
||||||
pub use app_main::AppLauncher;
|
pub use app_main::AppLauncher;
|
||||||
|
|
|
@ -24,9 +24,9 @@ use glazier::{
|
||||||
WindowHandle,
|
WindowHandle,
|
||||||
};
|
};
|
||||||
use parley::FontContext;
|
use parley::FontContext;
|
||||||
use xilem_core::Message;
|
|
||||||
|
|
||||||
use super::{PodFlags, WidgetState};
|
use super::{PodFlags, WidgetState};
|
||||||
|
use crate::Message;
|
||||||
|
|
||||||
// These contexts loosely follow Druid.
|
// These contexts loosely follow Druid.
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue