//! Explicit-construction UI runtime helpers. use ruin_reactivity::{EffectHandle, effect}; use crate::keyboard::KeyboardEvent; use crate::platform::{PlatformClosed, PlatformEvent, PlatformProxy, PlatformRuntime}; use crate::scene::SceneSnapshot; use crate::tree::CursorIcon; use crate::window::{WindowId, WindowSpec, WindowUpdate}; /// High-level UI-side owner of platform event consumption. /// /// The headless backend currently co-locates platform and rendering on one worker thread, but this /// runtime type deliberately treats them as logical subsystems behind an event/snapshot boundary so /// future backends can split them without rewriting the app-facing control flow. pub struct UiRuntime { platform: PlatformRuntime, } /// Explicit handle for one declarative window instance. /// /// The controller owns no native resources directly. Instead, it issues commands to the platform /// runtime and can host a reactive scene effect that pushes immutable scene snapshots whenever UI /// state changes. #[derive(Clone)] pub struct WindowController { id: WindowId, proxy: PlatformProxy, } /// Returned when the platform event stream closes before a matching event is observed. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct EventStreamClosed; impl std::fmt::Display for EventStreamClosed { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str("platform event stream closed") } } impl std::error::Error for EventStreamClosed {} impl UiRuntime { pub fn from_platform(platform: PlatformRuntime) -> Self { Self { platform } } /// Creates a UI runtime backed by the headless prototype backend. pub fn headless() -> Self { Self::from_platform(PlatformRuntime::headless()) } /// Returns a cloneable proxy for low-level platform commands. pub fn proxy(&self) -> PlatformProxy { self.platform.proxy() } /// Creates a new declarative window and returns a controller for it. pub fn create_window(&self, spec: WindowSpec) -> Result { let id = self.proxy().create_window(spec)?; Ok(WindowController { id, proxy: self.proxy(), }) } /// Waits for the next platform event. pub async fn next_event(&mut self) -> Option { self.platform.next_event().await } /// Drains any platform events that are already queued locally. pub fn take_pending_events(&mut self) -> Vec { self.platform.take_pending_events() } /// Waits until an event matches `predicate`. pub async fn wait_for_event_matching( &mut self, mut predicate: impl FnMut(&PlatformEvent) -> bool, ) -> Result { while let Some(event) = self.next_event().await { if predicate(&event) { return Ok(event); } } Err(EventStreamClosed) } /// Requests shutdown of the platform runtime. pub fn shutdown(&self) -> Result<(), PlatformClosed> { self.proxy().shutdown() } } impl WindowController { /// Returns the logical window identifier. pub const fn id(&self) -> WindowId { self.id } /// Applies a window update. pub fn update(&self, update: WindowUpdate) -> Result<(), PlatformClosed> { self.proxy.update_window(self.id, update) } /// Replaces the latest retained scene for this window. pub fn replace_scene(&self, scene: SceneSnapshot) -> Result<(), PlatformClosed> { self.proxy.replace_scene(self.id, scene) } /// Copies plain text to the platform primary-selection buffer for this window. #[cfg(target_os = "linux")] pub fn set_primary_selection_text( &self, text: impl Into, ) -> Result<(), PlatformClosed> { self.proxy.set_primary_selection_text(self.id, text) } /// Copies plain text to the platform clipboard for this window. pub fn set_clipboard_text(&self, text: impl Into) -> Result<(), PlatformClosed> { self.proxy.set_clipboard_text(self.id, text) } /// Requests the current plain-text clipboard contents from the platform. pub fn request_clipboard_text(&self) -> Result<(), PlatformClosed> { self.proxy.request_clipboard_text(self.id) } /// Requests the current plain-text primary selection contents from the platform. #[cfg(target_os = "linux")] pub fn request_primary_selection_text(&self) -> Result<(), PlatformClosed> { self.proxy.request_primary_selection_text(self.id) } /// Updates the platform cursor icon for this window. pub fn set_cursor_icon(&self, cursor: CursorIcon) -> Result<(), PlatformClosed> { self.proxy.set_cursor_icon(self.id, cursor) } /// Emits a close-request event for this window. pub fn emit_close_requested(&self) -> Result<(), PlatformClosed> { self.proxy.emit_close_requested(self.id) } /// Delivers a pointer event for this window through the platform event stream. pub fn emit_pointer_event( &self, event: crate::interaction::PointerEvent, ) -> Result<(), PlatformClosed> { self.proxy.emit_pointer_event(self.id, event) } /// Delivers a keyboard event for this window through the platform event stream. pub fn emit_keyboard_event(&self, event: KeyboardEvent) -> Result<(), PlatformClosed> { self.proxy.emit_keyboard_event(self.id, event) } /// Delivers an internal wake event for this window through the platform event stream. pub fn emit_wake(&self, token: u64) -> Result<(), PlatformClosed> { self.proxy.emit_wake(self.id, token) } /// Attaches a reactive effect that rebuilds and replaces the window scene whenever dependent UI /// state changes. pub fn attach_scene_effect( &self, build_scene: impl Fn() -> SceneSnapshot + 'static, ) -> EffectHandle { let controller = self.clone(); effect(move || { let scene = build_scene(); controller .replace_scene(scene) .expect("window controller should remain alive while scene effect is attached"); }) } } #[cfg(test)] mod tests { use super::UiRuntime; use crate::platform::PlatformEvent; use crate::scene::{Color, Point, PreparedText, Rect, SceneSnapshot, UiSize}; use crate::window::{WindowSpec, WindowUpdate}; use crate::{ KeyboardEvent, KeyboardEventKind, KeyboardKey, KeyboardModifiers, PointerEvent, PointerEventKind, }; use ruin_reactivity::cell; use ruin_runtime::{current_thread_handle, queue_future, run}; use std::future::Future; fn run_async_test(future: impl Future + 'static) { let main = current_thread_handle(); queue_future(async move { future.await; let _ = main.queue_task(|| {}); }); run(); } #[test] fn visibility_restore_re_presents_latest_retained_scene() { run_async_test(async move { let mut ui = UiRuntime::headless(); let window = ui .create_window(WindowSpec::new("visibility-policy").visible(true)) .expect("window should be created"); let mut scene = SceneSnapshot::new(1, UiSize::new(320.0, 180.0)); scene .push_quad( Rect::new(0.0, 0.0, 320.0, 180.0), Color::rgb(0x20, 0x22, 0x35), ) .push_text(PreparedText::monospace( "retained scene", Point::new(16.0, 28.0), 16.0, 8.0, Color::rgb(0xFF, 0xFF, 0xFF), )); window .replace_scene(scene) .expect("scene replacement should queue"); let _ = ui .wait_for_event_matching(|event| { matches!( event, PlatformEvent::FramePresented { window_id, scene_version: 1, .. } if *window_id == window.id() ) }) .await .expect("initial present should occur"); window .update(WindowUpdate::new().visible(false)) .expect("hide should queue"); let _ = ui .wait_for_event_matching(|event| { matches!( event, PlatformEvent::VisibilityChanged { window_id, visible: false, } if *window_id == window.id() ) }) .await .expect("hide event should occur"); window .update(WindowUpdate::new().visible(true)) .expect("show should queue"); let _ = ui .wait_for_event_matching(|event| { matches!( event, PlatformEvent::VisibilityChanged { window_id, visible: true, } if *window_id == window.id() ) }) .await .expect("show event should occur"); let event = ui .wait_for_event_matching(|event| { matches!( event, PlatformEvent::FramePresented { window_id, scene_version: 1, .. } if *window_id == window.id() ) }) .await .expect("visibility restore should re-present latest retained scene"); assert!(matches!( event, PlatformEvent::FramePresented { scene_version: 1, .. } )); ui.shutdown().expect("shutdown should queue"); }); } #[test] fn window_controller_emits_pointer_events_through_runtime() { run_async_test(async move { let mut ui = UiRuntime::headless(); let window = ui .create_window(WindowSpec::new("pointer-events").visible(true)) .expect("window should be created"); let _ = ui .wait_for_event_matching(|event| { matches!(event, PlatformEvent::Opened { window_id } if *window_id == window.id()) }) .await .expect("window should open before pointer delivery"); window .emit_pointer_event(PointerEvent::new( 0, Point::new(24.0, 32.0), PointerEventKind::Move, )) .expect("pointer event should queue"); let event = ui .wait_for_event_matching(|event| { matches!( event, PlatformEvent::Pointer { window_id, .. } if *window_id == window.id() ) }) .await .expect("pointer event should be delivered"); assert_eq!( event, PlatformEvent::Pointer { window_id: window.id(), event: PointerEvent::new(0, Point::new(24.0, 32.0), PointerEventKind::Move), } ); ui.shutdown().expect("shutdown should queue"); }); } #[test] fn window_controller_emits_keyboard_events_through_runtime() { run_async_test(async move { let mut ui = UiRuntime::headless(); let window = ui .create_window(WindowSpec::new("keyboard-events").visible(true)) .expect("window should be created"); let _ = ui .wait_for_event_matching(|event| { matches!(event, PlatformEvent::Opened { window_id } if *window_id == window.id()) }) .await .expect("window should open before keyboard delivery"); let keyboard_event = KeyboardEvent::new( 30, KeyboardEventKind::Pressed, KeyboardKey::Character("a".to_owned()), KeyboardModifiers::default(), Some("a".to_owned()), ); window .emit_keyboard_event(keyboard_event.clone()) .expect("keyboard event should queue"); let event = ui .wait_for_event_matching(|event| { matches!( event, PlatformEvent::Keyboard { window_id, .. } if *window_id == window.id() ) }) .await .expect("keyboard event should be delivered"); assert_eq!( event, PlatformEvent::Keyboard { window_id: window.id(), event: keyboard_event, } ); ui.shutdown().expect("shutdown should queue"); }); } #[test] fn async_signal_updates_represent_scene_without_external_events() { run_async_test(async move { let mut ui = UiRuntime::headless(); let window = ui .create_window(WindowSpec::new("async-scene-effect").visible(true)) .expect("window should be created"); let scene_version = std::rc::Rc::new(std::cell::Cell::new(0_u64)); let value = cell(0_u32); let _scene_effect = window.attach_scene_effect({ let scene_version = std::rc::Rc::clone(&scene_version); let value = value.clone(); move || { let next_version = scene_version.get() + 1; scene_version.set(next_version); let mut scene = SceneSnapshot::new(next_version, UiSize::new(320.0, 180.0)); scene.push_text(PreparedText::monospace( format!("value={}", value.get()), Point::new(16.0, 28.0), 16.0, 8.0, Color::rgb(0xFF, 0xFF, 0xFF), )); scene } }); let _ = ui .wait_for_event_matching(|event| { matches!( event, PlatformEvent::FramePresented { window_id, scene_version: 1, .. } if *window_id == window.id() ) }) .await .expect("initial scene should present"); queue_future({ let value = value.clone(); async move { ruin_runtime::yield_now().await; let _ = value.set(1); } }); let _ = ui .wait_for_event_matching(|event| { matches!( event, PlatformEvent::FramePresented { window_id, scene_version: 2, .. } if *window_id == window.id() ) }) .await .expect("async signal update should re-present the scene"); ui.shutdown().expect("shutdown should queue"); }); } }