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