use std::rc::Rc;
use cranpose_ui_graphics::{Color, Point, Rect, Size};
use cranpose_ui_layout::Alignment;
use super::{
box_widget::{Box, BoxSpec},
popup::{PopupDismissable, local_popup_viewport},
};
use crate::{Modifier, SemanticsWidgetRole, composable, safe_area::window_insets};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum DismissReason {
OutsideTap,
BackRequested,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct DialogSpec {
pub dismiss_on_outside_tap: bool,
pub dismiss_on_back: bool,
pub apply_window_insets: bool,
}
impl Default for DialogSpec {
fn default() -> Self {
Self {
dismiss_on_outside_tap: true,
dismiss_on_back: true,
apply_window_insets: true,
}
}
}
impl DialogSpec {
pub const fn required() -> Self {
Self {
dismiss_on_outside_tap: false,
dismiss_on_back: false,
apply_window_insets: true,
}
}
pub const fn with_dismiss_on_outside_tap(mut self, dismiss: bool) -> Self {
self.dismiss_on_outside_tap = dismiss;
self
}
pub const fn with_dismiss_on_back(mut self, dismiss: bool) -> Self {
self.dismiss_on_back = dismiss;
self
}
pub const fn with_window_insets(mut self, apply: bool) -> Self {
self.apply_window_insets = apply;
self
}
}
pub const DEFAULT_SCRIM: Color = Color(0.0, 0.0, 0.0, 0.4);
pub(crate) fn dialog_bounds(viewport: Size, insets: cranpose_ui_graphics::EdgeInsets) -> Rect {
let width = (viewport.width - insets.left - insets.right).max(0.0);
let height = (viewport.height - insets.top - insets.bottom).max(0.0);
Rect::from_origin_size(
Point {
x: insets.left,
y: insets.top,
},
Size { width, height },
)
}
#[composable]
pub fn Dialog<C>(spec: DialogSpec, on_dismiss: impl Fn(DismissReason) + 'static, content: C)
where
C: Fn() + 'static,
{
DialogWithScrim(spec, DEFAULT_SCRIM, on_dismiss, content);
}
#[composable]
pub fn DialogWithScrim<C>(
spec: DialogSpec,
scrim: Color,
on_dismiss: impl Fn(DismissReason) + 'static,
content: C,
) where
C: Fn() + 'static,
{
let own_modal_depth =
cranpose_core::remember(|| crate::modal::current_modal_depth() + 1).with(|depth| *depth);
let viewport = local_popup_viewport().current().get();
let insets = if spec.apply_window_insets {
window_insets().combined()
} else {
cranpose_ui_graphics::EdgeInsets::default()
};
let bounds = dialog_bounds(viewport, insets);
let on_dismiss = Rc::new(on_dismiss);
let content = Rc::new(content);
let dismiss_on_outside_tap = spec.dismiss_on_outside_tap;
let scrim_dismiss = {
let on_dismiss = Rc::clone(&on_dismiss);
move || {
if dismiss_on_outside_tap {
on_dismiss(DismissReason::OutsideTap);
}
}
};
let dismiss_on_back = spec.dismiss_on_back;
let back = {
let on_dismiss = Rc::clone(&on_dismiss);
cranpose_core::rememberUpdatedState::<Rc<dyn Fn()>>(Rc::new(move || {
if dismiss_on_back {
on_dismiss(DismissReason::BackRequested);
}
}))
};
cranpose_core::DisposableEffect((), move |scope| {
let registration = crate::modal::register_modal(Rc::new(move || (back.value())()));
scope.on_dispose(move || drop(registration))
});
PopupDismissable(bounds, Point { x: 0.0, y: 0.0 }, scrim_dismiss, move || {
let content = Rc::clone(&content);
Box(
Modifier::empty()
.size(Size {
width: bounds.width,
height: bounds.height,
})
.background(scrim)
.focus_target()
.semantics(move |config| {
config.role = Some(SemanticsWidgetRole::Dialog);
config.is_modal = true;
}),
BoxSpec::default().content_alignment(Alignment::CENTER),
move || {
let content = Rc::clone(&content);
cranpose_core::CompositionLocalProvider(
[crate::modal::local_modal_depth().provides(own_modal_depth)],
move || content(),
);
},
);
});
}
#[cfg(test)]
#[path = "tests/dialog_tests.rs"]
mod tests;