First pass on ruin_app, raw expanded example
This commit is contained in:
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