Many small improvements. Currently fiddling with interaction to select partitions

pull/2/head
Benedikt Terhechte 3 years ago
parent 2a0eb842a4
commit 39a33d423f

8
Cargo.lock generated

@ -748,6 +748,7 @@ dependencies = [
"thiserror",
"tracing",
"tracing-subscriber",
"treemap",
]
[[package]]
@ -1480,6 +1481,7 @@ dependencies = [
"hashlink",
"libsqlite3-sys",
"memchr",
"serde_json",
"smallvec",
]
@ -1796,6 +1798,12 @@ dependencies = [
"tracing-serde",
]
[[package]]
name = "treemap"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1571f89da27a5e1aa83304ee1ab9519ea8c6432b4c8903aaaa6c9a9eecb6f36"
[[package]]
name = "ttf-parser"
version = "0.6.2"

@ -10,7 +10,7 @@ eyre = "0.6.5"
thiserror = "1.0.29"
tracing = "0.1.28"
tracing-subscriber = "0.2.24"
rusqlite = {version = "0.25.3", features = ["chrono", "trace"]}
rusqlite = {version = "0.25.3", features = ["chrono", "trace", "serde_json"]}
regex = "1.5.3"
flate2 = "1.0.22"
email-parser = { git = "https://github.com/terhechte/email-parser", features = ["sender", "to", "in-reply-to", "date", "subject", "mime", "allow-duplicate-headers"]}
@ -22,6 +22,7 @@ serde = { version = "*", features = ["derive"]}
crossbeam-channel = "0.5.1"
eframe = { version = "*", optional = true}
rsql_builder = "0.1.2"
treemap = "0.3.2"
[features]
default = ["gui"]

@ -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
}

@ -1,20 +1,115 @@
use rusqlite::{self, Error, Row};
use std::str::FromStr;
pub trait RowConversion: Sized {
fn from_row<'stmt>(row: &Row<'stmt>) -> Result<Self, Error>;
use chrono::prelude::*;
use eyre::Result;
use rusqlite::{self, Row};
use super::query::{GroupByField, ValueField};
use super::query_result::QueryResult;
use crate::types::{EmailEntry, EmailMeta};
pub trait RowConversion<'a>: Sized {
fn grouped_from_row<'stmt>(fields: &'a [GroupByField], row: &Row<'stmt>) -> Result<Self>;
}
impl<'a> RowConversion<'a> for QueryResult<'a> {
fn grouped_from_row<'stmt>(fields: &'a [GroupByField], row: &Row<'stmt>) -> Result<Self> {
let amount: usize = row.get("amount")?;
let mut values = vec![];
for field in fields {
use GroupByField::*;
match field {
// Str fields
SenderDomain => values.push(ValueField::SenderDomain(
row.get::<&str, String>(field.into())?.into(),
)),
SenderLocalPart => values.push(ValueField::SenderLocalPart(
row.get::<&str, String>(field.into())?.into(),
)),
SenderName => values.push(ValueField::SenderName(
row.get::<&str, String>(field.into())?.into(),
)),
ToGroup => values.push(ValueField::ToGroup(
row.get::<&str, String>(field.into())?.into(),
)),
ToName => values.push(ValueField::ToName(
row.get::<&str, String>(field.into())?.into(),
)),
ToAddress => values.push(ValueField::ToAddress(
row.get::<&str, String>(field.into())?.into(),
)),
// usize field
Year => values.push(ValueField::Year(
row.get::<&str, usize>(field.into())?.into(),
)),
Month => values.push(ValueField::Day(
row.get::<&str, usize>(field.into())?.into(),
)),
Day => values.push(ValueField::Day(
row.get::<&str, usize>(field.into())?.into(),
)),
// bool field
IsReply => values.push(ValueField::IsReply(
row.get::<&str, bool>(field.into())?.into(),
)),
IsSend => values.push(ValueField::IsSend(
row.get::<&str, bool>(field.into())?.into(),
)),
}
}
Ok(QueryResult {
count: amount,
values,
})
}
}
/*impl RowConversion for EmailEntry {
fn from_row<'stmt>(row: &Row<'stmt>) -> Result<Self, Error> {
let path: String = row.get("path")?;
let domain: String = row.get("domain")?;
let local_part: String = row.get("local_part")?;
let year: usize = row.get("year")?;
let month: usize = row.get("month")?;
let day: usize = row.get("day")?;
let created = email_parser::time::DateTime::
Ok(EmailEntry {
path, domain, local_part, year, month, day
})
impl EmailEntry {
#[allow(unused)]
fn from_row<'stmt>(row: &Row<'stmt>) -> Result<Self> {
let path: String = row.get("path")?;
let path = std::path::PathBuf::from_str(&path)?;
let sender_domain: String = row.get("sender_domain")?;
let sender_local_part: String = row.get("sender_local_part")?;
let sender_name: String = row.get("sender_name")?;
let timestamp: i64 = row.get("timestamp")?;
let datetime = Utc.timestamp(timestamp, 0);
let subject: String = row.get("subject")?;
let to_count: usize = row.get("to_count")?;
let to_group: Option<String> = row.get("to_group")?;
let to_name: Option<String> = row.get("to_name")?;
let to_address: Option<String> = row.get("to_address")?;
let to_first = to_address.map(|a| (to_name.unwrap_or_default(), a));
let is_reply: bool = row.get("is_reply")?;
let is_send: bool = row.get("is_send")?;
// Parse EmailMeta
let meta_tags: Option<String> = row.get("meta_tags")?;
let meta_is_seen: Option<bool> = row.get("meta_is_seen")?;
let meta = match (meta_tags, meta_is_seen) {
(Some(a), Some(b)) => Some(EmailMeta::from(b, &a)),
_ => None,
};
Ok(EmailEntry {
path,
sender_domain,
sender_local_part,
sender_name,
datetime,
subject,
to_count,
to_group,
to_first,
is_reply,
is_send,
meta,
})
}
}
*/

@ -1,15 +1,16 @@
use chrono::Datelike;
use crossbeam_channel::{unbounded, Sender};
use eyre::{Report, Result};
use eyre::{bail, Report, Result};
use rusqlite::{self, params, Connection, Statement};
use core::panic;
use std::{
path::{Path, PathBuf},
thread::JoinHandle,
};
use super::{sql::*, DBMessage};
use crate::types::EmailEntry;
use super::{query_result::QueryResult, sql::*, DBMessage};
use crate::{database::RowConversion, types::EmailEntry};
#[derive(Debug)]
pub struct Database {
@ -38,6 +39,26 @@ impl Database {
})
}
pub fn query<'a>(&self, query: super::query::Query<'a>) -> Result<Vec<QueryResult<'a>>> {
use rusqlite::params_from_iter;
let c = match &self.connection {
Some(n) => n,
None => bail!("No connection to database available in query"),
};
let (sql, values) = query.to_sql();
let mut stmt = c.prepare(&sql)?;
let mut query_results = Vec::new();
let p = params_from_iter(values.iter());
let mut rows = stmt.query(p)?;
while let Some(row) = rows.next()? {
let result = QueryResult::grouped_from_row(&query.group_by, &row)?;
query_results.push(result);
}
Ok(query_results)
}
/// Begin the data import.
/// This will consume the `Database`. A new one has to be opened
/// afterwards in order to support multi-threading.
@ -121,6 +142,7 @@ fn insert_mail(statement: &mut Statement, entry: &EmailEntry) -> Result<()> {
let year = entry.datetime.date().year();
let month = entry.datetime.date().month();
let day = entry.datetime.date().day();
let timestamp = entry.datetime.timestamp();
let e = entry;
let to_name = e.to_first.as_ref().map(|e| &e.0);
let to_address = e.to_first.as_ref().map(|e| &e.1);
@ -134,6 +156,7 @@ fn insert_mail(statement: &mut Statement, entry: &EmailEntry) -> Result<()> {
year,
month,
day,
timestamp,
e.subject,
e.to_count,
e.to_group,

@ -1,6 +1,8 @@
mod conversion;
mod database;
mod db_message;
pub mod query;
pub mod query_result;
mod sql;
pub use conversion::RowConversion;

@ -40,22 +40,51 @@ pub enum GroupByField {
IsSend,
}
#[derive(Debug, PartialEq, Eq)]
impl<'a> ValueField<'a> {
pub fn as_field(&self) -> GroupByField {
use GroupByField::*;
match self {
ValueField::SenderDomain(_) => SenderDomain,
ValueField::SenderLocalPart(_) => SenderLocalPart,
ValueField::SenderName(_) => SenderName,
ValueField::Year(_) => Year,
ValueField::Month(_) => Month,
ValueField::Day(_) => Day,
ValueField::ToGroup(_) => ToGroup,
ValueField::ToName(_) => ToName,
ValueField::ToAddress(_) => ToAddress,
ValueField::IsReply(_) => IsReply,
ValueField::IsSend(_) => IsSend,
}
}
}
/*impl GroupByField {
pub fn make_str<'a>(value: &'a str, field: GroupByField) -> ValueField<'a> {
use GroupByField::*;
match field {
SenderDomain => ValueField::SenderDomain(value.into()),
_ => panic!(),
}
}
}*/
#[derive(Debug, PartialEq, Eq, Clone)]
pub enum ValueField<'a> {
SenderDomain(&'a str),
SenderLocalPart(&'a str),
SenderName(&'a str),
SenderDomain(Cow<'a, str>),
SenderLocalPart(Cow<'a, str>),
SenderName(Cow<'a, str>),
Year(usize),
Month(usize),
Day(usize),
ToGroup(&'a str),
ToName(&'a str),
ToAddress(&'a str),
ToGroup(Cow<'a, str>),
ToName(Cow<'a, str>),
ToAddress(Cow<'a, str>),
IsReply(bool),
IsSend(bool),
}
trait DynamicType<'a> {
pub trait DynamicType<'a> {
type BoolType;
type StrType;
type UsizeType;
@ -69,7 +98,7 @@ trait DynamicType<'a> {
impl<'a> DynamicType<'a> for ValueField<'a> {
type BoolType = &'a bool;
type StrType = &'a str;
type StrType = Cow<'a, str>;
type UsizeType = &'a usize;
fn is_str(&self) -> bool {
@ -94,7 +123,7 @@ impl<'a> DynamicType<'a> for ValueField<'a> {
use ValueField::*;
match self {
SenderDomain(a) | SenderLocalPart(a) | SenderName(a) | ToGroup(a) | ToName(a)
| ToAddress(a) => a,
| ToAddress(a) => a.clone(),
_ => panic!(),
}
}
@ -118,7 +147,7 @@ impl<'a> DynamicType<'a> for ValueField<'a> {
impl<'a> DynamicType<'a> for &VecOfMinOne<ValueField<'a>> {
type BoolType = Vec<bool>;
type StrType = Vec<&'a str>;
type StrType = Vec<Cow<'a, str>>;
type UsizeType = Vec<usize>;
fn is_str(&self) -> bool {
self.inner[0].is_str()
@ -198,8 +227,8 @@ impl From<&GroupByField> for &str {
}
pub struct Query<'a> {
filters: &'a [Filter<'a>],
group_by: &'a [GroupByField],
pub filters: &'a [Filter<'a>],
pub group_by: &'a [GroupByField],
}
impl<'a> Query<'a> {
@ -232,20 +261,21 @@ impl<'a> Query<'a> {
whr
};
let group_by = {
let group_by_fields: Vec<&str> = self.group_by.iter().map(|e| e.into()).collect();
format!("GROUP BY {}", group_by_fields.join(", "))
};
let group_by_fields: Vec<&str> = self.group_by.iter().map(|e| e.into()).collect();
let group_by = format!("GROUP BY {}", &group_by_fields.join(", "));
// If we have a group by, we always include the count
let header = if self.group_by.is_empty() {
"SELECT * FROM emails"
"SELECT * FROM emails".to_owned()
} else {
"SELECT count(path), * FROM emails"
format!(
"SELECT count(path) as amount, {} FROM emails",
group_by_fields.join(", ")
)
};
let (sql, values) = rsql_builder::B::prepare(
rsql_builder::B::new_sql(header)
rsql_builder::B::new_sql(&header)
.push_build(&mut conditions)
.push_sql(&group_by),
);
@ -263,8 +293,8 @@ mod tests {
let value = format!("bx");
let query = Query {
filters: &[
Filter::Is(ValueField::ToName("bam")),
Filter::Like(ValueField::SenderName(&value)),
Filter::Is(ValueField::ToName("bam".into())),
Filter::Like(ValueField::SenderName(value.into())),
Filter::Like(ValueField::Year(2323)),
],
group_by: &[GroupByField::Month],

@ -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>>,
}

@ -7,6 +7,7 @@ CREATE TABLE IF NOT EXISTS emails (
year INTEGER NOT NULL,
month INTEGER NOT NULL,
day INTEGER NOT NULL,
timestamp INTEGER NOT NULL,
subject TEXT NOT NULL,
to_count INTEGER NOT NULL,
to_group TEXT NULL,
@ -28,7 +29,7 @@ pub const QUERY_EMAILS: &str = r#"
INSERT INTO emails
(
path, sender_domain, sender_local_part, sender_name,
year, month, day, subject,
year, month, day, timestamp, subject,
to_count, to_group, to_name, to_address,
is_reply, is_send,
meta_tags, meta_is_seen
@ -36,7 +37,7 @@ INSERT INTO emails
VALUES
(
?, ?, ?, ?,
?, ?, ?, ?,
?, ?, ?, ?, ?,
?, ?, ?, ?,
?, ?,
?, ?

@ -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
}
}

@ -1,5 +1,6 @@
use tracing_subscriber::EnvFilter;
mod canvas_calc;
pub mod database;
pub mod filesystem;
#[cfg(feature = "gui")]

@ -9,9 +9,15 @@ pub struct EmailMeta {
pub is_seen: bool,
}
const TAG_SEP: &str = ":|:";
impl EmailMeta {
pub fn from(is_seen: bool, tag_string: &str) -> Self {
let tags = tag_string.split(TAG_SEP).map(|e| e.to_string()).collect();
EmailMeta { tags, is_seen }
}
pub fn tags_string(&self) -> String {
self.tags.join(":|:")
self.tags.join(TAG_SEP)
}
}

Loading…
Cancel
Save