rosace_widgets/tree/overlay.rs
1use std::cell::RefCell;
2use std::sync::Arc;
3use rosace_core::types::{Point, Rect};
4use rosace_render::Color;
5use super::BoxedWidget;
6
7// ── Public types ──────────────────────────────────────────────────────────────
8
9#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
10pub struct LayerId(pub u64);
11
12impl LayerId {
13 pub fn new() -> Self {
14 use std::sync::atomic::{AtomicU64, Ordering};
15 static COUNTER: AtomicU64 = AtomicU64::new(1);
16 LayerId(COUNTER.fetch_add(1, Ordering::Relaxed))
17 }
18}
19
20impl Default for LayerId {
21 fn default() -> Self { LayerId::new() }
22}
23
24/// Where the overlay widget is placed in window-pixel space.
25#[derive(Clone, Debug)]
26pub enum LayerPosition {
27 /// Top-left corner at this point. Widget chooses its own size.
28 Absolute(Point),
29 /// Centered in the window. Widget chooses its own size.
30 Centered,
31 /// Anchored to the bottom edge, full-width. Widget chooses height.
32 BottomAnchored,
33 /// Horizontally centered, floating 24px above the bottom edge (toasts).
34 BottomCenter,
35 /// Centered horizontally over the anchor rect, floating just above it
36 /// (tooltips). The anchor is in the ATTACHING widget's coordinate
37 /// space — the engine remaps it to window space and clamps on-screen.
38 AboveCentered(rosace_core::types::Rect),
39 /// Fills the entire window.
40 Fill,
41}
42
43/// Controls whether pointer events that miss the overlay widget's rect
44/// fall through to entries below / the main tree, or are absorbed.
45#[derive(Clone, Copy, Debug, PartialEq, Eq)]
46pub enum InputBehavior {
47 /// Misses fall through to the next entry or main tree.
48 PassThrough,
49 /// Misses are absorbed (or trigger scrim dismiss if configured).
50 Block,
51}
52
53/// Controls Tab focus traversal relative to this overlay entry.
54#[derive(Clone, Copy, Debug, PartialEq, Eq)]
55pub enum FocusBehavior {
56 /// Tab continues to entries below after this one is exhausted.
57 PassThrough,
58 /// Tab cycles only within this entry — cannot escape.
59 Trap,
60 /// No focusable nodes. Ignored by all Tab traversal.
61 Inert,
62}
63
64/// Optional translucent background drawn before the overlay widget.
65#[derive(Clone)]
66pub struct ScrimConfig {
67 pub color: Color,
68 /// If `Some`, called when a tap lands outside the overlay widget's rect.
69 pub on_tap: Option<Arc<dyn Fn() + Send + Sync>>,
70 /// A rect (in window space) that is exempt from `on_tap` even though
71 /// it's outside the overlay widget itself — e.g. a Dropdown's own
72 /// trigger button. Without this, clicking the trigger that opened the
73 /// overlay both fires `on_tap` (closing it) AND falls through to the
74 /// trigger's own base-tree click handler (reopening it) in the same
75 /// event, so the dropdown could never close via its own trigger.
76 pub exclude_rect: Option<Rect>,
77}
78
79impl std::fmt::Debug for ScrimConfig {
80 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
81 f.debug_struct("ScrimConfig")
82 .field("color", &self.color)
83 .field("on_tap", &self.on_tap.as_ref().map(|_| "<Fn>"))
84 .field("exclude_rect", &self.exclude_rect)
85 .finish()
86 }
87}
88
89/// A single entry in the overlay stack.
90///
91/// Entries are painted top-to-bottom in insertion order (last = topmost).
92/// See D058 in DECISIONS.md for the full architecture.
93pub struct OverlayEntry {
94 pub id: LayerId,
95 pub position: LayerPosition,
96 pub widget: BoxedWidget,
97 pub input: InputBehavior,
98 pub focus: FocusBehavior,
99 pub scrim: Option<ScrimConfig>,
100}
101
102impl OverlayEntry {
103 pub fn new(position: LayerPosition, widget: impl super::Widget + 'static) -> Self {
104 Self {
105 id: LayerId::new(),
106 position,
107 widget: Box::new(widget),
108 input: InputBehavior::PassThrough,
109 focus: FocusBehavior::PassThrough,
110 scrim: None,
111 }
112 }
113
114 pub fn input(mut self, b: InputBehavior) -> Self { self.input = b; self }
115 pub fn focus(mut self, b: FocusBehavior) -> Self { self.focus = b; self }
116 pub fn scrim(mut self, s: ScrimConfig) -> Self { self.scrim = Some(s); self }
117}
118
119// ── Thread-local registry ─────────────────────────────────────────────────────
120
121thread_local! {
122 static OVERLAY_ENTRIES: RefCell<Vec<OverlayEntry>> = const { RefCell::new(Vec::new()) };
123}
124
125/// Push an overlay entry from within a widget's `paint()` call.
126/// The entry will be composited above the main tree for this frame.
127pub fn push_overlay(entry: OverlayEntry) {
128 OVERLAY_ENTRIES.with(|v| v.borrow_mut().push(entry));
129}
130
131/// Drain all pending overlay entries. Called once per frame by the render loop
132/// after the main paint pass, before the second (overlay) recorder pass.
133pub fn drain_overlays() -> Vec<OverlayEntry> {
134 OVERLAY_ENTRIES.with(|v| v.borrow_mut().drain(..).collect())
135}
136
137/// Clear any leftover overlay entries from the previous frame.
138pub fn clear_overlays() {
139 OVERLAY_ENTRIES.with(|v| v.borrow_mut().clear());
140}