Early UI work.

This commit is contained in:
2026-03-20 16:46:18 -04:00
parent 9ab1167fef
commit 39ede248cf
15 changed files with 3560 additions and 1 deletions

26
lib/ui/src/lib.rs Normal file
View File

@@ -0,0 +1,26 @@
//! Shared explicit-construction UI substrate for RUIN.
//!
//! This crate intentionally starts with explicit Rust data construction rather than a proc-macro
//! authoring layer. The goal is to validate the threading, windowing, and scene handoff model
//! before committing to ergonomic surface syntax. Concrete platform and renderer backends live in
//! sibling crates.
pub(crate) mod trace_targets {
pub const PLATFORM: &str = "ruin_ui::platform";
pub const SCENE: &str = "ruin_ui::scene";
}
mod platform;
mod runtime;
mod scene;
mod window;
pub use platform::{PlatformClosed, PlatformEvent, PlatformProxy, PlatformRuntime, start_headless};
pub use runtime::{EventStreamClosed, UiRuntime, WindowController};
pub use scene::{
Color, DisplayItem, GlyphInstance, Point, PreparedText, Quad, Rect, SceneSnapshot, Translation,
UiSize,
};
pub use window::{
DecorationMode, WindowConfigured, WindowId, WindowLifecycle, WindowSpec, WindowUpdate,
};

647
lib/ui/src/platform.rs Normal file
View File

@@ -0,0 +1,647 @@
//! Headless platform/render worker for the explicit-construction prototype.
use std::cell::RefCell;
use std::collections::BTreeMap;
use std::fmt;
use std::rc::Rc;
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use ruin_runtime::channel::mpsc;
use ruin_runtime::{WorkerHandle, queue_future, queue_microtask, spawn_worker};
use tracing::{debug, info};
use crate::scene::{SceneSnapshot, UiSize};
use crate::trace_targets;
use crate::window::{WindowConfigured, WindowId, WindowLifecycle, WindowSpec, WindowUpdate};
#[derive(Clone)]
pub struct PlatformProxy {
command_tx: mpsc::UnboundedSender<PlatformCommand>,
next_window_id: Arc<AtomicU64>,
}
/// Low-level platform/runtime host for the explicit prototype.
///
/// The headless backend currently co-locates logical platform and rendering work on one runtime
/// worker thread, but the event/snapshot API is intentionally shaped so future backends can split
/// them if needed.
pub struct PlatformRuntime {
proxy: PlatformProxy,
events: mpsc::Receiver<PlatformEvent>,
_worker: WorkerHandle,
}
#[derive(Clone, Debug, PartialEq)]
pub enum PlatformEvent {
Opened {
window_id: WindowId,
},
Closed {
window_id: WindowId,
},
VisibilityChanged {
window_id: WindowId,
visible: bool,
},
Configured {
window_id: WindowId,
configuration: WindowConfigured,
},
FramePresented {
window_id: WindowId,
scene_version: u64,
item_count: usize,
},
CloseRequested {
window_id: WindowId,
},
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct PlatformClosed;
impl fmt::Display for PlatformClosed {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("platform worker is closed")
}
}
impl std::error::Error for PlatformClosed {}
#[derive(Clone, Debug)]
enum PlatformCommand {
CreateWindow {
window_id: WindowId,
spec: WindowSpec,
},
UpdateWindow {
window_id: WindowId,
update: WindowUpdate,
},
ReplaceScene {
window_id: WindowId,
scene: SceneSnapshot,
},
EmitCloseRequested {
window_id: WindowId,
},
Shutdown,
}
#[derive(Clone)]
struct HeadlessState {
events: mpsc::UnboundedSender<PlatformEvent>,
windows: BTreeMap<WindowId, HeadlessWindow>,
}
#[derive(Clone)]
struct HeadlessWindow {
spec: WindowSpec,
lifecycle: WindowLifecycle,
configuration: WindowConfigured,
pending_scene: Option<SceneSnapshot>,
render_scheduled: bool,
}
impl HeadlessWindow {
fn new(spec: WindowSpec) -> Self {
let actual_inner_size = spec
.requested_inner_size
.unwrap_or_else(|| UiSize::new(800.0, 600.0));
Self {
spec,
lifecycle: WindowLifecycle::LogicalClosed,
configuration: WindowConfigured {
actual_inner_size,
scale_factor: 1.0,
visible: false,
maximized: false,
fullscreen: false,
},
pending_scene: None,
render_scheduled: false,
}
}
}
impl PlatformRuntime {
pub fn headless() -> Self {
start_headless()
}
pub fn proxy(&self) -> PlatformProxy {
self.proxy.clone()
}
pub async fn next_event(&mut self) -> Option<PlatformEvent> {
self.events.recv().await
}
}
impl PlatformProxy {
pub fn create_window(&self, spec: WindowSpec) -> Result<WindowId, PlatformClosed> {
let window_id = WindowId::from_raw(self.next_window_id.fetch_add(1, Ordering::Relaxed));
self.send(PlatformCommand::CreateWindow { window_id, spec })?;
Ok(window_id)
}
pub fn update_window(
&self,
window_id: WindowId,
update: WindowUpdate,
) -> Result<(), PlatformClosed> {
self.send(PlatformCommand::UpdateWindow { window_id, update })
}
pub fn replace_scene(
&self,
window_id: WindowId,
scene: SceneSnapshot,
) -> Result<(), PlatformClosed> {
self.send(PlatformCommand::ReplaceScene { window_id, scene })
}
pub fn emit_close_requested(&self, window_id: WindowId) -> Result<(), PlatformClosed> {
self.send(PlatformCommand::EmitCloseRequested { window_id })
}
pub fn shutdown(&self) -> Result<(), PlatformClosed> {
self.send(PlatformCommand::Shutdown)
}
fn send(&self, command: PlatformCommand) -> Result<(), PlatformClosed> {
self.command_tx.send(command).map_err(|_| PlatformClosed)
}
}
/// Starts the headless prototype backend.
///
/// Policy note: when a window becomes visible again, the backend may re-present the latest retained
/// scene for that window even if the scene version itself did not change. Visibility restoration is
/// treated as a presentation-affecting state change.
pub fn start_headless() -> PlatformRuntime {
let (command_tx, mut command_rx) = mpsc::unbounded_channel::<PlatformCommand>();
let (event_tx, event_rx) = mpsc::unbounded_channel::<PlatformEvent>();
let worker = spawn_worker(
move || {
let state = Rc::new(RefCell::new(HeadlessState {
events: event_tx.clone(),
windows: BTreeMap::new(),
}));
queue_future(async move {
debug!(
target: trace_targets::PLATFORM,
event = "worker_started",
backend = "headless",
"starting headless platform worker"
);
while let Some(command) = command_rx.recv().await {
match command {
PlatformCommand::CreateWindow { window_id, spec } => {
handle_create_window(&state, window_id, spec);
}
PlatformCommand::UpdateWindow { window_id, update } => {
handle_update_window(&state, window_id, update);
}
PlatformCommand::ReplaceScene { window_id, scene } => {
handle_replace_scene(&state, window_id, scene);
}
PlatformCommand::EmitCloseRequested { window_id } => {
let sender = state.borrow().events.clone();
let _ = sender.send(PlatformEvent::CloseRequested { window_id });
}
PlatformCommand::Shutdown => {
debug!(
target: trace_targets::PLATFORM,
event = "worker_shutdown_requested",
"shutting down headless platform worker"
);
break;
}
}
}
});
},
|| {
debug!(
target: trace_targets::PLATFORM,
event = "worker_exited",
backend = "headless",
"headless platform worker exited"
);
},
);
PlatformRuntime {
proxy: PlatformProxy {
command_tx,
next_window_id: Arc::new(AtomicU64::new(1)),
},
events: event_rx,
_worker: worker,
}
}
fn handle_create_window(state: &Rc<RefCell<HeadlessState>>, window_id: WindowId, spec: WindowSpec) {
debug!(
target: trace_targets::PLATFORM,
event = "create_window",
window_id = window_id.raw(),
title = spec.title.as_str(),
open = spec.open,
visible = spec.visible,
"creating logical window"
);
state
.borrow_mut()
.windows
.insert(window_id, HeadlessWindow::new(spec.clone()));
if spec.open {
begin_open_transition(Rc::clone(state), window_id);
}
}
fn handle_update_window(
state: &Rc<RefCell<HeadlessState>>,
window_id: WindowId,
update: WindowUpdate,
) {
let mut action = UpdateAction::None;
{
let mut state_ref = state.borrow_mut();
let Some(window) = state_ref.windows.get_mut(&window_id) else {
return;
};
let was_open = window.spec.open;
let was_visible = window.spec.visible;
update.apply_to(&mut window.spec);
if let Some(requested_inner_size) = window.spec.requested_inner_size {
window.configuration.actual_inner_size = requested_inner_size;
}
window.configuration.maximized = window.spec.maximized;
window.configuration.fullscreen = window.spec.fullscreen;
match (was_open, window.spec.open) {
(false, true) => action = UpdateAction::Open,
(true, false) => action = UpdateAction::Close,
_ => {}
}
if was_open == window.spec.open && was_visible != window.spec.visible {
action = UpdateAction::Visibility(window.spec.visible);
}
}
match action {
UpdateAction::None => {
maybe_schedule_render(Rc::clone(state), window_id);
}
UpdateAction::Open => begin_open_transition(Rc::clone(state), window_id),
UpdateAction::Close => close_window(state, window_id),
UpdateAction::Visibility(visible) => set_visibility(state, window_id, visible),
}
}
fn handle_replace_scene(
state: &Rc<RefCell<HeadlessState>>,
window_id: WindowId,
scene: SceneSnapshot,
) {
#[cfg(debug_assertions)]
tracing::trace!(
target: trace_targets::PLATFORM,
event = "replace_scene",
window_id = window_id.raw(),
version = scene.version,
items = scene.item_count(),
"received scene snapshot"
);
if let Some(window) = state.borrow_mut().windows.get_mut(&window_id) {
window.pending_scene = Some(scene);
} else {
return;
}
maybe_schedule_render(Rc::clone(state), window_id);
}
fn maybe_schedule_render(state: Rc<RefCell<HeadlessState>>, window_id: WindowId) {
let should_schedule = {
let mut state_ref = state.borrow_mut();
let Some(window) = state_ref.windows.get_mut(&window_id) else {
return;
};
if window.render_scheduled {
false
} else {
window.render_scheduled = true;
true
}
};
if should_schedule {
queue_microtask(move || present_latest_scene(&state, window_id));
}
}
fn begin_open_transition(state: Rc<RefCell<HeadlessState>>, window_id: WindowId) {
{
let mut state_ref = state.borrow_mut();
let Some(window) = state_ref.windows.get_mut(&window_id) else {
return;
};
window.lifecycle = WindowLifecycle::Opening;
}
queue_microtask(move || {
{
let mut state_ref = state.borrow_mut();
let Some(window) = state_ref.windows.get_mut(&window_id) else {
return;
};
window.lifecycle = WindowLifecycle::AwaitingInitialConfigure;
window.configuration.visible = window.spec.visible;
window.configuration.maximized = window.spec.maximized;
window.configuration.fullscreen = window.spec.fullscreen;
window.lifecycle = if window.spec.visible {
WindowLifecycle::OpenVisible
} else {
WindowLifecycle::OpenHidden
};
}
emit_event(
&state,
PlatformEvent::Configured {
window_id,
configuration: state
.borrow()
.windows
.get(&window_id)
.expect("window should exist during configure")
.configuration,
},
);
emit_event(&state, PlatformEvent::Opened { window_id });
maybe_schedule_render(state, window_id);
});
}
fn close_window(state: &Rc<RefCell<HeadlessState>>, window_id: WindowId) {
let should_emit = {
let mut state_ref = state.borrow_mut();
let Some(window) = state_ref.windows.get_mut(&window_id) else {
return;
};
if window.lifecycle == WindowLifecycle::LogicalClosed {
false
} else {
window.lifecycle = WindowLifecycle::Closing;
window.render_scheduled = false;
window.configuration.visible = false;
window.lifecycle = WindowLifecycle::LogicalClosed;
true
}
};
if should_emit {
emit_event(state, PlatformEvent::Closed { window_id });
}
}
fn set_visibility(state: &Rc<RefCell<HeadlessState>>, window_id: WindowId, visible: bool) {
let should_emit = {
let mut state_ref = state.borrow_mut();
let Some(window) = state_ref.windows.get_mut(&window_id) else {
return;
};
if !window.spec.open {
false
} else {
window.configuration.visible = visible;
window.lifecycle = if visible {
WindowLifecycle::OpenVisible
} else {
WindowLifecycle::OpenHidden
};
true
}
};
if should_emit {
emit_event(
state,
PlatformEvent::VisibilityChanged { window_id, visible },
);
maybe_schedule_render(Rc::clone(state), window_id);
}
}
fn present_latest_scene(state: &Rc<RefCell<HeadlessState>>, window_id: WindowId) {
let presentation = {
let mut state_ref = state.borrow_mut();
let Some(window) = state_ref.windows.get_mut(&window_id) else {
return;
};
window.render_scheduled = false;
if window.lifecycle != WindowLifecycle::OpenVisible {
return;
}
let Some(scene) = window.pending_scene.clone() else {
return;
};
Some((scene.version, scene.item_count(), window.spec.title.clone()))
};
let Some((scene_version, item_count, title)) = presentation else {
return;
};
info!(
target: trace_targets::PLATFORM,
event = "present_scene",
backend = "headless",
window_id = window_id.raw(),
title = title.as_str(),
scene_version,
item_count,
"presenting latest scene snapshot"
);
emit_event(
state,
PlatformEvent::FramePresented {
window_id,
scene_version,
item_count,
},
);
}
fn emit_event(state: &Rc<RefCell<HeadlessState>>, event: PlatformEvent) {
let sender = state.borrow().events.clone();
let _ = sender.send(event);
}
enum UpdateAction {
None,
Open,
Close,
Visibility(bool),
}
#[cfg(test)]
mod tests {
use super::{PlatformEvent, start_headless};
use crate::scene::{Color, Point, PreparedText, Rect, SceneSnapshot, UiSize};
use crate::window::{WindowSpec, WindowUpdate};
use ruin_runtime::{current_thread_handle, queue_future, run};
use std::sync::{Arc, Mutex};
#[test]
fn headless_platform_coalesces_latest_scene_per_window() {
let observed = Arc::new(Mutex::new(Vec::<PlatformEvent>::new()));
let done = Arc::new(Mutex::new(false));
let main = current_thread_handle();
queue_future({
let observed = Arc::clone(&observed);
let done = Arc::clone(&done);
async move {
let mut platform = start_headless();
let proxy = platform.proxy();
let window_id = proxy
.create_window(
WindowSpec::new("coalesce").requested_inner_size(UiSize::new(320.0, 180.0)),
)
.expect("window should be created");
let mut one = SceneSnapshot::new(1, UiSize::new(320.0, 180.0));
one.push_quad(
Rect::new(0.0, 0.0, 320.0, 180.0),
Color::rgb(0x22, 0x22, 0x33),
);
let mut two = SceneSnapshot::new(2, UiSize::new(320.0, 180.0));
two.push_quad(
Rect::new(0.0, 0.0, 320.0, 180.0),
Color::rgb(0x33, 0x22, 0x22),
);
let mut three = SceneSnapshot::new(3, UiSize::new(320.0, 180.0));
three
.push_quad(
Rect::new(0.0, 0.0, 320.0, 180.0),
Color::rgb(0x22, 0x33, 0x22),
)
.push_text(PreparedText::monospace(
"v3",
Point::new(16.0, 28.0),
16.0,
8.0,
Color::rgb(0xFF, 0xFF, 0xFF),
));
proxy
.replace_scene(window_id, one)
.expect("scene v1 should queue");
proxy
.replace_scene(window_id, two)
.expect("scene v2 should queue");
proxy
.replace_scene(window_id, three)
.expect("scene v3 should queue");
while let Some(event) = platform.next_event().await {
observed.lock().unwrap().push(event.clone());
if let PlatformEvent::FramePresented {
window_id: presented,
scene_version,
..
} = event
{
assert_eq!(presented, window_id);
assert_eq!(scene_version, 3);
proxy.shutdown().expect("shutdown should queue");
break;
}
}
while platform.next_event().await.is_some() {}
*done.lock().unwrap() = true;
let _ = main.queue_task(|| {});
}
});
run();
assert!(*done.lock().unwrap());
}
#[test]
fn open_false_closes_and_reopen_reconfigures() {
let counts = Arc::new(Mutex::new((0usize, 0usize, 0usize)));
let done = Arc::new(Mutex::new(false));
let main = current_thread_handle();
queue_future({
let counts = Arc::clone(&counts);
let done = Arc::clone(&done);
async move {
let mut platform = start_headless();
let proxy = platform.proxy();
let window_id = proxy
.create_window(
WindowSpec::new("reopen").requested_inner_size(UiSize::new(640.0, 360.0)),
)
.expect("window should be created");
let mut saw_initial_open = false;
while let Some(event) = platform.next_event().await {
match event {
PlatformEvent::Configured { window_id: id, .. } if id == window_id => {
counts.lock().unwrap().0 += 1;
}
PlatformEvent::Opened { window_id: id } if id == window_id => {
counts.lock().unwrap().1 += 1;
if !saw_initial_open {
saw_initial_open = true;
proxy
.update_window(window_id, WindowUpdate::new().open(false))
.expect("close should queue");
proxy
.update_window(window_id, WindowUpdate::new().open(true))
.expect("reopen should queue");
}
}
PlatformEvent::Closed { window_id: id } if id == window_id => {
counts.lock().unwrap().2 += 1;
}
_ => {}
}
let (configured, opened, closed) = *counts.lock().unwrap();
if configured >= 2 && opened >= 2 && closed >= 1 {
proxy.shutdown().expect("shutdown should queue");
break;
}
}
while platform.next_event().await.is_some() {}
*done.lock().unwrap() = true;
let _ = main.queue_task(|| {});
}
});
run();
let (configured, opened, closed) = *counts.lock().unwrap();
assert_eq!(configured, 2);
assert_eq!(opened, 2);
assert_eq!(closed, 1);
assert!(*done.lock().unwrap());
}
}

238
lib/ui/src/runtime.rs Normal file
View File

@@ -0,0 +1,238 @@
//! Explicit-construction UI runtime helpers.
use ruin_reactivity::{EffectHandle, effect};
use crate::platform::{PlatformClosed, PlatformEvent, PlatformProxy, PlatformRuntime};
use crate::scene::SceneSnapshot;
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 {
/// Creates a UI runtime backed by the headless prototype backend.
pub fn headless() -> Self {
Self {
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<WindowController, PlatformClosed> {
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<PlatformEvent> {
self.platform.next_event().await
}
/// Waits until an event matches `predicate`.
pub async fn wait_for_event_matching(
&mut self,
mut predicate: impl FnMut(&PlatformEvent) -> bool,
) -> Result<PlatformEvent, EventStreamClosed> {
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)
}
/// Emits a close-request event for this window.
pub fn emit_close_requested(&self) -> Result<(), PlatformClosed> {
self.proxy.emit_close_requested(self.id)
}
/// 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 ruin_runtime::{current_thread_handle, queue_future, run};
use std::future::Future;
fn run_async_test(future: impl Future<Output = ()> + '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");
});
}
}

210
lib/ui/src/scene.rs Normal file
View File

@@ -0,0 +1,210 @@
//! Renderer-oriented scene snapshot types.
use tracing::debug;
use crate::trace_targets;
pub type SceneVersion = u64;
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Point {
pub x: f32,
pub y: f32,
}
impl Point {
pub const fn new(x: f32, y: f32) -> Self {
Self { x, y }
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct UiSize {
pub width: f32,
pub height: f32,
}
impl UiSize {
pub const fn new(width: f32, height: f32) -> Self {
Self { width, height }
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Rect {
pub origin: Point,
pub size: UiSize,
}
impl Rect {
pub const fn new(x: f32, y: f32, width: f32, height: f32) -> Self {
Self {
origin: Point::new(x, y),
size: UiSize::new(width, height),
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct Color {
pub r: u8,
pub g: u8,
pub b: u8,
pub a: u8,
}
impl Color {
pub const fn rgba(r: u8, g: u8, b: u8, a: u8) -> Self {
Self { r, g, b, a }
}
pub const fn rgb(r: u8, g: u8, b: u8) -> Self {
Self::rgba(r, g, b, 255)
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Translation {
pub x: f32,
pub y: f32,
}
impl Translation {
pub const fn new(x: f32, y: f32) -> Self {
Self { x, y }
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Quad {
pub rect: Rect,
pub color: Color,
}
impl Quad {
pub const fn new(rect: Rect, color: Color) -> Self {
Self { rect, color }
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct GlyphInstance {
pub glyph: String,
pub position: Point,
pub advance: f32,
}
#[derive(Clone, Debug, PartialEq)]
pub struct PreparedText {
pub text: String,
pub font_size: f32,
pub color: Color,
pub glyphs: Vec<GlyphInstance>,
}
impl PreparedText {
pub fn monospace(
text: impl Into<String>,
origin: Point,
font_size: f32,
advance: f32,
color: Color,
) -> Self {
let text = text.into();
let mut x = origin.x;
let mut glyphs = Vec::with_capacity(text.chars().count());
for ch in text.chars() {
glyphs.push(GlyphInstance {
glyph: ch.to_string(),
position: Point::new(x, origin.y),
advance,
});
x += advance;
}
Self {
text,
font_size,
color,
glyphs,
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum DisplayItem {
Quad(Quad),
Text(PreparedText),
PushClip(Rect),
PopClip,
PushTransform(Translation),
PopTransform,
LayerBegin { opacity: f32 },
LayerEnd,
}
#[derive(Clone, Debug, PartialEq)]
pub struct SceneSnapshot {
pub version: SceneVersion,
pub logical_size: UiSize,
pub items: Vec<DisplayItem>,
}
impl SceneSnapshot {
pub fn new(version: SceneVersion, logical_size: UiSize) -> Self {
debug!(
target: trace_targets::SCENE,
event = "create_scene",
version,
width = logical_size.width,
height = logical_size.height,
"creating scene snapshot"
);
Self {
version,
logical_size,
items: Vec::new(),
}
}
pub fn item_count(&self) -> usize {
self.items.len()
}
pub fn push_item(&mut self, item: DisplayItem) -> &mut Self {
self.items.push(item);
self
}
pub fn push_quad(&mut self, rect: Rect, color: Color) -> &mut Self {
self.push_item(DisplayItem::Quad(Quad::new(rect, color)))
}
pub fn push_text(&mut self, text: PreparedText) -> &mut Self {
self.push_item(DisplayItem::Text(text))
}
pub fn push_clip(&mut self, rect: Rect) -> &mut Self {
self.push_item(DisplayItem::PushClip(rect))
}
pub fn pop_clip(&mut self) -> &mut Self {
self.push_item(DisplayItem::PopClip)
}
pub fn push_transform(&mut self, translation: Translation) -> &mut Self {
self.push_item(DisplayItem::PushTransform(translation))
}
pub fn pop_transform(&mut self) -> &mut Self {
self.push_item(DisplayItem::PopTransform)
}
pub fn begin_layer(&mut self, opacity: f32) -> &mut Self {
self.push_item(DisplayItem::LayerBegin { opacity })
}
pub fn end_layer(&mut self) -> &mut Self {
self.push_item(DisplayItem::LayerEnd)
}
}

235
lib/ui/src/window.rs Normal file
View File

@@ -0,0 +1,235 @@
//! Common window model types.
use crate::scene::UiSize;
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct WindowId(u64);
impl WindowId {
pub const fn from_raw(raw: u64) -> Self {
Self(raw)
}
pub const fn raw(self) -> u64 {
self.0
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum DecorationMode {
Auto,
Server,
Client,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum WindowLifecycle {
LogicalClosed,
Opening,
AwaitingInitialConfigure,
OpenHidden,
OpenVisible,
Closing,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct WindowConfigured {
pub actual_inner_size: UiSize,
pub scale_factor: f32,
pub visible: bool,
pub maximized: bool,
pub fullscreen: bool,
}
#[derive(Clone, Debug, PartialEq)]
pub struct WindowSpec {
pub key: Option<String>,
pub title: String,
pub app_id: Option<String>,
pub open: bool,
pub visible: bool,
pub requested_inner_size: Option<UiSize>,
pub min_inner_size: Option<UiSize>,
pub max_inner_size: Option<UiSize>,
pub decorations: DecorationMode,
pub maximized: bool,
pub fullscreen: bool,
pub resizable: bool,
}
impl WindowSpec {
pub fn new(title: impl Into<String>) -> Self {
Self {
key: None,
title: title.into(),
app_id: None,
open: true,
visible: true,
requested_inner_size: None,
min_inner_size: None,
max_inner_size: None,
decorations: DecorationMode::Auto,
maximized: false,
fullscreen: false,
resizable: true,
}
}
pub fn key(mut self, key: impl Into<String>) -> Self {
self.key = Some(key.into());
self
}
pub fn app_id(mut self, app_id: impl Into<String>) -> Self {
self.app_id = Some(app_id.into());
self
}
pub fn open(mut self, open: bool) -> Self {
self.open = open;
self
}
pub fn visible(mut self, visible: bool) -> Self {
self.visible = visible;
self
}
pub fn requested_inner_size(mut self, size: UiSize) -> Self {
self.requested_inner_size = Some(size);
self
}
pub fn min_inner_size(mut self, size: UiSize) -> Self {
self.min_inner_size = Some(size);
self
}
pub fn max_inner_size(mut self, size: UiSize) -> Self {
self.max_inner_size = Some(size);
self
}
pub fn decorations(mut self, decorations: DecorationMode) -> Self {
self.decorations = decorations;
self
}
pub fn maximized(mut self, maximized: bool) -> Self {
self.maximized = maximized;
self
}
pub fn fullscreen(mut self, fullscreen: bool) -> Self {
self.fullscreen = fullscreen;
self
}
pub fn resizable(mut self, resizable: bool) -> Self {
self.resizable = resizable;
self
}
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct WindowUpdate {
pub title: Option<String>,
pub open: Option<bool>,
pub visible: Option<bool>,
pub requested_inner_size: Option<Option<UiSize>>,
pub min_inner_size: Option<Option<UiSize>>,
pub max_inner_size: Option<Option<UiSize>>,
pub decorations: Option<DecorationMode>,
pub maximized: Option<bool>,
pub fullscreen: Option<bool>,
pub resizable: Option<bool>,
}
impl WindowUpdate {
pub fn new() -> Self {
Self::default()
}
pub fn title(mut self, title: impl Into<String>) -> Self {
self.title = Some(title.into());
self
}
pub fn open(mut self, open: bool) -> Self {
self.open = Some(open);
self
}
pub fn visible(mut self, visible: bool) -> Self {
self.visible = Some(visible);
self
}
pub fn requested_inner_size(mut self, size: Option<UiSize>) -> Self {
self.requested_inner_size = Some(size);
self
}
pub fn min_inner_size(mut self, size: Option<UiSize>) -> Self {
self.min_inner_size = Some(size);
self
}
pub fn max_inner_size(mut self, size: Option<UiSize>) -> Self {
self.max_inner_size = Some(size);
self
}
pub fn decorations(mut self, decorations: DecorationMode) -> Self {
self.decorations = Some(decorations);
self
}
pub fn maximized(mut self, maximized: bool) -> Self {
self.maximized = Some(maximized);
self
}
pub fn fullscreen(mut self, fullscreen: bool) -> Self {
self.fullscreen = Some(fullscreen);
self
}
pub fn resizable(mut self, resizable: bool) -> Self {
self.resizable = Some(resizable);
self
}
pub(crate) fn apply_to(&self, spec: &mut WindowSpec) {
if let Some(title) = &self.title {
spec.title = title.clone();
}
if let Some(open) = self.open {
spec.open = open;
}
if let Some(visible) = self.visible {
spec.visible = visible;
}
if let Some(requested_inner_size) = self.requested_inner_size {
spec.requested_inner_size = requested_inner_size;
}
if let Some(min_inner_size) = self.min_inner_size {
spec.min_inner_size = min_inner_size;
}
if let Some(max_inner_size) = self.max_inner_size {
spec.max_inner_size = max_inner_size;
}
if let Some(decorations) = self.decorations {
spec.decorations = decorations;
}
if let Some(maximized) = self.maximized {
spec.maximized = maximized;
}
if let Some(fullscreen) = self.fullscreen {
spec.fullscreen = fullscreen;
}
if let Some(resizable) = self.resizable {
spec.resizable = resizable;
}
}
}