Aspirational examples for app developer contract.
This commit is contained in:
167
aspirational_examples/04_composition_and_context.rs
Normal file
167
aspirational_examples/04_composition_and_context.rs
Normal 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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user