diff --git a/Cargo.lock b/Cargo.lock index 4771b05d..e2e52159 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2564,6 +2564,13 @@ checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" [[package]] name = "todomvc" version = "0.1.0" +dependencies = [ + "xilem", +] + +[[package]] +name = "todomvc_web" +version = "0.1.0" dependencies = [ "console_error_panic_hook", "console_log", diff --git a/Cargo.toml b/Cargo.toml index dfa2c9ff..69d9d393 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ members = [ "crates/xilem_html/web_examples/counter", "crates/xilem_html/web_examples/counter_untyped", "crates/xilem_html/web_examples/todomvc", + "examples/todomvc", ".", ] diff --git a/crates/xilem_html/web_examples/todomvc/Cargo.toml b/crates/xilem_html/web_examples/todomvc/Cargo.toml index f6cff14c..63160ae5 100644 --- a/crates/xilem_html/web_examples/todomvc/Cargo.toml +++ b/crates/xilem_html/web_examples/todomvc/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "todomvc" +name = "todomvc_web" version = "0.1.0" license = "Apache-2.0" edition = "2021" diff --git a/examples/todomvc/Cargo.toml b/examples/todomvc/Cargo.toml new file mode 100644 index 00000000..96d89e70 --- /dev/null +++ b/examples/todomvc/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "todomvc" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +xilem = { version = "0.1.0", path = "../.." } diff --git a/examples/todomvc/src/main.rs b/examples/todomvc/src/main.rs new file mode 100644 index 00000000..1cab1252 --- /dev/null +++ b/examples/todomvc/src/main.rs @@ -0,0 +1,29 @@ +use xilem::view::{button, h_stack, v_stack}; +use xilem::{view::View, App, AppLauncher}; + +mod state; + +use state::{AppState, Filter, Todo}; + +fn app_logic(data: &mut AppState) -> impl View { + println!("{data:?}"); + // The actual UI Code starts here + v_stack(( + format!("There are {} todos", data.todos.len()), + h_stack(( + button("All", |state: &mut AppState| state.filter = Filter::All), + button("Active", |state: &mut AppState| { + state.filter = Filter::Active + }), + button("Completed", |state: &mut AppState| { + state.filter = Filter::Completed + }), + )), + )) + .with_spacing(20.0) +} + +fn main() { + let app = App::new(AppState::default(), app_logic); + AppLauncher::new(app).run() +} diff --git a/examples/todomvc/src/state.rs b/examples/todomvc/src/state.rs new file mode 100644 index 00000000..92025349 --- /dev/null +++ b/examples/todomvc/src/state.rs @@ -0,0 +1,84 @@ +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, + pub filter: Filter, + pub editing_id: Option, + pub focus_new_todo: bool, +} + +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)); + self.focus_new_todo = true; + } + + pub fn visible_todos(&mut self) -> impl Iterator { + 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, Default, PartialEq, Copy, Clone)] +pub enum Filter { + #[default] + All, + Active, + Completed, +} diff --git a/src/view/mod.rs b/src/view/mod.rs index 8bf92e82..1424e3d6 100644 --- a/src/view/mod.rs +++ b/src/view/mod.rs @@ -17,7 +17,7 @@ mod button; // mod layout_observer; // mod list; // mod scroll_view; -// mod text; +mod text; // mod use_state; mod linear_layout; mod list; diff --git a/src/view/text.rs b/src/view/text.rs index 240a7392..5e641cf4 100644 --- a/src/view/text.rs +++ b/src/view/text.rs @@ -14,17 +14,20 @@ use std::any::Any; -use crate::{event::MessageResult, id::Id, widget::ChangeFlags}; +use xilem_core::{Id, MessageResult}; -use super::{Cx, View}; +use crate::widget::{ChangeFlags, TextWidget}; +use super::{Cx, View, ViewMarker}; + +impl ViewMarker for String {} impl View for String { type State = (); - type Element = crate::widget::text::TextWidget; + type Element = TextWidget; fn build(&self, cx: &mut Cx) -> (Id, Self::State, Self::Element) { - let (id, element) = cx.with_new_id(|_| crate::widget::text::TextWidget::new(self.clone())); + let (id, element) = cx.with_new_id(|_| TextWidget::new(self.clone())); (id, (), element) } @@ -32,24 +35,61 @@ impl View for String { &self, _cx: &mut Cx, prev: &Self, - _id: &mut crate::id::Id, + _id: &mut Id, _state: &mut Self::State, element: &mut Self::Element, ) -> ChangeFlags { + let mut change_flags = ChangeFlags::empty(); if prev != self { - element.set_text(self.clone()) - } else { - ChangeFlags::empty() + change_flags |= element.set_text(self.clone()); } + change_flags } - fn event( + fn message( &self, - _id_path: &[crate::id::Id], + _id_path: &[Id], _state: &mut Self::State, _event: Box, _app_state: &mut T, ) -> MessageResult { - MessageResult::Stale + MessageResult::Nop + } +} + +impl ViewMarker for &'static str {} +impl View for &'static str { + type State = (); + + type Element = TextWidget; + + fn build(&self, cx: &mut Cx) -> (Id, Self::State, Self::Element) { + let (id, element) = cx.with_new_id(|_| TextWidget::new(self.to_string())); + (id, (), element) + } + + fn rebuild( + &self, + _cx: &mut Cx, + prev: &Self, + _id: &mut Id, + _state: &mut Self::State, + element: &mut Self::Element, + ) -> ChangeFlags { + let mut change_flags = ChangeFlags::empty(); + if prev != self { + change_flags |= element.set_text(self.to_string()); + } + change_flags + } + + fn message( + &self, + _id_path: &[Id], + _state: &mut Self::State, + _event: Box, + _app_state: &mut T, + ) -> MessageResult { + MessageResult::Nop } } diff --git a/src/widget/mod.rs b/src/widget/mod.rs index 1c592b31..a197fa20 100644 --- a/src/widget/mod.rs +++ b/src/widget/mod.rs @@ -22,7 +22,7 @@ mod linear_layout; mod piet_scene_helpers; mod raw_event; //mod scroll_view; -//mod text; +mod text; mod widget; pub use self::core::{ChangeFlags, Pod}; @@ -32,4 +32,5 @@ pub use button::Button; pub use contexts::{AccessCx, CxState, EventCx, LayoutCx, LifeCycleCx, PaintCx, UpdateCx}; pub use linear_layout::LinearLayout; pub use raw_event::{Event, LifeCycle, MouseEvent, ViewContext}; +pub use text::TextWidget; pub use widget::{AnyWidget, Widget}; diff --git a/src/widget/text.rs b/src/widget/text.rs index fb11bd34..f1f449e6 100644 --- a/src/widget/text.rs +++ b/src/widget/text.rs @@ -22,9 +22,8 @@ use vello::{ use crate::text::ParleyBrush; use super::{ - align::{FirstBaseline, LastBaseline, SingleAlignment, VertAlignment}, - contexts::LifeCycleCx, - AlignCx, ChangeFlags, EventCx, LayoutCx, LifeCycle, PaintCx, RawEvent, UpdateCx, Widget, + contexts::LifeCycleCx, BoxConstraints, ChangeFlags, Event, EventCx, LayoutCx, LifeCycle, + PaintCx, UpdateCx, Widget, }; pub struct TextWidget { @@ -49,7 +48,7 @@ impl TextWidget { } impl Widget for TextWidget { - fn event(&mut self, _cx: &mut EventCx, _event: &RawEvent) {} + fn event(&mut self, _cx: &mut EventCx, _event: &Event) {} fn lifecycle(&mut self, _cx: &mut LifeCycleCx, _event: &LifeCycle) {} @@ -59,14 +58,7 @@ impl Widget for TextWidget { cx.request_layout(); } - fn measure(&mut self, cx: &mut LayoutCx) -> (Size, Size) { - let min_size = Size::ZERO; - let max_size = Size::new(50.0, 50.0); - self.is_wrapped = false; - (min_size, max_size) - } - - fn layout(&mut self, cx: &mut LayoutCx, proposed_size: Size) -> Size { + fn layout(&mut self, cx: &mut LayoutCx, _proposed_size: &BoxConstraints) -> Size { let mut lcx = parley::LayoutContext::new(); let mut layout_builder = lcx.ranged_builder(cx.font_cx(), &self.text, 1.0); layout_builder.push_default(&parley::style::StyleProperty::Brush(ParleyBrush( @@ -76,10 +68,10 @@ impl Widget for TextWidget { // Question for Chad: is this needed? layout.break_all_lines(None, parley::layout::Alignment::Start); self.layout = Some(layout); - cx.widget_state.max_size + cx.widget_state.size } - fn align(&self, cx: &mut AlignCx, alignment: SingleAlignment) {} + fn accessibility(&mut self, cx: &mut super::AccessCx) {} fn paint(&mut self, cx: &mut PaintCx, builder: &mut SceneBuilder) { if let Some(layout) = &self.layout {