Skip to main content

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}