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]]
|
||||
name = "todomvc"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"xilem",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "todomvc_web"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"console_error_panic_hook",
|
||||
"console_log",
|
||||
|
|
|
@ -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",
|
||||
".",
|
||||
]
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
[package]
|
||||
name = "todomvc"
|
||||
name = "todomvc_web"
|
||||
version = "0.1.0"
|
||||
license = "Apache-2.0"
|
||||
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 list;
|
||||
// mod scroll_view;
|
||||
// mod text;
|
||||
mod text;
|
||||
// mod use_state;
|
||||
mod linear_layout;
|
||||
mod list;
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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};
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in New Issue