Many small improvements to the look

Also added a platform module for specific looks depending on the platform.
pull/1/head
Benedikt Terhechte 3 years ago
parent a606538524
commit 25b6ddda99

2
Cargo.lock generated

@ -758,6 +758,7 @@ name = "gmaildb"
version = "0.1.0"
dependencies = [
"chrono",
"cocoa",
"crossbeam-channel",
"eframe",
"email-parser",
@ -768,6 +769,7 @@ dependencies = [
"lru",
"mbox-reader",
"num-format",
"objc",
"rayon",
"regex",
"rsql_builder",

@ -37,6 +37,11 @@ default = ["gui"]
trace-sql = []
gui = ["eframe", "lru"]
[target."cfg(target_os = \"macos\")".dependencies.cocoa]
version = "0.24"
[target."cfg(target_os = \"macos\")".dependencies.objc]
version = "0.2.7"
[profile.dev]
split-debuginfo = "unpacked"

@ -1,17 +1,27 @@
use eframe::epi::{Frame, Storage};
use eframe::{
egui::{self, Stroke},
epi::{self, Frame, Storage},
};
use eyre::{Report, Result};
use eframe::{egui, epi};
use super::widgets::{self, Spinner};
use crate::model::{segmentations, Engine};
use crate::model::Engine;
use crate::types::Config;
#[derive(Default)]
pub struct UIState {
pub show_emails: bool,
pub show_filters: bool,
pub show_export: bool,
pub action_close: bool,
}
pub struct GmailDBApp {
_config: Config,
engine: Engine,
error: Option<Report>,
show_emails: bool,
state: UIState,
platform_custom_setup: bool,
}
impl GmailDBApp {
@ -21,7 +31,8 @@ impl GmailDBApp {
_config: config.clone(),
engine,
error: None,
show_emails: false,
state: UIState::default(),
platform_custom_setup: false,
})
}
}
@ -33,11 +44,18 @@ impl epi::App for GmailDBApp {
fn setup(
&mut self,
_ctx: &egui::CtxRef,
ctx: &egui::CtxRef,
_frame: &mut Frame<'_>,
_storage: Option<&dyn Storage>,
) {
self.error = self.engine.start().err();
super::platform::setup(ctx);
// Adapt to the platform colors
let platform_colors = super::platform::platform_colors();
let mut visuals = egui::Visuals::dark();
visuals.widgets.noninteractive.bg_fill = platform_colors.window_background_dark;
ctx.set_visuals(visuals);
}
fn update(&mut self, ctx: &egui::CtxRef, frame: &mut epi::Frame<'_>) {
@ -46,18 +64,34 @@ impl epi::App for GmailDBApp {
self.error = self.engine.process().err();
}
if !self.platform_custom_setup {
self.platform_custom_setup = true;
self.error = super::platform::initial_update(&ctx).err();
}
let Self {
engine,
error,
show_emails,
state,
..
} = self;
let platform_colors = super::platform::platform_colors();
if let Some(error) = error {
dbg!(&error);
egui::CentralPanel::default().show(ctx, |ui| ui.add(widgets::ErrorBox(error)));
} else {
if *show_emails {
let frame = egui::containers::Frame::none()
.fill(platform_colors.window_background_dark)
.stroke(Stroke::none());
egui::TopBottomPanel::top("my_panel")
.frame(frame)
.show(ctx, |ui| {
ui.add(super::top_bar::TopBar::new(engine, error, state));
});
if state.show_emails {
egui::SidePanel::right("my_left_panel")
.default_width(500.0)
.show(ctx, |ui| {
@ -65,45 +99,26 @@ impl epi::App for GmailDBApp {
});
}
egui::TopBottomPanel::top("my_panel").show(ctx, |ui| {
ui.add(super::top_bar::TopBar::new(engine, error));
});
egui::CentralPanel::default().show(ctx, |ui| {
if engine.segmentations().is_empty() {
ui.centered_and_justified(|ui| {
ui.add(Spinner::new(egui::vec2(50.0, 50.0)));
});
} else {
ui.vertical(|ui| {
ui.horizontal(|ui| {
if let Some((range, total)) = segmentations::segments_range(engine) {
ui.label("Limit");
let mut selected = total;
let response = ui.add(egui::Slider::new(&mut selected, range));
if response.changed() {
segmentations::set_segments_range(engine, Some(0..=selected));
}
}
// This is a hack to get right-alignment.
// we can't size the button, we can only size text. We will size text
// and then use ~that for the button
let text = "Mail";
let galley = ui
.painter()
.layout_no_wrap(egui::TextStyle::Button, text.to_owned());
ui.add_space(
ui.available_width()
- (galley.size.x + ui.spacing().button_padding.x * 2.0),
);
if ui.add(egui::Button::new(text)).clicked() {
*show_emails = !*show_emails;
}
egui::CentralPanel::default()
.frame(egui::containers::Frame::none())
.show(ctx, |ui| {
if engine.segmentations().is_empty() {
ui.centered_and_justified(|ui| {
ui.add(Spinner::new(egui::vec2(50.0, 50.0)));
});
ui.add(super::widgets::Rectangles::new(engine, error));
});
}
});
} else {
let stroke = Stroke::none();
let fill = platform_colors.content_background_dark;
super::widgets::background::background_color(ui, 15.0, stroke, fill, |ui| {
ui.vertical(|ui: &mut egui::Ui| {
ui.add(super::segmentation_bar::SegmentationBar::new(
engine, error,
));
ui.add(super::widgets::Rectangles::new(engine, error));
});
})
}
});
}
// Resize the native window to be just the size we need it to be:

@ -3,6 +3,8 @@ use eframe::{self, egui, epi};
mod app;
mod mail_panel;
mod platform;
mod segmentation_bar;
mod top_bar;
pub(crate) mod widgets;

@ -0,0 +1,21 @@
#![cfg(target_os = "linux")]
use eframe::egui;
use super::PlatformColors;
pub fn platform_colors() -> PlatformColors {
// From Google images, Gtk
PlatformColors {
window_background_dark: Color32::from_rgb(53, 53, 53),
window_background_light: Color32::from_rgb(246, 245, 244),
content_background_dark: Color32::from_rgb(34, 32, 40),
content_background_light: Color32::from_rgb(254, 254, 254),
}
}
/// This is called from `App::setup`
pub fn setup(ctx: &egui::CtxRef) {}
/// This is called once from `App::update` on the first run.
pub fn initial_update(ctx: &egui::CtxRef) -> Result<()> {}

@ -0,0 +1,34 @@
#![cfg(target_os = "macos")]
use cocoa;
use eframe::egui::{self, Color32};
use eyre::{bail, Result};
use objc::runtime::{Object, YES};
use super::PlatformColors;
pub fn platform_colors() -> PlatformColors {
PlatformColors {
window_background_dark: Color32::from_rgb(36, 30, 42),
window_background_light: Color32::from_rgb(238, 236, 242),
content_background_dark: Color32::from_rgb(20, 14, 26),
content_background_light: Color32::from_rgb(236, 234, 238),
}
}
/// This is called from `App::setup`
pub fn setup(_ctx: &egui::CtxRef) {}
/// This is called once from `App::update` on the first run.
pub fn initial_update(_ctx: &egui::CtxRef) -> Result<()> {
unsafe {
let app = cocoa::appkit::NSApp();
if app.is_null() {
bail!("Could not retrieve NSApp");
}
let main_window: *mut Object = msg_send![app, mainWindow];
let _: () = msg_send![main_window, setTitlebarAppearsTransparent: YES];
}
Ok(())
}

@ -0,0 +1,27 @@
#[cfg(target_os = "windows")]
mod windows;
#[cfg(target_os = "windows")]
pub use windows::{initial_update, platform_colors, setup};
#[cfg(target_os = "linux")]
mod linux;
#[cfg(target_os = "linux")]
pub use linux::{initial_update, platform_colors, setup};
#[cfg(target_os = "macos")]
mod macos;
#[cfg(target_os = "macos")]
pub use macos::{initial_update, platform_colors, setup};
use eframe::egui::Color32;
/// Platform-Native Colors
#[derive(Debug)]
pub struct PlatformColors {
pub window_background_dark: Color32,
pub window_background_light: Color32,
pub content_background_dark: Color32,
pub content_background_light: Color32,
}

@ -0,0 +1,21 @@
#![cfg(target_os = "windows")]
use eframe::egui;
use super::PlatformColors;
pub fn platform_colors() -> PlatformColors {
// From Google images, Windows 11
PlatformColors {
window_background_dark: Color32::from_rgb(32, 32, 32),
window_background_light: Color32::from_rgb(241, 243, 246),
content_background_dark: Color32::from_rgb(34, 32, 40),
content_background_light: Color32::from_rgb(251, 251, 253),
}
}
/// This is called from `App::setup`
pub fn setup(ctx: &egui::CtxRef) {}
/// This is called once from `App::update` on the first run.
pub fn initial_update(ctx: &egui::CtxRef) -> Result<()> {}

@ -0,0 +1,52 @@
use crate::model::{segmentations, Engine};
use eframe::egui::{self, Widget};
use eyre::Report;
pub struct SegmentationBar<'a> {
engine: &'a mut Engine,
error: &'a mut Option<Report>,
}
impl<'a> SegmentationBar<'a> {
pub fn new(engine: &'a mut Engine, error: &'a mut Option<Report>) -> Self {
Self { engine, error }
}
}
impl<'a> Widget for SegmentationBar<'a> {
fn ui(self, ui: &mut egui::Ui) -> egui::Response {
ui.horizontal(|ui| {
ui.set_height(30.0);
let groupings = segmentations::aggregated_by(self.engine);
let has_back = groupings.len() > 1;
for (id_index, group) in groupings.iter().enumerate() {
let alternatives = segmentations::aggregation_fields(self.engine, group);
if let Some(value) = group.value() {
let label = egui::Label::new(format!("{}: {}", group.name(), value));
ui.add(label);
} else if let Some(mut selected) = group.index(&alternatives) {
let p = egui::ComboBox::from_id_source(&id_index).show_index(
ui,
&mut selected,
alternatives.len(),
|i| alternatives[i].as_str().to_string(),
);
if p.changed() {
*self.error = segmentations::set_aggregation(
self.engine,
group,
&alternatives[selected],
)
.err();
}
}
}
if has_back && ui.button("Back").clicked() {
self.engine.pop();
}
})
.response
}
}

@ -1,50 +1,80 @@
use crate::model::{segmentations, Engine};
use crate::model::Engine;
use eframe::egui::{self, Widget};
use eyre::Report;
use super::app::UIState;
use super::widgets::FilterPanel;
pub struct TopBar<'a> {
engine: &'a mut Engine,
#[allow(unused)]
error: &'a mut Option<Report>,
state: &'a mut UIState,
}
impl<'a> TopBar<'a> {
pub fn new(engine: &'a mut Engine, error: &'a mut Option<Report>) -> Self {
TopBar { engine, error }
pub fn new(
engine: &'a mut Engine,
error: &'a mut Option<Report>,
state: &'a mut UIState,
) -> Self {
TopBar {
engine,
error,
state,
}
}
}
impl<'a> Widget for TopBar<'a> {
fn ui(self, ui: &mut egui::Ui) -> egui::Response {
ui.horizontal(|ui| {
let groupings = segmentations::aggregated_by(self.engine);
let has_back = groupings.len() > 1;
for (id_index, group) in groupings.iter().enumerate() {
let alternatives = segmentations::aggregation_fields(self.engine, group);
if let Some(value) = group.value() {
let label = egui::Label::new(format!("{}: {}", group.name(), value));
ui.add(label);
} else if let Some(mut selected) = group.index(&alternatives) {
let p = egui::ComboBox::from_id_source(&id_index).show_index(
ui,
&mut selected,
alternatives.len(),
|i| alternatives[i].as_str().to_string(),
);
if p.changed() {
*self.error = segmentations::set_aggregation(
self.engine,
group,
&alternatives[selected],
)
.err();
}
let response = ui
.horizontal(|ui| {
ui.set_height(40.0);
ui.add_space(15.0);
if ui.add(egui::Button::new("Close")).clicked() {
self.state.action_close = true;
}
}
if has_back && ui.button("Back").clicked() {
self.engine.pop();
}
})
.response
let filter_response = ui.add(egui::Button::new("Filters"));
let popup_id = ui.make_persistent_id("my_unique_id");
if filter_response.clicked() {
ui.memory().toggle_popup(popup_id);
}
egui::popup_below_widget(ui, popup_id, &filter_response, |ui| {
ui.add(FilterPanel::new(self.engine));
});
// This is a hack to get right-alignment.
// we can't size the button, we can only size text. We will size text
// and then use ~that for these buttons
let mut w = ui.available_width();
let mail_text = "Mails";
let mail_galley = ui
.painter()
.layout_no_wrap(egui::TextStyle::Button, mail_text.to_owned());
let filter_text = "Export";
let filter_galley = ui
.painter()
.layout_no_wrap(egui::TextStyle::Button, filter_text.to_owned());
w -= mail_galley.size.x + ui.spacing().button_padding.x * 4.0;
w -= filter_galley.size.x + ui.spacing().button_padding.x * 4.0;
ui.add_space(w);
ui.add(egui::Button::new(filter_text));
if ui.add(egui::Button::new(mail_text)).clicked() {
self.state.show_emails = !self.state.show_emails;
}
})
.response;
response
}
}

@ -0,0 +1,33 @@
/// This will draw Ui with a background color and margins.
/// This can be used for calls that don't provide a `Frame`,
/// such as `horizontal` or `vertical`
use eframe::egui::{self, Color32, Rect, Stroke, Ui};
pub fn background_color<R>(
ui: &mut Ui,
padding: f32,
stroke: Stroke,
fill: Color32,
show: impl FnOnce(&mut Ui) -> R,
) -> R {
let outer_rect_bounds = ui.available_rect_before_wrap();
let where_to_put_background = ui.painter().add(egui::Shape::Noop);
let margin = egui::Vec2::splat(padding);
let inner_rect = outer_rect_bounds.shrink2(margin);
let mut content_ui = ui.child_ui(inner_rect, *ui.layout());
let ret = show(&mut content_ui);
let outer_rect = Rect::from_min_max(outer_rect_bounds.min, content_ui.min_rect().max + margin);
let (rect, _) = ui.allocate_at_least(outer_rect.size(), egui::Sense::hover());
ui.painter().set(
where_to_put_background,
egui::epaint::Shape::Rect {
corner_radius: 0.0,
fill,
stroke,
rect,
},
);
ret
}

@ -0,0 +1,43 @@
//! A panel to edit filters
use eframe::egui::{self, vec2, Response, TextStyle, Vec2, Widget};
use crate::model::{segmentations, Engine};
pub struct FilterPanel<'a> {
engine: &'a mut Engine,
}
impl<'a> FilterPanel<'a> {
pub fn default_size() -> Vec2 {
vec2(200.0, 400.0)
}
pub fn new(engine: &'a mut Engine) -> Self {
Self { engine }
}
}
impl<'a> Widget for FilterPanel<'a> {
fn ui(self, ui: &mut egui::Ui) -> Response {
let Self { engine } = self;
ui.vertical(|ui| {
if let Some((range, total)) = segmentations::segments_range(engine) {
ui.horizontal(|ui| {
ui.label("Limit");
let mut selected = total;
let response = ui.add(egui::Slider::new(&mut selected, range));
if response.changed() {
segmentations::set_segments_range(engine, Some(0..=selected));
}
});
}
ui.label("label");
ui.label("label");
ui.label("label");
ui.label("label");
ui.label("label");
ui.label("label");
ui.label("label");
})
.response
}
}

@ -1,9 +1,12 @@
pub mod background;
mod error_box;
mod filter_panel;
mod rectangles;
mod spinner;
mod table;
pub use error_box::ErrorBox;
pub use filter_panel::FilterPanel;
pub use rectangles::Rectangles;
pub use spinner::Spinner;
pub use table::Table;

@ -59,13 +59,14 @@ fn rectangle_ui(ui: &mut egui::Ui, segment: &Segment) -> egui::Response {
let visuals = ui.style().interact_selectable(&response, true);
let stroke = if ui.ui_contains_pointer() {
Stroke::new(4.0, visuals.fg_stroke.color)
} else {
Stroke::default()
};
let stroke = Stroke::new(1.0, visuals.bg_fill);
let color = segment_to_color(segment);
let color = if ui.ui_contains_pointer() {
Rgba::from_rgb(color.r() + 0.1, color.g() + 0.1, color.b() + 0.1)
} else {
color
};
let painter = ui.painter();

@ -1,3 +1,7 @@
#![cfg(target_os = "macos")]
#[macro_use]
extern crate objc;
use tracing_subscriber::EnvFilter;
pub mod database;

Loading…
Cancel
Save