Skip to main content

repose_ui/
overlay.rs

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