use ruin_app::prelude::*; #[ruin_runtime::async_main] async fn main() -> ruin_app::Result<()> { App::new() .window( Window::new() .title("RUIN Workspace") .app_id("dev.ruin.composition-context") .size(1280.0, 840.0), ) .mount(view! { WorkspaceRoot() {} }) .run() .await } #[component] fn WorkspaceRoot() -> impl IntoView { let notifications = Notifications::new(use_signal(|| { "No workspace actions yet. Click a navigation button to emit a context-driven update." .to_string() })); let session = SessionInfo::guest(); view! { provide::(value = notifications.clone()) { provide::(value = session.clone()) { provide::(value = ThemePalette::workspace()) { WorkspaceShell() {} } } } } } #[component] fn WorkspaceShell() -> impl IntoView { let session = use_context::(); let notifications = use_context::(); let theme = use_context::(); use_window_title({ let session = session.clone(); move || format!("RUIN Workspace - {}", session.workspace) }); view! { column(gap = 16.0, padding = 20.0, background = theme.canvas) { block( padding = 14.0, gap = 8.0, background = theme.panel, border_radius = 12.0, border = (1.0, theme.accent), ) { text(role = TextRole::Heading(1), size = 30.0, weight = FontWeight::Semibold) { "Composition and typed context" } text(color = theme.muted, wrap = TextWrap::Word) { "This example implements real marker-keyed context providers. `AppThemeContext` \ and `SidebarThemeContext` both carry the same `ThemePalette` value type, but \ descendants resolve them independently by marker type. Nested providers of the \ same marker also shadow outer values as you would expect." } text(color = theme.muted) { "Last notification: "; notifications.last_message.clone() } } row(gap = 16.0) { provide::(value = ThemePalette::sidebar()) { Sidebar() {} } Dashboard() {} } } } } #[component] fn Sidebar() -> impl IntoView { let theme = use_context::(); let session = use_context::(); let notifications = use_context::(); view! { block( width = 300.0, padding = 16.0, gap = 12.0, background = theme.panel, border_radius = 14.0, border = (1.0, theme.accent), ) { text(size = 24.0, weight = FontWeight::Semibold, color = theme.text) { "Workspace nav" } text(color = theme.muted, wrap = TextWrap::Word) { "Signed in as "; session.user.clone(); " in "; session.workspace.clone(); } button(on_press = { let notifications = notifications.clone(); move |_| notifications.info("Opened the Home workspace panel") }) { "Home" } button(on_press = { let notifications = notifications.clone(); move |_| notifications.info("Opened the Projects workspace panel") }) { "Projects" } button(on_press = { let notifications = notifications.clone(); move |_| notifications.info("Opened the Settings workspace panel") }) { "Settings" } block( padding = 12.0, gap = 8.0, background = theme.canvas, border_radius = 10.0, ) { text(weight = FontWeight::Semibold, color = theme.accent) { "Sidebar theme" } text(color = theme.muted, wrap = TextWrap::Word) { "This panel resolves `SidebarThemeContext`, even though the rest of the \ workspace uses `AppThemeContext` with the same `ThemePalette` value type." } } } } } #[component] fn Dashboard() -> impl IntoView { let theme = use_context::(); let session = use_context::(); view! { column(flex = 1.0, gap = 16.0) { block( padding = 16.0, gap = 10.0, background = theme.panel, border_radius = 14.0, border = (1.0, theme.accent), ) { text(size = 24.0, weight = FontWeight::Semibold, color = theme.text) { "Workspace dashboard" } text(color = theme.muted, wrap = TextWrap::Word) { "The main content resolves `AppThemeContext` and `SessionContext` from the \ outer providers. These values are regular cloned Rust values; the context \ system is responsible only for scoping and lookup by marker type." } text(color = theme.muted) { "Active workspace: "; session.workspace.clone() } } column(gap = 16.0) { ThemeCard( title = "App theme".to_string(), subtitle = "Resolved from the outer `AppThemeContext` provider.".to_string(), theme = theme.clone(), ) {} provide::(value = ThemePalette::inspector()) { InspectorPanel() {} } } } } } #[component] fn ThemeCard(title: String, subtitle: String, theme: ThemePalette) -> impl IntoView { view! { block( padding = 16.0, gap = 10.0, background = theme.panel, border_radius = 14.0, border = (1.0, theme.accent), ) { text(size = 20.0, weight = FontWeight::Semibold, color = theme.text) { title } text(color = theme.muted, wrap = TextWrap::Word) { subtitle } block( height = 56.0, background = theme.canvas, border_radius = 10.0, border = (2.0, theme.accent), ) {} } } } #[component] fn InspectorPanel() -> impl IntoView { let theme = use_context::(); let notifications = use_context::(); view! { block( padding = 16.0, gap = 12.0, background = theme.canvas, border_radius = 14.0, border = (1.0, theme.accent), ) { text(size = 18.0, weight = FontWeight::Semibold, color = theme.accent) { "Inspector panel" } ThemeCard( title = "Nested override".to_string(), subtitle = "This panel shadows the outer `AppThemeContext` with a nearer provider of the same marker type.".to_string(), theme = theme.clone(), ) {} button(on_press = { let notifications = notifications.clone(); move |_| notifications.info("Inspector theme override is active") }) { "Emit inspector notification" } } } } #[context_provider(ThemePalette)] struct AppThemeContext; #[context_provider(ThemePalette)] struct SidebarThemeContext; #[context_provider(SessionInfo)] struct SessionContext; #[context_provider(Notifications)] struct NotificationsContext; #[derive(Clone)] struct ThemePalette { canvas: Color, panel: Color, accent: Color, text: Color, muted: Color, } impl ThemePalette { fn workspace() -> Self { Self { canvas: Color::rgb(0x0E, 0x14, 0x20), panel: Color::rgb(0x16, 0x1F, 0x30), accent: Color::rgb(0x71, 0xA7, 0xF7), text: Color::rgb(0xF5, 0xF7, 0xFB), muted: Color::rgb(0x9A, 0xA9, 0xC2), } } fn sidebar() -> Self { Self { canvas: Color::rgb(0x0F, 0x17, 0x24), panel: Color::rgb(0x1A, 0x24, 0x36), accent: Color::rgb(0x8B, 0xC3, 0x7E), text: Color::rgb(0xF3, 0xF7, 0xEF), muted: Color::rgb(0xA7, 0xB5, 0xA4), } } fn inspector() -> Self { Self { canvas: Color::rgb(0x22, 0x17, 0x0F), panel: Color::rgb(0x35, 0x24, 0x16), accent: Color::rgb(0xF4, 0xB7, 0x56), text: Color::rgb(0xFC, 0xF6, 0xEB), muted: Color::rgb(0xCF, 0xB8, 0x98), } } } #[derive(Clone)] struct SessionInfo { user: String, workspace: String, } impl SessionInfo { fn guest() -> Self { Self { user: "guest".to_string(), workspace: "ruin".to_string(), } } } #[derive(Clone)] struct Notifications { last_message: Signal, } impl Notifications { fn new(last_message: Signal) -> Self { Self { last_message } } fn info(&self, message: impl Into) { self.last_message.replace(message.into()); } } #[cfg(test)] mod tests { use super::*; #[test] fn inspector_panel_text_reaches_the_scene() { let root = WorkspaceRoot::builder().children(()); let view = ruin_app::__render_mountable_for_test(&root); let snapshot = ruin_ui::layout_snapshot(1, UiSize::new(1280.0, 840.0), view.element()); assert!( snapshot.scene.items.iter().any(|item| matches!( item, ruin_ui::DisplayItem::Text(text) if text.text.contains("Nested override") || text.text.contains("Inspector panel") )), "expected InspectorPanel text to appear in the rendered scene", ); } }