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