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,
44 pub alignment: Alignment,
47}
48
49impl Default for DialogSpec {
50 fn default() -> Self {
51 Self {
52 dismiss_on_outside_tap: true,
53 dismiss_on_back: true,
54 apply_window_insets: true,
55 alignment: Alignment::CENTER,
56 }
57 }
58}
59
60impl DialogSpec {
61 pub const fn required() -> Self {
64 Self {
65 dismiss_on_outside_tap: false,
66 dismiss_on_back: false,
67 apply_window_insets: true,
68 alignment: Alignment::CENTER,
69 }
70 }
71
72 pub const fn with_dismiss_on_outside_tap(mut self, dismiss: bool) -> Self {
74 self.dismiss_on_outside_tap = dismiss;
75 self
76 }
77
78 pub const fn with_dismiss_on_back(mut self, dismiss: bool) -> Self {
80 self.dismiss_on_back = dismiss;
81 self
82 }
83
84 pub const fn with_window_insets(mut self, apply: bool) -> Self {
86 self.apply_window_insets = apply;
87 self
88 }
89
90 pub const fn with_alignment(mut self, alignment: Alignment) -> Self {
92 self.alignment = alignment;
93 self
94 }
95}
96
97pub const DEFAULT_SCRIM: Color = Color(0.0, 0.0, 0.0, 0.4);
100
101pub(crate) fn dialog_bounds(viewport: Size, insets: cranpose_ui_graphics::EdgeInsets) -> Rect {
106 let width = (viewport.width - insets.left - insets.right).max(0.0);
107 let height = (viewport.height - insets.top - insets.bottom).max(0.0);
108 Rect::from_origin_size(
109 Point {
110 x: insets.left,
111 y: insets.top,
112 },
113 Size { width, height },
114 )
115}
116
117#[composable]
139pub fn Dialog<C>(spec: DialogSpec, on_dismiss: impl Fn(DismissReason) + 'static, content: C)
140where
141 C: Fn() + 'static,
142{
143 DialogWithScrim(spec, DEFAULT_SCRIM, on_dismiss, content);
144}
145
146#[composable]
148pub fn DialogWithScrim<C>(
149 spec: DialogSpec,
150 scrim: Color,
151 on_dismiss: impl Fn(DismissReason) + 'static,
152 content: C,
153) where
154 C: Fn() + 'static,
155{
156 DialogWithFadingScrim(spec, scrim, || 1.0, on_dismiss, content);
157}
158
159#[composable]
163pub fn DialogWithFadingScrim<C>(
164 spec: DialogSpec,
165 scrim: Color,
166 scrim_alpha: impl Fn() -> f32 + 'static,
167 on_dismiss: impl Fn(DismissReason) + 'static,
168 content: C,
169) where
170 C: Fn() + 'static,
171{
172 let own_modal_depth =
173 cranpose_core::remember(|| crate::modal::current_modal_depth() + 1).with(|depth| *depth);
174
175 let viewport = local_popup_viewport().current().get();
176 let insets = if spec.apply_window_insets {
177 window_insets().combined()
178 } else {
179 cranpose_ui_graphics::EdgeInsets::default()
180 };
181 let bounds = dialog_bounds(viewport, insets);
182
183 let on_dismiss = Rc::new(on_dismiss);
184 let content = Rc::new(content);
185
186 let dismiss_on_outside_tap = spec.dismiss_on_outside_tap;
187 let scrim_dismiss = {
188 let on_dismiss = Rc::clone(&on_dismiss);
189 move || {
190 if dismiss_on_outside_tap {
191 on_dismiss(DismissReason::OutsideTap);
192 }
193 }
194 };
195
196 let dismiss_on_back = spec.dismiss_on_back;
197 let back = {
198 let on_dismiss = Rc::clone(&on_dismiss);
199 cranpose_core::rememberUpdatedState::<Rc<dyn Fn()>>(Rc::new(move || {
200 if dismiss_on_back {
201 on_dismiss(DismissReason::BackRequested);
202 }
203 }))
204 };
205 cranpose_core::DisposableEffect((), move |scope| {
206 let registration = crate::modal::register_modal(Rc::new(move || (back.value())()));
207 scope.on_dispose(move || drop(registration))
208 });
209
210 let scrim_alpha = Rc::new(scrim_alpha);
211 PopupDismissable(bounds, Point { x: 0.0, y: 0.0 }, scrim_dismiss, move || {
212 let content = Rc::clone(&content);
213 let scrim_alpha = Rc::clone(&scrim_alpha);
214 Box(
215 Modifier::empty()
216 .size(Size {
217 width: bounds.width,
218 height: bounds.height,
219 })
220 .draw_behind(move |scope| {
221 let alpha = scrim.3 * scrim_alpha().clamp(0.0, 1.0);
222 scope.draw_rect(Brush::solid(Color(scrim.0, scrim.1, scrim.2, alpha)));
223 })
224 .focus_target()
225 .stable_semantics(move |config| {
226 config.role = Some(SemanticsWidgetRole::Dialog);
227 config.is_modal = true;
228 config.dismiss = dismiss_on_back.then(|| {
231 cranpose_foundation::SemanticsDismiss::new(move || {
232 (back.value())();
233 true
234 })
235 });
236 }),
237 BoxSpec::default().content_alignment(spec.alignment),
238 move || {
239 let content = Rc::clone(&content);
240 Box(
242 Modifier::empty().pointer_opaque(),
243 BoxSpec::default(),
244 move || {
245 let content = Rc::clone(&content);
246 cranpose_core::CompositionLocalProvider(
247 [crate::modal::local_modal_depth().provides(own_modal_depth)],
248 move || content(),
249 );
250 },
251 );
252 },
253 );
254 });
255}
256
257#[cfg(test)]
258#[path = "tests/dialog_tests.rs"]
259mod tests;