First pass on ruin_app, raw expanded example
This commit is contained in:
10
Cargo.lock
generated
10
Cargo.lock
generated
@@ -1627,6 +1627,16 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ruin_app"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"ruin-runtime",
|
||||||
|
"ruin_reactivity",
|
||||||
|
"ruin_ui",
|
||||||
|
"ruin_ui_platform_wayland",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ruin_reactivity"
|
name = "ruin_reactivity"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|||||||
14
lib/ruin_app/Cargo.toml
Normal file
14
lib/ruin_app/Cargo.toml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
[package]
|
||||||
|
name = "ruin_app"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
ruin_reactivity = { path = "../reactivity" }
|
||||||
|
ruin_runtime = { package = "ruin-runtime", path = "../runtime" }
|
||||||
|
ruin_ui = { path = "../ui" }
|
||||||
|
ruin_ui_platform_wayland = { path = "../ui_platform_wayland" }
|
||||||
|
|
||||||
|
[[example]]
|
||||||
|
name = "00_bootstrap_and_counter_raw"
|
||||||
|
path = "example/00_bootstrap_and_counter_raw.rs"
|
||||||
192
lib/ruin_app/example/00_bootstrap_and_counter_raw.rs
Normal file
192
lib/ruin_app/example/00_bootstrap_and_counter_raw.rs
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
use ruin_app::{Component, 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() -> impl View {
|
||||||
|
// let count = use_signal(|| 0);
|
||||||
|
// let doubled = use_memo(move || count.get() * 2);
|
||||||
|
//
|
||||||
|
// use_window_title(move || format!("RUIN Counter ({})", count.get()));
|
||||||
|
//
|
||||||
|
// view! {
|
||||||
|
// column(gap = 16, padding = 24) {
|
||||||
|
// text(role = TextRole::Heading(1), size = 32, weight = FontWeight::Semibold) {
|
||||||
|
// "RUIN counter"
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
#[ruin_runtime::async_main]
|
||||||
|
async fn main() -> ruin_app::Result<()> {
|
||||||
|
App::new()
|
||||||
|
.window(
|
||||||
|
Window::new()
|
||||||
|
.title("RUIN Counter")
|
||||||
|
.app_id("dev.ruin.counter")
|
||||||
|
.size(960.0, 640.0),
|
||||||
|
)
|
||||||
|
.mount(CounterApp::builder().children(()))
|
||||||
|
.run()
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
struct CounterApp {
|
||||||
|
count: Cell<i32>,
|
||||||
|
doubled: Memo<i32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
impl __CounterAppBuilder {
|
||||||
|
fn children(self, _children: ()) -> CounterApp {
|
||||||
|
CounterApp::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Component for CounterApp {
|
||||||
|
type Builder = __CounterAppBuilder;
|
||||||
|
|
||||||
|
fn builder() -> Self::Builder {
|
||||||
|
__CounterAppBuilder
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CounterApp {
|
||||||
|
fn new() -> Self {
|
||||||
|
let count = cell(0_i32);
|
||||||
|
let doubled = memo({
|
||||||
|
let count = count.clone();
|
||||||
|
move || count.get() * 2
|
||||||
|
});
|
||||||
|
Self { count, doubled }
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
239
lib/ruin_app/src/lib.rs
Normal file
239
lib/ruin_app/src/lib.rs
Normal file
@@ -0,0 +1,239 @@
|
|||||||
|
//! Minimal app/runtime glue for RUIN application experiments.
|
||||||
|
//!
|
||||||
|
//! 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::cell::{Cell as StdCell, RefCell};
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Window {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
spec: WindowSpec::new("RUIN App"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn title(mut self, title: impl Into<String>) -> Self {
|
||||||
|
self.spec.title = title.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn app_id(mut self, app_id: impl Into<String>) -> Self {
|
||||||
|
self.spec = self.spec.app_id(app_id);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn size(mut self, width: f32, height: f32) -> Self {
|
||||||
|
self.spec = self.spec.requested_inner_size(UiSize::new(width, height));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Window {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct App {
|
||||||
|
window: Window,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl App {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
window: Window::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn window(mut self, window: Window) -> Self {
|
||||||
|
self.window = window;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn mount<R: RootComponent>(self, root: R) -> MountedApp<R> {
|
||||||
|
MountedApp {
|
||||||
|
window: self.window,
|
||||||
|
root,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for App {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct MountedApp<R> {
|
||||||
|
window: Window,
|
||||||
|
root: R,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<R: RootComponent> MountedApp<R> {
|
||||||
|
pub async fn run(self) -> Result<()> {
|
||||||
|
let MountedApp {
|
||||||
|
window: app_window,
|
||||||
|
root,
|
||||||
|
} = self;
|
||||||
|
let mut ui = start_wayland_ui();
|
||||||
|
let window = ui.create_window(app_window.spec.clone())?;
|
||||||
|
let initial_viewport = app_window
|
||||||
|
.spec
|
||||||
|
.requested_inner_size
|
||||||
|
.unwrap_or(UiSize::new(960.0, 640.0));
|
||||||
|
|
||||||
|
let viewport = ruin_reactivity::cell(initial_viewport);
|
||||||
|
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::new(root);
|
||||||
|
let mut pointer_router = PointerRouter::new();
|
||||||
|
let mut current_cursor = CursorIcon::Default;
|
||||||
|
|
||||||
|
let _scene_effect = effect({
|
||||||
|
let window = window.clone();
|
||||||
|
let viewport = viewport.clone();
|
||||||
|
let text_system = Rc::clone(&text_system);
|
||||||
|
let interaction_tree = Rc::clone(&interaction_tree);
|
||||||
|
let root = Rc::clone(&root);
|
||||||
|
move || {
|
||||||
|
let viewport = viewport.get();
|
||||||
|
let version = scene_version.get().wrapping_add(1);
|
||||||
|
scene_version.set(version);
|
||||||
|
|
||||||
|
let tree = root.build(&BuildCx { viewport });
|
||||||
|
let LayoutSnapshot {
|
||||||
|
scene,
|
||||||
|
interaction_tree: next_interaction_tree,
|
||||||
|
} = layout_snapshot_with_text_system(
|
||||||
|
version,
|
||||||
|
viewport,
|
||||||
|
&tree,
|
||||||
|
&mut text_system.borrow_mut(),
|
||||||
|
);
|
||||||
|
*interaction_tree.borrow_mut() = Some(next_interaction_tree);
|
||||||
|
window
|
||||||
|
.replace_scene(scene)
|
||||||
|
.expect("window should remain alive while the app is running");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let Some(event) = ui.next_event().await else {
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
|
||||||
|
for event in iter::once(event).chain(ui.take_pending_events()) {
|
||||||
|
match event {
|
||||||
|
PlatformEvent::Configured {
|
||||||
|
window_id,
|
||||||
|
configuration,
|
||||||
|
} if window_id == window.id() => {
|
||||||
|
let _ = viewport.set(configuration.actual_inner_size);
|
||||||
|
}
|
||||||
|
PlatformEvent::Pointer { window_id, event } if window_id == window.id() => {
|
||||||
|
Self::handle_pointer_event(
|
||||||
|
&root,
|
||||||
|
&window,
|
||||||
|
&interaction_tree,
|
||||||
|
&mut pointer_router,
|
||||||
|
&mut current_cursor,
|
||||||
|
event,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
PlatformEvent::CloseRequested { window_id } if window_id == window.id() => {
|
||||||
|
let _ = window.update(WindowUpdate::new().open(false));
|
||||||
|
}
|
||||||
|
PlatformEvent::Closed { window_id } if window_id == window.id() => {
|
||||||
|
ui.shutdown()?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.shutdown()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_pointer_event(
|
||||||
|
root: &Rc<R>,
|
||||||
|
window: &WindowController,
|
||||||
|
interaction_tree: &RefCell<Option<InteractionTree>>,
|
||||||
|
pointer_router: &mut PointerRouter,
|
||||||
|
current_cursor: &mut CursorIcon,
|
||||||
|
event: PointerEvent,
|
||||||
|
) -> Result<()> {
|
||||||
|
let routed = {
|
||||||
|
let interaction_tree = interaction_tree.borrow();
|
||||||
|
let Some(interaction_tree) = interaction_tree.as_ref() else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
pointer_router.route(interaction_tree, event)
|
||||||
|
};
|
||||||
|
|
||||||
|
for routed_event in &routed {
|
||||||
|
root.handle_pointer(routed_event);
|
||||||
|
}
|
||||||
|
|
||||||
|
let next_cursor = pointer_router
|
||||||
|
.hovered_targets()
|
||||||
|
.last()
|
||||||
|
.map(|target| target.cursor)
|
||||||
|
.unwrap_or(CursorIcon::Default);
|
||||||
|
if next_cursor != *current_cursor {
|
||||||
|
*current_cursor = next_cursor;
|
||||||
|
window.set_cursor_icon(next_cursor)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user