From 82ca487bbf651251b3b7a447a8ff6b0a281beeb8 Mon Sep 17 00:00:00 2001 From: Will Temple Date: Sat, 21 Mar 2026 19:15:28 -0400 Subject: [PATCH] skunky example --- .../example/00_bootstrap_and_counter_raw.rs | 177 ++++++------------ lib/ruin_app/src/lib.rs | 8 +- 2 files changed, 59 insertions(+), 126 deletions(-) diff --git a/lib/ruin_app/example/00_bootstrap_and_counter_raw.rs b/lib/ruin_app/example/00_bootstrap_and_counter_raw.rs index 19212be..a1e5a92 100644 --- a/lib/ruin_app/example/00_bootstrap_and_counter_raw.rs +++ b/lib/ruin_app/example/00_bootstrap_and_counter_raw.rs @@ -1,4 +1,4 @@ -use ruin_app::{Component, prelude::*}; +use ruin_app::prelude::*; // This is the aspirational author-facing source we eventually want to support: // @@ -10,16 +10,16 @@ use ruin_app::{Component, prelude::*}; // .app_id("dev.ruin.counter") // .mount(view! { CounterApp {} }) // #[component] -// fn CounterApp() -> impl View { +// 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!("RUIN Counter ({})", count.get())); +// use_window_title(move || format!("{title} ({})", count.get())); // // view! { // column(gap = 16, padding = 24) { // text(role = TextRole::Heading(1), size = 32, weight = FontWeight::Semibold) { -// "RUIN counter" +// title // } // // row(gap = 8) { @@ -38,35 +38,53 @@ use ruin_app::{Component, 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. #[ruin_runtime::async_main] async fn main() -> ruin_app::Result<()> { + let title = "RUIN Counter"; App::new() .window( Window::new() - .title("RUIN Counter") + .title(title) .app_id("dev.ruin.counter") .size(960.0, 640.0), ) - .mount(CounterApp::builder().children(())) + .mount(CounterApp::builder().title(title).children(())) .run() .await } struct CounterApp { - count: Cell, - doubled: Memo, + 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 __CounterAppBuilder; +#[derive(Default)] +struct __CounterAppBuilder { + title: Option<&'static str>, +} impl __CounterAppBuilder { - fn children(self, _children: ()) -> CounterApp { - CounterApp::new() + fn title(&mut self, title: &'static str) -> &self { + self.title = Some(title); + self + } + + fn children(self, _children: ()) -> Result { + CounterApp::from_builder(self) } } @@ -74,119 +92,34 @@ impl Component for CounterApp { type Builder = __CounterAppBuilder; fn builder() -> Self::Builder { - __CounterAppBuilder + __CounterAppBuilder::default() + } + + fn from_builder(builder: Self::Builder) -> Result { + Ok(Self { + title: builder + .title + .ok_or_else(|| anyhow::anyhow!("Missing required property `title`"))?, + }) + } + + fn render(&self) -> View { + __render_CounterApp(self.title) } } -impl CounterApp { - fn new() -> Self { - let count = cell(0_i32); - let doubled = memo({ - let count = count.clone(); - move || count.get() * 2 - }); - Self { count, doubled } - } +#[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); - fn button(&self, label: &str, id: ElementId, fill: Color) -> Element { - Element::column() - .padding(Edges::symmetric(14.0, 10.0)) - .background(fill) - .corner_radius(10.0) - .cursor(CursorIcon::Pointer) - .focusable(true) - .id(id) - .child( - Element::text(label, body_text()) - .pointer_events(false) - .focusable(false), - ) - } -} - -impl RootComponent for CounterApp { - fn build(&self, cx: &BuildCx) -> View { - let count = self.count.get(); - let doubled = self.doubled.get(); - - Element::column() - .width(cx.viewport.width) - .height(cx.viewport.height) - .padding(Edges::all(24.0)) - .gap(16.0) - .background(Color::rgb(0x0F, 0x16, 0x25)) - .child( - Element::text("RUIN counter", title_text()) - .pointer_events(false) - .focusable(false), - ) - .child( - Element::paragraph( - "This is the raw, hand-written shape a future proc macro should lower the \ - ergonomic counter component into.", - body_text().with_wrap(TextWrap::Word), - ) - .pointer_events(false) - .focusable(false), - ) - .child( - Element::row() - .gap(8.0) - .child(self.button("-1", DECREMENT_BUTTON_ID, Color::rgb(0x2B, 0x3A, 0x67))) - .child(self.button("Reset", RESET_BUTTON_ID, Color::rgb(0x3D, 0x4B, 0x72))) - .child(self.button("+1", INCREMENT_BUTTON_ID, Color::rgb(0x2B, 0x3A, 0x67))), - ) - .child( - Element::column() - .padding(Edges::all(16.0)) - .gap(8.0) - .background(Color::rgb(0x1B, 0x26, 0x3D)) - .corner_radius(12.0) - .child( - Element::text(format!("count = {count}"), body_text()) - .pointer_events(false) - .focusable(false), - ) - .child( - Element::text( - format!("double = {doubled}"), - body_text().with_font_family(TextFontFamily::Monospace), - ) - .pointer_events(false) - .focusable(false), - ), - ) - } - - fn handle_pointer(&self, event: &ruin_ui::RoutedPointerEvent) { - if !matches!( - event.kind, - ruin_ui::RoutedPointerEventKind::Up { - button: PointerButton::Primary - } - ) { - return; - } - - match event.target.element_id { - Some(DECREMENT_BUTTON_ID) => { - self.count.update(|value| *value -= 1); - } - Some(RESET_BUTTON_ID) => { - let _ = self.count.set(0); - } - Some(INCREMENT_BUTTON_ID) => { - self.count.update(|value| *value += 1); - } - _ => {} - } - } -} - -fn title_text() -> TextStyle { - TextStyle::new(32.0, Color::rgb(0xFF, 0xFF, 0xFF)) -} - -fn body_text() -> TextStyle { - TextStyle::new(18.0, Color::rgb(0xDF, 0xE8, 0xF6)).with_line_height(24.0) + use_window_title(move || format!("{title} ({})", count.get())); + + // Expansion of view macro to return a concrete view goes here. + // ... + // ... } diff --git a/lib/ruin_app/src/lib.rs b/lib/ruin_app/src/lib.rs index 8ad783a..8514aa7 100644 --- a/lib/ruin_app/src/lib.rs +++ b/lib/ruin_app/src/lib.rs @@ -89,7 +89,7 @@ impl App { pub fn mount(self, root: R) -> MountedApp { MountedApp { window: self.window, - root, + root: Rc::new(root), } } } @@ -100,9 +100,9 @@ impl Default for App { } } -pub struct MountedApp { +pub struct MountedApp { window: Window, - root: R, + root: Rc, } impl MountedApp { @@ -122,7 +122,7 @@ impl MountedApp { let scene_version = StdCell::new(0_u64); let text_system = Rc::new(RefCell::new(TextSystem::new())); let interaction_tree = Rc::new(RefCell::new(None::)); - let root = Rc::new(root); + let root = Rc::clone(&root); let mut pointer_router = PointerRouter::new(); let mut current_cursor = CursorIcon::Default;