1use std::rc::Rc;
15
16use cranpose_ui_graphics::{Brush, Color, Point, Rect, Size};
17use cranpose_ui_layout::Alignment;
18
19use super::{
20 box_widget::{Box, BoxSpec},
21 popup::{PopupDismissable, local_popup_viewport},
22};
23use crate::{Modifier, SemanticsWidgetRole, composable, safe_area::window_insets};
24
25#[derive(Clone, Copy, Debug, PartialEq, Eq)]
27pub enum DismissReason {
28 OutsideTap,
30 BackRequested,
32}
33
34#[derive(Clone, Copy, Debug, PartialEq, Eq)]
36pub struct DialogSpec {
37 pub dismiss_on_outside_tap: bool,
39 pub dismiss_on_back: bool,
41 pub apply_window_insets: bool,
45 pub alignment: Alignment,
48}
49
50impl Default for DialogSpec {
51 fn default() -> Self {
52 Self {
53 dismiss_on_outside_tap: true,
54 dismiss_on_back: true,
55 apply_window_insets: true,
56 alignment: Alignment::CENTER,
57 }
58 }
59}
60
61impl DialogSpec {
62 pub const fn required() -> Self {
65 Self {
66 dismiss_on_outside_tap: false,
67 dismiss_on_back: false,
68 apply_window_insets: true,
69 alignment: Alignment::CENTER,
70 }
71 }
72
73 pub const fn with_dismiss_on_outside_tap(mut self, dismiss: bool) -> Self {
75 self.dismiss_on_outside_tap = dismiss;
76 self
77 }
78
79 pub const fn with_dismiss_on_back(mut self, dismiss: bool) -> Self {
81 self.dismiss_on_back = dismiss;
82 self
83 }
84
85 pub const fn with_window_insets(mut self, apply: bool) -> Self {
87 self.apply_window_insets = apply;
88 self
89 }
90
91 pub const fn with_alignment(mut self, alignment: Alignment) -> Self {
93 self.alignment = alignment;
94 self
95 }
96}
97
98pub const DEFAULT_SCRIM: Color = Color(0.0, 0.0, 0.0, 0.4);
101
102pub(crate) fn dialog_bounds(viewport: Size, insets: cranpose_ui_graphics::EdgeInsets) -> Rect {
107 let width = (viewport.width - insets.left - insets.right).max(0.0);
108 let height = (viewport.height - insets.top - insets.bottom).max(0.0);
109 Rect::from_origin_size(
110 Point {
111 x: insets.left,
112 y: insets.top,
113 },
114 Size { width, height },
115 )
116}
117
118#[composable]
140pub fn Dialog<C>(spec: DialogSpec, on_dismiss: impl Fn(DismissReason) + 'static, content: C)
141where
142 C: Fn() + 'static,
143{
144 DialogWithScrim(spec, DEFAULT_SCRIM, on_dismiss, content);
145}
146
147#[composable]
149pub fn DialogWithScrim<C>(
150 spec: DialogSpec,
151 scrim: Color,
152 on_dismiss: impl Fn(DismissReason) + 'static,
153 content: C,
154) where
155 C: Fn() + 'static,
156{
157 DialogWithFadingScrim(spec, scrim, || 1.0, on_dismiss, content);
158}
159
160#[composable]
164pub fn DialogWithFadingScrim<C>(
165 spec: DialogSpec,
166 scrim: Color,
167 scrim_alpha: impl Fn() -> f32 + 'static,
168 on_dismiss: impl Fn(DismissReason) + 'static,
169 content: C,
170) where
171 C: Fn() + 'static,
172{
173 let own_modal_depth =
174 cranpose_core::remember(|| crate::modal::current_modal_depth() + 1).with(|depth| *depth);
175
176 let viewport = local_popup_viewport().current().get();
177 let insets = if spec.apply_window_insets {
178 window_insets().combined()
179 } else {
180 cranpose_ui_graphics::EdgeInsets::default()
181 };
182 let bounds = dialog_bounds(viewport, insets);
183 let window = Rect::from_origin_size(Point { x: 0.0, y: 0.0 }, viewport);
184
185 let on_dismiss = Rc::new(on_dismiss);
186 let content = Rc::new(content);
187
188 let dismiss_on_outside_tap = spec.dismiss_on_outside_tap;
189 let scrim_dismiss = {
190 let on_dismiss = Rc::clone(&on_dismiss);
191 move || {
192 if dismiss_on_outside_tap {
193 on_dismiss(DismissReason::OutsideTap);
194 }
195 }
196 };
197
198 let dismiss_on_back = spec.dismiss_on_back;
199 let back = {
200 let on_dismiss = Rc::clone(&on_dismiss);
201 cranpose_core::rememberUpdatedState::<Rc<dyn Fn()>>(Rc::new(move || {
202 if dismiss_on_back {
203 on_dismiss(DismissReason::BackRequested);
204 }
205 }))
206 };
207 cranpose_core::DisposableEffect((), move |scope| {
208 let registration = crate::modal::register_modal(Rc::new(move || (back.value())()));
209 scope.on_dispose(move || drop(registration))
210 });
211
212 let scrim_alpha = Rc::new(scrim_alpha);
213 PopupDismissable(window, Point { x: 0.0, y: 0.0 }, scrim_dismiss, move || {
214 let content = Rc::clone(&content);
215 let scrim_alpha = Rc::clone(&scrim_alpha);
216 Box(
217 Modifier::empty()
218 .size(viewport)
219 .draw_behind(move |scope| {
220 let alpha = scrim.3 * scrim_alpha().clamp(0.0, 1.0);
221 scope.draw_rect(Brush::solid(Color(scrim.0, scrim.1, scrim.2, alpha)));
222 })
223 .focus_target()
224 .stable_semantics(move |config| {
225 config.role = Some(SemanticsWidgetRole::Dialog);
226 config.is_modal = true;
227 config.dismiss = dismiss_on_back.then(|| {
230 cranpose_foundation::SemanticsDismiss::new(move || {
231 (back.value())();
232 true
233 })
234 });
235 }),
236 BoxSpec::default(),
237 move || {
238 let content = Rc::clone(&content);
239 Box(
240 Modifier::empty()
241 .absolute_offset(bounds.x, bounds.y)
242 .size(Size {
243 width: bounds.width,
244 height: bounds.height,
245 }),
246 BoxSpec::default().content_alignment(spec.alignment),
247 move || {
248 let content = Rc::clone(&content);
249 Box(
251 Modifier::empty().pointer_opaque(),
252 BoxSpec::default(),
253 move || {
254 let content = Rc::clone(&content);
255 cranpose_core::CompositionLocalProvider(
256 [crate::modal::local_modal_depth().provides(own_modal_depth)],
257 move || content(),
258 );
259 },
260 );
261 },
262 );
263 },
264 );
265 });
266}
267
268#[cfg(test)]
269#[path = "tests/dialog_tests.rs"]
270mod tests;