repose_material/material3/
dialog.rs1#![allow(non_snake_case)]
2
3use std::rc::Rc;
4use std::sync::atomic::{AtomicU64, Ordering};
5
6use repose_core::*;
7use repose_ui::overlay::OverlayHandle;
8use repose_ui::{Box, Column, ViewExt, ZStack};
9use web_time::Duration;
10
11use super::AlertDialogDefaults;
12use super::{DatePicker, DatePickerConfig, DatePickerState};
13use super::{TimePicker, TimePickerConfig, TimePickerState};
14
15static DIALOG_COUNTER: AtomicU64 = AtomicU64::new(0);
16
17pub struct DialogState {
19 visible: Signal<bool>,
20 id: u64,
21}
22
23impl Default for DialogState {
24 fn default() -> Self {
25 Self::new()
26 }
27}
28
29impl DialogState {
30 pub fn new() -> Self {
31 Self {
32 visible: signal(false),
33 id: DIALOG_COUNTER.fetch_add(1, Ordering::Relaxed),
34 }
35 }
36
37 pub fn key(&self, suffix: &str) -> String {
38 format!("dlg_{}_{}", self.id, suffix)
39 }
40
41 pub fn is_visible(&self) -> bool {
42 self.visible.get()
43 }
44
45 pub fn show(&self) {
46 self.visible.set(true);
47 }
48
49 pub fn dismiss(&self) {
50 self.visible.set(false);
51 }
52}
53
54#[derive(Clone)]
57pub struct DialogProperties {
58 pub on_dismiss_request: Option<Rc<dyn Fn()>>,
62 pub dismiss_on_click_outside: bool,
65 pub dismiss_on_back_press: bool,
68}
69
70impl Default for DialogProperties {
71 fn default() -> Self {
72 Self {
73 on_dismiss_request: None,
74 dismiss_on_click_outside: true,
75 dismiss_on_back_press: true,
76 }
77 }
78}
79
80pub fn Dialog(
95 state: Rc<DialogState>,
96 overlay: OverlayHandle,
97 modifier: Modifier,
98 properties: DialogProperties,
99 content: View,
100) -> View {
101 let overlay_id = remember_with_key(state.key("oid"), || signal(0u64));
102
103 let current_content = remember_state_with_key(state.key("c"), || Box(Modifier::new()));
105 *current_content.borrow_mut() = content;
106
107 let props = remember_state_with_key(state.key("p"), || properties.clone());
109 *props.borrow_mut() = properties;
110
111 let spec = AnimationSpec::tween(Duration::from_millis(200), Easing::FastOutSlowIn);
113 let anim = remember_state_with_key(state.key("anim"), || AnimatedValue::new(0.0, spec));
114 let last_target = remember_state_with_key(state.key("atarget"), || f32::NAN);
115 let anim_target = if state.is_visible() { 1.0 } else { 0.0 };
116
117 {
118 let mut a = anim.borrow_mut();
119 let mut lt = last_target.borrow_mut();
120 if lt.is_nan() || (*lt - anim_target).abs() > 1e-6 {
121 a.set_spec(spec);
122 a.set_target(anim_target);
123 *lt = anim_target;
124 }
125 drop(lt);
126 if a.update() {
127 request_frame();
128 }
129 }
130
131 let progress = *anim.borrow().get();
132 let visible = state.is_visible() || progress > 0.01;
133
134 if visible {
135 if overlay_id.get() == 0 {
136 let builder: Rc<dyn Fn() -> View> = Rc::new({
137 let state = state.clone();
138 let anim = anim.clone();
139 let modifier = modifier.clone();
140 let current_content = current_content.clone();
141 let props = props.clone();
142 move || {
143 let progress = *anim.borrow().get();
144 let alpha = progress.min(1.0);
145 let scale = 0.8 + 0.2 * progress;
146 let th = theme();
147 let content = current_content.borrow().clone();
148 let _p = props.borrow().clone();
149
150 let dialog = Box(Modifier::new()
152 .min_width(280.0)
153 .max_width(560.0)
154 .then(modifier.clone())
155 .justify_content(JustifyContent::CENTER)
156 .background(th.surface_container_high)
157 .clip_rounded(th.shapes.extra_large)
158 .alpha(alpha)
159 .scale(scale)
160 .focus_group()
161 .clickable()
162 .focusable(false)
163 .on_key_event({
164 let s = state.clone();
165 let p = props.clone();
166 move |ke| {
167 use repose_core::input::{Key, KeyEventType};
168 if ke.key == Key::Escape && ke.event_type == KeyEventType::Down {
169 let (dismiss, cb) = {
170 let p = p.borrow();
171 (p.dismiss_on_back_press, p.on_dismiss_request.clone())
172 };
173 if dismiss {
174 if let Some(cb) = cb {
175 cb();
176 } else {
177 s.dismiss();
178 }
179 return true;
180 }
181 }
182 false
183 }
184 }))
185 .child(content);
186
187 let scrim_color = AlertDialogDefaults::scrim_color();
188 let scrim_alpha = (scrim_color.3 as f32 / 255.0) * alpha;
189 let scrim = Box(Modifier::new()
190 .fill_max_size()
191 .background(scrim_color.with_alpha_f32(scrim_alpha.clamp(0.0, 1.0)))
192 .focusable(false)
193 .input_blocker()
194 .on_scroll(|_| Vec2::default())
195 .on_click({
196 let s = state.clone();
197 let p = props.clone();
198 move || {
199 let (dismiss, cb) = {
200 let p = p.borrow();
201 (p.dismiss_on_click_outside, p.on_dismiss_request.clone())
202 };
203 if dismiss {
204 if let Some(cb) = cb {
205 cb();
206 } else {
207 s.dismiss();
208 }
209 }
210 }
211 }));
212
213 ZStack(Modifier::new().fill_max_size().absolute()).child((
214 scrim,
215 Box(Modifier::new()
216 .fill_max_size()
217 .justify_content(JustifyContent::CENTER)
218 .align_items(AlignItems::CENTER)
219 .hit_passthrough())
220 .child(dialog),
221 ))
222 }
223 });
224
225 let id = overlay.show_entry(builder, 900.0, false);
226 overlay_id.set(id);
227 }
228 } else {
229 let prev = overlay_id.get();
230 if prev != 0 {
231 let _ = overlay.dismiss(prev);
232 overlay_id.set(0);
233 }
234 }
235
236 Box(Modifier::new())
237}
238
239#[derive(Clone, Debug)]
241pub struct AlertDialogConfig {
242 pub modifier: Modifier,
243 pub scrim_color: Color,
244 pub min_width: f32,
245 pub max_width: f32,
246 pub horizontal_padding: f32,
247 pub shape_radius: Option<f32>,
248 pub container_color: Color,
249 pub tonal_elevation: f32,
250}
251
252impl Default for AlertDialogConfig {
253 fn default() -> Self {
254 Self {
255 modifier: Modifier::new(),
256 scrim_color: AlertDialogDefaults::scrim_color(),
257 min_width: AlertDialogDefaults::MIN_WIDTH,
258 max_width: AlertDialogDefaults::MAX_WIDTH,
259 horizontal_padding: AlertDialogDefaults::HORIZONTAL_PADDING,
260 shape_radius: None,
261 container_color: theme().surface_container_high,
262 tonal_elevation: 0.0,
263 }
264 }
265}
266
267pub fn AlertDialog(
272 state: Rc<DialogState>,
273 overlay: OverlayHandle,
274 title: View,
275 text: View,
276 confirm_button: View,
277 dismiss_button: Option<View>,
278 config: AlertDialogConfig,
279) -> View {
280 let content = Box(Modifier::new()
281 .background(config.container_color)
282 .clip_rounded(
283 config
284 .shape_radius
285 .unwrap_or_else(|| theme().shapes.extra_large),
286 ))
287 .child(super::alert_dialog_body(
288 title,
289 text,
290 confirm_button,
291 dismiss_button,
292 ));
293
294 Dialog(
295 state,
296 overlay,
297 Modifier::new()
298 .min_width(config.min_width)
299 .max_width(config.max_width)
300 .then(config.modifier),
301 DialogProperties::default(),
302 content,
303 )
304}
305
306#[derive(Clone)]
308pub struct DatePickerDialogConfig {
309 pub modifier: Modifier,
310 pub shape_radius: Option<f32>,
311 pub colors: super::DatePickerColors,
312}
313
314impl Default for DatePickerDialogConfig {
315 fn default() -> Self {
316 Self {
317 modifier: Modifier::new(),
318 shape_radius: None,
319 colors: super::DatePickerColors::default(),
320 }
321 }
322}
323
324pub fn DatePickerDialog(
330 state: Rc<DialogState>,
331 overlay: OverlayHandle,
332 picker_state: Rc<DatePickerState>,
333 on_confirm: Rc<dyn Fn(i32, u32, u32)>,
334 on_dismiss: Rc<dyn Fn()>,
335 config: DatePickerDialogConfig,
336) -> View {
337 let content = Box(Modifier::new()
338 .background(config.colors.container_color)
339 .clip_rounded(
340 config
341 .shape_radius
342 .unwrap_or_else(|| theme().shapes.extra_large),
343 ))
344 .child(Column(Modifier::new()).child((DatePicker(
345 picker_state.clone(),
346 on_confirm,
347 on_dismiss,
348 DatePickerConfig {
349 colors: config.colors,
350 ..DatePickerConfig::default()
351 },
352 ),)));
353
354 Dialog(
355 state,
356 overlay,
357 config.modifier,
358 DialogProperties::default(),
359 content,
360 )
361}
362
363#[derive(Clone)]
365pub struct TimePickerDialogConfig {
366 pub modifier: Modifier,
367 pub shape_radius: Option<f32>,
368 pub container_color: Color,
369 pub colors: super::TimePickerColors,
370}
371
372impl Default for TimePickerDialogConfig {
373 fn default() -> Self {
374 Self {
375 modifier: Modifier::new(),
376 shape_radius: None,
377 container_color: theme().surface_container_high,
378 colors: super::TimePickerColors::default(),
379 }
380 }
381}
382
383pub fn TimePickerDialog(
389 state: Rc<DialogState>,
390 overlay: OverlayHandle,
391 picker_state: Rc<TimePickerState>,
392 on_confirm: Rc<dyn Fn(u32, u32)>,
393 on_dismiss: Rc<dyn Fn()>,
394 config: TimePickerDialogConfig,
395) -> View {
396 let content = Box(Modifier::new()
397 .background(config.container_color)
398 .clip_rounded(
399 config
400 .shape_radius
401 .unwrap_or_else(|| theme().shapes.extra_large),
402 ))
403 .child(Column(Modifier::new()).child((TimePicker(
404 picker_state.clone(),
405 on_confirm,
406 on_dismiss,
407 TimePickerConfig {
408 colors: config.colors,
409 ..TimePickerConfig::default()
410 },
411 ),)));
412
413 Dialog(
414 state,
415 overlay,
416 config.modifier,
417 DialogProperties::default(),
418 content,
419 )
420}