283 lines
9.8 KiB
Rust
283 lines
9.8 KiB
Rust
use ruin_app::prelude::*;
|
|
|
|
#[derive(Clone)]
|
|
struct Project {
|
|
id: u64,
|
|
name: &'static str,
|
|
status: &'static str,
|
|
summary: &'static str,
|
|
}
|
|
|
|
#[ruin_runtime::async_main]
|
|
async fn main() -> ruin_app::Result<()> {
|
|
App::new()
|
|
.window(
|
|
Window::new()
|
|
.title("RUIN Children and Slots")
|
|
.app_id("dev.ruin.children-and-slots")
|
|
.size(1180.0, 820.0),
|
|
)
|
|
.mount(view! {
|
|
ChildrenAndSlotsExample() {}
|
|
})
|
|
.run()
|
|
.await
|
|
}
|
|
|
|
#[component]
|
|
fn ChildrenAndSlotsExample() -> impl IntoView {
|
|
let active_project = use_signal(|| 1_u64);
|
|
let dialog_open = use_signal(|| false);
|
|
let projects = [
|
|
Project {
|
|
id: 1,
|
|
name: "Renderer clip recovery",
|
|
status: "Ready for validation",
|
|
summary: "Nested clip propagation now preserves empty intersections all the way down the tree.",
|
|
},
|
|
Project {
|
|
id: 2,
|
|
name: "Runtime I/O dashboard",
|
|
status: "Shipped",
|
|
summary: "Filesystem snapshots and local TCP-backed endpoint previews now update immediately after async work completes.",
|
|
},
|
|
Project {
|
|
id: 3,
|
|
name: "Children contract support",
|
|
status: "In progress",
|
|
summary: "Custom components can now accept unnamed child content and typed child-like slot props without faking overlay APIs.",
|
|
},
|
|
];
|
|
let active = projects
|
|
.iter()
|
|
.find(|project| project.id == active_project.get())
|
|
.cloned()
|
|
.unwrap_or_else(|| projects[0].clone());
|
|
|
|
view! {
|
|
column(gap = 18.0, padding = 22.0, background = surfaces::canvas()) {
|
|
text(role = TextRole::Heading(1), size = 30.0, weight = FontWeight::Semibold) {
|
|
"Children and slots"
|
|
}
|
|
|
|
text(color = colors::muted(), wrap = TextWrap::Word) {
|
|
"This honest slice implements unnamed child contracts plus typed child-like slot props. It does not pretend modal overlays or keyed list primitives exist yet; the dialog below is rendered inline on purpose."
|
|
}
|
|
|
|
SplitLayout(
|
|
sidebar = vec![
|
|
view! {
|
|
text(role = TextRole::Heading(2), size = 22.0, weight = FontWeight::Semibold) {
|
|
"Views"
|
|
}
|
|
},
|
|
IntoView::into_view(FilterChip::builder().selected(active.id == 1).children("Rendering")),
|
|
IntoView::into_view(FilterChip::builder().selected(active.id == 2).children("Runtime I/O")),
|
|
IntoView::into_view(FilterChip::builder().selected(active.id == 3).children("Framework")),
|
|
view! {
|
|
text(color = colors::muted(), wrap = TextWrap::Word) {
|
|
"The sidebar itself is passed as a typed child-view slot, while the chips use a text-only child contract."
|
|
}
|
|
},
|
|
]
|
|
) {
|
|
column(gap = 16.0) {
|
|
CardFrame(
|
|
title = view! {
|
|
text(role = TextRole::Heading(2), size = 24.0, weight = FontWeight::Semibold) {
|
|
"Workspace focus"
|
|
}
|
|
},
|
|
toolbar = vec![
|
|
view! {
|
|
button(on_press = {
|
|
let dialog_open = dialog_open.clone();
|
|
move |_| {
|
|
let _ = dialog_open.set(true);
|
|
}
|
|
}) { "Open review dialog" }
|
|
},
|
|
view! {
|
|
button(on_press = {
|
|
let active_project = active_project.clone();
|
|
move |_| {
|
|
let next = if active_project.get() == 3 { 1 } else { active_project.get() + 1 };
|
|
let _ = active_project.set(next);
|
|
}
|
|
}) { "Cycle project" }
|
|
},
|
|
]
|
|
) {
|
|
text(color = colors::muted()) {
|
|
("Active project: ", active.name)
|
|
}
|
|
|
|
text(color = colors::muted(), wrap = TextWrap::Word) {
|
|
active.summary
|
|
}
|
|
|
|
column(gap = 10.0) {
|
|
button(on_press = {
|
|
let active_project = active_project.clone();
|
|
move |_| {
|
|
let _ = active_project.set(1);
|
|
}
|
|
}) { "Show rendering work" }
|
|
|
|
button(on_press = {
|
|
let active_project = active_project.clone();
|
|
move |_| {
|
|
let _ = active_project.set(2);
|
|
}
|
|
}) { "Show runtime I/O work" }
|
|
|
|
button(on_press = {
|
|
let active_project = active_project.clone();
|
|
move |_| {
|
|
let _ = active_project.set(3);
|
|
}
|
|
}) { "Show framework work" }
|
|
}
|
|
}
|
|
|
|
InlineDialog(
|
|
open = dialog_open.get(),
|
|
title = view! {
|
|
text(role = TextRole::Heading(2), size = 22.0, weight = FontWeight::Semibold) {
|
|
"Inline review dialog"
|
|
}
|
|
},
|
|
actions = vec![
|
|
view! {
|
|
button(on_press = {
|
|
let dialog_open = dialog_open.clone();
|
|
move |_| {
|
|
let _ = dialog_open.set(false);
|
|
}
|
|
}) { "Dismiss" }
|
|
},
|
|
view! {
|
|
button(on_press = {
|
|
let dialog_open = dialog_open.clone();
|
|
move |_| {
|
|
eprintln!("example07: accepted review for {}", active.name);
|
|
let _ = dialog_open.set(false);
|
|
}
|
|
}) { "Accept" }
|
|
},
|
|
]
|
|
) {
|
|
text(color = colors::muted(), wrap = TextWrap::Word) {
|
|
"This body arrives through the unnamed child contract. The title and action row are separate typed slot props."
|
|
}
|
|
|
|
text(color = colors::muted()) {
|
|
("Selected item status: ", active.status)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[component]
|
|
fn SplitLayout(sidebar: ChildViews, children: ChildViews) -> impl IntoView {
|
|
view! {
|
|
row(gap = 18.0) {
|
|
block(
|
|
width = 280.0,
|
|
gap = 12.0,
|
|
padding = 16.0,
|
|
background = surfaces::raised(),
|
|
border_radius = 16.0,
|
|
) {
|
|
sidebar
|
|
}
|
|
|
|
block(
|
|
flex = 1.0,
|
|
gap = 16.0,
|
|
padding = 16.0,
|
|
background = surfaces::raised(),
|
|
border_radius = 16.0,
|
|
) {
|
|
children
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[component]
|
|
fn FilterChip(selected: bool, children: TextValue) -> impl IntoView {
|
|
let background = if selected {
|
|
surfaces::interactive()
|
|
} else {
|
|
surfaces::interactive_muted()
|
|
};
|
|
let border = if selected {
|
|
colors::text()
|
|
} else {
|
|
colors::muted()
|
|
};
|
|
|
|
view! {
|
|
block(
|
|
padding = Edges::symmetric(14.0, 10.0),
|
|
background = background,
|
|
border = (1.0, border),
|
|
border_radius = 999.0,
|
|
) {
|
|
text(weight = FontWeight::Medium) {
|
|
children
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[component]
|
|
fn CardFrame(title: View, toolbar: ChildViews, children: ChildViews) -> impl IntoView {
|
|
let header = view! {
|
|
row(gap = 12.0) {
|
|
block(flex = 1.0) {
|
|
title
|
|
}
|
|
|
|
row(gap = 10.0) {
|
|
toolbar
|
|
}
|
|
}
|
|
};
|
|
let mut body = vec![header];
|
|
body.extend(children.into_vec());
|
|
block()
|
|
.gap(14.0)
|
|
.padding(18.0)
|
|
.background(surfaces::canvas())
|
|
.border_radius(16.0)
|
|
.children(body)
|
|
}
|
|
|
|
#[component]
|
|
fn InlineDialog(open: bool, title: View, actions: ChildViews, children: ChildViews) -> impl IntoView {
|
|
if open {
|
|
let actions_row = view! {
|
|
row(gap = 10.0) {
|
|
actions
|
|
}
|
|
};
|
|
let mut body = vec![title];
|
|
body.extend(children.into_vec());
|
|
body.push(actions_row);
|
|
block()
|
|
.gap(14.0)
|
|
.padding(18.0)
|
|
.background(surfaces::raised())
|
|
.border((1.0, colors::text()))
|
|
.border_radius(16.0)
|
|
.children(body)
|
|
} else {
|
|
column().children(())
|
|
}
|
|
}
|