unskunk the examples
This commit is contained in:
@@ -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"),
|
||||
))
|
||||
}
|
||||
|
||||
@@ -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<T> = std::result::Result<T, Box<dyn Error>>;
|
||||
|
||||
#[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<R: RootComponent>(self, root: R) -> MountedApp<R> {
|
||||
pub fn mount<M: Mountable>(self, root: M) -> MountedApp<M> {
|
||||
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<R: RootComponent> {
|
||||
window: Window,
|
||||
root: Rc<R>,
|
||||
pub trait Mountable: 'static {
|
||||
fn render(&self) -> View;
|
||||
}
|
||||
|
||||
impl<R: RootComponent> MountedApp<R> {
|
||||
pub trait Component: 'static {
|
||||
type Builder;
|
||||
|
||||
fn builder() -> Self::Builder;
|
||||
fn render(&self) -> View;
|
||||
}
|
||||
|
||||
impl<T: Component> 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<M: Mountable> {
|
||||
window: Window,
|
||||
root: Rc<M>,
|
||||
render_state: Rc<RenderState>,
|
||||
}
|
||||
|
||||
impl<M: Mountable> MountedApp<M> {
|
||||
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<R: RootComponent> MountedApp<R> {
|
||||
let scene_version = StdCell::new(0_u64);
|
||||
let text_system = Rc::new(RefCell::new(TextSystem::new()));
|
||||
let interaction_tree = Rc::new(RefCell::new(None::<InteractionTree>));
|
||||
let root = Rc::clone(&root);
|
||||
let bindings = Rc::new(RefCell::new(EventBindings::default()));
|
||||
let current_title = Rc::new(RefCell::new(None::<String>));
|
||||
let mut pointer_router = PointerRouter::new();
|
||||
let mut current_cursor = CursorIcon::Default;
|
||||
|
||||
@@ -131,23 +150,38 @@ impl<R: RootComponent> MountedApp<R> {
|
||||
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<R: RootComponent> MountedApp<R> {
|
||||
}
|
||||
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<R: RootComponent> MountedApp<R> {
|
||||
}
|
||||
|
||||
fn handle_pointer_event(
|
||||
root: &Rc<R>,
|
||||
window: &WindowController,
|
||||
interaction_tree: &RefCell<Option<InteractionTree>>,
|
||||
bindings: &RefCell<EventBindings>,
|
||||
pointer_router: &mut PointerRouter,
|
||||
current_cursor: &mut CursorIcon,
|
||||
event: PointerEvent,
|
||||
@@ -210,7 +244,7 @@ impl<R: RootComponent> MountedApp<R> {
|
||||
};
|
||||
|
||||
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<R: RootComponent> MountedApp<R> {
|
||||
}
|
||||
}
|
||||
|
||||
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<View>) -> 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<Element> 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<T: Component> IntoView for T {
|
||||
fn into_view(self) -> View {
|
||||
self.render()
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Children {
|
||||
fn into_views(self) -> Vec<View>;
|
||||
}
|
||||
|
||||
impl Children for () {
|
||||
fn into_views(self) -> Vec<View> {
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: IntoView> Children for T {
|
||||
fn into_views(self) -> Vec<View> {
|
||||
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<View> {
|
||||
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<T: std::fmt::Display + 'static> TextChildren for Signal<T> {
|
||||
fn into_text(self) -> String {
|
||||
self.with(|value| value.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: std::fmt::Display + 'static> TextChildren for Memo<T> {
|
||||
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<TextRole>,
|
||||
size: Option<f32>,
|
||||
weight: Option<FontWeight>,
|
||||
color: Option<Color>,
|
||||
font_family: Option<TextFontFamily>,
|
||||
}
|
||||
|
||||
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<PressHandler>,
|
||||
}
|
||||
|
||||
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<T> {
|
||||
inner: Rc<SignalInner<T>>,
|
||||
}
|
||||
|
||||
impl<T> Clone for Signal<T> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
inner: Rc::clone(&self.inner),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: 'static> Signal<T> {
|
||||
fn new(initial: T) -> Self {
|
||||
Self {
|
||||
inner: Rc::new(SignalInner {
|
||||
cell: ruin_reactivity::cell(initial),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with<R>(&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<R>(&self, f: impl FnOnce(&mut T) -> R) -> R {
|
||||
self.inner.cell.update(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Clone + 'static> Signal<T> {
|
||||
pub fn get(&self) -> T {
|
||||
self.inner.cell.get()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: PartialEq + 'static> Signal<T> {
|
||||
pub fn set(&self, value: T) -> Option<T> {
|
||||
self.inner.cell.set(value)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Memo<T> {
|
||||
compute: Rc<dyn Fn() -> T>,
|
||||
}
|
||||
|
||||
impl<T> Clone for Memo<T> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
compute: Rc::clone(&self.compute),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Memo<T> {
|
||||
pub fn with<R>(&self, f: impl FnOnce(&T) -> R) -> R {
|
||||
let value = (self.compute)();
|
||||
f(&value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Clone> Memo<T> {
|
||||
pub fn get(&self) -> T {
|
||||
(self.compute)()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn use_signal<T: 'static>(initial: impl FnOnce() -> T) -> Signal<T> {
|
||||
with_hook_slot(|| Signal::new(initial()), |signal| signal.clone())
|
||||
}
|
||||
|
||||
pub fn use_memo<T: 'static>(compute: impl Fn() -> T + 'static) -> Memo<T> {
|
||||
let compute: Rc<RefCell<Box<dyn Fn() -> T>>> =
|
||||
Rc::new(RefCell::new(Box::new(compute) as Box<dyn Fn() -> T>));
|
||||
with_hook_slot(
|
||||
{
|
||||
let compute = Rc::clone(&compute);
|
||||
move || MemoSlot::new(compute)
|
||||
},
|
||||
|slot: &mut MemoSlot<T>| {
|
||||
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<Vec<Box<dyn Any>>>,
|
||||
element_ids: RefCell<Vec<ElementId>>,
|
||||
next_element_id: StdCell<u64>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, PartialEq)]
|
||||
struct RenderSideEffects {
|
||||
window_title: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct RenderContext {
|
||||
state: Rc<RenderState>,
|
||||
hook_index: Rc<StdCell<usize>>,
|
||||
element_index: Rc<StdCell<usize>>,
|
||||
side_effects: Rc<RefCell<RenderSideEffects>>,
|
||||
}
|
||||
|
||||
struct RenderOutput {
|
||||
view: View,
|
||||
side_effects: RenderSideEffects,
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
static CURRENT_RENDER_CONTEXT: RefCell<Option<RenderContext>> = const { RefCell::new(None) };
|
||||
}
|
||||
|
||||
fn render_with_context(state: Rc<RenderState>, 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<Option<RenderContext>>,
|
||||
previous: Option<RenderContext>,
|
||||
}
|
||||
|
||||
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<R>(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<T: 'static, R>(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::<T>()
|
||||
.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<T> {
|
||||
compute: Rc<RefCell<Box<dyn Fn() -> T>>>,
|
||||
handle: Memo<T>,
|
||||
}
|
||||
|
||||
impl<T: 'static> MemoSlot<T> {
|
||||
fn new(compute: Rc<RefCell<Box<dyn Fn() -> 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<RefCell<Box<dyn Fn() -> 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<dyn Fn(&RoutedPointerEvent) + 'static>;
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
struct EventBindings {
|
||||
on_press: HashMap<ElementId, PressHandler>,
|
||||
}
|
||||
|
||||
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<T> {
|
||||
cell: ruin_reactivity::Cell<T>,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user