Impl `View` directly on text, and refactor action return types.

This commit is contained in:
Richard Dodd 2023-07-02 15:57:01 +01:00
parent da58556d15
commit 89ab953167
11 changed files with 373 additions and 261 deletions

37
Cargo.lock generated
View File

@ -1215,6 +1215,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28999cda5ef6916ffd33fb4a7b87e1de633c47c0dc6d97905fee1cdaa142b94d"
dependencies = [
"gloo-events",
"gloo-utils",
]
[[package]]
@ -1227,6 +1228,19 @@ dependencies = [
"web-sys",
]
[[package]]
name = "gloo-utils"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "037fcb07216cb3a30f7292bd0176b050b7b9a052ba830ef7d5d65f6dc64ba58e"
dependencies = [
"js-sys",
"serde",
"serde_json",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "glow"
version = "0.12.2"
@ -1498,6 +1512,12 @@ dependencies = [
"windows-sys",
]
[[package]]
name = "itoa"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6"
[[package]]
name = "js-sys"
version = "0.3.64"
@ -2244,6 +2264,12 @@ dependencies = [
"windows-sys",
]
[[package]]
name = "ryu"
version = "1.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041"
[[package]]
name = "scopeguard"
version = "1.1.0"
@ -2276,6 +2302,17 @@ dependencies = [
"syn 2.0.18",
]
[[package]]
name = "serde_json"
version = "1.0.99"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46266871c240a00b8f503b877622fe33430b3c7d963bdc0f2adc511e54a1eae3"
dependencies = [
"itoa",
"ryu",
"serde",
]
[[package]]
name = "serde_repr"
version = "0.1.12"

View File

@ -6,68 +6,7 @@ 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',
]
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"
@ -75,7 +14,7 @@ wasm-bindgen = "0.2.87"
kurbo = "0.9.1"
xilem_core = { path = "../xilem_core" }
log = "0.4.19"
gloo = { version = "0.8.1", default-features = false, features = ["events"] }
gloo = { version = "0.8.1", default-features = false, features = ["events", "utils"] }
[dependencies.web-sys]
version = "0.3.4"

View File

@ -10,19 +10,49 @@ macro_rules! events {
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>
pub struct $ty_name<T, A, V, F, OA>
where
V: crate::view::View<T, A>,
F: Fn(&mut T, &$crate::Event<$web_sys_ty, V::Element>) -> $crate::MessageResult<A>,
F: Fn(&mut T, &$crate::Event<$web_sys_ty, V::Element>) -> OA,
V::Element: 'static,
OA: $crate::event::OptionalAction<A>,
{
inner: crate::OnEvent<$web_sys_ty, V, F>,
data: std::marker::PhantomData<T>,
action: std::marker::PhantomData<A>,
optional_action: std::marker::PhantomData<OA>,
}
pub fn $builder_name<T, A, V, F, OA>(child: V, callback: F) -> $ty_name<T, A, V, F, OA>
where
V: crate::view::View<T, A>,
F: Fn(&mut T, &$crate::Event<$web_sys_ty, V::Element>) -> OA,
V::Element: 'static,
OA: $crate::event::OptionalAction<A>,
{
$ty_name {
inner: crate::on_event($name, child, callback),
data: std::marker::PhantomData,
action: std::marker::PhantomData,
optional_action: std::marker::PhantomData,
}
}
impl<T, A, V, F, OA> crate::view::ViewMarker for $ty_name<T, A, V, F, OA>
where
V: crate::view::View<T, A>,
F: Fn(&mut T, &$crate::Event<$web_sys_ty, V::Element>) -> OA,
V::Element: 'static,
OA: $crate::event::OptionalAction<A>,
{
}
impl<T, A, V, F, OA> crate::view::View<T, A> for $ty_name<T, A, V, F, OA>
where
V: crate::view::View<T, A>,
F: Fn(&mut T, &$crate::Event<$web_sys_ty, V::Element>) -> OA,
V::Element: 'static,
OA: $crate::event::OptionalAction<A>,
{
type State = crate::event::OnEventState<V::State>;
type Element = V::Element;
@ -31,7 +61,7 @@ macro_rules! event {
&self,
cx: &mut crate::context::Cx,
) -> (xilem_core::Id, Self::State, Self::Element) {
self.0.build(cx)
self.inner.build(cx)
}
fn rebuild(
@ -42,7 +72,7 @@ macro_rules! event {
state: &mut Self::State,
element: &mut Self::Element,
) -> crate::ChangeFlags {
self.0.rebuild(cx, &prev.0, id, state, element)
self.inner.rebuild(cx, &prev.inner, id, state, element)
}
fn message(
@ -52,7 +82,7 @@ macro_rules! event {
message: Box<dyn std::any::Any>,
app_state: &mut T,
) -> xilem_core::MessageResult<A> {
self.0.message(id_path, state, message, app_state)
self.inner.message(id_path, state, message, app_state)
}
}
};

View File

@ -37,12 +37,13 @@ impl<E, V, F> OnEvent<E, V, F> {
impl<E, V, F> ViewMarker for OnEvent<E, V, F> {}
impl<T, A, E, F, V> View<T, A> for OnEvent<E, V, F>
impl<T, A, E, F, V, OA> View<T, A> for OnEvent<E, V, F>
where
F: Fn(&mut T, &Event<E, V::Element>) -> MessageResult<A>,
F: Fn(&mut T, &Event<E, V::Element>) -> OA,
V: View<T, A>,
E: JsCast + 'static,
V::Element: 'static,
OA: OptionalAction<A>,
{
type State = OnEventState<V::State>;
@ -90,7 +91,10 @@ where
app_state: &mut T,
) -> MessageResult<A> {
if let Some(msg) = message.downcast_ref::<EventMsg<Event<E, V::Element>>>() {
(self.callback)(app_state, &msg.event)
match (self.callback)(app_state, &msg.event).action() {
Some(a) => MessageResult::Action(a),
None => MessageResult::Nop,
}
} else {
self.child
.message(id_path, &mut state.child_state, message, app_state)
@ -143,3 +147,39 @@ impl<Evt, El> Deref for Event<Evt, El> {
&self.raw
}
}
/// Implement this trait for types you want to use as actions.
///
/// The trait exists because otherwise we couldn't provide versions
/// of listeners that take `()`, `A` and `Option<A>`.
pub trait Action {}
/// Trait that allows callbacks to be polymorphic on return type
/// (`Action`, `Option<Action>` or `()`)
pub trait OptionalAction<A>: sealed::Sealed {
fn action(self) -> Option<A>;
}
mod sealed {
pub trait Sealed {}
}
impl sealed::Sealed for () {}
impl<A> OptionalAction<A> for () {
fn action(self) -> Option<A> {
None
}
}
impl<A: Action> sealed::Sealed for A {}
impl<A: Action> OptionalAction<A> for A {
fn action(self) -> Option<A> {
Some(self)
}
}
impl<A: Action> sealed::Sealed for Option<A> {}
impl<A: Action> OptionalAction<A> for Option<A> {
fn action(self) -> Option<A> {
self
}
}

View File

@ -14,7 +14,6 @@ mod context;
mod event;
//mod div;
mod element;
mod text;
mod view;
#[cfg(feature = "typed")]
mod view_ext;
@ -29,8 +28,7 @@ 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 event::{on_event, Action, Event, OnEvent, OnEventState, OptionalAction};
pub use view::{Adapt, AdaptThunk, Pod, View, ViewMarker, ViewSequence};
#[cfg(feature = "typed")]
pub use view_ext::ViewExt;

View File

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

View File

@ -4,7 +4,9 @@
//! Integration with xilem_core. This instantiates the View and related
//! traits for DOM node generation.
use std::{any::Any, ops::Deref};
use std::{any::Any, borrow::Cow, ops::Deref};
use xilem_core::{Id, MessageResult};
use crate::{context::Cx, ChangeFlags};
@ -98,3 +100,121 @@ impl Pod {
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}
impl ViewMarker for &'static str {}
impl<T, A> View<T, A> for &'static str {
type State = ();
type Element = web_sys::Text;
fn build(&self, _cx: &mut Cx) -> (Id, Self::State, Self::Element) {
let el = new_text(self);
let id = Id::next();
(id, (), el.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 != self {
element.set_data(self);
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
}
}
impl ViewMarker for String {}
impl<T, A> View<T, A> for String {
type State = ();
type Element = web_sys::Text;
fn build(&self, _cx: &mut Cx) -> (Id, Self::State, Self::Element) {
let el = new_text(self);
let id = Id::next();
(id, (), el.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 != self {
element.set_data(self);
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
}
}
impl ViewMarker for Cow<'static, str> {}
impl<T, A> View<T, A> for Cow<'static, str> {
type State = ();
type Element = web_sys::Text;
fn build(&self, _cx: &mut Cx) -> (Id, Self::State, Self::Element) {
let el = new_text(self);
let id = Id::next();
(id, (), el.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 != self {
element.set_data(self);
is_changed |= ChangeFlags::OTHER_CHANGE;
}
is_changed
}
fn message(
&self,
_id_path: &[Id],
_state: &mut Self::State,
_message: Box<dyn std::any::Any>,
_app_state: &mut T,
) -> MessageResult<A> {
MessageResult::Nop
}
}
fn new_text(text: &str) -> web_sys::Text {
web_sys::Text::new_with_data(text).unwrap()
}

View File

@ -3,61 +3,77 @@
use std::borrow::Cow;
use xilem_core::MessageResult;
use crate::{class::Class, events as e, view::View, Event};
use crate::{class::Class, event::OptionalAction, 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>,
fn on_click<
OA: OptionalAction<A>,
F: Fn(&mut T, &Event<web_sys::MouseEvent, Self::Element>) -> OA,
>(
self,
f: F,
) -> e::OnKeyDown<Self, F>;
) -> e::OnClick<T, A, Self, F, OA>;
fn on_dblclick<
OA: OptionalAction<A>,
F: Fn(&mut T, &Event<web_sys::MouseEvent, Self::Element>) -> OA,
>(
self,
f: F,
) -> e::OnDblClick<T, A, Self, F, OA>;
fn on_input<
OA: OptionalAction<A>,
F: Fn(&mut T, &Event<web_sys::InputEvent, Self::Element>) -> OA,
>(
self,
f: F,
) -> e::OnInput<T, A, Self, F, OA>;
fn on_keydown<
OA: OptionalAction<A>,
F: Fn(&mut T, &Event<web_sys::KeyboardEvent, Self::Element>) -> OA,
>(
self,
f: F,
) -> e::OnKeyDown<T, A, Self, F, OA>;
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>>(
fn on_click<
OA: OptionalAction<A>,
F: Fn(&mut T, &Event<web_sys::MouseEvent, Self::Element>) -> OA,
>(
self,
f: F,
) -> e::OnClick<Self, F> {
) -> e::OnClick<T, A, Self, F, OA> {
e::on_click(self, f)
}
fn on_dblclick<
F: Fn(&mut T, &Event<web_sys::MouseEvent, Self::Element>) -> MessageResult<A>,
OA: OptionalAction<A>,
F: Fn(&mut T, &Event<web_sys::MouseEvent, Self::Element>) -> OA,
>(
self,
f: F,
) -> e::OnDblClick<Self, F> {
) -> e::OnDblClick<T, A, Self, F, OA> {
e::on_dblclick(self, f)
}
fn on_input<F: Fn(&mut T, &Event<web_sys::InputEvent, Self::Element>) -> MessageResult<A>>(
fn on_input<
OA: OptionalAction<A>,
F: Fn(&mut T, &Event<web_sys::InputEvent, Self::Element>) -> OA,
>(
self,
f: F,
) -> e::OnInput<Self, F> {
) -> e::OnInput<T, A, Self, F, OA> {
crate::events::on_input(self, f)
}
fn on_keydown<
F: Fn(&mut T, &Event<web_sys::KeyboardEvent, Self::Element>) -> MessageResult<A>,
OA: OptionalAction<A>,
F: Fn(&mut T, &Event<web_sys::KeyboardEvent, Self::Element>) -> OA,
>(
self,
f: F,
) -> e::OnKeyDown<Self, F> {
) -> e::OnKeyDown<T, A, Self, F, OA> {
crate::events::on_keydown(self, f)
}
}

View File

@ -1,7 +1,8 @@
use wasm_bindgen::{prelude::*, JsValue};
use xilem_html::{
document_body, elements as el, events as evt, text, App, Event, MessageResult, Text, View,
ViewExt,
document_body, elements as el,
events::{self as evt},
App, Event, View, ViewExt,
};
#[derive(Default)]
@ -12,61 +13,54 @@ struct AppState {
}
impl AppState {
fn increment(&mut self) -> MessageResult<()> {
fn increment(&mut self) {
self.clicks += 1;
MessageResult::Nop
}
fn decrement(&mut self) -> MessageResult<()> {
fn decrement(&mut self) {
self.clicks -= 1;
MessageResult::Nop
}
fn reset(&mut self) -> MessageResult<()> {
fn reset(&mut self) {
self.clicks = 0;
MessageResult::Nop
}
fn change_class(&mut self) -> MessageResult<()> {
fn change_class(&mut self) {
if self.class == "gray" {
self.class = "green";
} else {
self.class = "gray";
}
MessageResult::Nop
}
fn change_text(&mut self) -> MessageResult<()> {
fn change_text(&mut self) {
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>
fn btn<A, F>(
label: &'static str,
click_fn: F,
) -> evt::OnClick<AppState, A, el::Button<&'static str>, F, ()>
where
F: Fn(
&mut AppState,
&Event<web_sys::MouseEvent, web_sys::HtmlButtonElement>,
) -> MessageResult<()>,
F: Fn(&mut AppState, &Event<web_sys::MouseEvent, web_sys::HtmlButtonElement>),
{
el::button(text(label)).on_click(click_fn)
el::button(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::span(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)),
btn("+1 click", |state, _| state.increment()),
btn("-1 click", |state, _| state.decrement()),
btn("reset clicks", |state, _| state.reset()),
btn("a different class", |state, _| state.change_class()),
btn("change text", |state, _| state.change_text()),
el::br(()),
text(state.text.clone()),
state.text.clone(),
))
}

View File

@ -1,7 +1,5 @@
use wasm_bindgen::{prelude::*, JsValue};
use xilem_html::{
document_body, element as el, on_event, text, App, Event, MessageResult, View, ViewMarker,
};
use xilem_html::{document_body, element as el, on_event, App, Event, View, ViewMarker};
#[derive(Default)]
struct AppState {
@ -9,32 +7,35 @@ struct AppState {
}
impl AppState {
fn increment(&mut self) -> MessageResult<()> {
fn increment(&mut self) {
self.clicks += 1;
MessageResult::Nop
}
fn decrement(&mut self) -> MessageResult<()> {
fn decrement(&mut self) {
self.clicks -= 1;
MessageResult::Nop
}
fn reset(&mut self) -> MessageResult<()> {
fn reset(&mut self) {
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<()>,
F: Fn(&mut AppState, &Event<web_sys::Event, web_sys::HtmlButtonElement>),
{
on_event("click", el("button", text(label)), click_fn)
on_event(
"click",
el("button", label),
move |state: &mut AppState, evt: &Event<_, _>| {
click_fn(state, evt);
},
)
}
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))),
el::<web_sys::HtmlElement, _>("span", 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)),

View File

@ -6,7 +6,8 @@ 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,
elements as el, events::on_click, get_element_by_id, Action, Adapt, App, Event, MessageResult,
View, ViewExt, ViewMarker,
};
// All of these actions arise from within a `Todo`, but we need access to the full state to reduce
@ -17,6 +18,8 @@ enum TodoAction {
Destroy(u64),
}
impl Action for TodoAction {}
fn todo_item(todo: &mut Todo, editing: bool) -> impl View<Todo, TodoAction> + ViewMarker {
let mut class = String::new();
if todo.completed {
@ -36,16 +39,12 @@ fn todo_item(todo: &mut Todo, editing: bool) -> impl View<Todo, TodoAction> + Vi
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::label(todo.title.clone())
.on_dblclick(|state: &mut Todo, _| TodoAction::SetEditing(state.id)),
el::button(())
.attr("class", "destroy")
.on_click(|state: &mut Todo, _| {
MessageResult::Action(TodoAction::Destroy(state.id))
}),
.on_click(|state: &mut Todo, _| TodoAction::Destroy(state.id)),
))
.attr("class", "view"),
el::input(())
@ -55,18 +54,17 @@ fn todo_item(todo: &mut Todo, editing: bool) -> impl View<Todo, TodoAction> + Vi
let key = evt.key();
if key == "Enter" {
state.save_editing();
MessageResult::Action(TodoAction::CancelEditing)
Some(TodoAction::CancelEditing)
} else if key == "Escape" {
MessageResult::Action(TodoAction::CancelEditing)
Some(TodoAction::CancelEditing)
} else {
MessageResult::Nop
None
}
})
.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)
@ -80,12 +78,12 @@ fn footer_view(state: &mut AppState) -> impl View<AppState> + ViewMarker {
};
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, _| {
on_click(
el::button("Clear completed").attr("class", "clear-completed"),
|state: &mut AppState, _| {
state.todos.retain(|todo| !todo.completed);
MessageResult::RequestRebuild
})
},
)
});
let filter_class = |filter| {
@ -98,40 +96,37 @@ fn footer_view(state: &mut AppState) -> impl View<AppState> + ViewMarker {
el::footer((
el::span((
el::strong(text(state.todos.len().to_string())),
text(format!(" {} left", item_str)),
el::strong(state.todos.len().to_string()),
format!(" {} left", item_str),
))
.attr("class", "todo-count"),
el::ul((
el::li(
el::a(text("All"))
el::li(on_click(
el::a("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("class", filter_class(Filter::All)),
|state: &mut AppState, _| {
state.filter = Filter::All;
},
)),
" ",
el::li(on_click(
el::a("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("class", filter_class(Filter::Active)),
|state: &mut AppState, _| {
state.filter = Filter::Active;
},
)),
" ",
el::li(on_click(
el::a("Completed")
.attr("href", "#/completed")
.attr("class", filter_class(Filter::Completed))
.on_click(|state: &mut AppState, _| {
state.filter = Filter::Completed;
MessageResult::RequestRebuild
}),
),
.attr("class", filter_class(Filter::Completed)),
|state: &mut AppState, _| {
state.filter = Filter::Completed;
},
)),
))
.attr("class", "filters"),
clear_button,
@ -177,23 +172,26 @@ fn app_logic(state: &mut AppState) -> impl View<AppState> {
let footer = (!state.todos.is_empty()).then(|| footer_view(state));
el::div((
el::header((
el::h1(text("TODOs")),
el::h1("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| {
.on_keydown(
|state: &mut AppState, evt| {
if evt.key() == "Enter" {
state.create_todo();
}
MessageResult::RequestRebuild
})
.on_input(|state: &mut AppState, evt| {
},
)
.on_input(
|state: &mut AppState,
evt: &Event<web_sys::InputEvent, web_sys::HtmlInputElement>| {
state.update_new_todo(&evt.target().value());
evt.prevent_default();
MessageResult::RequestRebuild
}),
},
),
))
.attr("class", "header"),
main,