use crate::event::{Event, EventCtx, Key, MouseButton, NamedKey};
use crate::geometry::{Color, Rect, Size};
use crate::painter::Painter;
use crate::theme::Theme;
use crate::widget::{PopupRequest, Widget};
use crate::widgets::modal::Modal;
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),
)),
);
}
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 wants_ticks(&self) -> bool {
self.modal.wants_ticks()
}
}
struct MessageBody {
icon: DialogIcon,
message: String,
rect: Rect,
button_pressed: bool,
button_armed: bool,
}
impl MessageBody {
fn new(icon: DialogIcon, message: String) -> Self {
Self {
icon,
message,
rect: Rect::new(0, 0, 0, 0),
button_pressed: false,
button_armed: false,
}
}
fn button_rect(&self) -> Rect {
let bx = self.rect.x + (self.rect.w - BUTTON_W) / 2;
let by = self.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;
}
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;
}
let btn = self.button_rect();
let pressed = self.button_pressed && self.button_armed;
painter.button(btn, theme, pressed, true);
let inset = if pressed { 1 } else { 0 };
painter.text_centered(
Rect::new(btn.x + inset, btn.y + inset, btn.w, btn.h),
"OK",
theme.font_size,
theme.text,
);
}
fn event(&mut self, event: &Event, ctx: &mut EventCtx) {
let btn = self.button_rect();
match event {
Event::PointerDown {
pos,
button: MouseButton::Left,
} if btn.contains(*pos) => {
self.button_pressed = true;
self.button_armed = true;
ctx.request_paint();
}
Event::PointerMove { pos } if self.button_pressed => {
let in_btn = btn.contains(*pos);
if in_btn != self.button_armed {
self.button_armed = in_btn;
ctx.request_paint();
}
}
Event::PointerUp {
pos,
button: MouseButton::Left,
} if self.button_pressed => {
let fire = self.button_armed && btn.contains(*pos);
self.button_pressed = false;
self.button_armed = false;
ctx.request_paint();
if fire {
ctx.request_dismiss();
}
}
Event::KeyDown {
key: Key::Named(NamedKey::Enter | NamedKey::Space),
..
} => {
ctx.request_dismiss();
}
_ => {}
}
}
}
type ConfirmHandler = Box<dyn FnMut(&mut EventCtx)>;
struct ConfirmBody {
icon: DialogIcon,
message: String,
affirm: String,
on_confirm: ConfirmHandler,
rect: Rect,
affirm_rect: Rect,
cancel_rect: Rect,
pressed: Option<bool>,
armed: bool,
}
impl ConfirmBody {
fn new(icon: DialogIcon, message: String, affirm: String, on_confirm: ConfirmHandler) -> Self {
Self {
icon,
message,
affirm,
on_confirm,
rect: Rect::new(0, 0, 0, 0),
affirm_rect: Rect::new(0, 0, 0, 0),
cancel_rect: Rect::new(0, 0, 0, 0),
pressed: None,
armed: false,
}
}
fn layout_buttons(&mut self, painter: &Painter, theme: &Theme) {
let button_w = |label: &str| {
(painter.measure_text(label, theme.font_size).w + 2 * CONFIRM_BTN_PAD).max(BUTTON_W)
};
let aw = button_w(&self.affirm);
let cw = button_w(CANCEL_LABEL);
let total = aw + CONFIRM_BTN_GAP + cw;
let bx = self.rect.x + (self.rect.w - total) / 2;
let by = self.rect.bottom() - BUTTON_H - PADDING;
self.affirm_rect = Rect::new(bx, by, aw, BUTTON_H);
self.cancel_rect = Rect::new(bx + aw + CONFIRM_BTN_GAP, by, cw, BUTTON_H);
}
fn draw_button(painter: &mut Painter, theme: &Theme, rect: Rect, label: &str, default: bool) {
painter.button(rect, theme, false, default);
painter.text_centered(rect, label, theme.font_size, theme.text);
}
}
impl Widget for ConfirmBody {
fn bounds(&self) -> Rect {
self.rect
}
fn layout(&mut self, bounds: Rect) {
self.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.layout_buttons(painter, theme);
let affirm = self.affirm_rect;
let cancel = self.cancel_rect;
match (self.pressed, self.armed) {
(Some(true), true) => {
painter.button(affirm, theme, true, false);
painter.text_centered(
Rect::new(affirm.x + 1, affirm.y + 1, affirm.w, affirm.h),
&self.affirm,
theme.font_size,
theme.text,
);
Self::draw_button(painter, theme, cancel, CANCEL_LABEL, true);
}
(Some(false), true) => {
Self::draw_button(painter, theme, affirm, &self.affirm, false);
painter.button(cancel, theme, true, true);
painter.text_centered(
Rect::new(cancel.x + 1, cancel.y + 1, cancel.w, cancel.h),
CANCEL_LABEL,
theme.font_size,
theme.text,
);
}
_ => {
Self::draw_button(painter, theme, affirm, &self.affirm, false);
Self::draw_button(painter, theme, cancel, CANCEL_LABEL, true);
}
}
}
fn event(&mut self, event: &Event, ctx: &mut EventCtx) {
match event {
Event::PointerDown {
pos,
button: MouseButton::Left,
} => {
if self.affirm_rect.contains(*pos) {
self.pressed = Some(true);
self.armed = true;
ctx.request_paint();
} else if self.cancel_rect.contains(*pos) {
self.pressed = Some(false);
self.armed = true;
ctx.request_paint();
}
}
Event::PointerMove { pos } if self.pressed.is_some() => {
let rect = if self.pressed == Some(true) {
self.affirm_rect
} else {
self.cancel_rect
};
let over = rect.contains(*pos);
if over != self.armed {
self.armed = over;
ctx.request_paint();
}
}
Event::PointerUp {
pos,
button: MouseButton::Left,
} if self.pressed.is_some() => {
let affirm = self.pressed == Some(true);
let rect = if affirm {
self.affirm_rect
} else {
self.cancel_rect
};
let fire = self.armed && rect.contains(*pos);
self.pressed = None;
self.armed = false;
ctx.request_paint();
if fire {
if affirm {
(self.on_confirm)(ctx);
}
ctx.request_dismiss();
}
}
Event::KeyDown {
key: Key::Named(NamedKey::Enter | NamedKey::Space),
..
} => {
ctx.request_dismiss();
}
_ => {}
}
}
}
fn draw_icon(painter: &mut Painter, x: i32, y: i32, size: i32, icon: DialogIcon) {
match icon {
DialogIcon::None => {}
DialogIcon::Warning => {
let yellow = Color::rgb(0xFF, 0xCC, 0x00);
let black = Color::BLACK;
let apex_x = x + size / 2;
let bottom_y = y + size - 1;
for row in 0..size {
let half = (row as f32 * (size as f32 / 2.0) / size as f32).round() as i32;
let line_x = apex_x - half;
let line_w = (half * 2 + 1).max(1);
painter.h_line(line_x, y + row, line_w, yellow);
}
for row in 0..size {
let half = (row as f32 * (size as f32 / 2.0) / size as f32).round() as i32;
painter.pixel(apex_x - half, y + row, black);
painter.pixel(apex_x + half, y + row, black);
}
painter.h_line(x, bottom_y, size, black);
let bar_x = apex_x - 1;
painter.fill_rect(Rect::new(bar_x, y + 10, 2, 12), black);
painter.fill_rect(Rect::new(bar_x, y + 24, 2, 2), black);
}
DialogIcon::Info => {
let blue = Color::NAVY;
let white = Color::WHITE;
painter.fill_rect(Rect::new(x + 2, y, size - 4, size), blue);
painter.fill_rect(Rect::new(x, y + 2, size, size - 4), blue);
painter.fill_rect(Rect::new(x + 1, y + 1, size - 2, size - 2), blue);
let mid = x + size / 2 - 1;
painter.fill_rect(Rect::new(mid, y + 6, 2, 2), white);
painter.fill_rect(Rect::new(mid, y + 11, 2, 14), white);
}
DialogIcon::Error => {
let red = Color::RED;
let white = Color::WHITE;
painter.fill_rect(Rect::new(x + 2, y, size - 4, size), red);
painter.fill_rect(Rect::new(x, y + 2, size, size - 4), red);
painter.fill_rect(Rect::new(x + 1, y + 1, size - 2, size - 2), red);
for i in 0..size - 12 {
painter.pixel(x + 6 + i, y + 6 + i, white);
painter.pixel(x + 6 + i + 1, y + 6 + i, white);
painter.pixel(x + size - 7 - i, y + 6 + i, white);
painter.pixel(x + size - 7 - i - 1, y + 6 + i, white);
}
}
}
}