cranpose_ui/widgets/
dialog.rs1use std::rc::Rc;
15
16use cranpose_ui_graphics::{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}
45
46impl Default for DialogSpec {
47 fn default() -> Self {
48 Self {
49 dismiss_on_outside_tap: true,
50 dismiss_on_back: true,
51 apply_window_insets: true,
52 }
53 }
54}
55
56impl DialogSpec {
57 pub const fn required() -> Self {
60 Self {
61 dismiss_on_outside_tap: false,
62 dismiss_on_back: false,
63 apply_window_insets: true,
64 }
65 }
66
67 pub const fn with_dismiss_on_outside_tap(mut self, dismiss: bool) -> Self {
69 self.dismiss_on_outside_tap = dismiss;
70 self
71 }
72
73 pub const fn with_dismiss_on_back(mut self, dismiss: bool) -> Self {
75 self.dismiss_on_back = dismiss;
76 self
77 }
78
79 pub const fn with_window_insets(mut self, apply: bool) -> Self {
81 self.apply_window_insets = apply;
82 self
83 }
84}
85
86pub const DEFAULT_SCRIM: Color = Color(0.0, 0.0, 0.0, 0.4);
89
90pub(crate) fn dialog_bounds(viewport: Size, insets: cranpose_ui_graphics::EdgeInsets) -> Rect {
95 let width = (viewport.width - insets.left - insets.right).max(0.0);
96 let height = (viewport.height - insets.top - insets.bottom).max(0.0);
97 Rect::from_origin_size(
98 Point {
99 x: insets.left,
100 y: insets.top,
101 },
102 Size { width, height },
103 )
104}
105
106#[composable]
128pub fn Dialog<C>(spec: DialogSpec, on_dismiss: impl Fn(DismissReason) + 'static, content: C)
129where
130 C: Fn() + 'static,
131{
132 DialogWithScrim(spec, DEFAULT_SCRIM, on_dismiss, content);
133}
134
135#[composable]
137pub fn DialogWithScrim<C>(
138 spec: DialogSpec,
139 scrim: Color,
140 on_dismiss: impl Fn(DismissReason) + 'static,
141 content: C,
142) where
143 C: Fn() + 'static,
144{
145 let own_modal_depth =
146 cranpose_core::remember(|| crate::modal::current_modal_depth() + 1).with(|depth| *depth);
147
148 let viewport = local_popup_viewport().current().get();
149 let insets = if spec.apply_window_insets {
150 window_insets().combined()
151 } else {
152 cranpose_ui_graphics::EdgeInsets::default()
153 };
154 let bounds = dialog_bounds(viewport, insets);
155
156 let on_dismiss = Rc::new(on_dismiss);
157 let content = Rc::new(content);
158
159 let dismiss_on_outside_tap = spec.dismiss_on_outside_tap;
160 let scrim_dismiss = {
161 let on_dismiss = Rc::clone(&on_dismiss);
162 move || {
163 if dismiss_on_outside_tap {
164 on_dismiss(DismissReason::OutsideTap);
165 }
166 }
167 };
168
169 let dismiss_on_back = spec.dismiss_on_back;
170 let back = {
171 let on_dismiss = Rc::clone(&on_dismiss);
172 cranpose_core::rememberUpdatedState::<Rc<dyn Fn()>>(Rc::new(move || {
173 if dismiss_on_back {
174 on_dismiss(DismissReason::BackRequested);
175 }
176 }))
177 };
178 cranpose_core::DisposableEffect((), move |scope| {
179 let registration = crate::modal::register_modal(Rc::new(move || (back.value())()));
180 scope.on_dispose(move || drop(registration))
181 });
182
183 PopupDismissable(bounds, Point { x: 0.0, y: 0.0 }, scrim_dismiss, move || {
184 let content = Rc::clone(&content);
185 Box(
186 Modifier::empty()
187 .size(Size {
188 width: bounds.width,
189 height: bounds.height,
190 })
191 .background(scrim)
192 .focus_target()
193 .semantics(move |config| {
194 config.role = Some(SemanticsWidgetRole::Dialog);
195 config.is_modal = true;
196 }),
197 BoxSpec::default().content_alignment(Alignment::CENTER),
198 move || {
199 let content = Rc::clone(&content);
200 cranpose_core::CompositionLocalProvider(
201 [crate::modal::local_modal_depth().provides(own_modal_depth)],
202 move || content(),
203 );
204 },
205 );
206 });
207}
208
209#[cfg(test)]
210#[path = "tests/dialog_tests.rs"]
211mod tests;