unskunk the examples

This commit is contained in:
2026-03-21 20:23:33 -04:00
parent 82ca487bbf
commit 497dff987e
2 changed files with 915 additions and 82 deletions

View File

@@ -6,9 +6,17 @@ use ruin_app::prelude::*;
// #[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 {} })
// .window(
// Window::new()
// .title("RUIN Counter")
// .app_id("dev.ruin.counter")
// .size(960.0, 640.0),
// )
// .mount(view! { CounterApp(title = "RUIN Counter") {} })
// .run()
// .await
// }
//
// #[component]
// fn CounterApp(title: &'static str) -> impl View {
// let count = use_signal(|| 0);
@@ -40,14 +48,16 @@ use ruin_app::prelude::*;
// 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.
// - `mount` should accept any mountable root, including component instances and already-built
// views.
// - The component render function runs as-authored. Hook storage and event dispatch belong to the
// app runtime, not to ad hoc wrapper structs per component.
// - `view!` should lower component and primitive calls to builder invocations, with `children(...)`
// as the finalizing step.
// - The macro cannot ask rustc whether an arbitrary token names a primitive or a component. It has
// to decide syntactically. The workable rule is lowercase/intrinsic names (`row`, `text`,
// `button`) lower to primitive builders, while `UpperCamelCase` paths lower to component
// builders.
#[ruin_runtime::async_main]
async fn main() -> ruin_app::Result<()> {
@@ -68,39 +78,42 @@ 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);
struct __CounterAppTitleMissing;
struct __CounterAppTitlePresent(&'static str);
#[derive(Default)]
struct __CounterAppBuilder {
title: Option<&'static str>,
struct __CounterAppBuilder<TitleState> {
title: TitleState,
}
impl __CounterAppBuilder {
fn title(&mut self, title: &'static str) -> &self {
self.title = Some(title);
self
impl __CounterAppBuilder<__CounterAppTitleMissing> {
fn title(self, title: &'static str) -> __CounterAppBuilder<__CounterAppTitlePresent> {
__CounterAppBuilder {
title: __CounterAppTitlePresent(title),
}
}
}
fn children(self, _children: ()) -> Result<CounterApp, anyhow::Error> {
impl __CounterAppBuilder<__CounterAppTitlePresent> {
fn children(self, _children: ()) -> CounterApp {
CounterApp::from_builder(self)
}
}
impl CounterApp {
fn from_builder(builder: __CounterAppBuilder<__CounterAppTitlePresent>) -> Self {
Self {
title: builder.title.0,
}
}
}
impl Component for CounterApp {
type Builder = __CounterAppBuilder;
type Builder = __CounterAppBuilder<__CounterAppTitleMissing>;
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`"))?,
})
__CounterAppBuilder {
title: __CounterAppTitleMissing,
}
}
fn render(&self) -> View {
@@ -108,18 +121,116 @@ impl Component for CounterApp {
}
}
struct CounterActions {
count: Signal<i32>,
}
struct __CounterActionsCountMissing;
struct __CounterActionsCountPresent(Signal<i32>);
struct __CounterActionsBuilder<CountState> {
count: CountState,
}
impl __CounterActionsBuilder<__CounterActionsCountMissing> {
fn count(self, count: Signal<i32>) -> __CounterActionsBuilder<__CounterActionsCountPresent> {
__CounterActionsBuilder {
count: __CounterActionsCountPresent(count),
}
}
}
impl __CounterActionsBuilder<__CounterActionsCountPresent> {
fn children(self, _children: ()) -> CounterActions {
CounterActions::from_builder(self)
}
}
impl CounterActions {
fn from_builder(builder: __CounterActionsBuilder<__CounterActionsCountPresent>) -> Self {
Self {
count: builder.count.0,
}
}
}
impl Component for CounterActions {
type Builder = __CounterActionsBuilder<__CounterActionsCountMissing>;
fn builder() -> Self::Builder {
__CounterActionsBuilder {
count: __CounterActionsCountMissing,
}
}
fn render(&self) -> View {
__render_CounterActions(self.count.clone())
}
}
#[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);
let count = use_signal(|| 0_i32);
let doubled = use_memo({
let count = count.clone();
move || count.get() * 2
});
use_window_title(move || format!("{title} ({})", count.get()));
use_window_title({
let count = count.clone();
move || format!("{title} ({})", count.get())
});
// Expansion of view macro to return a concrete view goes here.
// ...
// ...
column().gap(16.0).padding(24.0).children((
text()
.role(TextRole::Heading(1))
.size(32.0)
.weight(FontWeight::Semibold)
.children(title),
// `CounterActions(...) {}` in `view!` would lower to a component builder invocation, not a
// primitive builder invocation. The resulting component instance is then rendered as a
// child view by the runtime.
CounterActions::builder().count(count.clone()).children(()),
block()
.padding(16.0)
.gap(8.0)
.background(surfaces::raised())
.border_radius(12.0)
.children((
text().size(18.0).children(("count = ", count.clone())),
text()
.color(colors::muted())
.children(("double = ", doubled.clone())),
)),
))
}
#[allow(non_snake_case)]
fn __render_CounterActions(count: Signal<i32>) -> View {
row().gap(8.0).children((
button()
.on_press({
let count = count.clone();
move |_| {
count.update(|value| *value -= 1);
}
})
.children("-1"),
button()
.on_press({
let count = count.clone();
move |_| {
let _ = count.set(0);
}
})
.children("Reset"),
button()
.on_press({
let count = count.clone();
move |_| {
count.update(|value| *value += 1);
}
})
.children("+1"),
))
}