mirror of https://github.com/linebender/xilem
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:
parent
f99baa3969
commit
21573905e8
|
@ -2564,6 +2564,13 @@ checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb"
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "todomvc"
|
name = "todomvc"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"xilem",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "todomvc_web"
|
||||||
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"console_error_panic_hook",
|
"console_error_panic_hook",
|
||||||
"console_log",
|
"console_log",
|
||||||
|
|
|
@ -6,6 +6,7 @@ members = [
|
||||||
"crates/xilem_html/web_examples/counter",
|
"crates/xilem_html/web_examples/counter",
|
||||||
"crates/xilem_html/web_examples/counter_untyped",
|
"crates/xilem_html/web_examples/counter_untyped",
|
||||||
"crates/xilem_html/web_examples/todomvc",
|
"crates/xilem_html/web_examples/todomvc",
|
||||||
|
"examples/todomvc",
|
||||||
".",
|
".",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
[package]
|
[package]
|
||||||
name = "todomvc"
|
name = "todomvc_web"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
license = "Apache-2.0"
|
license = "Apache-2.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
|
@ -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 = "../.." }
|
|
@ -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()
|
||||||
|
}
|
|
@ -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,
|
||||||
|
}
|
|
@ -17,7 +17,7 @@ mod button;
|
||||||
// mod layout_observer;
|
// mod layout_observer;
|
||||||
// mod list;
|
// mod list;
|
||||||
// mod scroll_view;
|
// mod scroll_view;
|
||||||
// mod text;
|
mod text;
|
||||||
// mod use_state;
|
// mod use_state;
|
||||||
mod linear_layout;
|
mod linear_layout;
|
||||||
mod list;
|
mod list;
|
||||||
|
|
|
@ -14,17 +14,20 @@
|
||||||
|
|
||||||
use std::any::Any;
|
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 {
|
impl<T, A> View<T, A> for String {
|
||||||
type State = ();
|
type State = ();
|
||||||
|
|
||||||
type Element = crate::widget::text::TextWidget;
|
type Element = TextWidget;
|
||||||
|
|
||||||
fn build(&self, cx: &mut Cx) -> (Id, Self::State, Self::Element) {
|
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)
|
(id, (), element)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -32,24 +35,61 @@ impl<T, A> View<T, A> for String {
|
||||||
&self,
|
&self,
|
||||||
_cx: &mut Cx,
|
_cx: &mut Cx,
|
||||||
prev: &Self,
|
prev: &Self,
|
||||||
_id: &mut crate::id::Id,
|
_id: &mut Id,
|
||||||
_state: &mut Self::State,
|
_state: &mut Self::State,
|
||||||
element: &mut Self::Element,
|
element: &mut Self::Element,
|
||||||
) -> ChangeFlags {
|
) -> ChangeFlags {
|
||||||
|
let mut change_flags = ChangeFlags::empty();
|
||||||
if prev != self {
|
if prev != self {
|
||||||
element.set_text(self.clone())
|
change_flags |= element.set_text(self.clone());
|
||||||
} else {
|
|
||||||
ChangeFlags::empty()
|
|
||||||
}
|
}
|
||||||
|
change_flags
|
||||||
}
|
}
|
||||||
|
|
||||||
fn event(
|
fn message(
|
||||||
&self,
|
&self,
|
||||||
_id_path: &[crate::id::Id],
|
_id_path: &[Id],
|
||||||
_state: &mut Self::State,
|
_state: &mut Self::State,
|
||||||
_event: Box<dyn Any>,
|
_event: Box<dyn Any>,
|
||||||
_app_state: &mut T,
|
_app_state: &mut T,
|
||||||
) -> MessageResult<A> {
|
) -> 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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,7 +22,7 @@ mod linear_layout;
|
||||||
mod piet_scene_helpers;
|
mod piet_scene_helpers;
|
||||||
mod raw_event;
|
mod raw_event;
|
||||||
//mod scroll_view;
|
//mod scroll_view;
|
||||||
//mod text;
|
mod text;
|
||||||
mod widget;
|
mod widget;
|
||||||
|
|
||||||
pub use self::core::{ChangeFlags, Pod};
|
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 contexts::{AccessCx, CxState, EventCx, LayoutCx, LifeCycleCx, PaintCx, UpdateCx};
|
||||||
pub use linear_layout::LinearLayout;
|
pub use linear_layout::LinearLayout;
|
||||||
pub use raw_event::{Event, LifeCycle, MouseEvent, ViewContext};
|
pub use raw_event::{Event, LifeCycle, MouseEvent, ViewContext};
|
||||||
|
pub use text::TextWidget;
|
||||||
pub use widget::{AnyWidget, Widget};
|
pub use widget::{AnyWidget, Widget};
|
||||||
|
|
|
@ -22,9 +22,8 @@ use vello::{
|
||||||
use crate::text::ParleyBrush;
|
use crate::text::ParleyBrush;
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
align::{FirstBaseline, LastBaseline, SingleAlignment, VertAlignment},
|
contexts::LifeCycleCx, BoxConstraints, ChangeFlags, Event, EventCx, LayoutCx, LifeCycle,
|
||||||
contexts::LifeCycleCx,
|
PaintCx, UpdateCx, Widget,
|
||||||
AlignCx, ChangeFlags, EventCx, LayoutCx, LifeCycle, PaintCx, RawEvent, UpdateCx, Widget,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct TextWidget {
|
pub struct TextWidget {
|
||||||
|
@ -49,7 +48,7 @@ impl TextWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Widget for 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) {}
|
fn lifecycle(&mut self, _cx: &mut LifeCycleCx, _event: &LifeCycle) {}
|
||||||
|
|
||||||
|
@ -59,14 +58,7 @@ impl Widget for TextWidget {
|
||||||
cx.request_layout();
|
cx.request_layout();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn measure(&mut self, cx: &mut LayoutCx) -> (Size, Size) {
|
fn layout(&mut self, cx: &mut LayoutCx, _proposed_size: &BoxConstraints) -> 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 {
|
|
||||||
let mut lcx = parley::LayoutContext::new();
|
let mut lcx = parley::LayoutContext::new();
|
||||||
let mut layout_builder = lcx.ranged_builder(cx.font_cx(), &self.text, 1.0);
|
let mut layout_builder = lcx.ranged_builder(cx.font_cx(), &self.text, 1.0);
|
||||||
layout_builder.push_default(&parley::style::StyleProperty::Brush(ParleyBrush(
|
layout_builder.push_default(&parley::style::StyleProperty::Brush(ParleyBrush(
|
||||||
|
@ -76,10 +68,10 @@ impl Widget for TextWidget {
|
||||||
// Question for Chad: is this needed?
|
// Question for Chad: is this needed?
|
||||||
layout.break_all_lines(None, parley::layout::Alignment::Start);
|
layout.break_all_lines(None, parley::layout::Alignment::Start);
|
||||||
self.layout = Some(layout);
|
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) {
|
fn paint(&mut self, cx: &mut PaintCx, builder: &mut SceneBuilder) {
|
||||||
if let Some(layout) = &self.layout {
|
if let Some(layout) = &self.layout {
|
||||||
|
|
Loading…
Reference in New Issue