Skip to content

Instantly share code, notes, and snippets.

@MartinKavik
Last active December 4, 2020 18:49
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save MartinKavik/c0049ab16934ee676b5a2426f086d698 to your computer and use it in GitHub Desktop.
Save MartinKavik/c0049ab16934ee676b5a2426f086d698 to your computer and use it in GitHub Desktop.
zoon - draft - todos
use zoon::*;
use std::ops::Not;
blocks!{
#[el]
pub fn root() -> View {
view![
font::size(14),
font::family!("Helvetica Neue", "Helvetica", "Arial", font::sans_serif()),
font::color(hsl(0, 0, 5.1))
background::color(hsl(0, 0, 96.5)),
column![
width!(fill(), minimum(230), maximum(550)),
center_x(),
header(),
panel(),
footer(),
]
]
}
#[el]
fn header() -> El {
el![
region::header(),
width!(fill()),
padding!(top(35), bottom(32)),
el![
region::h1(),
center_x(),
font::size(80),
font::color(hsl(10.5, 62.8, 44.5)),
font::extra_light(),
"todos",
],
]
}
#[el]
fn panel() -> Column {
let todos_exist = super::todos_exist().inner();
column![
region::section(),
width!(fill()),
background::color(hsl(0, 0, 100)),
border::shadow!(
shadow::offsetXY(0, 2),
shadow::size(0),
shadow::blur(4),
shadow::color(hsla(0, 0, 0, 20)),
),
border::shadow!(
shadow::offsetXY(0, 25),
shadow::size(0),
shadow::blur(50),
shadow::color(hsla(0, 0, 0, 10)),
),
panel_header(),
todos_exist().then(|| elements![
todos(),
panel_footer(),
]),
]
}
#[el]
fn panel_header() -> Row {
let todos_exist = super::todos_exist().inner();
row![
width!(fill()),
background::color(hsla(0, 0, 0, 0.3)),
padding!(16),
border::shadow!(
shadow::inner(),
shadow::offsetXY(-2, 1),
shadow::size(0),
shadow::color(hsla(0, 0, 0, 3)),
),
todos_exist.then(toggle_all_checkbox),
new_todo_title(),
]
}
#[el]
fn toggle_all_checkbox() -> Checkbox {
let checked = super::are_all_completed().inner();
checkbox![
checkbox::checked(checked),
checkbox::on_change(super::check_or_uncheck_all),
input::label_hidden("Toggle All"),
el![
font::color(hsla(0, 0, if checked { 48.4 } else { 91.3 })),
font::size(22),
rotate(90),
"❯",
],
]
}
#[el]
fn new_todo_title() -> TextInput {
let new_todo_title = super::new_todo_title().inner();
text_input![
do_once(focus),
text_input::on_change(super::set_new_todo_title),
input::label_hidden("New Todo Title"),
placeholder![
font::italic(),
font::light(),
font::color(hsla(0, 0, 0, 40)),
placeholder::text("what needs to be done?"),
],
on_key_down(|event| if let Key::Enter = event.key { super::add_todo() }),
new_todo_title,
]
}
#[el]
fn todos() -> Column {
let filtered_todos = super::filtered_todos()
.inner();
.map(|todos| todos.iter_vars().map(todo));
column![
filtered_todos
]
}
fn active_todo_checkbox_icon() -> &'static str {
"data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23ededed%22%20stroke-width%3D%223%22/%3E%3C/svg%3E"
}
fn completed_todo_checkbox_icon() -> &'static str {
"data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23bddad5%22%20stroke-width%3D%223%22/%3E%3Cpath%20fill%3D%22%235dc2af%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22/%3E%3C/svg%3E"
}
#[el]
fn todo(todo: Var<super::Todo>) -> Row {
let selected = Some(todo) == super::selected_todo();
let checkbox_id = el_var(ElementId::new);
let row_hovered = el_var(|| false);
row![
font::size(24),
padding!(15),
spacing(10),
on_hovered_change(row_hovered.setter()),
todo_checkbox(checkbox_id, todo),
selected.not().then(|| todo_label(checkbox_id, todo)),
selected.then(selected_todo_title),
row_hovered.inner().then(|| remove_todo_button(todo)),
]
}
#[el]
fn todo_checkbox(checkbox_id: ElVar<ElementId>, todo: Var<super::Todo>) -> CheckBox {
let completed = todo.map(|todo| todo.completed);
checkbox![
id(checkbox_id.inner()),
checkbox::checked(completed),
checkbox::on_change(|_| super::toggle_todo(todo)),
el![
background::image(if completed {
completed_todo_checkbox_icon()
} else {
active_todo_checkbox_icon()
}),
],
]
}
#[el]
fn todo_label(checkbox_id: ElVar<ElementId>, todo: Var<super::Todo>) -> Label {
label![
label::for_input(checkbox_id.inner()),
checked.then(font::strike),
font::regular(),
font::color(hsl(0, 0, 32.7)),
on_double_click(|| select_todo(Some(todo))),
todo.map(|todo| todo.title.clone()),
]
}
#[el]
fn selected_todo_title() -> TextInput {
let selected_todo = super::selected_todo().inner().unwrap();
text_input![
width!(fill()),
paddingXY(16, 12),
border::solid(),
border::width!(1),
border::color(hsl(0, 0, 63.2)),
border::shadow!(
shadow::inner(),
shadow::offsetXY(-1, 5),
shadow::size(0),
shadow::color(hsla(0, 0, 0, 20)),
),
do_once(focus),
on_blur(super::save_selected_todo),
on_key_down(|event| {
match event.key {
Key::Escape => super::select_todo(None),
Key::Enter => super::save_selected_todo(),
_ => (),
}
}),
text_input::on_change(super::set_selected_todo_title),
selected_todo.map(|todo| todo.title.clone()),
]
}
#[el]
fn remove_todo_button(todo: Var<super::Todo>) -> Button {
let hovered = el_var(|| false);
button![
size::width!(20),
size::height!(20),
font::size(30),
font::color(hsl(12.2, 34.7, 68.2)),
on_hovered_change(hovered.setter()),
font::color(if hovered().inner() { hsl(10.5, 37.7, 48.8) } else { hsl(12.2, 34.7, 68.2) }),
button::on_press(|| super::remove_todo(todo)),
"×",
]
}
#[el]
fn panel_footer() -> Row {
let completed_exist = super::completed_exist();
row![
active_items_count(),
filters(),
completed_exist.then(clear_completed_button),
]
}
#[el]
fn active_items_count() -> Paragraph {
let active_count = super::active_count().inner();
paragraph![
el![
font::bold(),
active_count,
],
format!(" item{} left", if active_count == 1 { "" } else { "s" }),
]
}
#[el]
fn filters() -> Row {
let filters = super::filters();
row![
filters.map(|filters| filters.iter().map(filter))
]
}
#[el]
fn filter(filter: super::Filter) -> Button {
let selected = super::selected_filter().inner() == filter;
let hovered = el_var(|| false);
let (title, route) = match filter {
super::Filter::All => ("All", super::Route::root()),
super::Filter::Active => ("Active", super::Route::active()),
super::Filter::Completed => ("Completed", super::Route::completed()),
};
let border_alpha = if selected { 20 } else if hovered { 10 } else { 0 };
button![
on_hovered_change(hovered.setter()),
paddingXY(7, 3),
border::solid(),
border::width!(1),
border::color(hsla(12.2, 72.8, 40.2, border_alpha)),
button::on_press(|| super::set_route(route)),
title,
]
}
#[el]
fn clear_completed_button() -> Button {
let hovered = el_var(|| false);
button![
on_hovered_change(hovered.setter()),
hovered.inner().then(font::underline),
button::on_press(super::remove_completed),
"Clear completed",
]
}
#[el]
fn footer() -> Column {
column![
paragraph![
"Double-click to edit a todo",
],
paragraph![
"Created by ",
link![
link::new_tab(),
link::url("https://github.com/MartinKavik"),
"Martin Kavík",
],
],
paragraph![
"Part of ",
link![
link::new_tab(),
link::url("http://todomvc.com"),
"TodoMVC",
],
],
]
}
}
use zoon::*;
use serde::{Deserialize, Serialize};
use strum::IntoEnumIterator;
use strum_macros::EnumIter;
use ulid::Ulid;
use im::Vector;
mod els;
const STORAGE_KEY: &str = "todos-zoon";
type TodoId = Ulid;
blocks!{
append_blocks![els]
// ------ Route ------
#[route]
#[derive(Copy, Clone)]
enum Route {
#[route("active")]
Active,
#[route("completed")]
Completed,
#[route()]
Root,
Unknown,
}
#[cache]
fn route() -> Route {
url().map(Route::from)
}
#[update]
fn set_route(route: Route) {
url().set(Url::from(route))
}
// ------ Filters ------
#[derive(Copy, Clone, Eq, PartialEq, EnumIter)]
enum Filter {
All,
Active,
Completed,
}
#[var]
fn filters() -> Vec<Filter> {
Filter::iter().collect()
}
#[cache]
fn selected_filter() -> Filter {
match route().inner() {
Route::Active => Filter::Active,
Route::Completed => Filter::Completed,
_ => Filter::All,
}
}
// ------ SelectedTodo ------
#[var]
fn selected_todo() -> Option<Var<Todo>> {
None
}
#[update]
fn select_todo(todo: Option<Var<Todo>>) {
selected_todo().set(todo)
}
#[var]
fn selected_todo_title() -> Option<String> {
let todo = selected_todo().inner()?;
let title = todo.map(|todo| todo.title.clone());
Some(title)
}
#[update]
fn set_selected_todo_title(title: String) {
selected_todo_title().set(title)
}
#[update]
fn save_selected_todo() {
let title = selected_todo_title().map_mut(Option::take);
let todo = selected_todo().map_mut(Option::take);
todo.update_mut(move |todo| todo.title = title);
}
// ------ Todos ------
#[derive(Deserialize, Serialize)]
struct Todo {
id: TodoId,
title: String,
completed: bool,
}
#[var]
fn todo_update_handler() -> VarUpdateHandler<Todo> {
VarUpdateHandler::new(|_| todos().mark_updated())
}
#[var]
fn new_todo_title() -> String {
String::new()
}
#[update]
fn set_new_todo_title(title: String) {
new_todo_title().set(title);
}
#[update]
fn add_todo() {
let title = new_todo_title().map(String::trim);
if title.is_empty() {
return;
}
new_todo_title().update_mut(String::clear);
todos().update_mut(|todos| {
let todo = var(Todo {
id: TodoId::new(),
title,
completed: false,
});
todos.push_front(todo);
})
}
#[update]
fn remove_todo(todo: Var<Todo>) {
if Some(todo) == selected_todo().inner() {
selected_todo().set(None);
}
todos().update_mut(|todos| {
let position = todos.iter_vars().position(|t| t == todo).unwrap();
todos.remove(position);
});
}
#[update]
fn toggle_todo(todo: Var<Todo>) {
todo.update_mut(|todo| todo.checked = !todo.checked);
}
// -- all --
#[var]
fn todos() -> Vector<VarC<Todo>> {
LocalStorage::get(STORAGE_KEY).unwrap_or_default()
}
#[subscription]
fn store_todos() {
todos().use_ref(|todos| LocalStorage::insert(STORAGE_KEY, todos));
}
#[update]
fn check_or_uncheck_all(checked: bool) {
stop!{
if are_all_completed().inner() {
todos().use_ref(|todos| todos.iter().for_each(toggle_todo));
} else {
active_todos().use_ref(|todos| todos.iter().for_each(toggle_todo));
}
}
}
#[cache]
fn todos_count() -> usize {
todos().map(Vector::len)
}
#[cache]
fn todos_exist() -> bool {
todos_count().inner() != 0
}
// -- completed --
#[cache]
fn completed_todos() -> Vector<VarC<Todo>> {
let mut todos = todos().inner();
todos.retain(|todo| todo.map(|todo| todo.completed))
todos
}
#[update]
fn remove_completed() {
stop!{
completed_todos().use_ref(|todos| todos.iter().for_each(remove_todo));
}
}
#[cache]
fn completed_count() -> usize {
completed_todos().map(Vector::len)
}
#[cache]
fn completed_exist() -> bool {
completed_count().inner() != 0
}
#[cache]
fn are_all_completed() -> bool {
todos_count().inner() == completed_count().inner()
}
// -- active --
#[cache]
fn active_todos() -> Vector<VarC<Todo>> {
let mut todos = todos().inner();
todos.retain(|todo| todo.map(|todo| !todo.completed));
todos
}
#[cache]
fn active_count() -> usize {
active_todos().map(Vector::len)
}
// -- filtered --
#[cache]
fn filtered_todos() -> Cache<Vector<VarC<Todo>>> {
match selected_filter().inner() {
Filter::All => todos().to_cache(),
Filter::Active => active_todos(),
Filter::Completed => completed_todos(),
}
}
}
mod app;
fn main() {
start!(app)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment