Start work on a TodoMVC implementation in xilem.

I didn't make much progress because some basic widgets are missing, but I did find the experience similar to the DOM version and it would be easy to do both while remaining in the same register.
This commit is contained in:
Richard Dodd 2023-07-05 20:48:32 +01:00
parent f99baa3969
commit 21573905e8
10 changed files with 191 additions and 28 deletions

7
Cargo.lock generated
View File

@ -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",

View File

@ -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",
".",
]

View File

@ -1,5 +1,5 @@
[package]
name = "todomvc"
name = "todomvc_web"
version = "0.1.0"
license = "Apache-2.0"
edition = "2021"

View File

@ -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 = "../.." }

View File

@ -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<AppState> {
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()
}

View File

@ -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<Todo>,
pub filter: Filter,
pub editing_id: Option<u64>,
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<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, Default, PartialEq, Copy, Clone)]
pub enum Filter {
#[default]
All,
Active,
Completed,
}

View File

@ -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;

View File

@ -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<T, A> View<T, A> 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<T, A> View<T, A> 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<dyn Any>,
_app_state: &mut T,
) -> MessageResult<A> {
MessageResult::Stale
MessageResult::Nop
}
}
impl ViewMarker for &'static str {}
impl<T, A> View<T, A> 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<dyn Any>,
_app_state: &mut T,
) -> MessageResult<A> {
MessageResult::Nop
}
}

View File

@ -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};

View File

@ -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 {