mirror of https://github.com/terhechte/postsack
Many small improvements. Currently fiddling with interaction to select partitions
parent
2a0eb842a4
commit
39a33d423f
@ -0,0 +1,12 @@
|
||||
use gmaildb;
|
||||
|
||||
#[cfg(feature = "gui")]
|
||||
fn main() {
|
||||
let config = gmaildb::make_config();
|
||||
gmaildb::gui::run_gui(config);
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "gui"))]
|
||||
fn main() {
|
||||
println!("Gui not selected")
|
||||
}
|
@ -0,0 +1,184 @@
|
||||
//! Runs a continuous thread to calculate the canvas.
|
||||
//! Receives as input the current gui app state and size via a channel,
|
||||
//! Then performs the SQLite query
|
||||
//! Then performs the calculation to the `TreeMap`
|
||||
//! And finally uses a channel to submit the result back to the UI
|
||||
//! Runs its own connection to the SQLite database.
|
||||
|
||||
use std::convert::{TryFrom, TryInto};
|
||||
use std::fmt::Display;
|
||||
use std::thread::JoinHandle;
|
||||
|
||||
use crossbeam_channel::{unbounded, Receiver, Sender};
|
||||
use eframe::egui::Rect as EguiRect;
|
||||
use eyre::{bail, Report, Result};
|
||||
use treemap::{Mappable, Rect, TreemapLayout};
|
||||
|
||||
use crate::database::{
|
||||
query::{DynamicType, Filter, GroupByField, Query, ValueField},
|
||||
query_result::QueryResult,
|
||||
Database,
|
||||
};
|
||||
use crate::gui::state::State;
|
||||
use crate::types::Config;
|
||||
|
||||
#[derive(Debug, Hash, Clone)]
|
||||
pub enum Value {
|
||||
Number(usize),
|
||||
String(String),
|
||||
Bool(bool),
|
||||
}
|
||||
|
||||
impl Display for Value {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Value::Number(n) => f.write_str(&n.to_string()),
|
||||
Value::Bool(n) => f.write_str(&n.to_string()),
|
||||
Value::String(n) => f.write_str(&n),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Partition {
|
||||
pub field: GroupByField,
|
||||
pub count: usize,
|
||||
pub value: Value,
|
||||
/// A TreeMap Rect
|
||||
pub rect: Rect,
|
||||
}
|
||||
|
||||
impl Partition {
|
||||
/// Perform rect conversion from TreeMap to Egui
|
||||
pub fn layout_rect(&self) -> EguiRect {
|
||||
use eframe::egui::pos2;
|
||||
EguiRect {
|
||||
min: pos2(self.rect.x as f32, self.rect.y as f32),
|
||||
max: pos2(
|
||||
self.rect.x as f32 + self.rect.w as f32,
|
||||
self.rect.y as f32 + self.rect.h as f32,
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A small NewType so that we can keep all the `TreeMap` code in here and don't
|
||||
/// have to do the layout calculation in a widget.
|
||||
pub struct Partitions {
|
||||
pub items: Vec<Partition>,
|
||||
pub selected: Option<Partition>,
|
||||
}
|
||||
|
||||
impl Partitions {
|
||||
pub fn new(items: Vec<Partition>) -> Self {
|
||||
Self {
|
||||
items,
|
||||
selected: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Update the layout information in the partitions
|
||||
/// based on the current size
|
||||
pub fn update_layout(&mut self, rect: EguiRect) {
|
||||
let layout = TreemapLayout::new();
|
||||
let bounds = Rect::from_points(
|
||||
rect.left() as f64,
|
||||
rect.top() as f64,
|
||||
rect.width() as f64,
|
||||
rect.height() as f64,
|
||||
);
|
||||
layout.layout_items(&mut self.items, bounds);
|
||||
}
|
||||
}
|
||||
|
||||
impl Mappable for Partition {
|
||||
fn size(&self) -> f64 {
|
||||
self.count as f64
|
||||
}
|
||||
|
||||
fn bounds(&self) -> &Rect {
|
||||
&self.rect
|
||||
}
|
||||
|
||||
fn set_bounds(&mut self, bounds: Rect) {
|
||||
self.rect = bounds;
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<&'a QueryResult<'a>> for Partition {
|
||||
type Error = Report;
|
||||
fn try_from(r: &'a QueryResult<'a>) -> Result<Self> {
|
||||
// so far we can only support one group by at a time.
|
||||
// at least in here. The queries support it
|
||||
let field = r
|
||||
.values
|
||||
.first()
|
||||
.ok_or(eyre::eyre!("No group by fields available"))?;
|
||||
|
||||
let value = match (field.is_bool(), field.is_str(), field.is_usize()) {
|
||||
(true, false, false) => Value::Bool(*field.as_bool()),
|
||||
(false, true, false) => Value::String(field.as_str().to_string()),
|
||||
(false, false, true) => Value::Number(*field.as_usize()),
|
||||
_ => bail!("Invalid field: {:?}", &field),
|
||||
};
|
||||
|
||||
Ok(Partition {
|
||||
field: field.as_field(),
|
||||
count: r.count,
|
||||
value,
|
||||
rect: Rect::new(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub type InputSender = Sender<State>;
|
||||
pub type OutputReciever = Receiver<Result<Partitions>>;
|
||||
pub type Handle = JoinHandle<Result<(), Report>>;
|
||||
|
||||
pub fn run(config: &Config) -> Result<(InputSender, OutputReciever, Handle)> {
|
||||
let database = Database::new(&config.database_path)?;
|
||||
let (input_sender, input_receiver) = unbounded();
|
||||
let (output_sender, output_receiver) = unbounded();
|
||||
let handle = std::thread::spawn(move || inner_loop(database, input_receiver, output_sender));
|
||||
Ok((input_sender, output_receiver, handle))
|
||||
}
|
||||
|
||||
fn inner_loop(
|
||||
database: Database,
|
||||
input_receiver: Receiver<State>,
|
||||
output_sender: Sender<Result<Partitions>>,
|
||||
) -> Result<()> {
|
||||
loop {
|
||||
let task = input_receiver.recv()?;
|
||||
let filters = convert_filters(&task);
|
||||
let group_by = vec![GroupByField::Year];
|
||||
let query = Query {
|
||||
filters: &filters,
|
||||
group_by: &group_by,
|
||||
};
|
||||
let result = database.query(query)?;
|
||||
let partitions = calculate_partitions(&result)?;
|
||||
output_sender.send(Ok(Partitions::new(partitions)))?
|
||||
}
|
||||
}
|
||||
|
||||
fn calculate_partitions<'a>(result: &[QueryResult<'a>]) -> Result<Vec<Partition>> {
|
||||
let mut partitions = Vec::new();
|
||||
for r in result.iter() {
|
||||
let partition = r.try_into()?;
|
||||
partitions.push(partition);
|
||||
}
|
||||
|
||||
Ok(partitions)
|
||||
}
|
||||
|
||||
fn convert_filters<'a>(state: &'a State) -> Vec<Filter<'a>> {
|
||||
let mut filters = Vec::new();
|
||||
if let Some(ref n) = state.domain_filter {
|
||||
filters.push(Filter::Like(ValueField::SenderDomain(n.into())));
|
||||
}
|
||||
if let Some(n) = state.year_filter {
|
||||
filters.push(Filter::Is(ValueField::Year(n)));
|
||||
}
|
||||
filters
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
use super::query::ValueField;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct QueryResult<'a> {
|
||||
/// How many items did we find?
|
||||
pub count: usize,
|
||||
/// All the itmes that we grouped by including their values.
|
||||
/// So that we can use each of them to limit the next query.
|
||||
pub values: Vec<ValueField<'a>>,
|
||||
}
|
@ -0,0 +1,148 @@
|
||||
use eframe::epi::{Frame, Storage};
|
||||
use eyre::{Report, Result};
|
||||
|
||||
use eframe::{egui, epi};
|
||||
|
||||
use super::state::State;
|
||||
use super::widgets::Spinner;
|
||||
use crate::canvas_calc::{run, Handle, InputSender, OutputReciever, Partitions};
|
||||
use crate::types::Config;
|
||||
|
||||
struct Link {
|
||||
input_sender: InputSender,
|
||||
output_receiver: OutputReciever,
|
||||
handle: Handle,
|
||||
}
|
||||
|
||||
pub struct MyApp {
|
||||
config: Config,
|
||||
link: Option<Result<Link>>,
|
||||
state: State,
|
||||
partitions: Vec<Partitions>,
|
||||
error: Option<Report>,
|
||||
is_rendering: bool,
|
||||
}
|
||||
|
||||
impl MyApp {
|
||||
pub fn new(config: &Config) -> Self {
|
||||
let state = State {
|
||||
year_filter: None,
|
||||
domain_filter: None,
|
||||
};
|
||||
Self {
|
||||
config: config.clone(),
|
||||
link: None,
|
||||
state,
|
||||
partitions: Vec::new(),
|
||||
error: None,
|
||||
is_rendering: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl epi::App for MyApp {
|
||||
fn name(&self) -> &str {
|
||||
"My egui App"
|
||||
}
|
||||
|
||||
fn setup(
|
||||
&mut self,
|
||||
_ctx: &egui::CtxRef,
|
||||
_frame: &mut Frame<'_>,
|
||||
_storage: Option<&dyn Storage>,
|
||||
) {
|
||||
let link = run(&self.config).map(|(input_sender, output_receiver, handle)| Link {
|
||||
input_sender,
|
||||
output_receiver,
|
||||
handle,
|
||||
});
|
||||
|
||||
if let Ok(l) = &link {
|
||||
l.input_sender.send(self.state.clone());
|
||||
}
|
||||
|
||||
self.link = Some(link);
|
||||
}
|
||||
|
||||
fn update(&mut self, ctx: &egui::CtxRef, frame: &mut epi::Frame<'_>) {
|
||||
// If we have a selection, load the next one
|
||||
if let Some(partition) = self.partitions.last() {
|
||||
if let Some(sel) = &partition.selected {
|
||||
if let Some(Ok(ref link)) = self.link {
|
||||
link.input_sender.send(self.state.clone());
|
||||
}
|
||||
self.is_rendering = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Receive new data
|
||||
if let Some(Ok(ref link)) = self.link {
|
||||
match link.output_receiver.try_recv() {
|
||||
Ok(Ok(p)) => {
|
||||
self.partitions.push(p);
|
||||
self.is_rendering = false;
|
||||
}
|
||||
Err(_) => (),
|
||||
Ok(Err(e)) => self.error = Some(e),
|
||||
}
|
||||
|
||||
// Check if the thread is still running
|
||||
// FIXME: Not sure how to do this, joinhandle doesn't expose anything..
|
||||
//if link.handle.
|
||||
}
|
||||
|
||||
let Self {
|
||||
config,
|
||||
link,
|
||||
state,
|
||||
partitions,
|
||||
is_rendering,
|
||||
error,
|
||||
..
|
||||
} = self;
|
||||
|
||||
egui::SidePanel::left("my_left_panel").show(ctx, |ui| {
|
||||
ui.heading("My egui Application");
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Search");
|
||||
//ui.text_edit_singleline(state.domain_filter);
|
||||
});
|
||||
//ui.add(egui::Slider::new(age, 0..=120).text("age"));
|
||||
//if ui.button("Click each year").clicked() {
|
||||
// if let Some(Ok(link)) = link {
|
||||
// link.input_sender.send(state.clone());
|
||||
// *is_rendering = true;
|
||||
// }
|
||||
//}
|
||||
});
|
||||
|
||||
egui::TopBottomPanel::top("my_panel").show(ctx, |ui| {
|
||||
ui.label("GmailDB");
|
||||
});
|
||||
|
||||
egui::CentralPanel::default().show(ctx, |ui| {
|
||||
match (partitions.last_mut(), *is_rendering) {
|
||||
(_, true) | (None, false) => {
|
||||
ui.centered_and_justified(|ui| {
|
||||
ui.add(Spinner::new(egui::vec2(50.0, 50.0)));
|
||||
});
|
||||
}
|
||||
(Some(p), _) => {
|
||||
ui.add(super::widgets::rectangles(p));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Resize the native window to be just the size we need it to be:
|
||||
frame.set_window_size(ctx.used_size());
|
||||
|
||||
// If we're waiting for a computation to succeed,
|
||||
// we re-render again.
|
||||
// We do this because calling
|
||||
// ctx.request_repaint();
|
||||
// somehow didn't work..
|
||||
if *is_rendering == true {
|
||||
ctx.request_repaint();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
use crate::types::Config;
|
||||
|
||||
mod app;
|
||||
pub(crate) mod state;
|
||||
pub(crate) mod widgets;
|
||||
|
||||
pub fn run_gui(config: Config) {
|
||||
let options = eframe::NativeOptions::default();
|
||||
eframe::run_native(Box::new(app::MyApp::new(&config)), options);
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct State {
|
||||
pub year_filter: Option<usize>,
|
||||
pub domain_filter: Option<String>,
|
||||
}
|
@ -0,0 +1,63 @@
|
||||
/*pub fn toggle_ui(ui: &mut egui::Ui, on: &mut bool) -> egui::Response {
|
||||
// Widget code can be broken up in four steps:
|
||||
// 1. Decide a size for the widget
|
||||
// 2. Allocate space for it
|
||||
// 3. Handle interactions with the widget (if any)
|
||||
// 4. Paint the widget
|
||||
|
||||
// 1. Deciding widget size:
|
||||
// You can query the `ui` how much space is available,
|
||||
// but in this example we have a fixed size widget based on the height of a standard button:
|
||||
let desired_size = ui.spacing().interact_size.y * egui::vec2(2.0, 1.0);
|
||||
|
||||
// 2. Allocating space:
|
||||
// This is where we get a region of the screen assigned.
|
||||
// We also tell the Ui to sense clicks in the allocated region.
|
||||
let (rect, mut response) = ui.allocate_exact_size(desired_size, egui::Sense::click());
|
||||
|
||||
// 3. Interact: Time to check for clicks!
|
||||
if response.clicked() {
|
||||
*on = !*on;
|
||||
response.mark_changed(); // report back that the value changed
|
||||
}
|
||||
|
||||
// Attach some meta-data to the response which can be used by screen readers:
|
||||
response.widget_info(|| egui::WidgetInfo::selected(egui::WidgetType::Checkbox, *on, ""));
|
||||
|
||||
// 4. Paint!
|
||||
// First let's ask for a simple animation from egui.
|
||||
// egui keeps track of changes in the boolean associated with the id and
|
||||
// returns an animated value in the 0-1 range for how much "on" we are.
|
||||
let how_on = ui.ctx().animate_bool(response.id, *on);
|
||||
|
||||
// We will follow the current style by asking
|
||||
// "how should something that is being interacted with be painted?".
|
||||
// This will, for instance, give us different colors when the widget is hovered or clicked.
|
||||
let visuals = ui.style().interact_selectable(&response, *on);
|
||||
|
||||
// All coordinates are in absolute screen coordinates so we use `rect` to place the elements.
|
||||
let rect = rect.expand(visuals.expansion);
|
||||
let radius = 0.5 * rect.height();
|
||||
ui.painter()
|
||||
.rect(rect, radius, visuals.bg_fill, visuals.bg_stroke);
|
||||
|
||||
// Paint the circle, animating it from left to right with `how_on`:
|
||||
let circle_x = egui::lerp((rect.left() + radius)..=(rect.right() - radius), how_on);
|
||||
let center = egui::pos2(circle_x, rect.center().y);
|
||||
ui.painter()
|
||||
.circle(center, 0.75 * radius, visuals.bg_fill, visuals.fg_stroke);
|
||||
|
||||
// All done! Return the interaction response so the user can check what happened
|
||||
// (hovered, clicked, ...) and maybe show a tooltip:
|
||||
response
|
||||
}
|
||||
|
||||
pub fn toggle(on: &mut bool) -> impl egui::Widget + '_ {
|
||||
move |ui: &mut egui::Ui| toggle_ui(ui, on)
|
||||
}
|
||||
|
||||
pub fn url_to_file_source_code() -> String {
|
||||
format!("https://github.com/emilk/egui/blob/master/{}", file!())
|
||||
}
|
||||
|
||||
*/
|
@ -0,0 +1,5 @@
|
||||
mod rectangles;
|
||||
mod spinner;
|
||||
|
||||
pub use rectangles::rectangles;
|
||||
pub use spinner::Spinner;
|
@ -0,0 +1,83 @@
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
|
||||
use crate::canvas_calc::{Partition, Partitions};
|
||||
use eframe::egui::{self, Align2, Rgba, Stroke, TextStyle};
|
||||
|
||||
fn partition_to_color(partition: &Partition) -> Rgba {
|
||||
let mut hasher = DefaultHasher::new();
|
||||
use std::hash::{Hash, Hasher};
|
||||
partition.value.hash(&mut hasher);
|
||||
let value = hasher.finish();
|
||||
let [r1, r2, g1, g2, b1, b2, _, _] = value.to_be_bytes();
|
||||
|
||||
Rgba::from_rgb(
|
||||
(r1 as f32 + r2 as f32) / (u8::MAX as f32 * 2.0),
|
||||
(g1 as f32 + g2 as f32) / (u8::MAX as f32 * 2.0),
|
||||
(b1 as f32 + b2 as f32) / (u8::MAX as f32 * 2.0),
|
||||
)
|
||||
}
|
||||
|
||||
fn rectangles_ui(ui: &mut egui::Ui, partitions: &mut Partitions) -> egui::Response {
|
||||
let size = ui.available_size();
|
||||
let (rect, mut response) = ui.allocate_exact_size(size, egui::Sense::click());
|
||||
|
||||
// let visuals = ui.style().interact_selectable(&response, true);
|
||||
|
||||
partitions.update_layout(rect);
|
||||
|
||||
for item in &partitions.items {
|
||||
if ui.put(item.layout_rect(), rectangle(&item)).clicked() {
|
||||
dbg!("clicked");
|
||||
partitions.selected = Some(item.clone());
|
||||
response.mark_changed();
|
||||
}
|
||||
}
|
||||
|
||||
response
|
||||
}
|
||||
|
||||
pub fn rectangles(partitions: &mut Partitions) -> impl egui::Widget + '_ {
|
||||
move |ui: &mut egui::Ui| rectangles_ui(ui, partitions)
|
||||
}
|
||||
|
||||
fn rectangle_ui(ui: &mut egui::Ui, partition: &Partition) -> egui::Response {
|
||||
let size = ui.available_size();
|
||||
let (rect, mut response) = ui.allocate_exact_size(size, egui::Sense::click());
|
||||
|
||||
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 color = partition_to_color(&partition);
|
||||
|
||||
let painter = ui.painter();
|
||||
|
||||
painter.rect(rect, 0.0, color, stroke);
|
||||
let center = rect.center();
|
||||
|
||||
let label = format!("{}\n{}", &partition.value, &partition.count);
|
||||
|
||||
let style = TextStyle::Body;
|
||||
|
||||
let galley = painter.layout_multiline(style, label.clone(), 32.0);
|
||||
if galley.size.x < rect.width() && galley.size.y < rect.height() {
|
||||
// Can't just paint the galley as it has no `anchor` prop..
|
||||
painter.text(
|
||||
center,
|
||||
Align2::CENTER_CENTER,
|
||||
&label,
|
||||
style,
|
||||
Rgba::BLACK.into(),
|
||||
);
|
||||
}
|
||||
|
||||
response.on_hover_text(&label)
|
||||
}
|
||||
|
||||
fn rectangle(partition: &Partition) -> impl egui::Widget + '_ {
|
||||
move |ui: &mut egui::Ui| rectangle_ui(ui, partition)
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
use eframe::egui::{self, lerp, vec2, Color32, Pos2, Response, Shape, Stroke, Vec2, Widget};
|
||||
|
||||
/// A simple spinner
|
||||
pub struct Spinner(Vec2);
|
||||
|
||||
impl Spinner {
|
||||
pub fn new(size: Vec2) -> Self {
|
||||
Self(size)
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for Spinner {
|
||||
fn ui(self, ui: &mut egui::Ui) -> Response {
|
||||
let Spinner(size) = self;
|
||||
|
||||
let (outer_rect, response) = ui.allocate_exact_size(size, egui::Sense::hover());
|
||||
let visuals = ui.style().visuals.clone();
|
||||
|
||||
let corner_radius = outer_rect.height() / 2.0;
|
||||
|
||||
let n_points = 20;
|
||||
let start_angle = ui.input().time as f64 * 360f64.to_radians();
|
||||
let end_angle = start_angle + 240f64.to_radians() * ui.input().time.sin();
|
||||
let circle_radius = corner_radius - 2.0;
|
||||
let points: Vec<Pos2> = (0..n_points)
|
||||
.map(|i| {
|
||||
let angle = lerp(start_angle..=end_angle, i as f64 / n_points as f64);
|
||||
let (sin, cos) = angle.sin_cos();
|
||||
outer_rect.right_center()
|
||||
+ circle_radius * vec2(cos as f32, sin as f32)
|
||||
+ vec2(-corner_radius, 0.0)
|
||||
})
|
||||
.collect();
|
||||
ui.painter().add(Shape::Path {
|
||||
points,
|
||||
closed: false,
|
||||
fill: Color32::TRANSPARENT,
|
||||
stroke: Stroke::new(8.0, visuals.strong_text_color()),
|
||||
});
|
||||
|
||||
ui.ctx().request_repaint();
|
||||
|
||||
response
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue