use crate::{DesignTokens, UiExt as _};
use egui::NumExt;
#[derive(Default)]
pub struct ModalHandler {
modal: Option<Modal>,
should_open: bool,
}
impl ModalHandler {
pub fn open(&mut self) {
self.should_open = true;
}
pub fn ui<R>(
&mut self,
ctx: &egui::Context,
make_modal: impl FnOnce() -> Modal,
content_ui: impl FnOnce(&mut egui::Ui, &mut bool) -> R,
) -> Option<R> {
if self.modal.is_none() && self.should_open {
self.modal = Some(make_modal());
self.should_open = false;
}
if let Some(modal) = &mut self.modal {
let ModalResponse { inner, open } = modal.ui(ctx, content_ui);
if !open {
self.modal = None;
}
inner
} else {
None
}
}
}
pub struct ModalResponse<R> {
pub inner: Option<R>,
pub open: bool,
}
pub struct Modal {
title: String,
min_width: Option<f32>,
min_height: Option<f32>,
default_height: Option<f32>,
full_span_content: bool,
}
impl Modal {
pub fn new(title: &str) -> Self {
Self {
title: title.to_owned(),
min_width: None,
min_height: None,
default_height: None,
full_span_content: false,
}
}
#[inline]
pub fn min_width(mut self, min_width: f32) -> Self {
self.min_width = Some(min_width);
self
}
#[inline]
pub fn min_height(mut self, min_height: f32) -> Self {
self.min_height = Some(min_height);
self
}
#[inline]
pub fn default_height(mut self, default_height: f32) -> Self {
self.default_height = Some(default_height);
self
}
#[inline]
pub fn full_span_content(mut self, full_span_content: bool) -> Self {
self.full_span_content = full_span_content;
self
}
pub fn ui<R>(
&mut self,
ctx: &egui::Context,
content_ui: impl FnOnce(&mut egui::Ui, &mut bool) -> R,
) -> ModalResponse<R> {
Self::dim_background(ctx);
let mut open = ctx.input_mut(|i| !i.consume_key(egui::Modifiers::NONE, egui::Key::Escape));
let screen_height = ctx.screen_rect().height();
let modal_vertical_margins = (75.0).at_most(screen_height * 0.1);
let mut window = egui::Window::new(&self.title)
.pivot(egui::Align2::CENTER_TOP)
.fixed_pos(ctx.screen_rect().center_top() + egui::vec2(0.0, modal_vertical_margins))
.constrain_to(ctx.screen_rect())
.max_height(screen_height - 2.0 * modal_vertical_margins)
.collapsible(false)
.resizable(true)
.frame(egui::Frame {
fill: ctx.style().visuals.panel_fill,
..Default::default()
})
.title_bar(false);
if let Some(min_width) = self.min_width {
window = window.min_width(min_width);
}
if let Some(min_height) = self.min_height {
window = window.min_height(min_height);
}
if let Some(default_height) = self.default_height {
window = window.default_height(default_height);
}
let response = window.show(ctx, |ui| {
let item_spacing_y = ui.spacing().item_spacing.y;
ui.spacing_mut().item_spacing.y = 0.0;
egui::Frame {
inner_margin: egui::Margin::symmetric(DesignTokens::view_padding(), 0.0),
..Default::default()
}
.show(ui, |ui| {
ui.add_space(DesignTokens::view_padding());
Self::title_bar(ui, &self.title, &mut open);
ui.add_space(DesignTokens::view_padding());
ui.full_span_separator();
if self.full_span_content {
content_ui(ui, &mut open)
} else {
ui.add_space(item_spacing_y);
egui::Frame {
inner_margin: egui::Margin {
bottom: DesignTokens::view_padding(),
..Default::default()
},
..Default::default()
}
.show(ui, |ui| {
ui.spacing_mut().item_spacing.y = item_spacing_y;
content_ui(ui, &mut open)
})
.inner
}
})
.inner
});
let cursor_was_over_window = response
.as_ref()
.and_then(|response| {
ctx.input(|i| i.pointer.interact_pos())
.map(|interact_pos| response.response.rect.contains(interact_pos))
})
.unwrap_or(false);
if !cursor_was_over_window && ctx.input(|i| i.pointer.any_pressed()) {
open = false;
}
ModalResponse {
inner: response.and_then(|response| response.inner),
open,
}
}
#[allow(clippy::needless_pass_by_ref_mut)]
fn dim_background(ctx: &egui::Context) {
let painter = egui::Painter::new(
ctx.clone(),
egui::LayerId::new(egui::Order::PanelResizeLine, egui::Id::new("DimLayer")),
egui::Rect::EVERYTHING,
);
painter.add(egui::Shape::rect_filled(
ctx.screen_rect(),
egui::Rounding::ZERO,
egui::Color32::from_black_alpha(128),
));
}
fn title_bar(ui: &mut egui::Ui, title: &str, open: &mut bool) {
ui.horizontal(|ui| {
ui.strong(title);
ui.add_space(16.0);
let mut ui = ui.new_child(
egui::UiBuilder::new()
.max_rect(ui.max_rect())
.layout(egui::Layout::right_to_left(egui::Align::Center)),
);
if ui.small_icon_button(&crate::icons::CLOSE).clicked() {
*open = false;
}
});
}
}