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, ViewExt, ZStack};
9use web_time::Duration;
10
11static DIALOG_COUNTER: AtomicU64 = AtomicU64::new(0);
12
13/// State controlling dialog visibility.
14pub struct DialogState {
15    visible: Signal<bool>,
16    id: u64,
17}
18
19impl Default for DialogState {
20    fn default() -> Self {
21        Self::new()
22    }
23}
24
25impl DialogState {
26    pub fn new() -> Self {
27        Self {
28            visible: signal(false),
29            id: DIALOG_COUNTER.fetch_add(1, Ordering::Relaxed),
30        }
31    }
32
33    pub fn key(&self, suffix: &str) -> String {
34        format!("dlg_{}_{}", self.id, suffix)
35    }
36
37    pub fn is_visible(&self) -> bool {
38        self.visible.get()
39    }
40
41    pub fn show(&self) {
42        self.visible.set(true);
43    }
44
45    pub fn dismiss(&self) {
46        self.visible.set(false);
47    }
48}
49
50/// A modal dialog rendered in the overlay layer with scrim and spring animation.
51///
52/// Unlike the inline `AlertDialog`, this version renders outside the layout tree
53/// so it is never clipped by parent containers, scroll areas, or stacks.
54///
55/// Caller should create a `DialogState` and manage visibility via `show()`/`dismiss()`.
56pub fn Dialog(
57    state: Rc<DialogState>,
58    overlay: OverlayHandle,
59    modifier: Modifier,
60    content: View,
61) -> View {
62    let overlay_id = remember_with_key(state.key("oid"), || signal(0u64));
63
64    // RefCell holding the latest content so the overlay builder reads fresh state each frame
65    let current_content = remember_state_with_key(state.key("c"), || Box(Modifier::new()));
66    *current_content.borrow_mut() = content;
67
68    // Animated scale/alpha for enter/exit
69    let spec = AnimationSpec::tween(Duration::from_millis(200), Easing::FastOutSlowIn);
70    let anim = remember_state_with_key(state.key("anim"), || AnimatedValue::new(0.0, spec));
71    let last_target = remember_state_with_key(state.key("atarget"), || f32::NAN);
72    let anim_target = if state.is_visible() { 1.0 } else { 0.0 };
73
74    {
75        let mut a = anim.borrow_mut();
76        let mut lt = last_target.borrow_mut();
77        if lt.is_nan() || (*lt - anim_target).abs() > 1e-6 {
78            a.set_spec(spec);
79            a.set_target(anim_target);
80            *lt = anim_target;
81        }
82        drop(lt);
83        if a.update() {
84            request_frame();
85        }
86    }
87
88    let progress = *anim.borrow().get();
89    let visible = state.is_visible() || progress > 0.01;
90
91    if visible {
92        if overlay_id.get() == 0 {
93            let builder: Rc<dyn Fn() -> View> = Rc::new({
94                let state = state.clone();
95                let anim = anim.clone();
96                let modifier = modifier.clone();
97                let current_content = current_content.clone();
98                move || {
99                    let progress = *anim.borrow().get();
100                    let alpha = progress.min(1.0);
101                    let th = theme();
102                    let content = current_content.borrow().clone();
103
104                    let dialog = Box(Modifier::new()
105                        .min_width(280.0)
106                        .max_width(560.0)
107                        .then(modifier.clone())
108                        .justify_content(JustifyContent::Center)
109                        .background(th.surface_container_high)
110                        .clip_rounded(th.shapes.extra_large)
111                        .alpha(alpha))
112                    .child(content);
113
114                    let scrim = Box(Modifier::new()
115                        .fill_max_size()
116                        .background(th.scrim.with_alpha((85.0 * alpha) as u8))
117                        .on_pointer_down({
118                            let s = state.clone();
119                            move |_| s.dismiss()
120                        }));
121
122                    ZStack(Modifier::new().fill_max_size().absolute()).child((
123                        scrim,
124                        Box(Modifier::new()
125                            .fill_max_size()
126                            .justify_content(JustifyContent::Center)
127                            .align_items(AlignItems::Center)
128                            .hit_passthrough())
129                        .child(dialog),
130                    ))
131                }
132            });
133
134            let id = overlay.show_entry(builder, 900.0, false);
135            overlay_id.set(id);
136        }
137    } else {
138        let prev = overlay_id.get();
139        if prev != 0 {
140            let _ = overlay.dismiss(prev);
141            overlay_id.set(0);
142        }
143    }
144
145    Box(Modifier::new())
146}
147
148/// An improved AlertDialog using the overlay-based `Dialog`.
149///
150/// Shows a centered modal surface with title, text, confirm button, and optional
151/// dismiss button. Managed via a shared `DialogState`.
152pub fn AlertDialog(
153    state: Rc<DialogState>,
154    overlay: OverlayHandle,
155    title: View,
156    text: View,
157    confirm_button: View,
158    dismiss_button: Option<View>,
159) -> View {
160    Dialog(
161        state,
162        overlay,
163        Modifier::new(),
164        super::alert_dialog_body(title, text, confirm_button, dismiss_button),
165    )
166}