Files
ruin/lib/ruin_app/example/00_bootstrap_and_counter_raw.rs
2026-03-21 19:15:28 -04:00

126 lines
4.3 KiB
Rust

use ruin_app::prelude::*;
// This is the aspirational author-facing source we eventually want to support:
//
// ```ignore
// #[ruin_runtime::async_main]
// async fn main() -> ruin::Result<()> {
// App::new()
// .window(Window::new().title("RUIN Counter").size(960.0, 640.0))
// .app_id("dev.ruin.counter")
// .mount(view! { CounterApp {} })
// #[component]
// fn CounterApp(title: &'static str) -> impl View {
// let count = use_signal(|| 0);
// let doubled = use_memo(move || count.get() * 2);
//
// use_window_title(move || format!("{title} ({})", count.get()));
//
// view! {
// column(gap = 16, padding = 24) {
// text(role = TextRole::Heading(1), size = 32, weight = FontWeight::Semibold) {
// title
// }
//
// row(gap = 8) {
// button(on_press = move |_| count.update(|value| *value -= 1)) { "-1" }
// button(on_press = move |_| count.set(0)) { "Reset" }
// button(on_press = move |_| count.update(|value| *value += 1)) { "+1" }
// }
//
// block(padding = 16, gap = 8, background = surfaces::raised()) {
// text(size = 18) { "count = "; count }
// text(color = colors::muted()) { "double = "; doubled }
// }
// }
// }
// }
// ```
//
// The hand-written code below is the kind of lower-level shape the proc macro should expand to.
//
// Dev notes:
// - `mount` needs to take _any_ component. It should work with an arbitrary expansion of `view!`, not just a
// specially-designated root component.
// - The `RootComponent` trait is awkward and unnecessary.
// - The component render function needs to run as-defined. Hook lifecycles will be managed by runtime context. We don't
// want to get into the business of significantly transforming the execution semantics of the component function.
// - The logic is that the syntax inside of `view!` expands to an instantiation of a struct that implements `Component`,
// by calling the builder associated functions. Each parameter to the render function becomes a method of the builder,
// with `children` as a special finalizing method and reserved property name. This is the only way to stay sane.
#[ruin_runtime::async_main]
async fn main() -> ruin_app::Result<()> {
let title = "RUIN Counter";
App::new()
.window(
Window::new()
.title(title)
.app_id("dev.ruin.counter")
.size(960.0, 640.0),
)
.mount(CounterApp::builder().title(title).children(()))
.run()
.await
}
struct CounterApp {
title: &'static str,
}
const DECREMENT_BUTTON_ID: ElementId = ElementId::new(1);
const RESET_BUTTON_ID: ElementId = ElementId::new(2);
const INCREMENT_BUTTON_ID: ElementId = ElementId::new(3);
#[derive(Default)]
struct __CounterAppBuilder {
title: Option<&'static str>,
}
impl __CounterAppBuilder {
fn title(&mut self, title: &'static str) -> &self {
self.title = Some(title);
self
}
fn children(self, _children: ()) -> Result<CounterApp, anyhow::Error> {
CounterApp::from_builder(self)
}
}
impl Component for CounterApp {
type Builder = __CounterAppBuilder;
fn builder() -> Self::Builder {
__CounterAppBuilder::default()
}
fn from_builder(builder: Self::Builder) -> Result<Self, anyhow::Error> {
Ok(Self {
title: builder
.title
.ok_or_else(|| anyhow::anyhow!("Missing required property `title`"))?,
})
}
fn render(&self) -> View {
__render_CounterApp(self.title)
}
}
#[allow(non_snake_case)]
fn __render_CounterApp(title: &'static str) -> View {
// The state will be managed by the runtime, like how it's done in react. When rerunning the render function,
// use_signal and use_memo will return the same state/memo instances as the previous run, so the state is preserved
// across renders as appropriate. This will require correlating the state/memo instances to call traces using
// thread-local state on the UI thread.
let count = use_signal(|| 0);
let doubled = use_memo(move || count.get() * 2);
use_window_title(move || format!("{title} ({})", count.get()));
// Expansion of view macro to return a concrete view goes here.
// ...
// ...
}