149 lines
4.2 KiB
Rust
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()
|
|
}
|