mirror of
https://github.com/jwinterm/grim.git
synced 2026-10-17 14:21:24 +02:00
722 lines
27 KiB
Rust
722 lines
27 KiB
Rust
// 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.
|
||
|
||
use lazy_static::lazy_static;
|
||
use std::sync::atomic::{AtomicI32, Ordering};
|
||
use egui::emath::GuiRounding;
|
||
use egui::epaint::text::TextWrapping;
|
||
use egui::epaint::{Color32, FontId, PathShape, PathStroke, RectShape, Stroke};
|
||
use egui::load::SizedTexture;
|
||
use egui::os::OperatingSystem;
|
||
use egui::text::{LayoutJob, TextFormat};
|
||
use egui::{lerp, Button, CornerRadius, CursorIcon, Rect, Response, Rgba, RichText, Sense, SizeHint, Spinner, StrokeKind, TextureHandle, TextureOptions, UiBuilder, Widget};
|
||
use egui_extras::image::load_svg_bytes_with_size;
|
||
|
||
use crate::gui::icons::{CHECK_FAT, CHECK_SQUARE, SQUARE};
|
||
use crate::gui::views::types::LinePosition;
|
||
use crate::gui::Colors;
|
||
use crate::AppConfig;
|
||
|
||
pub struct View;
|
||
|
||
impl View {
|
||
/// Check if current platform is desktop
|
||
pub fn is_desktop() -> bool {
|
||
let os = OperatingSystem::from_target_os();
|
||
os != OperatingSystem::Android && os != OperatingSystem::IOS
|
||
}
|
||
|
||
/// Format timestamp in seconds with local UTC offset.
|
||
pub fn format_time(ts: i64) -> String {
|
||
let utc_offset = chrono::Local::now().offset().local_minus_utc();
|
||
let utc_time = ts + utc_offset as i64;
|
||
let tx_time = chrono::DateTime::from_timestamp(utc_time, 0).unwrap();
|
||
tx_time.format("%d/%m/%Y %H:%M:%S").to_string()
|
||
}
|
||
|
||
/// Get default stroke around views.
|
||
pub fn default_stroke() -> Stroke {
|
||
Stroke { width: 1.0, color: Colors::stroke() }
|
||
}
|
||
|
||
/// Get default stroke around item buttons.
|
||
pub fn item_stroke() -> Stroke {
|
||
Stroke { width: 1.0, color: Colors::item_stroke() }
|
||
}
|
||
|
||
/// Get stroke for hovered items and buttons.
|
||
pub fn hover_stroke() -> Stroke {
|
||
Stroke { width: 1.0, color: Colors::item_hover() }
|
||
}
|
||
|
||
/// Draw content with maximum width value.
|
||
pub fn max_width_ui(ui: &mut egui::Ui,
|
||
max_width: f32,
|
||
add_content: impl FnOnce(&mut egui::Ui)) {
|
||
// Setup content width.
|
||
let mut width = ui.available_width();
|
||
if width == 0.0 {
|
||
return;
|
||
}
|
||
let mut rect = ui.available_rect_before_wrap();
|
||
width = f32::min(width, max_width);
|
||
rect.set_width(width);
|
||
|
||
// Draw content.
|
||
ui.vertical_centered(|ui| {
|
||
ui.allocate_ui(rect.size(), |ui| {
|
||
(add_content)(ui);
|
||
});
|
||
});
|
||
}
|
||
|
||
/// Get width and height of app window.
|
||
pub fn window_size(ctx: &egui::Context) -> (f32, f32) {
|
||
let rect = ctx.content_rect();
|
||
(rect.width(), rect.height())
|
||
}
|
||
|
||
/// Calculate margin for far left view based on display insets (cutouts).
|
||
pub fn far_left_inset_margin(ui: &mut egui::Ui) -> f32 {
|
||
if ui.available_rect_before_wrap().min.x == 0.0 {
|
||
Self::get_left_inset()
|
||
} else {
|
||
0.0
|
||
}
|
||
}
|
||
|
||
/// Calculate margin for far left view based on display insets (cutouts).
|
||
pub fn far_right_inset_margin(ui: &mut egui::Ui) -> f32 {
|
||
let container_width = ui.available_rect_before_wrap().max.x as i32;
|
||
let window_size = Self::window_size(ui.ctx());
|
||
let display_width = window_size.0 as i32;
|
||
// Means end of the screen.
|
||
if container_width == display_width {
|
||
Self::get_right_inset()
|
||
} else {
|
||
0.0
|
||
}
|
||
}
|
||
|
||
/// Content padding for current platform.
|
||
pub fn content_padding() -> f32 {
|
||
if View::is_desktop() {
|
||
4.0
|
||
} else {
|
||
8.0
|
||
}
|
||
}
|
||
|
||
/// Cut long text with ﹍ character.
|
||
fn ellipsize(text: String, size: f32, color: Color32) -> LayoutJob {
|
||
let mut job = LayoutJob::single_section(text, TextFormat {
|
||
font_id: FontId::proportional(size), color, ..Default::default()
|
||
});
|
||
job.wrap = TextWrapping {
|
||
max_rows: 1,
|
||
break_anywhere: true,
|
||
overflow_character: Option::from('﹍'),
|
||
..Default::default()
|
||
};
|
||
job
|
||
}
|
||
|
||
/// Draw ellipsized text.
|
||
pub fn ellipsize_text(ui: &mut egui::Ui, text: String, size: f32, color: Color32) {
|
||
ui.label(Self::ellipsize(text, size, color));
|
||
}
|
||
|
||
/// Draw animated ellipsized text.
|
||
pub fn animate_text(ui: &mut egui::Ui, text: String, size: f32, color: Color32, animate: bool) {
|
||
// Setup text color animation if needed.
|
||
let (dark, bright) = (0.3, 1.0);
|
||
let color_factor = if animate {
|
||
lerp(dark..=bright, ui.input(|i| i.time).cos().abs()) as f32
|
||
} else {
|
||
bright as f32
|
||
};
|
||
|
||
// Draw subtitle text.
|
||
let sub_color_rgba = Rgba::from(color) * color_factor;
|
||
let sub_color = Color32::from(sub_color_rgba);
|
||
View::ellipsize_text(ui, text, size, sub_color);
|
||
|
||
// Repaint delay based on animation status.
|
||
if animate {
|
||
ui.ctx().request_repaint();
|
||
}
|
||
}
|
||
|
||
/// Draw horizontally centered subtitle 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(false)));
|
||
});
|
||
ui.add_space(4.0);
|
||
}
|
||
|
||
/// Draw big size title button.
|
||
pub fn title_button_big(ui: &mut egui::Ui, icon: &str, action: impl FnOnce(&mut egui::Ui)) {
|
||
Self::title_button(ui, 22.0, icon, action);
|
||
}
|
||
|
||
/// Draw small size title button.
|
||
pub fn title_button_small(ui: &mut egui::Ui, icon: &str, action: impl FnOnce(&mut egui::Ui)) {
|
||
Self::title_button(ui, 16.0, icon, action);
|
||
}
|
||
|
||
/// Draw title button with transparent background color, contains only icon.
|
||
fn title_button(ui: &mut egui::Ui, size: f32, icon: &str, action: impl FnOnce(&mut egui::Ui)) {
|
||
ui.scope(|ui| {
|
||
// Setup padding for title buttons.
|
||
if !View::is_desktop() {
|
||
ui.style_mut().spacing.button_padding = egui::vec2(20.0, 8.0);
|
||
}
|
||
// Disable strokes.
|
||
ui.style_mut().visuals.widgets.inactive.bg_stroke = Stroke::NONE;
|
||
ui.style_mut().visuals.widgets.hovered.bg_stroke = Stroke::NONE;
|
||
ui.style_mut().visuals.widgets.active.bg_stroke = Stroke::NONE;
|
||
ui.style_mut().visuals.widgets.active.corner_radius = CornerRadius::default();
|
||
ui.style_mut().visuals.widgets.active.expansion = 0.0;
|
||
|
||
// Setup text.
|
||
let wt = RichText::new(icon.to_string()).size(size).color(Colors::title(true));
|
||
// Draw button.
|
||
let br = Button::new(wt)
|
||
.sense(if View::is_desktop() {
|
||
Sense::click()
|
||
} else {
|
||
Sense::click_and_drag()
|
||
})
|
||
.fill(Colors::TRANSPARENT)
|
||
.ui(ui)
|
||
.on_hover_cursor(CursorIcon::PointingHand);
|
||
br.surrender_focus();
|
||
if br.clicked() || (!View::is_desktop() && br.drag_stopped()) {
|
||
action(ui);
|
||
}
|
||
});
|
||
}
|
||
|
||
/// Padding for tab items.
|
||
pub const TAB_ITEMS_PADDING: f32 = 5.0;
|
||
|
||
/// Tab button with white background fill color, contains only icon.
|
||
pub fn tab_button(ui: &mut egui::Ui,
|
||
icon: &str,
|
||
color: Option<Color32>,
|
||
selected: Option<bool>,
|
||
action: impl FnOnce(&mut egui::Ui)) {
|
||
ui.scope(|ui| {
|
||
let text_color = if let Some(c) = color {
|
||
if selected.is_none() {
|
||
Colors::inactive_text()
|
||
} else {
|
||
c
|
||
}
|
||
} else {
|
||
if let Some(active) = selected {
|
||
match active {
|
||
true => Colors::gray(),
|
||
false => Colors::item_button_text()
|
||
}
|
||
} else {
|
||
Colors::inactive_text()
|
||
}
|
||
};
|
||
|
||
let mut button = Button::new(RichText::new(icon).size(22.0).color(text_color));
|
||
|
||
let active_not_selected = selected.is_some() && !selected.unwrap();
|
||
if active_not_selected {
|
||
// Disable expansion on click/hover.
|
||
ui.style_mut().visuals.widgets.hovered.expansion = 0.0;
|
||
ui.style_mut().visuals.widgets.active.expansion = 0.0;
|
||
// Setup fill colors.
|
||
ui.visuals_mut().widgets.inactive.weak_bg_fill = Colors::white_or_black(false);
|
||
ui.visuals_mut().widgets.hovered.weak_bg_fill = Colors::fill_lite();
|
||
ui.visuals_mut().widgets.active.weak_bg_fill = Colors::fill();
|
||
// Setup stroke colors.
|
||
ui.visuals_mut().widgets.inactive.bg_stroke = Self::default_stroke();
|
||
ui.visuals_mut().widgets.hovered.bg_stroke = Self::hover_stroke();
|
||
ui.visuals_mut().widgets.active.bg_stroke = Self::item_stroke();
|
||
} else {
|
||
button = button.fill(Colors::fill()).stroke(Stroke::NONE);
|
||
}
|
||
|
||
// Setup paddings for tab buttons.
|
||
if Self::is_desktop() {
|
||
ui.style_mut().spacing.button_padding = egui::vec2(10.0, 4.0);
|
||
} else {
|
||
ui.style_mut().spacing.button_padding = egui::vec2(14.0, 8.0);
|
||
};
|
||
|
||
// Setup pointer style.
|
||
let br = if active_not_selected {
|
||
button.ui(ui).on_hover_cursor(CursorIcon::PointingHand)
|
||
} else {
|
||
button.ui(ui)
|
||
};
|
||
|
||
br.surrender_focus();
|
||
if br.clicked() && active_not_selected {
|
||
action(ui);
|
||
}
|
||
});
|
||
}
|
||
|
||
/// Draw [`Button`] with specified background fill and text color.
|
||
fn button_resp(ui: &mut egui::Ui, text: String, text_color: Color32, bg: Color32) -> Response {
|
||
let button_text = Self::ellipsize(text.to_uppercase(), 17.0, text_color);
|
||
Button::new(button_text)
|
||
.stroke(Self::default_stroke())
|
||
.fill(bg)
|
||
.ui(ui)
|
||
.on_hover_cursor(CursorIcon::PointingHand)
|
||
}
|
||
|
||
/// Draw [`Button`] with specified background fill color and default text color.
|
||
pub fn button(ui: &mut egui::Ui, text: String, fill: Color32, action: impl FnOnce()) {
|
||
let br = Self::button_resp(ui, text, Colors::text_button(), fill);
|
||
if br.clicked() {
|
||
action();
|
||
}
|
||
}
|
||
|
||
/// Draw [`Button`] with specified background fill color and text color.
|
||
pub fn colored_text_button(ui: &mut egui::Ui,
|
||
text: String,
|
||
text_color: Color32,
|
||
fill: Color32,
|
||
action: impl FnOnce()) {
|
||
let br = Self::button_resp(ui, text, text_color, fill);
|
||
if br.clicked() {
|
||
action();
|
||
}
|
||
}
|
||
|
||
/// Draw [`Button`] with specified background fill color and text color.
|
||
pub fn colored_text_button_ui(ui: &mut egui::Ui,
|
||
text: String,
|
||
text_color: Color32,
|
||
fill: Color32,
|
||
action: impl FnOnce(&mut egui::Ui)) {
|
||
let br = Self::button_resp(ui, text, text_color, fill);
|
||
if br.clicked() {
|
||
action(ui);
|
||
}
|
||
}
|
||
|
||
/// Draw gold action [`Button`].
|
||
pub fn action_button(ui: &mut egui::Ui,
|
||
text: String, action: impl FnOnce()) {
|
||
Self::colored_text_button(ui, text, Colors::title(true), Colors::gold(), action);
|
||
}
|
||
|
||
/// Draw [`Button`] with specified background fill color and ui at callback.
|
||
pub fn button_ui(ui: &mut egui::Ui,
|
||
text: String,
|
||
fill: Color32,
|
||
action: impl FnOnce(&mut egui::Ui)) {
|
||
let button_text = Self::ellipsize(text.to_uppercase(), 17.0, Colors::text_button());
|
||
let br = Button::new(button_text)
|
||
.stroke(Self::default_stroke())
|
||
.fill(fill)
|
||
.ui(ui)
|
||
.on_hover_cursor(CursorIcon::PointingHand);
|
||
if br.clicked() {
|
||
action(ui);
|
||
}
|
||
}
|
||
|
||
/// Draw list item [`Button`] with provided rounding.
|
||
pub fn item_button(ui: &mut egui::Ui,
|
||
rounding: CornerRadius,
|
||
text: &'static str,
|
||
color: Option<Color32>,
|
||
action: impl FnOnce()) {
|
||
// Setup button size.
|
||
let mut rect = ui.available_rect_before_wrap();
|
||
rect.set_width(42.0);
|
||
let button_size = rect.size();
|
||
|
||
ui.scope(|ui| {
|
||
// Setup padding for item buttons.
|
||
let padding = if Self::is_desktop() {
|
||
14.0
|
||
} else {
|
||
18.0
|
||
};
|
||
ui.style_mut().spacing.button_padding = egui::vec2(padding, 0.0);
|
||
// Disable expansion on click/hover.
|
||
ui.style_mut().visuals.widgets.hovered.expansion = 0.0;
|
||
ui.style_mut().visuals.widgets.active.expansion = 0.0;
|
||
// Setup fill colors.
|
||
ui.visuals_mut().widgets.inactive.weak_bg_fill = Colors::white_or_black(false);
|
||
ui.visuals_mut().widgets.hovered.weak_bg_fill = Colors::fill_lite();
|
||
ui.visuals_mut().widgets.active.weak_bg_fill = Colors::fill();
|
||
// Disable strokes.
|
||
ui.visuals_mut().widgets.inactive.bg_stroke = Stroke::NONE;
|
||
ui.visuals_mut().widgets.hovered.bg_stroke = Stroke::NONE;
|
||
ui.visuals_mut().widgets.active.bg_stroke = Stroke::NONE;
|
||
|
||
// Setup button text color.
|
||
let text_color = if let Some(c) = color { c } else { Colors::item_button_text() };
|
||
|
||
// Show button.
|
||
let br = Button::new(RichText::new(text).size(20.0).color(text_color))
|
||
.corner_radius(rounding)
|
||
.min_size(button_size)
|
||
.ui(ui)
|
||
.on_hover_cursor(CursorIcon::PointingHand);
|
||
br.surrender_focus();
|
||
if br.clicked() {
|
||
action();
|
||
}
|
||
|
||
// Draw stroke.
|
||
let r = {
|
||
let mut r = ui.available_rect_before_wrap();
|
||
r.min = br.rect.min;
|
||
r.min.x += 0.5;
|
||
r
|
||
};
|
||
Self::line(ui, LinePosition::LEFT, &r, Colors::item_stroke());
|
||
});
|
||
}
|
||
|
||
/// Calculate item background/button rounding based on item index.
|
||
pub fn item_rounding(index: usize, len: usize, is_button: bool) -> CornerRadius {
|
||
let corners = if is_button {
|
||
if len == 1 {
|
||
[false, true, true, false]
|
||
} else if index == 0 {
|
||
[false, true, false, false]
|
||
} else if index == len - 1 {
|
||
[false, false, true, false]
|
||
} else {
|
||
[false, false, false, false]
|
||
}
|
||
} else {
|
||
if len == 1 {
|
||
[true, true, true, true]
|
||
} else if index == 0 {
|
||
[true, true, false, false]
|
||
} else if index == len - 1 {
|
||
[false, false, true, true]
|
||
} else {
|
||
[false, false, false, false]
|
||
}
|
||
};
|
||
CornerRadius {
|
||
nw: if corners[0] { 8.0 as u8 } else { 0.0 as u8 },
|
||
ne: if corners[1] { 8.0 as u8 } else { 0.0 as u8 },
|
||
sw: if corners[3] { 8.0 as u8 } else { 0.0 as u8 },
|
||
se: if corners[2] { 8.0 as u8 } else { 0.0 as u8 },
|
||
}
|
||
}
|
||
|
||
/// Draw selected item check.
|
||
pub fn selected_item_check(ui: &mut egui::Ui) {
|
||
let padding = if View::is_desktop() {
|
||
14.0
|
||
} else {
|
||
18.0
|
||
};
|
||
ui.add_space(padding);
|
||
ui.label(RichText::new(CHECK_FAT).size(20.0).color(Colors::green()));
|
||
ui.add_space(padding);
|
||
}
|
||
|
||
/// Draw rounded box with some value and label in the middle,
|
||
/// where is r = (top_left, top_right, bottom_left, bottom_right).
|
||
/// | VALUE |
|
||
/// | label |
|
||
pub fn label_box(ui: &mut egui::Ui, text: String, label: String, r: [bool; 4]) {
|
||
let rect = ui.available_rect_before_wrap();
|
||
|
||
// Create background shape.
|
||
let mut bg_shape = RectShape::new(rect, CornerRadius {
|
||
nw: if r[0] { 8.0 as u8 } else { 0.0 as u8 },
|
||
ne: if r[1] { 8.0 as u8 } else { 0.0 as u8 },
|
||
sw: if r[2] { 8.0 as u8 } else { 0.0 as u8 },
|
||
se: if r[3] { 8.0 as u8 } else { 0.0 as u8 },
|
||
}, Colors::fill_lite(), Self::item_stroke(), StrokeKind::Outside);
|
||
let bg_idx = ui.painter().add(bg_shape.clone());
|
||
|
||
// Draw box content.
|
||
let content_resp = ui.scope_builder(UiBuilder::new().max_rect(rect), |ui| {
|
||
ui.vertical_centered_justified(|ui| {
|
||
ui.add_space(4.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(text, TextFormat {
|
||
font_id: FontId::proportional(17.0),
|
||
color: Colors::white_or_black(true),
|
||
..Default::default()
|
||
});
|
||
job.wrap = TextWrapping {
|
||
max_rows: 1,
|
||
break_anywhere: true,
|
||
overflow_character: Option::from('﹍'),
|
||
..Default::default()
|
||
};
|
||
ui.label(job);
|
||
|
||
// Draw box label.
|
||
ui.label(RichText::new(label).color(Colors::gray()).size(15.0));
|
||
});
|
||
ui.add_space(2.0);
|
||
});
|
||
}).response;
|
||
|
||
// Setup background shape size.
|
||
bg_shape.rect = content_resp.rect;
|
||
ui.painter().set(bg_idx, bg_shape);
|
||
}
|
||
|
||
/// Draw content in the center of current layout with specified width and height.
|
||
pub fn center_content(ui: &mut egui::Ui, height: f32, content: impl FnOnce(&mut egui::Ui)) {
|
||
ui.vertical_centered(|ui| {
|
||
let mut rect = ui.available_rect_before_wrap();
|
||
let side_margin = 28.0;
|
||
rect.min += egui::emath::vec2(side_margin, ui.available_height() / 2.0 - height / 2.0);
|
||
rect.max -= egui::emath::vec2(side_margin, 0.0);
|
||
ui.scope_builder(UiBuilder::new().max_rect(rect), |ui| {
|
||
content(ui);
|
||
});
|
||
});
|
||
}
|
||
|
||
/// Draw loading spinner.
|
||
pub fn loading_spinner(ui: &mut egui::Ui, size: f32) {
|
||
Spinner::new().size(size).color(Colors::gold()).ui(ui);
|
||
}
|
||
|
||
/// Size of big loading spinner.
|
||
pub const BIG_SPINNER_SIZE: f32 = 104.0;
|
||
|
||
/// Draw big gold loading spinner.
|
||
pub fn big_loading_spinner(ui: &mut egui::Ui) {
|
||
View::loading_spinner(ui, View::BIG_SPINNER_SIZE);
|
||
}
|
||
|
||
/// Size of big loading spinner.
|
||
pub const SMALL_SPINNER_SIZE: f32 = 32.0;
|
||
|
||
/// Draw small gold loading spinner.
|
||
pub fn small_loading_spinner(ui: &mut egui::Ui) {
|
||
View::loading_spinner(ui, View::SMALL_SPINNER_SIZE);
|
||
}
|
||
|
||
/// Draw the button that looks like checkbox with callback on check.
|
||
pub fn checkbox(ui: &mut egui::Ui, checked: bool, text: String, action: impl FnOnce()) {
|
||
let (text_value, color) = match checked {
|
||
true => (format!("{} {}", CHECK_SQUARE, text), Colors::text_button()),
|
||
false => (format!("{} {}", SQUARE, text), Colors::checkbox())
|
||
};
|
||
|
||
let br = Button::new(RichText::new(text_value).size(17.0).color(color))
|
||
.frame(false)
|
||
.stroke(Stroke::NONE)
|
||
.fill(Colors::TRANSPARENT)
|
||
.ui(ui)
|
||
.on_hover_cursor(CursorIcon::PointingHand);
|
||
if br.clicked() {
|
||
action();
|
||
}
|
||
}
|
||
|
||
/// 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) {
|
||
ui.scope(|ui| {
|
||
// Setup background color.
|
||
ui.visuals_mut().widgets.inactive.bg_fill = Colors::fill_deep();
|
||
// Draw radio button.
|
||
let mut response = ui.radio(*current == value, text)
|
||
.on_hover_cursor(CursorIcon::PointingHand);
|
||
if response.clicked() && *current != value {
|
||
*current = value;
|
||
response.mark_changed();
|
||
}
|
||
});
|
||
}
|
||
|
||
/// 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(),
|
||
line_rect.center().y.round_to_pixels(painter.pixels_per_point()),
|
||
Stroke { width: 1.0, color });
|
||
}
|
||
|
||
/// Draw line for panel content.
|
||
pub fn line(ui: &mut egui::Ui, pos: LinePosition, rect: &Rect, color: Color32) {
|
||
let points = match pos {
|
||
LinePosition::RIGHT => {
|
||
vec![{
|
||
let mut r = rect.clone();
|
||
r.min.x = r.max.x;
|
||
r.min
|
||
}, rect.max]
|
||
}
|
||
LinePosition::BOTTOM => {
|
||
vec![{
|
||
let mut r = rect.clone();
|
||
r.min.y = r.max.y;
|
||
r.min
|
||
}, rect.max]
|
||
}
|
||
LinePosition::LEFT => {
|
||
vec![rect.min, {
|
||
let mut r = rect.clone();
|
||
r.max.x = r.min.x;
|
||
r.max
|
||
}]
|
||
}
|
||
LinePosition::TOP => {
|
||
vec![rect.min, {
|
||
let mut r = rect.clone();
|
||
r.max.y = r.min.y;
|
||
r.max
|
||
}]
|
||
}
|
||
};
|
||
let stroke = PathShape {
|
||
points,
|
||
closed: false,
|
||
fill: Default::default(),
|
||
stroke: PathStroke::new(1.0, color),
|
||
};
|
||
ui.painter().add(stroke);
|
||
}
|
||
|
||
/// Draw SVG image from provided data with optional provided size.
|
||
pub fn svg_image(ui: &mut egui::Ui,
|
||
name: &str,
|
||
svg: &[u8],
|
||
size: SizeHint) -> TextureHandle {
|
||
let color_img = load_svg_bytes_with_size(svg, size, &usvg::Options::default()).unwrap();
|
||
// Create image texture.
|
||
let texture_handle = ui.ctx().load_texture(name,
|
||
color_img.clone(),
|
||
TextureOptions::default());
|
||
let img_size = egui::emath::vec2(color_img.width() as f32,
|
||
color_img.height() as f32);
|
||
let sized_img = SizedTexture::new(texture_handle.id(), img_size);
|
||
// Add image to content.
|
||
ui.add(egui::Image::from_texture(sized_img)
|
||
.max_height(ui.available_width())
|
||
.fit_to_original_size(1.0));
|
||
texture_handle
|
||
}
|
||
|
||
/// Draw application logo image with name and version.
|
||
pub fn app_logo_name_version(ui: &mut egui::Ui) {
|
||
ui.add_space(-1.0);
|
||
let logo = if AppConfig::dark_theme().unwrap_or(false) {
|
||
egui::include_image!("../../../img/logo_light.png")
|
||
} else {
|
||
egui::include_image!("../../../img/logo.png")
|
||
};
|
||
// Show application logo and name.
|
||
ui.scope(|ui| {
|
||
ui.set_opacity(0.9);
|
||
egui::Image::new(logo).fit_to_exact_size(egui::vec2(182.0, 182.0)).ui(ui);
|
||
});
|
||
ui.add_space(-11.0);
|
||
ui.label(RichText::new("GRIM")
|
||
.size(24.0)
|
||
.color(Colors::white_or_black(true))
|
||
);
|
||
ui.add_space(-2.0);
|
||
ui.label(RichText::new(crate::VERSION)
|
||
.size(16.0)
|
||
.color(Colors::title(false))
|
||
);
|
||
}
|
||
|
||
/// Draw semi-transparent cover at specified area.
|
||
pub fn content_cover_ui(ui: &mut egui::Ui,
|
||
rect: Rect,
|
||
id: impl std::hash::Hash,
|
||
mut on_click: impl FnMut()) {
|
||
let resp = ui.interact(rect, egui::Id::new(id), Sense::click_and_drag());
|
||
if resp.clicked() || resp.dragged() {
|
||
on_click();
|
||
}
|
||
let shape = RectShape::filled(resp.rect,
|
||
CornerRadius::ZERO,
|
||
Colors::semi_transparent().gamma_multiply(0.7));
|
||
ui.painter().add(shape);
|
||
}
|
||
|
||
/// Get top display inset (cutout) size.
|
||
pub fn get_top_inset() -> f32 {
|
||
TOP_DISPLAY_INSET.load(Ordering::Relaxed) as f32
|
||
}
|
||
|
||
/// Get right display inset (cutout) size.
|
||
pub fn get_right_inset() -> f32 {
|
||
RIGHT_DISPLAY_INSET.load(Ordering::Relaxed) as f32
|
||
}
|
||
|
||
/// Get bottom display inset (cutout) size.
|
||
pub fn get_bottom_inset() -> f32 {
|
||
BOTTOM_DISPLAY_INSET.load(Ordering::Relaxed) as f32
|
||
}
|
||
|
||
/// Get left display inset (cutout) size.
|
||
pub fn get_left_inset() -> f32 {
|
||
LEFT_DISPLAY_INSET.load(Ordering::Relaxed) as f32
|
||
}
|
||
|
||
}
|
||
|
||
lazy_static! {
|
||
static ref TOP_DISPLAY_INSET: AtomicI32 = AtomicI32::new(0);
|
||
static ref RIGHT_DISPLAY_INSET: AtomicI32 = AtomicI32::new(0);
|
||
static ref BOTTOM_DISPLAY_INSET: AtomicI32 = AtomicI32::new(0);
|
||
static ref LEFT_DISPLAY_INSET: AtomicI32 = AtomicI32::new(0);
|
||
}
|
||
|
||
#[allow(dead_code)]
|
||
#[cfg(target_os = "android")]
|
||
#[allow(non_snake_case)]
|
||
#[no_mangle]
|
||
/// Callback from Java code to update display insets (cutouts).
|
||
pub extern "C" fn Java_mw_gri_android_MainActivity_onDisplayInsets(
|
||
_env: jni::JNIEnv,
|
||
_class: jni::objects::JObject,
|
||
cutouts: jni::sys::jarray
|
||
) {
|
||
use jni::objects::{JObject, JPrimitiveArray};
|
||
|
||
let mut array: [i32; 4] = [0; 4];
|
||
unsafe {
|
||
let j_obj = JObject::from_raw(cutouts);
|
||
let j_arr = JPrimitiveArray::from(j_obj);
|
||
_env.get_int_array_region(j_arr, 0, array.as_mut()).unwrap();
|
||
}
|
||
TOP_DISPLAY_INSET.store(array[0], Ordering::Relaxed);
|
||
RIGHT_DISPLAY_INSET.store(array[1], Ordering::Relaxed);
|
||
BOTTOM_DISPLAY_INSET.store(array[2], Ordering::Relaxed);
|
||
LEFT_DISPLAY_INSET.store(array[3], Ordering::Relaxed);
|
||
} |