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, Row, Spacer, Text, ViewExt, ZStack};
9use web_time::Duration;
10
11use super::{AlertDialogDefaults, Button, ButtonConfig, TextButton};
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 th = theme();
146 let content = current_content.borrow().clone();
147 let p = props.borrow().clone();
148
149 let dialog = Box(Modifier::new()
151 .min_width(280.0)
152 .max_width(560.0)
153 .then(modifier.clone())
154 .justify_content(JustifyContent::Center)
155 .background(th.surface_container_high)
156 .clip_rounded(th.shapes.extra_large)
157 .alpha(alpha)
158 .focus_group()
159 .on_key_event({
160 let s = state.clone();
161 let p = props.clone();
162 move |ke| {
163 use repose_core::input::{Key, KeyEventType};
164 if ke.key == Key::Escape && ke.event_type == KeyEventType::Down {
165 let (dismiss, cb) = {
166 let p = p.borrow();
167 (p.dismiss_on_back_press, p.on_dismiss_request.clone())
168 };
169 if dismiss {
170 if let Some(cb) = cb {
171 cb();
172 } else {
173 s.dismiss();
174 }
175 return true;
176 }
177 }
178 false
179 }
180 }))
181 .child(content);
182
183 let scrim = Box(Modifier::new()
185 .fill_max_size()
186 .background(th.scrim.with_alpha((85.0 * alpha) as u8))
187 .focusable(false)
188 .on_click({
189 let s = state.clone();
190 let p = props.clone();
191 move || {
192 let (dismiss, cb) = {
193 let p = p.borrow();
194 (p.dismiss_on_click_outside, p.on_dismiss_request.clone())
195 };
196 if dismiss {
197 if let Some(cb) = cb {
198 cb();
199 } else {
200 s.dismiss();
201 }
202 }
203 }
204 }));
205
206 ZStack(Modifier::new().fill_max_size().absolute()).child((
207 scrim,
208 Box(Modifier::new()
209 .fill_max_size()
210 .justify_content(JustifyContent::Center)
211 .align_items(AlignItems::Center)
212 .hit_passthrough())
213 .child(dialog),
214 ))
215 }
216 });
217
218 let id = overlay.show_entry(builder, 900.0, false);
219 overlay_id.set(id);
220 }
221 } else {
222 let prev = overlay_id.get();
223 if prev != 0 {
224 let _ = overlay.dismiss(prev);
225 overlay_id.set(0);
226 }
227 }
228
229 Box(Modifier::new())
230}
231
232#[derive(Clone, Debug)]
234pub struct AlertDialogConfig {
235 pub modifier: Modifier,
236 pub scrim_color: Color,
237 pub min_width: f32,
238 pub max_width: f32,
239 pub horizontal_padding: f32,
240 pub shape_radius: Option<f32>,
241 pub container_color: Color,
242 pub tonal_elevation: f32,
243}
244
245impl Default for AlertDialogConfig {
246 fn default() -> Self {
247 Self {
248 modifier: Modifier::new(),
249 scrim_color: AlertDialogDefaults::scrim_color(),
250 min_width: AlertDialogDefaults::MIN_WIDTH,
251 max_width: AlertDialogDefaults::MAX_WIDTH,
252 horizontal_padding: AlertDialogDefaults::HORIZONTAL_PADDING,
253 shape_radius: None,
254 container_color: theme().surface_container_high,
255 tonal_elevation: 0.0,
256 }
257 }
258}
259
260pub fn AlertDialog(
265 state: Rc<DialogState>,
266 overlay: OverlayHandle,
267 title: View,
268 text: View,
269 confirm_button: View,
270 dismiss_button: Option<View>,
271 config: AlertDialogConfig,
272) -> View {
273 let content = Box(Modifier::new()
274 .background(config.container_color)
275 .clip_rounded(
276 config
277 .shape_radius
278 .unwrap_or_else(|| theme().shapes.extra_large),
279 ))
280 .child(super::alert_dialog_body(
281 title,
282 text,
283 confirm_button,
284 dismiss_button,
285 ));
286
287 Dialog(
288 state,
289 overlay,
290 Modifier::new()
291 .min_width(config.min_width)
292 .max_width(config.max_width)
293 .then(config.modifier),
294 DialogProperties::default(),
295 content,
296 )
297}
298
299#[derive(Clone)]
301pub struct DatePickerDialogConfig {
302 pub modifier: Modifier,
303 pub shape_radius: Option<f32>,
304 pub colors: super::DatePickerColors,
305}
306
307impl Default for DatePickerDialogConfig {
308 fn default() -> Self {
309 Self {
310 modifier: Modifier::new(),
311 shape_radius: None,
312 colors: super::DatePickerColors::default(),
313 }
314 }
315}
316
317pub fn DatePickerDialog(
323 state: Rc<DialogState>,
324 overlay: OverlayHandle,
325 picker_state: Rc<DatePickerState>,
326 on_confirm: Rc<dyn Fn(i32, u32, u32)>,
327 on_dismiss: Rc<dyn Fn()>,
328 config: DatePickerDialogConfig,
329) -> View {
330 let content = Box(Modifier::new()
331 .background(config.colors.container_color)
332 .clip_rounded(
333 config
334 .shape_radius
335 .unwrap_or_else(|| theme().shapes.extra_large),
336 ))
337 .child(Column(Modifier::new()).child((DatePicker(
338 picker_state.clone(),
339 on_confirm,
340 on_dismiss,
341 DatePickerConfig {
342 colors: config.colors,
343 ..DatePickerConfig::default()
344 },
345 ),)));
346
347 Dialog(
348 state,
349 overlay,
350 config.modifier,
351 DialogProperties::default(),
352 content,
353 )
354}
355
356#[derive(Clone)]
358pub struct TimePickerDialogConfig {
359 pub modifier: Modifier,
360 pub shape_radius: Option<f32>,
361 pub container_color: Color,
362 pub colors: super::TimePickerColors,
363}
364
365impl Default for TimePickerDialogConfig {
366 fn default() -> Self {
367 Self {
368 modifier: Modifier::new(),
369 shape_radius: None,
370 container_color: theme().surface_container_high,
371 colors: super::TimePickerColors::default(),
372 }
373 }
374}
375
376pub fn TimePickerDialog(
382 state: Rc<DialogState>,
383 overlay: OverlayHandle,
384 picker_state: Rc<TimePickerState>,
385 on_confirm: Rc<dyn Fn(u32, u32)>,
386 on_dismiss: Rc<dyn Fn()>,
387 config: TimePickerDialogConfig,
388) -> View {
389 let content = Box(Modifier::new()
390 .background(config.container_color)
391 .clip_rounded(
392 config
393 .shape_radius
394 .unwrap_or_else(|| theme().shapes.extra_large),
395 ))
396 .child(Column(Modifier::new()).child((TimePicker(
397 picker_state.clone(),
398 on_confirm,
399 on_dismiss,
400 TimePickerConfig {
401 colors: config.colors,
402 ..TimePickerConfig::default()
403 },
404 ),)));
405
406 Dialog(
407 state,
408 overlay,
409 config.modifier,
410 DialogProperties::default(),
411 content,
412 )
413}