Files
ruin/aspirational_examples/03_fine_grained_list.rs

149 lines
4.2 KiB
Rust

//! Aspirational RUIN app API for fine-grained reactivity and keyed layout islands.
//!
//! Intentionally non-compiling; this is a design sketch.
use ruin::prelude::*;
#[ruin_runtime::async_main]
async fn main() -> ruin::Result<()> {
App::new()
.window(Window::new().title("RUIN Tasks").size(1180.0, 780.0))
.mount::<TaskBoard>()
.run()
.await
}
#[component]
fn TaskBoard() -> impl View {
let filter = use_signal(|| TaskFilter::All);
let tasks = use_signal_vec(seed_tasks());
let visible_ids = use_memo(move || {
tasks.iter()
.filter(|task| filter.get().matches(task.status))
.map(|task| task.id)
.collect::<Vec<_>>()
});
view! {
column(fill = true, gap = 16, padding = 20) {
text(role = TextRole::Heading(1), size = 28, weight = FontWeight::Semibold) {
"Task board"
}
row(gap = 8) {
button(
kind = if matches!(filter.get(), TaskFilter::All) {
ButtonKind::Primary
} else {
ButtonKind::Secondary
},
on_press = move |_| filter.set(TaskFilter::All),
) { "All" }
button(
kind = if matches!(filter.get(), TaskFilter::OpenOnly) {
ButtonKind::Primary
} else {
ButtonKind::Secondary
},
on_press = move |_| filter.set(TaskFilter::OpenOnly),
) { "Open" }
button(
kind = if matches!(filter.get(), TaskFilter::CompletedOnly) {
ButtonKind::Primary
} else {
ButtonKind::Secondary
},
on_press = move |_| filter.set(TaskFilter::CompletedOnly),
) { "Completed" }
}
text(color = colors::muted()) {
"The goal here is fine-grained change tracking: reordering, toggling, and editing \
a single row should not force unrelated rows to rebuild or the entire layout tree \
to be recomputed."
}
column(fill = true, gap = 10) {
for task_id in visible_ids keyed by *task_id {
layout_boundary(key = task_id) {
TaskRow(task = tasks.item(task_id)) {}
}
}
}
}
}
}
#[component]
fn TaskRow(task: ItemSignal<Task>) -> impl View {
let expanded = use_signal(|| false);
let title = use_memo(move || task.with(|task| task.title.clone()));
let done = use_memo(move || task.with(|task| task.done));
view! {
row(gap = 12, padding = 12, align = Align::Center) {
checkblock(
checked = done,
on_toggle = move |checked| task.update(|task| task.done = checked),
) {}
column(fill = true, gap = 6) {
text(weight = if done.get() { FontWeight::Medium } else { FontWeight::Semibold }) {
title
}
if expanded.get() {
text(color = colors::muted()) {
task.with(|task| task.notes.clone())
}
}
}
icon_button(
icon = if expanded.get() { Icon::ChevronUp } else { Icon::ChevronDown },
on_press = move |_| expanded.toggle(),
) {}
}
}
}
#[derive(Clone)]
struct Task {
id: u64,
title: String,
notes: String,
status: TaskStatus,
done: bool,
}
#[derive(Clone, Copy)]
enum TaskStatus {
Backlog,
Doing,
Done,
}
#[derive(Clone, Copy)]
enum TaskFilter {
All,
OpenOnly,
CompletedOnly,
}
impl TaskFilter {
fn matches(self, status: TaskStatus) -> bool {
match self {
TaskFilter::All => true,
TaskFilter::OpenOnly => !matches!(status, TaskStatus::Done),
TaskFilter::CompletedOnly => matches!(status, TaskStatus::Done),
}
}
}
fn seed_tasks() -> Vec<Task> {
Vec::new()
}