2023-06-02 02:05:34 +03:00
|
|
|
// Copyright 2023 The Grim Developers
|
|
|
|
//
|
|
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
// you may not use this file except in compliance with the License.
|
|
|
|
// You may obtain a copy of the License at
|
|
|
|
//
|
|
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
//
|
|
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
// See the License for the specific language governing permissions and
|
|
|
|
// limitations under the License.
|
|
|
|
|
2023-07-21 03:56:46 +03:00
|
|
|
use std::sync::RwLock;
|
2023-06-02 02:05:34 +03:00
|
|
|
use std::sync::atomic::{AtomicBool, Ordering};
|
|
|
|
|
2023-06-26 18:51:19 +03:00
|
|
|
use egui::{Align2, RichText, Rounding, Stroke, Vec2};
|
2023-06-02 02:05:34 +03:00
|
|
|
use egui::epaint::RectShape;
|
2023-07-13 03:54:27 +03:00
|
|
|
use lazy_static::lazy_static;
|
2023-06-02 02:05:34 +03:00
|
|
|
|
2023-06-15 23:54:41 +03:00
|
|
|
use crate::gui::Colors;
|
2023-07-16 22:06:08 +03:00
|
|
|
use crate::gui::views::{Root, View};
|
2023-06-02 02:05:34 +03:00
|
|
|
|
2023-07-13 03:54:27 +03:00
|
|
|
lazy_static! {
|
|
|
|
/// Showing [`Modal`] state to be accessible from different ui parts.
|
|
|
|
static ref MODAL_STATE: RwLock<ModalState> = RwLock::new(ModalState::default());
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Default)]
|
|
|
|
struct ModalState {
|
|
|
|
modal: Option<Modal>
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Contains ids to draw current [`Modal`] at this ui container if it's possible.
|
2023-06-26 18:51:19 +03:00
|
|
|
pub trait ModalContainer {
|
2023-07-13 03:54:27 +03:00
|
|
|
/// List of [`Modal`] ids to draw at current ui container.
|
2023-06-29 03:42:03 +03:00
|
|
|
fn modal_ids(&self) -> &Vec<&'static str>;
|
2023-06-26 18:51:19 +03:00
|
|
|
|
2023-07-13 03:54:27 +03:00
|
|
|
/// Check if it's possible to draw [`Modal`] at current ui container.
|
|
|
|
fn can_draw_modal(&self) -> bool {
|
|
|
|
let modal_id = Modal::opened();
|
|
|
|
modal_id.is_some() && self.modal_ids().contains(&modal_id.unwrap())
|
2023-06-26 18:51:19 +03:00
|
|
|
}
|
2023-06-02 02:05:34 +03:00
|
|
|
}
|
|
|
|
|
2023-06-26 18:51:19 +03:00
|
|
|
/// Position of [`Modal`] on the screen.
|
2023-06-02 02:05:34 +03:00
|
|
|
pub enum ModalPosition {
|
|
|
|
CenterTop,
|
|
|
|
Center
|
|
|
|
}
|
|
|
|
|
2023-07-13 03:54:27 +03:00
|
|
|
/// Stores data to draw modal [`egui::Window`] at ui.
|
2023-06-02 02:05:34 +03:00
|
|
|
pub struct Modal {
|
2023-06-23 22:12:30 +03:00
|
|
|
/// Identifier for modal.
|
|
|
|
pub(crate) id: &'static str,
|
2023-06-02 02:05:34 +03:00
|
|
|
/// Position on the screen.
|
|
|
|
position: ModalPosition,
|
|
|
|
/// Flag to show the content.
|
|
|
|
open: AtomicBool,
|
|
|
|
/// To check if it can be closed.
|
|
|
|
closeable: AtomicBool,
|
|
|
|
/// Title text
|
|
|
|
title: Option<String>
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Modal {
|
2023-07-16 22:06:08 +03:00
|
|
|
/// Margin from [`Modal`] window at top/left/right.
|
|
|
|
const DEFAULT_MARGIN: f32 = 10.0;
|
2023-07-31 01:04:41 +03:00
|
|
|
/// Maximum width of the content.
|
|
|
|
const DEFAULT_WIDTH: f32 = Root::SIDE_PANEL_WIDTH - (2.0 * Self::DEFAULT_MARGIN);
|
2023-06-02 02:05:34 +03:00
|
|
|
|
2023-06-26 18:51:19 +03:00
|
|
|
/// Create opened and closeable Modal with center position.
|
|
|
|
pub fn new(id: &'static str) -> Self {
|
2023-06-02 02:05:34 +03:00
|
|
|
Self {
|
|
|
|
id,
|
|
|
|
position: ModalPosition::Center,
|
|
|
|
open: AtomicBool::new(true),
|
|
|
|
closeable: AtomicBool::new(true),
|
|
|
|
title: None
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-07-13 03:54:27 +03:00
|
|
|
/// Setup position of [`Modal`] on the screen.
|
2023-06-02 02:05:34 +03:00
|
|
|
pub fn position(mut self, position: ModalPosition) -> Self {
|
|
|
|
self.position = position;
|
|
|
|
self
|
|
|
|
}
|
|
|
|
|
2023-07-13 03:54:27 +03:00
|
|
|
/// Check if [`Modal`] is open.
|
2023-06-02 02:05:34 +03:00
|
|
|
pub fn is_open(&self) -> bool {
|
|
|
|
self.open.load(Ordering::Relaxed)
|
|
|
|
}
|
|
|
|
|
2023-07-13 03:54:27 +03:00
|
|
|
/// Mark [`Modal`] closed.
|
2023-06-02 02:05:34 +03:00
|
|
|
pub fn close(&self) {
|
|
|
|
self.open.store(false, Ordering::Relaxed);
|
|
|
|
}
|
|
|
|
|
2023-07-13 03:54:27 +03:00
|
|
|
/// Setup possibility to close [`Modal`].
|
2023-06-02 02:05:34 +03:00
|
|
|
pub fn closeable(self, closeable: bool) -> Self {
|
|
|
|
self.closeable.store(closeable, Ordering::Relaxed);
|
|
|
|
self
|
|
|
|
}
|
|
|
|
|
2023-07-13 03:54:27 +03:00
|
|
|
/// Disable possibility to close [`Modal`].
|
2023-06-02 02:05:34 +03:00
|
|
|
pub fn disable_closing(&self) {
|
|
|
|
self.closeable.store(false, Ordering::Relaxed);
|
|
|
|
}
|
|
|
|
|
2023-07-13 03:54:27 +03:00
|
|
|
/// Check if [`Modal`] is closeable.
|
2023-06-02 02:05:34 +03:00
|
|
|
pub fn is_closeable(&self) -> bool {
|
|
|
|
self.closeable.load(Ordering::Relaxed)
|
|
|
|
}
|
|
|
|
|
2023-07-23 11:48:28 +03:00
|
|
|
/// Set title text on [`Modal`] creation.
|
2023-06-02 02:05:34 +03:00
|
|
|
pub fn title(mut self, title: String) -> Self {
|
|
|
|
self.title = Some(title.to_uppercase());
|
|
|
|
self
|
|
|
|
}
|
|
|
|
|
2023-07-23 11:48:28 +03:00
|
|
|
/// Set [`Modal`] instance into state to show at ui.
|
|
|
|
pub fn show(self) {
|
2023-07-13 03:54:27 +03:00
|
|
|
let mut w_nav = MODAL_STATE.write().unwrap();
|
2023-07-23 11:48:28 +03:00
|
|
|
w_nav.modal = Some(self);
|
2023-07-13 03:54:27 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
/// Remove [`Modal`] from [`MODAL_STATE`] if it's showing and can be closed.
|
|
|
|
/// Return `false` if Modal existed in [`MODAL_STATE`] before call.
|
|
|
|
pub fn on_back() -> bool {
|
|
|
|
let mut w_state = MODAL_STATE.write().unwrap();
|
|
|
|
|
|
|
|
// If Modal is showing and closeable, remove it from state.
|
|
|
|
if w_state.modal.is_some() {
|
|
|
|
let modal = w_state.modal.as_ref().unwrap();
|
|
|
|
if modal.is_closeable() {
|
|
|
|
w_state.modal = None;
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
true
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Return id of opened [`Modal`] or remove its instance from [`MODAL_STATE`] if it was closed.
|
|
|
|
pub fn opened() -> Option<&'static str> {
|
|
|
|
// Check if Modal is showing.
|
|
|
|
{
|
|
|
|
if MODAL_STATE.read().unwrap().modal.is_none() {
|
|
|
|
return None;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check if Modal is open.
|
|
|
|
let (is_open, id) = {
|
|
|
|
let r_state = MODAL_STATE.read().unwrap();
|
|
|
|
let modal = r_state.modal.as_ref().unwrap();
|
|
|
|
(modal.is_open(), modal.id)
|
|
|
|
};
|
|
|
|
|
|
|
|
// If Modal is not open, remove it from navigator state.
|
|
|
|
if !is_open {
|
|
|
|
let mut w_state = MODAL_STATE.write().unwrap();
|
|
|
|
w_state.modal = None;
|
|
|
|
return None;
|
|
|
|
}
|
|
|
|
Some(id)
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Draw opened [`Modal`] content.
|
|
|
|
pub fn ui(ui: &mut egui::Ui, add_content: impl FnOnce(&mut egui::Ui, &Modal)) {
|
|
|
|
if let Some(modal) = &MODAL_STATE.read().unwrap().modal {
|
|
|
|
if modal.is_open() {
|
2023-07-16 22:06:08 +03:00
|
|
|
modal.window_ui(ui, add_content);
|
2023-07-13 03:54:27 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-07-16 22:06:08 +03:00
|
|
|
/// Draw [`egui::Window`] with provided content.
|
|
|
|
fn window_ui(&self, ui: &mut egui::Ui, add_content: impl FnOnce(&mut egui::Ui, &Modal)) {
|
2023-07-21 03:56:46 +03:00
|
|
|
let rect = ui.ctx().screen_rect();
|
2023-06-26 18:51:19 +03:00
|
|
|
egui::Window::new("modal_bg_window")
|
2023-06-02 02:05:34 +03:00
|
|
|
.title_bar(false)
|
|
|
|
.resizable(false)
|
|
|
|
.collapsible(false)
|
2023-07-11 17:53:20 +03:00
|
|
|
.fixed_rect(rect)
|
2023-06-02 02:05:34 +03:00
|
|
|
.frame(egui::Frame {
|
2023-06-03 11:22:51 +03:00
|
|
|
fill: Colors::SEMI_TRANSPARENT,
|
2023-06-02 02:05:34 +03:00
|
|
|
..Default::default()
|
|
|
|
})
|
|
|
|
.show(ui.ctx(), |ui| {
|
2023-07-11 17:53:20 +03:00
|
|
|
ui.set_min_size(rect.size());
|
2023-06-02 02:05:34 +03:00
|
|
|
});
|
|
|
|
|
2023-07-16 22:06:08 +03:00
|
|
|
// Setup width of modal content.
|
|
|
|
let side_insets = View::get_left_inset() + View::get_right_inset();
|
2023-08-01 02:19:09 +03:00
|
|
|
let available_width = ui.available_width() - (side_insets + Self::DEFAULT_MARGIN);
|
|
|
|
let width = f32::min(available_width, Self::DEFAULT_WIDTH);
|
2023-06-23 22:12:30 +03:00
|
|
|
|
|
|
|
// Show main content Window at given position.
|
|
|
|
let (content_align, content_offset) = self.modal_position();
|
2023-07-03 21:17:49 +03:00
|
|
|
let layer_id = egui::Window::new(format!("modal_window_{}", self.id))
|
2023-06-02 02:05:34 +03:00
|
|
|
.title_bar(false)
|
|
|
|
.resizable(false)
|
|
|
|
.collapsible(false)
|
2023-06-02 10:04:54 +03:00
|
|
|
.default_width(width)
|
2023-06-23 22:12:30 +03:00
|
|
|
.anchor(content_align, content_offset)
|
2023-06-02 02:05:34 +03:00
|
|
|
.frame(egui::Frame {
|
|
|
|
rounding: Rounding::same(8.0),
|
2023-06-03 11:22:51 +03:00
|
|
|
fill: Colors::YELLOW,
|
2023-06-02 02:05:34 +03:00
|
|
|
..Default::default()
|
|
|
|
})
|
|
|
|
.show(ui.ctx(), |ui| {
|
|
|
|
if self.title.is_some() {
|
2023-07-16 22:06:08 +03:00
|
|
|
self.title_ui(ui);
|
2023-06-02 02:05:34 +03:00
|
|
|
}
|
2023-07-16 22:06:08 +03:00
|
|
|
self.content_ui(ui, add_content);
|
2023-06-02 02:05:34 +03:00
|
|
|
}).unwrap().response.layer_id;
|
|
|
|
|
2023-06-23 22:12:30 +03:00
|
|
|
// Always show main content Window above background Window.
|
2023-06-02 02:05:34 +03:00
|
|
|
ui.ctx().move_to_top(layer_id);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Get [`egui::Window`] position based on [`ModalPosition`].
|
2023-06-23 22:12:30 +03:00
|
|
|
fn modal_position(&self) -> (Align2, Vec2) {
|
|
|
|
let align = match self.position {
|
2023-07-03 21:17:49 +03:00
|
|
|
ModalPosition::CenterTop => Align2::CENTER_TOP,
|
|
|
|
ModalPosition::Center => Align2::CENTER_CENTER
|
2023-06-23 22:12:30 +03:00
|
|
|
};
|
2023-07-16 22:06:08 +03:00
|
|
|
let x_align = View::get_left_inset() - View::get_right_inset();
|
|
|
|
let y_align = View::get_top_inset() + Self::DEFAULT_MARGIN;
|
2023-06-23 22:12:30 +03:00
|
|
|
let offset = match self.position {
|
2023-07-16 22:06:08 +03:00
|
|
|
ModalPosition::CenterTop => Vec2::new(x_align, y_align),
|
|
|
|
ModalPosition::Center => Vec2::new(x_align, 0.0)
|
2023-06-23 22:12:30 +03:00
|
|
|
};
|
|
|
|
(align, offset)
|
2023-06-02 02:05:34 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
/// Draw provided content.
|
2023-07-16 22:06:08 +03:00
|
|
|
fn content_ui(&self, ui: &mut egui::Ui, add_content: impl FnOnce(&mut egui::Ui, &Modal)) {
|
2023-06-02 02:05:34 +03:00
|
|
|
let mut rect = ui.available_rect_before_wrap();
|
|
|
|
rect.min += egui::emath::vec2(6.0, 0.0);
|
|
|
|
rect.max -= egui::emath::vec2(6.0, 0.0);
|
|
|
|
|
|
|
|
// Create background shape.
|
|
|
|
let rounding = if self.title.is_some() {
|
|
|
|
Rounding {
|
|
|
|
nw: 0.0,
|
|
|
|
ne: 0.0,
|
|
|
|
sw: 8.0,
|
|
|
|
se: 8.0,
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
Rounding::same(8.0)
|
|
|
|
};
|
|
|
|
let mut bg_shape = RectShape {
|
|
|
|
rect,
|
|
|
|
rounding,
|
2023-06-03 11:22:51 +03:00
|
|
|
fill: Colors::FILL,
|
2023-06-02 21:19:34 +03:00
|
|
|
stroke: Stroke::NONE,
|
2023-06-02 02:05:34 +03:00
|
|
|
};
|
|
|
|
let bg_idx = ui.painter().add(bg_shape);
|
|
|
|
|
|
|
|
// Draw main content.
|
2023-06-02 02:16:41 +03:00
|
|
|
let mut content_rect = ui.allocate_ui_at_rect(rect, |ui| {
|
2023-06-02 10:04:54 +03:00
|
|
|
(add_content)(ui, self);
|
2023-06-02 02:05:34 +03:00
|
|
|
}).response.rect;
|
|
|
|
|
|
|
|
// Setup background shape to be painted behind main content.
|
2023-06-02 02:16:41 +03:00
|
|
|
content_rect.min -= egui::emath::vec2(6.0, 0.0);
|
|
|
|
content_rect.max += egui::emath::vec2(6.0, 0.0);
|
|
|
|
bg_shape.rect = content_rect;
|
2023-06-02 02:05:34 +03:00
|
|
|
ui.painter().set(bg_idx, bg_shape);
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Draw the title.
|
2023-07-16 22:06:08 +03:00
|
|
|
fn title_ui(&self, ui: &mut egui::Ui) {
|
2023-06-02 02:05:34 +03:00
|
|
|
let rect = ui.available_rect_before_wrap();
|
|
|
|
|
|
|
|
// Create background shape.
|
|
|
|
let mut bg_shape = RectShape {
|
|
|
|
rect,
|
|
|
|
rounding: Rounding {
|
|
|
|
nw: 8.0,
|
|
|
|
ne: 8.0,
|
|
|
|
sw: 0.0,
|
|
|
|
se: 0.0,
|
|
|
|
},
|
2023-06-03 11:22:51 +03:00
|
|
|
fill: Colors::YELLOW,
|
2023-06-02 02:05:34 +03:00
|
|
|
stroke: Stroke::NONE,
|
|
|
|
};
|
|
|
|
let bg_idx = ui.painter().add(bg_shape);
|
|
|
|
|
|
|
|
// Draw title content.
|
|
|
|
let title_resp = ui.allocate_ui_at_rect(rect, |ui| {
|
|
|
|
ui.vertical_centered_justified(|ui| {
|
|
|
|
ui.add_space(8.0);
|
2023-06-03 11:22:51 +03:00
|
|
|
ui.label(RichText::new(self.title.as_ref().unwrap())
|
2023-07-14 03:51:06 +03:00
|
|
|
.size(19.0)
|
2023-06-03 11:22:51 +03:00
|
|
|
.color(Colors::TITLE)
|
|
|
|
);
|
2023-06-02 02:05:34 +03:00
|
|
|
ui.add_space(8.0);
|
|
|
|
});
|
|
|
|
}).response;
|
|
|
|
|
|
|
|
// Setup background shape to be painted behind title content.
|
|
|
|
bg_shape.rect = title_resp.rect;
|
|
|
|
ui.painter().set(bg_idx, bg_shape);
|
2023-06-02 21:19:34 +03:00
|
|
|
|
2023-06-03 11:22:51 +03:00
|
|
|
// Draw line below title.
|
2023-06-21 02:13:47 +03:00
|
|
|
View::horizontal_line(ui, Colors::STROKE);
|
2023-06-02 02:05:34 +03:00
|
|
|
}
|
|
|
|
}
|