#![allow(non_snake_case)]
use std::rc::Rc;
use std::sync::atomic::{AtomicU64, Ordering};
use repose_core::*;
use repose_ui::overlay::OverlayHandle;
use repose_ui::{Box, Column, ViewExt, ZStack};
use web_time::Duration;
use super::AlertDialogDefaults;
use super::{DatePicker, DatePickerConfig, DatePickerState};
use super::{TimePicker, TimePickerConfig, TimePickerState};
static DIALOG_COUNTER: AtomicU64 = AtomicU64::new(0);
pub struct DialogState {
visible: Signal<bool>,
id: u64,
}
impl Default for DialogState {
fn default() -> Self {
Self::new()
}
}
impl DialogState {
pub fn new() -> Self {
Self {
visible: signal(false),
id: DIALOG_COUNTER.fetch_add(1, Ordering::Relaxed),
}
}
pub fn key(&self, suffix: &str) -> String {
format!("dlg_{}_{}", self.id, suffix)
}
pub fn is_visible(&self) -> bool {
self.visible.get()
}
pub fn show(&self) {
self.visible.set(true);
}
pub fn dismiss(&self) {
self.visible.set(false);
}
}
#[derive(Clone)]
pub struct DialogProperties {
pub on_dismiss_request: Option<Rc<dyn Fn()>>,
pub dismiss_on_click_outside: bool,
pub dismiss_on_back_press: bool,
pub use_platform_default_width: bool,
pub use_platform_insets: bool,
}
impl Default for DialogProperties {
fn default() -> Self {
Self {
on_dismiss_request: None,
dismiss_on_click_outside: true,
dismiss_on_back_press: true,
use_platform_default_width: true,
use_platform_insets: true,
}
}
}
fn preferred_dialog_width_dp(container_w: f32, container_h: f32) -> f32 {
let smallest = container_w.min(container_h);
if smallest >= 600.0 {
super::DialogDefaults::PREFERRED_WIDTH_EXPANDED
} else if smallest >= 480.0 {
super::DialogDefaults::PREFERRED_WIDTH_MEDIUM
} else {
super::DialogDefaults::PREFERRED_WIDTH_COMPACT
}
}
fn dialog_available_bounds(
use_platform_default_width: bool,
use_platform_insets: bool,
) -> (f32 /*max_w*/, f32 /*max_h*/, PaddingValues) {
let win_w = get_window_container_width().max(0.0);
let win_h = get_window_container_height().max(0.0);
let mut pad = PaddingValues::default();
if use_platform_insets {
let insets = window_insets();
pad.left = px_to_dp(insets.left);
pad.right = px_to_dp(insets.right);
pad.top = px_to_dp(insets.top);
pad.bottom = px_to_dp(insets.bottom) + px_to_dp(insets.ime_bottom);
}
let avail_w = (win_w - pad.left - pad.right).max(0.0);
let avail_h = (win_h - pad.top - pad.bottom).max(0.0);
let max_w = if use_platform_default_width {
preferred_dialog_width_dp(win_w, win_h)
.min(avail_w)
.min(super::DialogDefaults::MAX_WIDTH)
} else {
avail_w.min(super::DialogDefaults::MAX_WIDTH)
};
let max_h = avail_h;
(max_w.max(0.0), max_h.max(0.0), pad)
}
fn clamp_dialog_modifier(mut m: Modifier, platform_max_w: f32, platform_max_h: f32) -> Modifier {
let max_w = m
.max_width
.unwrap_or(platform_max_w)
.min(platform_max_w)
.max(0.0);
let max_h = m
.max_height
.unwrap_or(platform_max_h)
.min(platform_max_h)
.max(0.0);
m.max_width = Some(max_w);
m.max_height = Some(max_h);
if let Some(min_w) = m.min_width {
m.min_width = Some(min_w.min(max_w).max(0.0));
} else {
m.min_width = Some(super::DialogDefaults::MIN_WIDTH.min(max_w).max(0.0));
}
if let Some(min_h) = m.min_height {
m.min_height = Some(min_h.min(max_h).max(0.0));
}
m
}
pub fn Dialog(
state: Rc<DialogState>,
overlay: OverlayHandle,
modifier: Modifier,
properties: DialogProperties,
content: View,
) -> View {
let overlay_id = remember_with_key(state.key("oid"), || signal(0u64));
let current_content = remember_state_with_key(state.key("c"), || Box(Modifier::new()));
*current_content.borrow_mut() = content;
let props = remember_state_with_key(state.key("p"), || properties.clone());
*props.borrow_mut() = properties;
let scroll_state: Rc<repose_core::scroll::ScrollState> =
remember_with_key(state.key("scroll"), repose_core::scroll::ScrollState::new);
let spec = AnimationSpec::tween(Duration::from_millis(200), Easing::FastOutSlowIn);
let anim = remember_state_with_key(state.key("anim"), || AnimatedValue::new(0.0, spec));
let last_target = remember_state_with_key(state.key("atarget"), || f32::NAN);
let anim_target = if state.is_visible() { 1.0 } else { 0.0 };
{
let mut a = anim.borrow_mut();
let mut lt = last_target.borrow_mut();
if lt.is_nan() || (*lt - anim_target).abs() > 1e-6 {
a.set_spec(spec);
a.set_target(anim_target);
*lt = anim_target;
}
drop(lt);
if a.update() {
request_frame();
}
}
let progress = *anim.borrow().get();
let visible = state.is_visible() || progress > 0.01;
if visible {
if overlay_id.get() == 0 {
let builder: Rc<dyn Fn() -> View> = Rc::new({
let state = state.clone();
let anim = anim.clone();
let modifier = modifier.clone();
let current_content = current_content.clone();
let props = props.clone();
let scroll_state = scroll_state.clone();
move || {
let progress = *anim.borrow().get();
let alpha = progress.min(1.0);
let scale = 0.8 + 0.2 * progress;
let th = theme();
let content = current_content.borrow().clone();
let p = props.borrow().clone();
let (platform_max_w, platform_max_h, inset_pad) = dialog_available_bounds(
p.use_platform_default_width,
p.use_platform_insets,
);
let dialog_mod = clamp_dialog_modifier(
Modifier::new()
.min_width(super::DialogDefaults::MIN_WIDTH)
.max_width(super::DialogDefaults::MAX_WIDTH)
.then(modifier.clone())
.justify_content(JustifyContent::CENTER)
.background(th.surface_container_high)
.clip_rounded(th.shapes.extra_large)
.alpha(alpha)
.scale(scale)
.focus_group()
.clickable()
.focusable(false)
.on_key_event({
let s = state.clone();
let props = props.clone();
move |ke| {
use repose_core::input::{Key, KeyEventType};
if ke.key == Key::Escape && ke.event_type == KeyEventType::Down
{
let (dismiss, cb) = {
let p = props.borrow();
(p.dismiss_on_back_press, p.on_dismiss_request.clone())
};
if dismiss {
if let Some(cb) = cb {
cb();
} else {
s.dismiss();
}
return true;
}
}
false
}
}),
platform_max_w,
platform_max_h,
);
let axis_binding = match scroll_state.to_binding() {
repose_core::scroll::ScrollBinding::Vertical(a) => a,
_ => unreachable!(),
};
let scrollable_body = Box(Modifier::new()
.fill_max_width()
.max_height(platform_max_h)
.vertical_scroll(axis_binding))
.child(content);
let dialog = Box(dialog_mod).child(scrollable_body);
let scrim_color = AlertDialogDefaults::scrim_color();
let scrim_alpha = (scrim_color.3 as f32 / 255.0) * alpha;
let scrim = Box(Modifier::new()
.fill_max_size()
.background(scrim_color.with_alpha_f32(scrim_alpha.clamp(0.0, 1.0)))
.focusable(false)
.input_blocker()
.on_scroll(|_| Vec2::default())
.on_click({
let s = state.clone();
let props = props.clone();
move || {
let (dismiss, cb) = {
let p = props.borrow();
(p.dismiss_on_click_outside, p.on_dismiss_request.clone())
};
if dismiss {
if let Some(cb) = cb {
cb();
} else {
s.dismiss();
}
}
}
}));
ZStack(Modifier::new().fill_max_size().absolute()).child((
scrim,
Box(Modifier::new()
.fill_max_size()
.padding_values(inset_pad)
.justify_content(JustifyContent::CENTER)
.align_items(AlignItems::CENTER)
.hit_passthrough())
.child(dialog),
))
}
});
let id = overlay.show_entry(builder, 900.0, false);
overlay_id.set(id);
}
} else {
let prev = overlay_id.get();
if prev != 0 {
let _ = overlay.dismiss(prev);
overlay_id.set(0);
}
}
Box(Modifier::new())
}
#[derive(Clone, Debug)]
pub struct AlertDialogConfig {
pub modifier: Modifier,
pub scrim_color: Color,
pub min_width: f32,
pub max_width: f32,
pub horizontal_padding: f32,
pub shape_radius: Option<f32>,
pub container_color: Color,
pub tonal_elevation: f32,
}
impl Default for AlertDialogConfig {
fn default() -> Self {
Self {
modifier: Modifier::new(),
scrim_color: AlertDialogDefaults::scrim_color(),
min_width: AlertDialogDefaults::MIN_WIDTH,
max_width: AlertDialogDefaults::MAX_WIDTH,
horizontal_padding: AlertDialogDefaults::HORIZONTAL_PADDING,
shape_radius: None,
container_color: theme().surface_container_high,
tonal_elevation: 0.0,
}
}
}
pub fn AlertDialog(
state: Rc<DialogState>,
overlay: OverlayHandle,
title: View,
text: View,
confirm_button: View,
dismiss_button: Option<View>,
config: AlertDialogConfig,
) -> View {
let content = Box(Modifier::new()
.background(config.container_color)
.clip_rounded(
config
.shape_radius
.unwrap_or_else(|| theme().shapes.extra_large),
))
.child(super::alert_dialog_body(
title,
text,
confirm_button,
dismiss_button,
));
Dialog(
state,
overlay,
Modifier::new()
.min_width(config.min_width)
.max_width(config.max_width)
.then(config.modifier),
DialogProperties::default(),
content,
)
}
#[derive(Clone)]
pub struct DatePickerDialogConfig {
pub modifier: Modifier,
pub shape_radius: Option<f32>,
pub colors: super::DatePickerColors,
}
impl Default for DatePickerDialogConfig {
fn default() -> Self {
Self {
modifier: Modifier::new(),
shape_radius: None,
colors: super::DatePickerColors::default(),
}
}
}
pub fn DatePickerDialog(
state: Rc<DialogState>,
overlay: OverlayHandle,
picker_state: Rc<DatePickerState>,
on_confirm: Rc<dyn Fn(i32, u32, u32)>,
on_dismiss: Rc<dyn Fn()>,
config: DatePickerDialogConfig,
) -> View {
let content = Box(Modifier::new()
.background(config.colors.container_color)
.clip_rounded(
config
.shape_radius
.unwrap_or_else(|| theme().shapes.extra_large),
))
.child(Column(Modifier::new()).child((DatePicker(
picker_state.clone(),
on_confirm,
on_dismiss,
DatePickerConfig {
colors: config.colors,
..DatePickerConfig::default()
},
),)));
Dialog(
state,
overlay,
config.modifier,
DialogProperties::default(),
content,
)
}
#[derive(Clone)]
pub struct TimePickerDialogConfig {
pub modifier: Modifier,
pub shape_radius: Option<f32>,
pub container_color: Color,
pub colors: super::TimePickerColors,
}
impl Default for TimePickerDialogConfig {
fn default() -> Self {
Self {
modifier: Modifier::new(),
shape_radius: None,
container_color: theme().surface_container_high,
colors: super::TimePickerColors::default(),
}
}
}
pub fn TimePickerDialog(
state: Rc<DialogState>,
overlay: OverlayHandle,
picker_state: Rc<TimePickerState>,
on_confirm: Rc<dyn Fn(u32, u32)>,
on_dismiss: Rc<dyn Fn()>,
config: TimePickerDialogConfig,
) -> View {
let content = Box(Modifier::new()
.background(config.container_color)
.clip_rounded(
config
.shape_radius
.unwrap_or_else(|| theme().shapes.extra_large),
))
.child(Column(Modifier::new()).child((TimePicker(
picker_state.clone(),
on_confirm,
on_dismiss,
TimePickerConfig {
colors: config.colors,
..TimePickerConfig::default()
},
),)));
Dialog(
state,
overlay,
config.modifier,
DialogProperties::default(),
content,
)
}