use crate::event::{Event, EventCtx};
use crate::geometry::{Rect, Size};
use crate::include_svg;
use crate::painter::Painter;
use crate::svg::SvgImage;
use crate::theme::Theme;
use crate::widget::{PopupRequest, Widget};
use crate::widgets::modal::Modal;
use crate::widgets::{Button, Container};
const BUTTON_W: i32 = 70;
const BUTTON_H: i32 = 26;
const ICON_SIZE: i32 = 32;
const PADDING: i32 = 16;
const BUTTON_GAP: i32 = 16;
const MSG_LINE_HEIGHT: i32 = 16;
const DEFAULT_WIDTH: i32 = 340;
const CONFIRM_BTN_PAD: i32 = 14;
const CONFIRM_BTN_GAP: i32 = 10;
const CANCEL_LABEL: &str = "Cancel";
const APPROX_CHAR_W: i32 = 8;
fn message_box_size(message: &str, icon: DialogIcon) -> Size {
let lines = message.split('\n').count() as i32;
let text_h = lines * MSG_LINE_HEIGHT;
let icon_h = if icon == DialogIcon::None {
0
} else {
ICON_SIZE
};
let content_h = text_h.max(icon_h);
let height = PADDING + content_h + BUTTON_GAP + BUTTON_H + PADDING;
Size::new(DEFAULT_WIDTH, height)
}
fn confirm_button_w(label: &str) -> i32 {
(label.chars().count() as i32 * APPROX_CHAR_W + 2 * CONFIRM_BTN_PAD).max(BUTTON_W)
}
fn confirm_box_size(message: &str, icon: DialogIcon, affirm: &str) -> Size {
let base = message_box_size(message, icon);
let longest = message
.split('\n')
.map(|l| l.chars().count() as i32)
.max()
.unwrap_or(0);
let icon_w = if icon == DialogIcon::None {
0
} else {
ICON_SIZE + PADDING
};
let text_w = PADDING + icon_w + longest * APPROX_CHAR_W + PADDING;
let buttons_w = PADDING
+ confirm_button_w(affirm)
+ CONFIRM_BTN_GAP
+ confirm_button_w(CANCEL_LABEL)
+ PADDING;
Size::new(base.w.max(text_w).max(buttons_w), base.h)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum DialogIcon {
None,
Info,
Warning,
Error,
}
pub struct Dialog {
modal: Modal,
size: Option<Size>,
}
impl Dialog {
pub fn new() -> Self {
Self {
modal: Modal::new(),
size: None,
}
}
pub fn with_size(mut self, width: i32, height: i32) -> Self {
self.size = Some(Size::new(width.max(120), height.max(60)));
self
}
pub fn on_dismiss(mut self, handler: impl FnMut(&mut EventCtx) + 'static) -> Self {
self.modal.set_on_dismiss(handler);
self
}
pub fn show(&mut self, title: impl Into<String>, message: impl Into<String>, icon: DialogIcon) {
let message = message.into();
let size = self
.size
.unwrap_or_else(|| message_box_size(&message, icon));
self.modal
.show(title, size, Box::new(MessageBody::new(icon, message)));
}
pub fn show_warning(&mut self, title: impl Into<String>, message: impl Into<String>) {
self.show(title, message, DialogIcon::Warning);
}
pub fn show_info(&mut self, title: impl Into<String>, message: impl Into<String>) {
self.show(title, message, DialogIcon::Info);
}
pub fn show_error(&mut self, title: impl Into<String>, message: impl Into<String>) {
self.show(title, message, DialogIcon::Error);
}
pub fn show_confirm(
&mut self,
title: impl Into<String>,
message: impl Into<String>,
affirm: impl Into<String>,
on_confirm: impl FnMut(&mut EventCtx) + 'static,
) {
let message = message.into();
let affirm = affirm.into();
let icon = DialogIcon::Warning;
let size = self
.size
.unwrap_or_else(|| confirm_box_size(&message, icon, &affirm));
self.modal.show(
title,
size,
Box::new(ConfirmBody::new(
icon,
message,
affirm,
Box::new(on_confirm),
size,
)),
);
}
pub fn dismiss(&mut self) {
self.modal.dismiss();
}
pub fn is_open(&self) -> bool {
self.modal.is_open()
}
}
impl Default for Dialog {
fn default() -> Self {
Self::new()
}
}
impl Widget for Dialog {
fn bounds(&self) -> Rect {
self.modal.bounds()
}
fn layout(&mut self, bounds: Rect) {
self.modal.layout(bounds);
}
fn paint(&mut self, painter: &mut Painter, theme: &Theme) {
self.modal.paint(painter, theme);
}
fn paint_overlay(&mut self, painter: &mut Painter, theme: &Theme) {
self.modal.paint_overlay(painter, theme);
}
fn event(&mut self, event: &Event, ctx: &mut EventCtx) {
self.modal.event(event, ctx);
}
fn captures_pointer(&self) -> bool {
self.modal.captures_pointer()
}
fn accepts_accelerators(&self) -> bool {
self.modal.accepts_accelerators()
}
fn popup_request(&self) -> Option<PopupRequest> {
self.modal.popup_request()
}
fn collect_popups(&self, out: &mut Vec<PopupRequest>) {
self.modal.collect_popups(out);
}
fn wants_ticks(&self) -> bool {
self.modal.wants_ticks()
}
}
struct MessageBody {
icon: DialogIcon,
message: String,
ok: Button,
rect: Rect,
}
impl MessageBody {
fn new(icon: DialogIcon, message: String) -> Self {
let ok = Button::new(Rect::new(0, 0, BUTTON_W, BUTTON_H), "OK")
.default(true)
.on_click(|ctx| ctx.request_dismiss());
Self {
icon,
message,
ok,
rect: Rect::new(0, 0, 0, 0),
}
}
fn button_rect(rect: Rect) -> Rect {
let bx = rect.x + (rect.w - BUTTON_W) / 2;
let by = rect.bottom() - BUTTON_H - PADDING;
Rect::new(bx, by, BUTTON_W, BUTTON_H)
}
}
impl Widget for MessageBody {
fn bounds(&self) -> Rect {
self.rect
}
fn layout(&mut self, bounds: Rect) {
self.rect = bounds;
self.ok.rect = Self::button_rect(bounds);
}
fn paint(&mut self, painter: &mut Painter, theme: &Theme) {
let body = self.rect;
let body_y = body.y + PADDING;
let icon_x = body.x + PADDING;
if self.icon != DialogIcon::None {
draw_icon(painter, icon_x, body_y, ICON_SIZE, self.icon);
}
let msg_x = if self.icon == DialogIcon::None {
body.x + PADDING
} else {
icon_x + ICON_SIZE + PADDING
};
let mut msg_y = body_y;
for line in self.message.split('\n') {
painter.text(msg_x, msg_y, line, theme.font_size, theme.text);
msg_y += (theme.font_size as i32) + 3;
}
self.ok.paint(painter, theme);
}
fn event(&mut self, event: &Event, ctx: &mut EventCtx) {
self.ok.event(event, ctx);
}
fn focusable(&self) -> bool {
true
}
fn focus_first(&mut self) -> bool {
self.ok.set_focused(true);
true
}
fn captures_pointer(&self) -> bool {
self.ok.captures_pointer()
}
}
type ConfirmHandler = Box<dyn FnMut(&mut EventCtx)>;
struct ConfirmBody {
icon: DialogIcon,
message: String,
rect: Rect,
body: Container,
}
impl ConfirmBody {
fn new(
icon: DialogIcon,
message: String,
affirm: String,
mut on_confirm: ConfirmHandler,
size: Size,
) -> Self {
let aw = confirm_button_w(&affirm);
let cw = confirm_button_w(CANCEL_LABEL);
let total = aw + CONFIRM_BTN_GAP + cw;
let bx = (size.w - total) / 2;
let by = size.h - BUTTON_H - PADDING;
let affirm_btn =
Button::new(Rect::new(bx, by, aw, BUTTON_H), affirm).on_click(move |ctx| {
on_confirm(ctx);
ctx.request_dismiss();
});
let cancel_btn = Button::new(
Rect::new(bx + aw + CONFIRM_BTN_GAP, by, cw, BUTTON_H),
CANCEL_LABEL,
)
.default(true)
.on_click(|ctx| ctx.request_dismiss());
let mut body = Container::new(size.w, size.h);
body.push(affirm_btn);
body.push(cancel_btn);
Self {
icon,
message,
rect: Rect::new(0, 0, 0, 0),
body,
}
}
}
impl Widget for ConfirmBody {
fn bounds(&self) -> Rect {
self.rect
}
fn layout(&mut self, bounds: Rect) {
self.rect = bounds;
self.body.layout(bounds);
}
fn paint(&mut self, painter: &mut Painter, theme: &Theme) {
let body = self.rect;
let body_y = body.y + PADDING;
let icon_x = body.x + PADDING;
if self.icon != DialogIcon::None {
draw_icon(painter, icon_x, body_y, ICON_SIZE, self.icon);
}
let msg_x = if self.icon == DialogIcon::None {
body.x + PADDING
} else {
icon_x + ICON_SIZE + PADDING
};
let mut msg_y = body_y;
for line in self.message.split('\n') {
painter.text(msg_x, msg_y, line, theme.font_size, theme.text);
msg_y += (theme.font_size as i32) + 3;
}
self.body.paint(painter, theme);
}
fn event(&mut self, event: &Event, ctx: &mut EventCtx) {
self.body.event(event, ctx);
}
fn focusable(&self) -> bool {
self.body.focusable()
}
fn focus_first(&mut self) -> bool {
self.body.focus_child(1)
}
fn captures_pointer(&self) -> bool {
self.body.captures_pointer()
}
}
const INFO_ICON: SvgImage = include_svg!("assets/dialog/info.svg");
const WARNING_ICON: SvgImage = include_svg!("assets/dialog/warning.svg");
const ERROR_ICON: SvgImage = include_svg!("assets/dialog/error.svg");
fn draw_icon(painter: &mut Painter, x: i32, y: i32, size: i32, icon: DialogIcon) {
let svg = match icon {
DialogIcon::None => return,
DialogIcon::Info => &INFO_ICON,
DialogIcon::Warning => &WARNING_ICON,
DialogIcon::Error => &ERROR_ICON,
};
svg.draw(painter, Rect::new(x, y, size, size));
}