use eframe::emath::NumExt as _;
use egui::{Frame, ModalResponse};
use crate::{DesignTokens, UiExt as _, context_ext::ContextExt as _};
#[derive(Default)]
pub struct ModalHandler {
modal: Option<ModalWrapper>,
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() -> ModalWrapper,
content_ui: impl FnOnce(&mut egui::Ui) -> 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 response = modal.ui(ctx, content_ui);
if response.should_close() {
self.modal = None;
}
Some(response.inner)
} else {
None
}
}
}
pub struct ModalWrapper {
title: String,
min_width: Option<f32>,
min_height: Option<f32>,
default_height: Option<f32>,
full_span_content: bool,
set_side_margins: bool,
scrollable: egui::Vec2b,
}
impl ModalWrapper {
pub fn new(title: &str) -> Self {
Self {
title: title.to_owned(),
min_width: None,
min_height: None,
default_height: None,
full_span_content: false,
set_side_margins: true,
scrollable: false.into(),
}
}
#[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
}
#[inline]
pub fn set_side_margin(mut self, set_side_margins: bool) -> Self {
self.set_side_margins = set_side_margins;
self
}
#[inline]
pub fn scrollable(mut self, scrollable: impl Into<egui::Vec2b>) -> Self {
self.scrollable = scrollable.into();
self
}
pub fn ui<R>(
&self,
ctx: &egui::Context,
content_ui: impl FnOnce(&mut egui::Ui) -> R,
) -> ModalResponse<R> {
let tokens = ctx.tokens();
let id = egui::Id::new(&self.title);
let mut area = egui::Modal::default_area(id);
if let Some(default_height) = self.default_height {
area = area.default_height(default_height);
}
let modal_response = egui::Modal::new(id.with("modal"))
.frame(Frame::new())
.area(area)
.show(ctx, |ui| {
prevent_shrinking(ui);
egui::Frame {
fill: ctx.style().visuals.panel_fill,
..Default::default()
}
.show(ui, |ui| {
ui.set_clip_rect(ui.max_rect());
let item_spacing_y = ui.spacing().item_spacing.y;
ui.spacing_mut().item_spacing.y = 0.0;
if let Some(min_width) = self.min_width {
ui.set_min_width(min_width);
}
if let Some(min_height) = self.min_height {
ui.set_min_height(min_height);
}
view_padding_frame(
tokens,
&ViewPaddingFrameParams {
left_and_right: true,
top: true,
bottom: false,
},
)
.show(ui, |ui| {
Self::title_bar(ui, &self.title);
ui.add_space(tokens.view_padding() as f32);
ui.full_span_separator();
});
let wrapped_content_ui = |ui: &mut egui::Ui| -> R {
view_padding_frame(
tokens,
&ViewPaddingFrameParams {
left_and_right: self.set_side_margins,
top: false,
bottom: false,
},
)
.show(ui, |ui| {
if self.full_span_content {
content_ui(ui)
} else {
ui.add_space(item_spacing_y);
view_padding_frame(
tokens,
&ViewPaddingFrameParams {
left_and_right: false,
top: false,
bottom: true,
},
)
.show(ui, |ui| {
ui.spacing_mut().item_spacing.y = item_spacing_y;
content_ui(ui)
})
.inner
}
})
.inner
};
if self.scrollable.any() {
let max_height = 0.85 * ui.ctx().screen_rect().height();
let min_height = 0.3 * ui.ctx().screen_rect().height().at_most(max_height);
egui::ScrollArea::new(self.scrollable)
.min_scrolled_height(max_height)
.max_height(max_height)
.show(ui, |ui| {
let res = wrapped_content_ui(ui);
if ui.min_rect().height() < min_height {
ui.add_space(min_height - ui.min_rect().height());
}
res
})
.inner
} else {
wrapped_content_ui(ui)
}
})
.inner
});
modal_response
}
fn title_bar(ui: &mut egui::Ui, title: &str) {
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, "Close")
.clicked()
{
ui.close();
}
});
}
}
struct ViewPaddingFrameParams {
left_and_right: bool,
top: bool,
bottom: bool,
}
#[inline]
fn view_padding_frame(tokens: &DesignTokens, params: &ViewPaddingFrameParams) -> egui::Frame {
let ViewPaddingFrameParams {
left_and_right,
top,
bottom,
} = *params;
egui::Frame {
inner_margin: egui::Margin {
left: if left_and_right {
tokens.view_padding()
} else {
0
},
right: if left_and_right {
tokens.view_padding()
} else {
0
},
top: if top { tokens.view_padding() } else { 0 },
bottom: if bottom { tokens.view_padding() } else { 0 },
},
..Default::default()
}
}
pub fn prevent_shrinking(ui: &mut egui::Ui) {
let last_rect = ui.response().rect;
let screen_size = ui.ctx().screen_rect().size();
let id = ui.id().with("prevent_shrinking");
let screen_size_changed = ui.data_mut(|d| {
let last_screen_size = d.get_temp_mut_or_insert_with(id, || screen_size);
let changed = *last_screen_size != screen_size;
*last_screen_size = screen_size;
changed
});
if last_rect.is_positive() && !screen_size_changed {
let min_size = ui.min_size();
ui.set_min_size(egui::Vec2::max(last_rect.size(), min_size));
}
}