2023-05-18 03:53:38 +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-06-21 02:13:47 +03:00
|
|
|
|
use egui::{AboveOrBelow, Button, PointerState, Response, RichText, ScrollArea, Sense, Spinner, Widget, WidgetText};
|
2023-06-03 21:35:38 +03:00
|
|
|
|
use egui::epaint::{Color32, FontId, RectShape, Rounding, Stroke};
|
2023-06-02 02:05:34 +03:00
|
|
|
|
use egui::epaint::text::TextWrapping;
|
2023-06-03 11:22:51 +03:00
|
|
|
|
use egui::text::{LayoutJob, TextFormat};
|
2023-05-18 03:53:38 +03:00
|
|
|
|
|
2023-06-03 11:22:51 +03:00
|
|
|
|
use crate::gui::Colors;
|
2023-06-21 02:13:47 +03:00
|
|
|
|
use crate::gui::icons::{CARET_DOWN, CHECK_SQUARE, CIRCLE, RADIO_BUTTON, SQUARE};
|
2023-05-18 03:53:38 +03:00
|
|
|
|
|
|
|
|
|
pub struct View;
|
|
|
|
|
|
|
|
|
|
impl View {
|
2023-06-02 02:05:34 +03:00
|
|
|
|
/// Default stroke around views.
|
2023-06-03 11:22:51 +03:00
|
|
|
|
pub const DEFAULT_STROKE: Stroke = Stroke { width: 1.0, color: Colors::STROKE };
|
2023-05-18 03:53:38 +03:00
|
|
|
|
|
2023-06-02 02:05:34 +03:00
|
|
|
|
/// Default width of side panel at application UI.
|
|
|
|
|
pub const SIDE_PANEL_MIN_WIDTH: i64 = 400;
|
|
|
|
|
|
|
|
|
|
/// Check if UI can show side panel and screen at same time.
|
|
|
|
|
pub fn is_dual_panel_mode(frame: &mut eframe::Frame) -> bool {
|
|
|
|
|
let w = frame.info().window_info.size.x;
|
|
|
|
|
let h = frame.info().window_info.size.y;
|
|
|
|
|
// Screen is wide if width is greater than height or just 20% smaller.
|
|
|
|
|
let is_wide_screen = w > h || w + (w * 0.2) >= h;
|
|
|
|
|
// Dual panel mode is available when window is wide and its width is at least 2 times
|
|
|
|
|
// greater than minimal width of the side panel.
|
|
|
|
|
is_wide_screen && w >= Self::SIDE_PANEL_MIN_WIDTH as f32 * 2.0
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Show and cut long text with ﹍ character.
|
|
|
|
|
pub fn ellipsize_text(ui: &mut egui::Ui, text: String, size: f32, color: Color32) {
|
|
|
|
|
let mut job = LayoutJob::single_section(text, TextFormat {
|
|
|
|
|
font_id: FontId::proportional(size), color, .. Default::default()
|
|
|
|
|
});
|
|
|
|
|
job.wrap = TextWrapping {
|
|
|
|
|
max_rows: 1,
|
|
|
|
|
break_anywhere: false,
|
|
|
|
|
overflow_character: Option::from('﹍'),
|
|
|
|
|
..Default::default()
|
|
|
|
|
};
|
|
|
|
|
ui.label(job);
|
|
|
|
|
}
|
|
|
|
|
|
2023-06-20 00:38:25 +03:00
|
|
|
|
/// Draw horizontally centered sub-title with space below.
|
|
|
|
|
pub fn sub_title(ui: &mut egui::Ui, text: String) {
|
|
|
|
|
ui.vertical_centered_justified(|ui| {
|
|
|
|
|
ui.label(RichText::new(text.to_uppercase()).size(16.0).color(Colors::TEXT));
|
|
|
|
|
});
|
|
|
|
|
ui.add_space(4.0);
|
2023-06-02 02:05:34 +03:00
|
|
|
|
}
|
|
|
|
|
|
2023-06-21 02:13:47 +03:00
|
|
|
|
/// Temporary click optimization for touch screens, return `true` if it was clicked.
|
|
|
|
|
fn touched(ui: &mut egui::Ui, resp: Response) -> bool {
|
2023-06-20 00:38:25 +03:00
|
|
|
|
let drag_resp = resp.interact(Sense::click_and_drag());
|
2023-06-02 02:05:34 +03:00
|
|
|
|
// Clear pointer event if dragging is out of button area
|
2023-06-20 00:38:25 +03:00
|
|
|
|
if drag_resp.dragged() && !ui.rect_contains_pointer(drag_resp.rect) {
|
2023-06-02 02:05:34 +03:00
|
|
|
|
ui.input_mut().pointer = PointerState::default();
|
|
|
|
|
}
|
2023-06-20 00:38:25 +03:00
|
|
|
|
if drag_resp.drag_released() || drag_resp.clicked() {
|
2023-06-21 02:13:47 +03:00
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
false
|
2023-06-02 02:05:34 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Title button with transparent background fill color, contains only icon.
|
2023-05-18 03:53:38 +03:00
|
|
|
|
pub fn title_button(ui: &mut egui::Ui, icon: &str, action: impl FnOnce()) {
|
2023-05-23 23:45:16 +03:00
|
|
|
|
ui.scope(|ui| {
|
|
|
|
|
// Disable stroke around title buttons on hover
|
|
|
|
|
ui.style_mut().visuals.widgets.active.bg_stroke = Stroke::NONE;
|
2023-06-03 11:22:51 +03:00
|
|
|
|
let wt = RichText::new(icon.to_string()).size(24.0).color(Colors::TITLE);
|
2023-06-02 02:05:34 +03:00
|
|
|
|
let br = Button::new(wt)
|
2023-06-03 11:22:51 +03:00
|
|
|
|
.fill(Colors::TRANSPARENT)
|
2023-06-20 00:38:25 +03:00
|
|
|
|
.ui(ui);
|
2023-06-21 02:13:47 +03:00
|
|
|
|
if Self::touched(ui, br) {
|
|
|
|
|
(action)();
|
|
|
|
|
}
|
2023-05-23 23:45:16 +03:00
|
|
|
|
});
|
2023-05-18 03:53:38 +03:00
|
|
|
|
}
|
|
|
|
|
|
2023-06-02 02:05:34 +03:00
|
|
|
|
/// Tab button with white background fill color, contains only icon.
|
|
|
|
|
pub fn tab_button(ui: &mut egui::Ui, icon: &str, active: bool, action: impl FnOnce()) {
|
|
|
|
|
let text_color = match active {
|
2023-06-03 11:22:51 +03:00
|
|
|
|
true => { Colors::TITLE }
|
2023-06-03 21:35:38 +03:00
|
|
|
|
false => { Colors::TEXT }
|
2023-06-02 02:05:34 +03:00
|
|
|
|
};
|
2023-05-18 03:53:38 +03:00
|
|
|
|
let stroke = match active {
|
|
|
|
|
true => { Stroke::NONE }
|
|
|
|
|
false => { Self::DEFAULT_STROKE }
|
|
|
|
|
};
|
|
|
|
|
let color = match active {
|
2023-06-03 11:22:51 +03:00
|
|
|
|
true => { Colors::FILL }
|
|
|
|
|
false => { Colors::WHITE }
|
2023-05-18 03:53:38 +03:00
|
|
|
|
};
|
2023-06-21 02:13:47 +03:00
|
|
|
|
let br = Button::new(RichText::new(icon.to_string()).size(24.0).color(text_color))
|
2023-05-18 03:53:38 +03:00
|
|
|
|
.stroke(stroke)
|
|
|
|
|
.fill(color)
|
2023-06-20 00:38:25 +03:00
|
|
|
|
.ui(ui);
|
2023-06-21 02:13:47 +03:00
|
|
|
|
if Self::touched(ui, br) {
|
|
|
|
|
(action)();
|
|
|
|
|
}
|
2023-05-18 03:53:38 +03:00
|
|
|
|
}
|
|
|
|
|
|
2023-06-02 21:19:34 +03:00
|
|
|
|
/// Draw [`Button`] with specified background fill color.
|
|
|
|
|
pub fn button(ui: &mut egui::Ui, text: String, fill_color: Color32, action: impl FnOnce()) {
|
2023-06-23 22:12:30 +03:00
|
|
|
|
let br = Button::new(RichText::new(text.to_uppercase()).size(18.0).color(Colors::TEXT_BUTTON))
|
2023-06-02 02:05:34 +03:00
|
|
|
|
.stroke(Self::DEFAULT_STROKE)
|
2023-06-02 21:19:34 +03:00
|
|
|
|
.fill(fill_color)
|
2023-06-20 00:38:25 +03:00
|
|
|
|
.ui(ui);
|
2023-06-21 02:13:47 +03:00
|
|
|
|
if Self::touched(ui, br) {
|
|
|
|
|
(action)();
|
|
|
|
|
}
|
2023-05-18 03:53:38 +03:00
|
|
|
|
}
|
|
|
|
|
|
2023-06-02 02:05:34 +03:00
|
|
|
|
/// Draw rounded box with some value and label in the middle,
|
|
|
|
|
/// where is r = (top_left, top_right, bottom_left, bottom_right).
|
2023-05-18 03:53:38 +03:00
|
|
|
|
/// | VALUE |
|
|
|
|
|
/// | label |
|
|
|
|
|
pub fn rounded_box(ui: &mut egui::Ui, value: String, label: String, r: [bool; 4]) {
|
2023-06-15 23:54:41 +03:00
|
|
|
|
let rect = ui.available_rect_before_wrap();
|
2023-05-18 03:53:38 +03:00
|
|
|
|
|
2023-06-03 21:35:38 +03:00
|
|
|
|
// Create background shape.
|
|
|
|
|
let mut bg_shape = RectShape {
|
2023-05-18 03:53:38 +03:00
|
|
|
|
rect,
|
2023-06-03 21:35:38 +03:00
|
|
|
|
rounding: Rounding {
|
2023-05-18 03:53:38 +03:00
|
|
|
|
nw: if r[0] { 8.0 } else { 0.0 },
|
|
|
|
|
ne: if r[1] { 8.0 } else { 0.0 },
|
|
|
|
|
sw: if r[2] { 8.0 } else { 0.0 },
|
|
|
|
|
se: if r[3] { 8.0 } else { 0.0 },
|
|
|
|
|
},
|
2023-06-03 21:35:38 +03:00
|
|
|
|
fill: Colors::WHITE,
|
|
|
|
|
stroke: Stroke { width: 1.0, color: Colors::ITEM_STROKE },
|
|
|
|
|
};
|
|
|
|
|
let bg_idx = ui.painter().add(bg_shape);
|
|
|
|
|
|
|
|
|
|
// Draw box content.
|
|
|
|
|
let content_resp = ui.allocate_ui_at_rect(rect, |ui| {
|
|
|
|
|
ui.vertical_centered_justified(|ui| {
|
2023-06-20 00:38:25 +03:00
|
|
|
|
ui.add_space(2.0);
|
|
|
|
|
|
|
|
|
|
ui.scope(|ui| {
|
|
|
|
|
// Correct vertical spacing between items.
|
|
|
|
|
ui.style_mut().spacing.item_spacing.y = -3.0;
|
|
|
|
|
|
|
|
|
|
// Draw box value.
|
|
|
|
|
let mut job = LayoutJob::single_section(value, TextFormat {
|
|
|
|
|
font_id: FontId::proportional(18.0),
|
|
|
|
|
color: Colors::BLACK,
|
|
|
|
|
..Default::default()
|
|
|
|
|
});
|
|
|
|
|
job.wrap = TextWrapping {
|
|
|
|
|
max_rows: 1,
|
|
|
|
|
break_anywhere: false,
|
|
|
|
|
overflow_character: Option::from('﹍'),
|
|
|
|
|
..Default::default()
|
|
|
|
|
};
|
|
|
|
|
ui.label(job);
|
|
|
|
|
|
|
|
|
|
// Draw box label.
|
|
|
|
|
ui.label(RichText::new(label).color(Colors::GRAY).size(15.0));
|
2023-06-03 21:35:38 +03:00
|
|
|
|
});
|
2023-06-20 00:38:25 +03:00
|
|
|
|
|
|
|
|
|
ui.add_space(2.0);
|
2023-05-18 03:53:38 +03:00
|
|
|
|
});
|
2023-06-03 21:35:38 +03:00
|
|
|
|
}).response;
|
|
|
|
|
|
|
|
|
|
// Setup background shape to be painted behind box content.
|
|
|
|
|
bg_shape.rect = content_resp.rect;
|
|
|
|
|
ui.painter().set(bg_idx, bg_shape);
|
2023-05-18 03:53:38 +03:00
|
|
|
|
}
|
2023-06-02 21:19:34 +03:00
|
|
|
|
|
|
|
|
|
/// Draw content in the center of current layout with specified width and height.
|
2023-06-03 21:35:38 +03:00
|
|
|
|
pub fn center_content(ui: &mut egui::Ui, height: f32, content: impl FnOnce(&mut egui::Ui)) {
|
2023-06-02 21:19:34 +03:00
|
|
|
|
ui.vertical_centered(|ui| {
|
|
|
|
|
let mut rect = ui.available_rect_before_wrap();
|
2023-06-03 21:35:38 +03:00
|
|
|
|
let side_margin = 24.0;
|
|
|
|
|
rect.min += egui::emath::vec2(side_margin, ui.available_height() / 2.0 - height / 2.0);
|
2023-06-02 21:19:34 +03:00
|
|
|
|
rect.max -= egui::emath::vec2(side_margin, 0.0);
|
|
|
|
|
ui.allocate_ui_at_rect(rect, |ui| {
|
|
|
|
|
(content)(ui);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
2023-06-17 13:23:31 +03:00
|
|
|
|
|
|
|
|
|
/// Draw big gold loading spinner.
|
|
|
|
|
pub fn big_loading_spinner(ui: &mut egui::Ui) {
|
|
|
|
|
Spinner::new().size(104.0).color(Colors::GOLD).ui(ui);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Draw small gold loading spinner.
|
|
|
|
|
pub fn small_loading_spinner(ui: &mut egui::Ui) {
|
|
|
|
|
Spinner::new().size(48.0).color(Colors::GOLD).ui(ui);
|
|
|
|
|
}
|
|
|
|
|
|
2023-06-20 00:38:25 +03:00
|
|
|
|
/// Draw the button that looks like checkbox with callback on check.
|
2023-06-19 01:29:15 +03:00
|
|
|
|
pub fn checkbox(ui: &mut egui::Ui, checked: bool, text: String, callback: impl FnOnce()) {
|
2023-06-20 00:38:25 +03:00
|
|
|
|
let (text_value, color) = match checked {
|
2023-06-23 22:12:30 +03:00
|
|
|
|
true => { (format!("{} {}", CHECK_SQUARE, text), Colors::TEXT_BUTTON) }
|
2023-06-20 00:38:25 +03:00
|
|
|
|
false => { (format!("{} {}", SQUARE, text), Colors::TEXT) }
|
2023-06-17 13:23:31 +03:00
|
|
|
|
};
|
2023-06-21 02:13:47 +03:00
|
|
|
|
let br = Button::new(RichText::new(text_value).size(18.0).color(color))
|
2023-06-20 00:38:25 +03:00
|
|
|
|
.frame(false)
|
|
|
|
|
.stroke(Stroke::NONE)
|
|
|
|
|
.fill(Colors::TRANSPARENT)
|
|
|
|
|
.ui(ui);
|
2023-06-21 02:13:47 +03:00
|
|
|
|
if Self::touched(ui, br) {
|
|
|
|
|
(callback)();
|
|
|
|
|
}
|
2023-06-20 00:38:25 +03:00
|
|
|
|
}
|
|
|
|
|
|
2023-06-21 02:13:47 +03:00
|
|
|
|
/// Show a [`RadioButton`]. It is selected if `*current_value == selected_value`.
|
|
|
|
|
/// If clicked, `selected_value` is assigned to `*current_value`.
|
|
|
|
|
pub fn radio_value<T: PartialEq>(ui: &mut egui::Ui, current: &mut T, value: T, text: String) {
|
|
|
|
|
let mut response = ui.radio(*current == value, text);
|
|
|
|
|
if Self::touched(ui, response.clone()) && *current != value {
|
|
|
|
|
*current = value;
|
|
|
|
|
response.mark_changed();
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-06-17 13:23:31 +03:00
|
|
|
|
|
2023-06-21 02:13:47 +03:00
|
|
|
|
/// Draw horizontal line
|
|
|
|
|
pub fn horizontal_line(ui: &mut egui::Ui, color: Color32) {
|
|
|
|
|
let line_size = egui::Vec2::new(ui.available_width(), 1.0);
|
|
|
|
|
let (line_rect, _) = ui.allocate_exact_size(line_size, Sense::hover());
|
|
|
|
|
let painter = ui.painter();
|
|
|
|
|
painter.hline(line_rect.x_range(),
|
|
|
|
|
painter.round_to_pixel(line_rect.center().y),
|
|
|
|
|
Stroke { width: 1.0, color });
|
2023-06-17 13:23:31 +03:00
|
|
|
|
}
|
2023-05-18 03:53:38 +03:00
|
|
|
|
}
|