use std::time::Duration;
use crate::anim::Easing;
use crate::base::{Point, Rect, Size};
use crate::layout::{Dimension, Style as LayoutStyle};
use crate::reactive::{after, animate, Scope};
use crate::render::{Cell, Style};
use crate::theme::TokenId;
use crate::ui::{Element, View};
use super::overlays::{LayerHandle, Overlays};
use super::theme::current_theme;
pub const MODAL_Z: i32 = 1000;
pub const TOAST_Z: i32 = 2000;
pub struct Modal {
layer: LayerHandle,
scope: Scope,
}
impl Modal {
pub fn open(
overlays: &Overlays,
cx: Scope,
viewport: Size,
size: Size,
build: impl FnOnce(Scope) -> View,
) -> Modal {
let scope = cx.child();
let bounds = modal_bounds(viewport, size);
let tokens = ¤t_theme().tokens;
let ground = tokens.get(TokenId::Overlay);
let ink = tokens.get(TokenId::Text);
let mut content = build(scope);
content.for_each_style_mut(&mut floor_declared_size);
let panel = Element::new()
.style(
LayoutStyle::default()
.width(Dimension::Percent(1.0))
.height(Dimension::Percent(1.0))
.padding(crate::layout::Edges::all(1)),
)
.role(crate::ui::Role::Dialog)
.focus_trap()
.draw(move |canvas, rect| {
canvas.fill_styled(rect, ' ', &Style::new().fg(ink).bg(ground));
})
.child(content)
.build();
let layer = overlays.layer_tree(MODAL_Z, bounds, true, scope, panel);
let vp_sig = super::viewport::use_viewport(scope);
let panel_layer = layer.clone();
let ov = overlays.clone();
scope.effect_labeled("modal-reclamp", move || {
let vp = vp_sig.get(); if vp.w <= 0 || vp.h <= 0 {
return; }
let want = modal_bounds(vp, size);
let Some(old) = panel_layer.bounds() else {
return; };
if want == old {
return;
}
if want.size() != old.size() {
panel_layer.with_surface(|s| s.resize(want.size(), Cell::EMPTY));
if let Some(mut tree) = panel_layer.tree() {
tree.set_viewport(want.size());
}
}
panel_layer.set_offset(want.origin());
panel_layer.damage();
ov.damage_root_under_rect(old);
});
Modal { layer, scope }
}
pub fn layer(&self) -> &LayerHandle {
&self.layer
}
pub fn close(&self) {
self.layer.remove();
self.scope.dispose();
}
pub fn share(&self) -> Modal {
Modal {
layer: self.layer.clone(),
scope: self.scope,
}
}
}
fn modal_bounds(viewport: Size, size: Size) -> Rect {
Rect::new(
((viewport.w - size.w) / 2).max(0),
((viewport.h - size.h) / 2).max(0),
size.w.min(viewport.w),
size.h.min(viewport.h),
)
}
fn floor_declared_size(style: &mut LayoutStyle) {
if let Dimension::Cells(h) = style.height {
if style.min_height.is_none() {
style.min_height = Some(h);
}
}
if let Dimension::Cells(w) = style.width {
if style.min_width.is_none() {
style.min_width = Some(w);
}
}
}
pub struct Toast;
impl Toast {
pub fn show(
overlays: &Overlays,
cx: Scope,
viewport: Size,
message: impl Into<String>,
duration: Duration,
) -> LayerHandle {
Self::show_with_motion(
overlays,
cx,
viewport,
message,
duration,
Duration::from_millis(200),
)
}
pub fn show_with_motion(
overlays: &Overlays,
cx: Scope,
viewport: Size,
message: impl Into<String>,
duration: Duration,
motion: Duration,
) -> LayerHandle {
let message = message.into();
let tokens = ¤t_theme().tokens;
let ground = tokens.get(TokenId::SurfaceRaised);
let ink = tokens.get(TokenId::Accent);
let w = (crate::text::width(&message) + 2).min(viewport.w.max(1));
let rest = Point::new((viewport.w - w - 1).max(0), 1);
let start = Point::new(rest.x, 0);
let bounds = Rect::new(start.x, start.y, w, 1);
let text = message.clone();
let layer = overlays.layer_draw(TOAST_Z, bounds, move |canvas, rect| {
let style = Style::new().fg(ink).bg(ground);
canvas.fill_styled(rect, ' ', &style);
canvas.print_styled(Point::new(rect.x + 1, rect.y), &text, &style);
});
let progress = cx.signal(0.0f32);
let eased = animate(cx, progress, Easing::EaseOut, motion);
let closing = cx.signal(false);
let handle = layer.clone();
cx.effect_labeled("toast-motion", move || {
let t = eased.get().clamp(0.0, 1.0);
if !handle.is_alive() {
return; }
let y = start.y as f32 + (rest.y - start.y) as f32 * t;
handle.set_offset(Point::new(rest.x, y.round() as i32));
handle.set_opacity(t);
if closing.get() && t <= f32::EPSILON {
handle.remove();
}
});
progress.set(1.0);
after(duration, move || {
closing.set(true);
progress.set(0.0);
});
layer
}
}