use crate::event::{Event, EventCtx, Key, NamedKey};
use crate::geometry::{Point, Rect, Size};
use crate::painter::Painter;
use crate::theme::Theme;
use crate::widget::{PopupKind, PopupRequest, Widget};
type DismissHandler = Box<dyn FnMut(&mut EventCtx)>;
pub struct Modal {
open: bool,
title: String,
size: Size,
parent_bounds: Rect,
frozen_origin: Option<Point>,
content: Option<Box<dyn Widget>>,
on_dismiss: Option<DismissHandler>,
}
impl Modal {
pub fn new() -> Self {
Self {
open: false,
title: String::new(),
size: Size::new(320, 160),
parent_bounds: Rect::new(0, 0, 0, 0),
frozen_origin: None,
content: None,
on_dismiss: None,
}
}
pub fn on_dismiss(mut self, handler: impl FnMut(&mut EventCtx) + 'static) -> Self {
self.on_dismiss = Some(Box::new(handler));
self
}
pub fn set_on_dismiss(&mut self, handler: impl FnMut(&mut EventCtx) + 'static) {
self.on_dismiss = Some(Box::new(handler));
}
pub fn show(&mut self, title: impl Into<String>, size: Size, content: Box<dyn Widget>) {
self.title = title.into();
self.size = Size::new(size.w.max(80), size.h.max(48));
self.open = true;
self.frozen_origin = self.centered_origin();
let mut content = content;
if let Some(rect) = self.frozen_origin.map(|o| self.rect_at(o)) {
content.layout(rect);
}
content.focus_first();
self.content = Some(content);
}
pub fn dismiss(&mut self) {
self.open = false;
self.frozen_origin = None;
self.content = None;
}
pub fn is_open(&self) -> bool {
self.open
}
fn centered_origin(&self) -> Option<Point> {
if self.parent_bounds.w <= 0 || self.parent_bounds.h <= 0 {
return None;
}
let px = self.parent_bounds.x + (self.parent_bounds.w - self.size.w) / 2;
let py = self.parent_bounds.y + (self.parent_bounds.h - self.size.h) / 2;
Some(Point::new(px.max(0), py.max(0)))
}
fn rect_at(&self, origin: Point) -> Rect {
Rect::new(origin.x, origin.y, self.size.w, self.size.h)
}
fn rect(&self) -> Rect {
let origin = self.frozen_origin.unwrap_or_else(|| {
let px = self.parent_bounds.x + (self.parent_bounds.w - self.size.w) / 2;
let py = self.parent_bounds.y + (self.parent_bounds.h - self.size.h) / 2;
Point::new(px.max(0), py.max(0))
});
self.rect_at(origin)
}
fn fire_dismiss(&mut self, ctx: &mut EventCtx) {
ctx.dismiss_requested = false;
if let Some(handler) = self.on_dismiss.as_mut() {
handler(ctx);
}
self.dismiss();
ctx.request_paint();
}
}
impl Default for Modal {
fn default() -> Self {
Self::new()
}
}
impl Widget for Modal {
fn bounds(&self) -> Rect {
if self.open {
self.rect()
} else {
Rect::new(0, 0, 0, 0)
}
}
fn layout(&mut self, bounds: Rect) {
self.parent_bounds = bounds;
if self.open {
if self.frozen_origin.is_none() {
self.frozen_origin = self.centered_origin();
}
let rect = self.rect();
if let Some(content) = self.content.as_mut() {
content.layout(rect);
}
}
}
fn paint(&mut self, _painter: &mut Painter, _theme: &Theme) {
}
fn paint_overlay(&mut self, painter: &mut Painter, theme: &Theme) {
if !self.open || !painter.is_popup_pass() {
return;
}
let rect = self.rect();
let is_our_pass = painter.popup_anchor() == Some(rect);
if is_our_pass {
painter.fill_rect(rect, theme.background);
}
if let Some(content) = self.content.as_mut() {
if is_our_pass {
content.paint(painter, theme);
}
content.paint_overlay(painter, theme);
}
}
fn event(&mut self, event: &Event, ctx: &mut EventCtx) {
if !self.open {
return;
}
if let Some(content) = self.content.as_mut() {
content.event(event, ctx);
}
if ctx.is_dismiss_requested() {
self.fire_dismiss(ctx);
return;
}
if matches!(
event,
Event::KeyDown {
key: Key::Named(NamedKey::Escape),
..
}
) && !ctx.is_consumed()
{
if let Some(content) = self.content.as_mut() {
content.on_cancel(ctx);
}
self.fire_dismiss(ctx);
}
}
fn captures_pointer(&self) -> bool {
self.open
}
fn accepts_accelerators(&self) -> bool {
self.open
}
fn popup_request(&self) -> Option<PopupRequest> {
if !self.open {
return None;
}
Some(PopupRequest {
rect: self.rect(),
kind: PopupKind::Dialog,
title: Some(self.title.clone()),
})
}
fn collect_popups(&self, out: &mut Vec<PopupRequest>) {
if let Some(req) = self.popup_request() {
out.push(req);
if let Some(content) = self.content.as_ref() {
content.collect_popups(out);
}
}
}
fn wants_ticks(&self) -> bool {
self.open && self.content.as_ref().is_some_and(|c| c.wants_ticks())
}
}