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 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}