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
12pub fn snackbar_set_dismissing(v: bool) {
15 SNACKBAR_DISMISSING.with(|c| c.set(v));
16}
17
18pub 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 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 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}