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 a1e5a92..9c114b3 100644 --- a/lib/ruin_app/example/00_bootstrap_and_counter_raw.rs +++ b/lib/ruin_app/example/00_bootstrap_and_counter_raw.rs @@ -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 { + 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 { +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 { - 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, +} + +struct __CounterActionsCountMissing; +struct __CounterActionsCountPresent(Signal); + +struct __CounterActionsBuilder { + count: CountState, +} + +impl __CounterActionsBuilder<__CounterActionsCountMissing> { + fn count(self, count: Signal) -> __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) -> 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"), + )) } diff --git a/lib/ruin_app/src/lib.rs b/lib/ruin_app/src/lib.rs index 8514aa7..51ed1be 100644 --- a/lib/ruin_app/src/lib.rs +++ b/lib/ruin_app/src/lib.rs @@ -3,39 +3,24 @@ //! This crate is intentionally low-level. It is the substrate that a future proc-macro-driven //! component system can expand to, not the final ergonomic authoring API. +use std::any::Any; use std::cell::{Cell as StdCell, RefCell}; +use std::collections::HashMap; use std::error::Error; use std::iter; use std::rc::Rc; use ruin_reactivity::effect; use ruin_ui::{ - CursorIcon, Element, InteractionTree, LayoutSnapshot, PlatformEvent, PointerEvent, - PointerRouter, RoutedPointerEvent, TextSystem, UiSize, WindowController, WindowSpec, - WindowUpdate, layout_snapshot_with_text_system, + Color, CursorIcon, Edges, Element, ElementId, InteractionTree, LayoutSnapshot, PlatformEvent, + PointerButton, PointerEvent, PointerRouter, RoutedPointerEvent, RoutedPointerEventKind, + TextFontFamily, TextSpan, TextSpanWeight, TextStyle, TextSystem, UiSize, WindowController, + WindowSpec, WindowUpdate, layout_snapshot_with_text_system, }; use ruin_ui_platform_wayland::start_wayland_ui; -pub use ruin_reactivity::{Cell, Memo, cell, memo}; -pub use ruin_ui::{ - Color, Edges, ElementId, Point, PointerButton, PointerEventKind, Rect, RoutedPointerEventKind, - TextAlign, TextFontFamily, TextSpan, TextSpanWeight, TextStyle, TextWrap, -}; - -pub type View = Element; pub type Result = std::result::Result>; -#[derive(Clone, Copy, Debug)] -pub struct BuildCx { - pub viewport: UiSize, -} - -pub trait RootComponent: 'static { - fn build(&self, cx: &BuildCx) -> View; - - fn handle_pointer(&self, _event: &RoutedPointerEvent) {} -} - #[derive(Clone, Debug)] pub struct Window { spec: WindowSpec, @@ -86,10 +71,11 @@ impl App { self } - pub fn mount(self, root: R) -> MountedApp { + pub fn mount(self, root: M) -> MountedApp { MountedApp { window: self.window, root: Rc::new(root), + render_state: Rc::new(RenderState::default()), } } } @@ -100,17 +86,49 @@ impl Default for App { } } -pub struct MountedApp { - window: Window, - root: Rc, +pub trait Mountable: 'static { + fn render(&self) -> View; } -impl MountedApp { +pub trait Component: 'static { + type Builder; + + fn builder() -> Self::Builder; + fn render(&self) -> View; +} + +impl Mountable for T { + fn render(&self) -> View { + Component::render(self) + } +} + +impl Mountable for View { + fn render(&self) -> View { + self.clone() + } +} + +impl Mountable for Element { + fn render(&self) -> View { + View::from_element(self.clone()) + } +} + +pub struct MountedApp { + window: Window, + root: Rc, + render_state: Rc, +} + +impl MountedApp { pub async fn run(self) -> Result<()> { let MountedApp { window: app_window, root, + render_state, } = self; + let mut ui = start_wayland_ui(); let window = ui.create_window(app_window.spec.clone())?; let initial_viewport = app_window @@ -122,7 +140,8 @@ 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::clone(&root); + let bindings = Rc::new(RefCell::new(EventBindings::default())); + let current_title = Rc::new(RefCell::new(None::)); let mut pointer_router = PointerRouter::new(); let mut current_cursor = CursorIcon::Default; @@ -131,23 +150,38 @@ impl MountedApp { let viewport = viewport.clone(); let text_system = Rc::clone(&text_system); let interaction_tree = Rc::clone(&interaction_tree); + let bindings = Rc::clone(&bindings); + let current_title = Rc::clone(¤t_title); let root = Rc::clone(&root); + let render_state = Rc::clone(&render_state); move || { let viewport = viewport.get(); let version = scene_version.get().wrapping_add(1); scene_version.set(version); - let tree = root.build(&BuildCx { viewport }); + let render_output = render_with_context(Rc::clone(&render_state), || root.render()); + + if render_output.side_effects.window_title != *current_title.borrow() { + if let Some(title) = &render_output.side_effects.window_title { + window + .update(WindowUpdate::new().title(title.clone())) + .expect("window should remain alive while the app is running"); + } + *current_title.borrow_mut() = render_output.side_effects.window_title.clone(); + } + let LayoutSnapshot { scene, interaction_tree: next_interaction_tree, } = layout_snapshot_with_text_system( version, viewport, - &tree, + render_output.view.element(), &mut text_system.borrow_mut(), ); + *interaction_tree.borrow_mut() = Some(next_interaction_tree); + *bindings.borrow_mut() = render_output.view.bindings; window .replace_scene(scene) .expect("window should remain alive while the app is running"); @@ -169,9 +203,9 @@ impl MountedApp { } PlatformEvent::Pointer { window_id, event } if window_id == window.id() => { Self::handle_pointer_event( - &root, &window, &interaction_tree, + &bindings, &mut pointer_router, &mut current_cursor, event, @@ -194,9 +228,9 @@ impl MountedApp { } fn handle_pointer_event( - root: &Rc, window: &WindowController, interaction_tree: &RefCell>, + bindings: &RefCell, pointer_router: &mut PointerRouter, current_cursor: &mut CursorIcon, event: PointerEvent, @@ -210,7 +244,7 @@ impl MountedApp { }; for routed_event in &routed { - root.handle_pointer(routed_event); + bindings.borrow().dispatch_press(routed_event); } let next_cursor = pointer_router @@ -227,13 +261,701 @@ impl MountedApp { } } -pub mod prelude { - pub use crate::{ - App, BuildCx, Cell, Color, Edges, ElementId, Memo, Point, Result, RootComponent, View, - Window, cell, memo, - }; - pub use ruin_ui::{ - CursorIcon, Element, PointerButton, PointerEventKind, RoutedPointerEvent, - RoutedPointerEventKind, TextFontFamily, TextStyle, TextWrap, UiSize, +#[derive(Clone, Default)] +pub struct View { + element: Element, + bindings: EventBindings, +} + +impl View { + pub fn from_element(element: Element) -> Self { + Self { + element, + bindings: EventBindings::default(), + } + } + + pub fn element(&self) -> &Element { + &self.element + } + + fn with_press_handler(mut self, element_id: ElementId, handler: PressHandler) -> Self { + self.bindings.on_press.insert(element_id, handler); + self + } + + fn from_container(element: Element, children: Vec) -> Self { + let mut composed = Self::from_element(element); + let mut element = composed.element; + + for child in children { + element = element.child(child.element); + composed.bindings.extend(child.bindings); + } + + composed.element = element; + composed + } +} + +impl From for View { + fn from(element: Element) -> Self { + Self::from_element(element) + } +} + +pub trait IntoView { + fn into_view(self) -> View; +} + +impl IntoView for View { + fn into_view(self) -> View { + self + } +} + +impl IntoView for Element { + fn into_view(self) -> View { + View::from_element(self) + } +} + +impl IntoView for T { + fn into_view(self) -> View { + self.render() + } +} + +pub trait Children { + fn into_views(self) -> Vec; +} + +impl Children for () { + fn into_views(self) -> Vec { + Vec::new() + } +} + +impl Children for T { + fn into_views(self) -> Vec { + vec![self.into_view()] + } +} + +macro_rules! impl_children_tuple { + ($($name:ident),+ $(,)?) => { + #[allow(non_camel_case_types)] + impl<$($name: IntoView),+> Children for ($($name,)+) { + fn into_views(self) -> Vec { + let ($($name,)+) = self; + vec![$($name.into_view(),)+] + } + } }; } + +impl_children_tuple!(a, b); +impl_children_tuple!(a, b, c); +impl_children_tuple!(a, b, c, d); +impl_children_tuple!(a, b, c, d, e); +impl_children_tuple!(a, b, c, d, e, f); +impl_children_tuple!(a, b, c, d, e, f, g); +impl_children_tuple!(a, b, c, d, e, f, g, h); + +pub trait TextChildren { + fn into_text(self) -> String; +} + +impl TextChildren for &'static str { + fn into_text(self) -> String { + self.to_string() + } +} + +impl TextChildren for String { + fn into_text(self) -> String { + self + } +} + +impl TextChildren for i32 { + fn into_text(self) -> String { + self.to_string() + } +} + +impl TextChildren for i64 { + fn into_text(self) -> String { + self.to_string() + } +} + +impl TextChildren for usize { + fn into_text(self) -> String { + self.to_string() + } +} + +impl TextChildren for u64 { + fn into_text(self) -> String { + self.to_string() + } +} + +impl TextChildren for f32 { + fn into_text(self) -> String { + self.to_string() + } +} + +impl TextChildren for Signal { + fn into_text(self) -> String { + self.with(|value| value.to_string()) + } +} + +impl TextChildren for Memo { + fn into_text(self) -> String { + self.with(|value| value.to_string()) + } +} + +macro_rules! impl_text_children_tuple { + ($($name:ident),+ $(,)?) => { + #[allow(non_camel_case_types)] + impl<$($name: TextChildren),+> TextChildren for ($($name,)+) { + fn into_text(self) -> String { + let ($($name,)+) = self; + let mut text = String::new(); + $(text.push_str(&$name.into_text());)+ + text + } + } + }; +} + +impl_text_children_tuple!(a, b); +impl_text_children_tuple!(a, b, c); +impl_text_children_tuple!(a, b, c, d); +impl_text_children_tuple!(a, b, c, d, e); +impl_text_children_tuple!(a, b, c, d, e, f); + +pub trait IntoEdges { + fn into_edges(self) -> Edges; +} + +impl IntoEdges for Edges { + fn into_edges(self) -> Edges { + self + } +} + +impl IntoEdges for f32 { + fn into_edges(self) -> Edges { + Edges::all(self) + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum TextRole { + Body, + Heading(u8), +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum FontWeight { + Normal, + Medium, + Semibold, + Bold, +} + +impl FontWeight { + const fn to_text_span_weight(self) -> TextSpanWeight { + match self { + Self::Normal => TextSpanWeight::Normal, + Self::Medium => TextSpanWeight::Medium, + Self::Semibold => TextSpanWeight::Semibold, + Self::Bold => TextSpanWeight::Bold, + } + } +} + +pub mod colors { + use ruin_ui::Color; + + pub const fn text() -> Color { + Color::rgb(0xFF, 0xFF, 0xFF) + } + + pub const fn muted() -> Color { + Color::rgb(0xB6, 0xC2, 0xD9) + } +} + +pub mod surfaces { + use ruin_ui::Color; + + pub const fn canvas() -> Color { + Color::rgb(0x0F, 0x16, 0x25) + } + + pub const fn raised() -> Color { + Color::rgb(0x1B, 0x26, 0x3D) + } + + pub const fn interactive() -> Color { + Color::rgb(0x2B, 0x3A, 0x67) + } + + pub const fn interactive_muted() -> Color { + Color::rgb(0x3D, 0x4B, 0x72) + } +} + +pub fn column() -> ContainerBuilder { + ContainerBuilder { + element: Element::column().background(surfaces::canvas()), + } +} + +pub fn row() -> ContainerBuilder { + ContainerBuilder { + element: Element::row(), + } +} + +pub fn block() -> ContainerBuilder { + ContainerBuilder { + element: Element::column(), + } +} + +pub fn text() -> TextBuilder { + TextBuilder::default() +} + +pub fn button() -> ButtonBuilder { + ButtonBuilder::default() +} + +pub struct ContainerBuilder { + element: Element, +} + +impl ContainerBuilder { + pub fn gap(mut self, gap: f32) -> Self { + self.element = self.element.gap(gap); + self + } + + pub fn padding(mut self, padding: impl IntoEdges) -> Self { + self.element = self.element.padding(padding.into_edges()); + self + } + + pub fn background(mut self, color: Color) -> Self { + self.element = self.element.background(color); + self + } + + pub fn border_radius(mut self, radius: f32) -> Self { + self.element = self.element.corner_radius(radius); + self + } + + pub fn width(mut self, width: f32) -> Self { + self.element = self.element.width(width); + self + } + + pub fn height(mut self, height: f32) -> Self { + self.element = self.element.height(height); + self + } + + pub fn flex(mut self, flex: f32) -> Self { + self.element = self.element.flex(flex); + self + } + + pub fn children(self, children: impl Children) -> View { + View::from_container(self.element, children.into_views()) + } +} + +#[derive(Default)] +pub struct TextBuilder { + role: Option, + size: Option, + weight: Option, + color: Option, + font_family: Option, +} + +impl TextBuilder { + pub fn role(mut self, role: TextRole) -> Self { + self.role = Some(role); + self + } + + pub fn size(mut self, size: f32) -> Self { + self.size = Some(size); + self + } + + pub fn weight(mut self, weight: FontWeight) -> Self { + self.weight = Some(weight); + self + } + + pub fn color(mut self, color: Color) -> Self { + self.color = Some(color); + self + } + + pub fn font_family(mut self, family: TextFontFamily) -> Self { + self.font_family = Some(family); + self + } + + pub fn children(self, children: impl TextChildren) -> View { + let size = self + .size + .unwrap_or_else(|| match self.role.unwrap_or(TextRole::Body) { + TextRole::Body => 18.0, + TextRole::Heading(1) => 32.0, + TextRole::Heading(2) => 28.0, + TextRole::Heading(_) => 24.0, + }); + let color = self.color.unwrap_or(colors::text()); + let weight = self + .weight + .unwrap_or_else(|| match self.role.unwrap_or(TextRole::Body) { + TextRole::Body => FontWeight::Normal, + TextRole::Heading(_) => FontWeight::Semibold, + }); + + let mut style = TextStyle::new(size, color).with_line_height(size * 1.2); + if let Some(font_family) = self.font_family { + style = style.with_font_family(font_family); + } + + let span = TextSpan::new(children.into_text()) + .color(color) + .weight(weight.to_text_span_weight()); + View::from_element(Element::spans([span], style)) + } +} + +#[derive(Default)] +pub struct ButtonBuilder { + on_press: Option, +} + +impl ButtonBuilder { + pub fn on_press(mut self, handler: impl Fn(&RoutedPointerEvent) + 'static) -> Self { + self.on_press = Some(Rc::new(handler)); + self + } + + pub fn children(self, children: impl TextChildren) -> View { + let id = allocate_element_id(); + let label = children.into_text(); + let view = View::from_element( + Element::column() + .id(id) + .padding(Edges::symmetric(14.0, 10.0)) + .background(surfaces::interactive()) + .corner_radius(10.0) + .cursor(CursorIcon::Pointer) + .focusable(true) + .child( + Element::spans( + [TextSpan::new(label).weight(TextSpanWeight::Medium)], + TextStyle::new(18.0, colors::text()).with_line_height(21.6), + ) + .pointer_events(false) + .focusable(false), + ), + ); + + match self.on_press { + Some(handler) => view.with_press_handler(id, handler), + None => view, + } + } +} + +pub struct Signal { + inner: Rc>, +} + +impl Clone for Signal { + fn clone(&self) -> Self { + Self { + inner: Rc::clone(&self.inner), + } + } +} + +impl Signal { + fn new(initial: T) -> Self { + Self { + inner: Rc::new(SignalInner { + cell: ruin_reactivity::cell(initial), + }), + } + } + + pub fn with(&self, f: impl FnOnce(&T) -> R) -> R { + self.inner.cell.with(f) + } + + pub fn replace(&self, value: T) -> T { + self.inner.cell.replace(value) + } + + pub fn update(&self, f: impl FnOnce(&mut T) -> R) -> R { + self.inner.cell.update(f) + } +} + +impl Signal { + pub fn get(&self) -> T { + self.inner.cell.get() + } +} + +impl Signal { + pub fn set(&self, value: T) -> Option { + self.inner.cell.set(value) + } +} + +pub struct Memo { + compute: Rc T>, +} + +impl Clone for Memo { + fn clone(&self) -> Self { + Self { + compute: Rc::clone(&self.compute), + } + } +} + +impl Memo { + pub fn with(&self, f: impl FnOnce(&T) -> R) -> R { + let value = (self.compute)(); + f(&value) + } +} + +impl Memo { + pub fn get(&self) -> T { + (self.compute)() + } +} + +pub fn use_signal(initial: impl FnOnce() -> T) -> Signal { + with_hook_slot(|| Signal::new(initial()), |signal| signal.clone()) +} + +pub fn use_memo(compute: impl Fn() -> T + 'static) -> Memo { + let compute: Rc T>>> = + Rc::new(RefCell::new(Box::new(compute) as Box T>)); + with_hook_slot( + { + let compute = Rc::clone(&compute); + move || MemoSlot::new(compute) + }, + |slot: &mut MemoSlot| { + slot.replace_compute(Rc::clone(&compute)); + slot.handle.clone() + }, + ) +} + +pub fn use_window_title(compute: impl FnOnce() -> String) { + with_render_context_state(|context| { + context.side_effects.borrow_mut().window_title = Some(compute()); + }); +} + +#[derive(Default)] +struct RenderState { + hooks: RefCell>>, + element_ids: RefCell>, + next_element_id: StdCell, +} + +#[derive(Clone, Default, PartialEq)] +struct RenderSideEffects { + window_title: Option, +} + +#[derive(Clone)] +struct RenderContext { + state: Rc, + hook_index: Rc>, + element_index: Rc>, + side_effects: Rc>, +} + +struct RenderOutput { + view: View, + side_effects: RenderSideEffects, +} + +thread_local! { + static CURRENT_RENDER_CONTEXT: RefCell> = const { RefCell::new(None) }; +} + +fn render_with_context(state: Rc, render: impl FnOnce() -> View) -> RenderOutput { + let context = RenderContext { + state, + hook_index: Rc::new(StdCell::new(0)), + element_index: Rc::new(StdCell::new(0)), + side_effects: Rc::new(RefCell::new(RenderSideEffects::default())), + }; + + CURRENT_RENDER_CONTEXT.with(|slot| { + let previous = slot.replace(Some(context.clone())); + + struct Guard<'a> { + slot: &'a RefCell>, + previous: Option, + } + + impl Drop for Guard<'_> { + fn drop(&mut self) { + let _ = self.slot.replace(self.previous.take()); + } + } + + let _guard = Guard { slot, previous }; + let view = render(); + let side_effects = context.side_effects.borrow().clone(); + RenderOutput { view, side_effects } + }) +} + +fn with_render_context_state(f: impl FnOnce(&RenderContext) -> R) -> R { + CURRENT_RENDER_CONTEXT.with(|slot| { + let context = slot.borrow(); + let context = context + .as_ref() + .expect("ruin_app hooks can only run while rendering a mounted component"); + f(context) + }) +} + +fn with_hook_slot(init: impl FnOnce() -> T, f: impl FnOnce(&mut T) -> R) -> R { + with_render_context_state(|context| { + let index = context.hook_index.get(); + context.hook_index.set(index + 1); + + let mut hooks = context.state.hooks.borrow_mut(); + if hooks.len() == index { + hooks.push(Box::new(init())); + } + + let slot = hooks[index] + .downcast_mut::() + .expect("ruin_app hook call order changed between renders"); + f(slot) + }) +} + +fn allocate_element_id() -> ElementId { + with_render_context_state(|context| { + let index = context.element_index.get(); + context.element_index.set(index + 1); + + let mut element_ids = context.state.element_ids.borrow_mut(); + if element_ids.len() == index { + let next = context.state.next_element_id.get().wrapping_add(1); + context.state.next_element_id.set(next); + element_ids.push(ElementId::new(next)); + } + element_ids[index] + }) +} + +struct MemoSlot { + compute: Rc T>>>, + handle: Memo, +} + +impl MemoSlot { + fn new(compute: Rc T>>>) -> Self { + let handle = Memo { + compute: Rc::new({ + let compute = Rc::clone(&compute); + move || { + let compute = compute.borrow(); + (compute.as_ref())() + } + }), + }; + Self { compute, handle } + } + + fn replace_compute(&mut self, compute: Rc T>>>) { + self.compute = Rc::clone(&compute); + self.handle.compute = Rc::new({ + let compute = Rc::clone(&compute); + move || { + let compute = compute.borrow(); + (compute.as_ref())() + } + }); + } +} + +type PressHandler = Rc; + +#[derive(Clone, Default)] +struct EventBindings { + on_press: HashMap, +} + +impl EventBindings { + fn extend(&mut self, other: EventBindings) { + self.on_press.extend(other.on_press); + } + + fn dispatch_press(&self, event: &RoutedPointerEvent) { + let Some(element_id) = event.target.element_id else { + return; + }; + if !matches!( + event.kind, + RoutedPointerEventKind::Up { + button: PointerButton::Primary + } + ) { + return; + } + + if let Some(handler) = self.on_press.get(&element_id) { + handler(event); + } + } +} + +pub mod prelude { + pub use crate::{ + App, ButtonBuilder, Children, Component, ContainerBuilder, FontWeight, IntoEdges, IntoView, + Memo, Mountable, Result, Signal, TextBuilder, TextChildren, TextRole, View, Window, block, + button, colors, column, row, surfaces, text, use_memo, use_signal, use_window_title, + }; + pub use ruin_ui::{ + Color, CursorIcon, Edges, Element, ElementId, PointerButton, PointerEventKind, + RoutedPointerEvent, RoutedPointerEventKind, TextFontFamily, TextStyle, TextWrap, UiSize, + }; +} +struct SignalInner { + cell: ruin_reactivity::Cell, +}