Aspirational examples for app developer contract.

This commit is contained in:
2026-03-21 17:05:25 -04:00
parent d4ff472a14
commit f59d519448
8 changed files with 866 additions and 0 deletions

View File

@@ -0,0 +1,167 @@
//! Aspirational RUIN app API for composition, typed context providers, and app-wide services.
//!
//! 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 Workspace").size(1400.0, 900.0))
.mount::<WorkspaceRoot>()
.run()
.await
}
#[component]
fn WorkspaceRoot() -> impl View {
let route = use_router(Route::Home);
let session = use_resource(|| async { Session::restore().await });
let notifications = use_service::<Notifications>();
view! {
provide::<NotificationsContext>(notifications.clone()) {
provide::<ThemeContext>(Theme::dark()) {
match session.read() {
Pending => view! { ProgressSpinner(label = "Restoring session...") {} },
Ready(session) => view! {
provide::<SessionContext>(session?) {
WorkspaceShell(route = route) {}
}
},
}
}
}
}
}
#[component]
fn WorkspaceShell(route: Signal<Route>) -> impl View {
let session = use_context::<SessionContext>();
let notifications = use_context::<NotificationsContext>();
use_effect(move || {
if !session.is_authenticated() {
notifications.info("Signed in as guest");
}
});
view! {
row(fill = true) {
block(
role = LandmarkRole::Navigation,
width = 280,
gap = 12,
padding = 20,
) {
BrandMark() {}
NavLink(route = Route::Home, active = route.is(Route::Home)) { "Home" }
NavLink(route = Route::Projects, active = route.is(Route::Projects)) { "Projects" }
NavLink(route = Route::Settings, active = route.is(Route::Settings)) { "Settings" }
}
block(fill = true, padding = 20) {
match route.get() {
Route::Home => view! { HomeScreen() {} },
Route::Projects => view! { ProjectsScreen() {} },
Route::Settings => view! { SettingsScreen() {} },
}
}
}
}
}
#[component]
fn HomeScreen() -> impl View {
let theme = use_context::<ThemeContext>();
view! {
column(fill = true, gap = 16) {
text(role = TextRole::Heading(1), size = 30, weight = FontWeight::Semibold) {
"Workspace"
}
text(color = theme.muted_text()) {
"Context should be keyed by provider identity, not only by value type. That allows \
multiple providers for the same `T` to coexist in the tree while descendants borrow \
the correct value by asking for a specific provider marker."
}
}
}
}
#[component]
fn ProjectsScreen() -> impl View {
view! {
EmptyState(title = "Projects go here") {
"This screen exists only to show route switching and app-level composition."
}
}
}
#[component]
fn SettingsScreen() -> impl View {
view! {
column(gap = 12) {
text(role = TextRole::Heading(2), size = 24, weight = FontWeight::Semibold) {
"Settings"
}
toggle(label = "Use reduced motion", value = Preferences::reduced_motion()) {}
toggle(label = "Use system theme", value = Preferences::system_theme()) {}
}
}
}
#[context_provider(Theme)]
struct ThemeContext;
#[context_provider(Session)]
struct SessionContext;
#[context_provider(Notifications)]
struct NotificationsContext;
struct Session;
struct Notifications;
struct Theme;
struct Preferences;
#[derive(Clone, Copy)]
enum Route {
Home,
Projects,
Settings,
}
impl Session {
async fn restore() -> ruin::Result<Self> {
Ok(Self)
}
fn is_authenticated(&self) -> bool {
false
}
}
impl Notifications {
fn info(&self, _message: &str) {}
}
impl Theme {
fn dark() -> Self {
Self
}
fn muted_text(&self) -> Color {
Color::rgb(0x94, 0xA3, 0xB8)
}
}
impl Preferences {
fn reduced_motion() -> bool {
false
}
fn system_theme() -> bool {
true
}
}