Port example 04
This commit is contained in:
340
lib/ruin_app/example/04_composition_and_context.rs
Normal file
340
lib/ruin_app/example/04_composition_and_context.rs
Normal file
@@ -0,0 +1,340 @@
|
||||
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::<NotificationsContext>(value = notifications.clone()) {
|
||||
provide::<SessionContext>(value = session.clone()) {
|
||||
provide::<AppThemeContext>(value = ThemePalette::workspace()) {
|
||||
WorkspaceShell() {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn WorkspaceShell() -> impl IntoView {
|
||||
let session = use_context::<SessionContext>();
|
||||
let notifications = use_context::<NotificationsContext>();
|
||||
let theme = use_context::<AppThemeContext>();
|
||||
|
||||
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::<SidebarThemeContext>(value = ThemePalette::sidebar()) {
|
||||
Sidebar() {}
|
||||
}
|
||||
|
||||
Dashboard() {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn Sidebar() -> impl IntoView {
|
||||
let theme = use_context::<SidebarThemeContext>();
|
||||
let session = use_context::<SessionContext>();
|
||||
let notifications = use_context::<NotificationsContext>();
|
||||
|
||||
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::<AppThemeContext>();
|
||||
let session = use_context::<SessionContext>();
|
||||
|
||||
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::<AppThemeContext>(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::<AppThemeContext>();
|
||||
let notifications = use_context::<NotificationsContext>();
|
||||
|
||||
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<String>,
|
||||
}
|
||||
|
||||
impl Notifications {
|
||||
fn new(last_message: Signal<String>) -> Self {
|
||||
Self { last_message }
|
||||
}
|
||||
|
||||
fn info(&self, message: impl Into<String>) {
|
||||
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",
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user