Skip to main content

repose_ui/
overlay.rs

1use std::cell::RefCell;
2use std::collections::VecDeque;
3use std::rc::Rc;
4
5use repose_core::{request_frame, Modifier, View, ViewKind};
6
7thread_local! {
8    static SNACKBAR_TICK: RefCell<Option<Rc<dyn Fn(u32)>>> = RefCell::new(None);
9}
10
11#[derive(Clone)]
12pub struct OverlayHandle {
13    inner: Rc<RefCell<OverlayState>>,
14}
15
16#[derive(Default)]
17struct OverlayState {
18    next_id: u64,
19    entries: Vec<OverlayEntry>,
20}
21
22#[derive(Clone)]
23pub struct OverlayEntry {
24    pub id: u64,
25    pub builder: Rc<dyn Fn() -> View>,
26    pub z_index: f32,
27    pub pass_through: bool,
28}
29
30impl OverlayHandle {
31    pub fn new() -> Self {
32        Self {
33            inner: Rc::new(RefCell::new(OverlayState {
34                next_id: 1,
35                entries: Vec::new(),
36            })),
37        }
38    }
39
40    pub fn show(&self, view: View) -> u64 {
41        self.show_with(view, 0.0, false)
42    }
43
44    pub fn show_with(&self, view: View, z_index: f32, pass_through: bool) -> u64 {
45        let builder = Rc::new(move || view.clone());
46        self.show_entry(builder, z_index, pass_through)
47    }
48
49    pub fn show_builder(&self, builder: Rc<dyn Fn() -> View>) -> u64 {
50        self.show_entry(builder, 0.0, false)
51    }
52
53    pub fn show_entry(
54        &self,
55        builder: Rc<dyn Fn() -> View>,
56        z_index: f32,
57        pass_through: bool,
58    ) -> u64 {
59        let mut inner = self.inner.borrow_mut();
60        let id = inner.next_id;
61        inner.next_id += 1;
62        inner.entries.push(OverlayEntry {
63            id,
64            builder,
65            z_index,
66            pass_through,
67        });
68        request_frame();
69        id
70    }
71
72    pub fn dismiss(&self, id: u64) -> bool {
73        let mut inner = self.inner.borrow_mut();
74        let before = inner.entries.len();
75        inner.entries.retain(|entry| entry.id != id);
76        let removed = inner.entries.len() != before;
77        if removed {
78            request_frame();
79        }
80        removed
81    }
82
83    pub fn clear(&self) {
84        let mut inner = self.inner.borrow_mut();
85        if !inner.entries.is_empty() {
86            inner.entries.clear();
87            request_frame();
88        }
89    }
90
91    pub fn host(&self, modifier: Modifier, content: View) -> View {
92        let mut root = View::new(0, ViewKind::OverlayHost).modifier(modifier);
93        root.children.push(content);
94        let mut overlays = self.inner.borrow().entries.clone();
95        // Sort ascending by z_index so higher z_index overlays are painted last (on top)
96        overlays.sort_by(|a, b| {
97            a.z_index
98                .partial_cmp(&b.z_index)
99                .unwrap_or(std::cmp::Ordering::Equal)
100        });
101
102        for entry in overlays {
103            let view = (entry.builder)();
104            let mut modifier = view
105                .modifier
106                .clone()
107                .z_index(entry.z_index)
108                .render_z_index(entry.z_index + 1000.0);
109            if entry.pass_through {
110                modifier = modifier.hit_passthrough();
111            }
112            root.children.push(view.modifier(modifier));
113        }
114        root
115    }
116}
117
118#[derive(Clone)]
119pub struct SnackbarController {
120    inner: Rc<RefCell<SnackbarState>>,
121    overlay: OverlayHandle,
122}
123
124#[derive(Clone)]
125pub struct SnackbarAction {
126    pub label: String,
127    pub on_click: Rc<dyn Fn()>,
128}
129
130#[derive(Clone)]
131pub struct SnackbarRequest {
132    pub message: String,
133    pub action: Option<SnackbarAction>,
134    pub duration_ms: u32,
135    pub builder: Rc<dyn Fn() -> View>,
136}
137
138struct SnackbarState {
139    queue: VecDeque<SnackbarRequest>,
140    active: Option<ActiveSnackbar>,
141}
142
143struct ActiveSnackbar {
144    id: u64,
145    message: String,
146    action: Option<SnackbarAction>,
147    remaining_ms: u32,
148}
149
150impl SnackbarController {
151    pub fn new(overlay: OverlayHandle) -> Self {
152        let controller = Self {
153            inner: Rc::new(RefCell::new(SnackbarState {
154                queue: VecDeque::new(),
155                active: None,
156            })),
157            overlay,
158        };
159
160        let tick = {
161            let controller = controller.clone();
162            Rc::new(move |elapsed_ms| controller.tick(elapsed_ms))
163        };
164        SNACKBAR_TICK.with(|slot| *slot.borrow_mut() = Some(tick));
165        controller
166    }
167
168    pub fn tick_for_frame(elapsed_ms: u32) {
169        SNACKBAR_TICK.with(|tick| {
170            if let Some(cb) = &*tick.borrow() {
171                cb(elapsed_ms);
172            }
173        });
174    }
175
176    pub fn show(&self, request: SnackbarRequest) {
177        let mut inner = self.inner.borrow_mut();
178        inner.queue.push_back(request.clone());
179        if inner.active.is_none() {
180            drop(inner);
181            self.activate_next((request.builder)(), request);
182        }
183    }
184
185    pub fn tick(&self, elapsed_ms: u32) {
186        let mut inner = self.inner.borrow_mut();
187        if let Some(active) = inner.active.as_mut() {
188            if elapsed_ms >= active.remaining_ms {
189                self.overlay.dismiss(active.id);
190                inner.active = None;
191            } else {
192                active.remaining_ms -= elapsed_ms;
193            }
194        }
195        drop(inner);
196        self.activate_next_if_needed();
197    }
198
199    pub fn dismiss(&self) {
200        let mut inner = self.inner.borrow_mut();
201        if let Some(active) = inner.active.take() {
202            self.overlay.dismiss(active.id);
203        }
204        drop(inner);
205        self.activate_next_if_needed();
206    }
207
208    pub fn current(&self) -> Option<(String, Option<SnackbarAction>)> {
209        let inner = self.inner.borrow();
210        inner
211            .active
212            .as_ref()
213            .map(|active| (active.message.clone(), active.action.clone()))
214    }
215
216    fn activate_next_if_needed(&self) {
217        let (view, req) = {
218            let mut inner = self.inner.borrow_mut();
219            if inner.active.is_some() {
220                return;
221            }
222            let Some(req) = inner.queue.pop_front() else {
223                return;
224            };
225            let view = (req.builder)();
226            (view, req)
227        };
228        self.activate_next(view, req);
229    }
230
231    fn activate_next(&self, view: View, req: SnackbarRequest) {
232        let mut inner = self.inner.borrow_mut();
233        if inner.active.is_some() {
234            return;
235        }
236        let id = self.overlay.show_with(view, 900.0, true);
237        inner.active = Some(ActiveSnackbar {
238            id,
239            message: req.message,
240            action: req.action,
241            remaining_ms: req.duration_ms.max(1),
242        });
243    }
244}