Skip to main content

repose_material/material3/
dialog.rs

1#![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, DatePickerState};
13use super::{TimePicker, TimePickerState};
14
15static DIALOG_COUNTER: AtomicU64 = AtomicU64::new(0);
16
17/// State controlling dialog visibility.
18pub 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/// A modal dialog rendered in the overlay layer with scrim and spring animation.
55///
56/// Unlike the inline `AlertDialog`, this version renders outside the layout tree
57/// so it is never clipped by parent containers, scroll areas, or stacks.
58///
59/// Caller should create a `DialogState` and manage visibility via `show()`/`dismiss()`.
60pub fn Dialog(
61    state: Rc<DialogState>,
62    overlay: OverlayHandle,
63    modifier: Modifier,
64    content: View,
65) -> View {
66    let overlay_id = remember_with_key(state.key("oid"), || signal(0u64));
67
68    // RefCell holding the latest content so the overlay builder reads fresh state each frame
69    let current_content = remember_state_with_key(state.key("c"), || Box(Modifier::new()));
70    *current_content.borrow_mut() = content;
71
72    // Animated scale/alpha for enter/exit
73    let spec = AnimationSpec::tween(Duration::from_millis(200), Easing::FastOutSlowIn);
74    let anim = remember_state_with_key(state.key("anim"), || AnimatedValue::new(0.0, spec));
75    let last_target = remember_state_with_key(state.key("atarget"), || f32::NAN);
76    let anim_target = if state.is_visible() { 1.0 } else { 0.0 };
77
78    {
79        let mut a = anim.borrow_mut();
80        let mut lt = last_target.borrow_mut();
81        if lt.is_nan() || (*lt - anim_target).abs() > 1e-6 {
82            a.set_spec(spec);
83            a.set_target(anim_target);
84            *lt = anim_target;
85        }
86        drop(lt);
87        if a.update() {
88            request_frame();
89        }
90    }
91
92    let progress = *anim.borrow().get();
93    let visible = state.is_visible() || progress > 0.01;
94
95    if visible {
96        if overlay_id.get() == 0 {
97            let builder: Rc<dyn Fn() -> View> = Rc::new({
98                let state = state.clone();
99                let anim = anim.clone();
100                let modifier = modifier.clone();
101                let current_content = current_content.clone();
102                move || {
103                    let progress = *anim.borrow().get();
104                    let alpha = progress.min(1.0);
105                    let th = theme();
106                    let content = current_content.borrow().clone();
107
108                    let dialog = Box(Modifier::new()
109                        .min_width(280.0)
110                        .max_width(560.0)
111                        .then(modifier.clone())
112                        .justify_content(JustifyContent::Center)
113                        .background(th.surface_container_high)
114                        .clip_rounded(th.shapes.extra_large)
115                        .alpha(alpha))
116                    .child(content);
117
118                    let scrim = Box(Modifier::new()
119                        .fill_max_size()
120                        .background(th.scrim.with_alpha((85.0 * alpha) as u8))
121                        .on_pointer_down({
122                            let s = state.clone();
123                            move |_| s.dismiss()
124                        }));
125
126                    ZStack(Modifier::new().fill_max_size().absolute()).child((
127                        scrim,
128                        Box(Modifier::new()
129                            .fill_max_size()
130                            .justify_content(JustifyContent::Center)
131                            .align_items(AlignItems::Center)
132                            .hit_passthrough())
133                        .child(dialog),
134                    ))
135                }
136            });
137
138            let id = overlay.show_entry(builder, 900.0, false);
139            overlay_id.set(id);
140        }
141    } else {
142        let prev = overlay_id.get();
143        if prev != 0 {
144            let _ = overlay.dismiss(prev);
145            overlay_id.set(0);
146        }
147    }
148
149    Box(Modifier::new())
150}
151
152/// Configuration for alert dialog.
153#[derive(Clone, Debug)]
154pub struct AlertDialogConfig {
155    pub modifier: Modifier,
156    pub scrim_color: Color,
157    pub min_width: f32,
158    pub max_width: f32,
159    pub horizontal_padding: f32,
160}
161
162impl Default for AlertDialogConfig {
163    fn default() -> Self {
164        Self {
165            modifier: Modifier::new(),
166            scrim_color: AlertDialogDefaults::scrim_color(),
167            min_width: AlertDialogDefaults::MIN_WIDTH,
168            max_width: AlertDialogDefaults::MAX_WIDTH,
169            horizontal_padding: AlertDialogDefaults::HORIZONTAL_PADDING,
170        }
171    }
172}
173
174/// An improved AlertDialog using the overlay-based `Dialog`.
175///
176/// Shows a centered modal surface with title, text, confirm button, and optional
177/// dismiss button. Managed via a shared `DialogState`.
178pub fn AlertDialog(
179    state: Rc<DialogState>,
180    overlay: OverlayHandle,
181    title: View,
182    text: View,
183    confirm_button: View,
184    dismiss_button: Option<View>,
185    config: AlertDialogConfig,
186) -> View {
187    Dialog(
188        state,
189        overlay,
190        Modifier::new()
191            .min_width(config.min_width)
192            .max_width(config.max_width)
193            .then(config.modifier),
194        super::alert_dialog_body(title, text, confirm_button, dismiss_button),
195    )
196}
197
198/// M3 Date Picker Dialog - wraps [`DatePicker`] inside a modal [`Dialog`]
199/// with confirm/cancel buttons. Equivalent to Compose's `DatePickerDialog`.
200///
201/// The `on_confirm` callback fires when a day is clicked or the OK button is pressed.
202/// The `on_dismiss` fires on Cancel or scrim tap.
203pub fn DatePickerDialog(
204    state: Rc<DialogState>,
205    overlay: OverlayHandle,
206    picker_state: Rc<DatePickerState>,
207    on_confirm: Rc<dyn Fn(i32, u32, u32)>,
208    on_dismiss: Rc<dyn Fn()>,
209) -> View {
210    Dialog(
211        state,
212        overlay,
213        Modifier::new(),
214        Column(Modifier::new()).child((DatePicker(picker_state.clone(), on_confirm, on_dismiss),)),
215    )
216}
217
218/// M3 Time Picker Dialog - wraps [`TimePicker`] inside a modal [`Dialog`]
219/// with confirm/cancel buttons. Equivalent to Compose's `TimePickerDialog`.
220///
221/// The `on_confirm` callback fires when OK is pressed.
222/// The `on_dismiss` fires on Cancel or scrim tap.
223pub fn TimePickerDialog(
224    state: Rc<DialogState>,
225    overlay: OverlayHandle,
226    picker_state: Rc<TimePickerState>,
227    on_confirm: Rc<dyn Fn(u32, u32)>,
228    on_dismiss: Rc<dyn Fn()>,
229) -> View {
230    Dialog(
231        state,
232        overlay,
233        Modifier::new(),
234        Column(Modifier::new()).child((TimePicker(picker_state.clone(), on_confirm, on_dismiss),)),
235    )
236}