Basic list of emails works!

pull/1/head
Benedikt Terhechte 3 years ago
parent fbb16eb2ae
commit fc9b7ccc5e

181
Cargo.lock generated

@ -90,6 +90,26 @@ dependencies = [
"nodrop",
]
[[package]]
name = "async-mutex"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "479db852db25d9dbf6204e6cb6253698f175c15726470f78af0d918e99d6156e"
dependencies = [
"event-listener",
]
[[package]]
name = "async-trait"
version = "0.1.51"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44318e776df68115a881de9a8fd1b9e53368d7a4a5ce4cc48517da3393233a5e"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "atomic_refcell"
version = "0.1.8"
@ -135,6 +155,40 @@ version = "3.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9df67f7bf9ef8498769f994239c45613ef0c5899415fb58e9add412d2c1a538"
[[package]]
name = "cached"
version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b99e696f7b2696ed5eae0d462a9eeafaea111d99e39b2c8ceb418afe1013bcfc"
dependencies = [
"async-mutex",
"async-trait",
"cached_proc_macro",
"cached_proc_macro_types",
"futures",
"hashbrown 0.9.1",
"once_cell",
]
[[package]]
name = "cached_proc_macro"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25a685ba39b57a91a53d149dcbef854f50fbe204d1ff6081ea0bec3529a0c456"
dependencies = [
"async-mutex",
"cached_proc_macro_types",
"darling",
"quote",
"syn",
]
[[package]]
name = "cached_proc_macro_types"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a4f925191b4367301851c6d99b09890311d74b0d43f274c0b34c86d308a3663"
[[package]]
name = "calloop"
version = "0.6.5"
@ -566,6 +620,12 @@ dependencies = [
"egui",
]
[[package]]
name = "event-listener"
version = "2.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7531096570974c3a9dcf9e4b8e1cede1ec26cf5046219fb3b9d897503b9be59"
[[package]]
name = "eyre"
version = "0.6.5"
@ -621,6 +681,100 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
[[package]]
name = "futures"
version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a12aa0eb539080d55c3f2d45a67c3b58b6b0773c1a3ca2dfec66d58c97fd66ca"
dependencies = [
"futures-channel",
"futures-core",
"futures-executor",
"futures-io",
"futures-sink",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-channel"
version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5da6ba8c3bb3c165d3c7319fc1cc8304facf1fb8db99c5de877183c08a273888"
dependencies = [
"futures-core",
"futures-sink",
]
[[package]]
name = "futures-core"
version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88d1c26957f23603395cd326b0ffe64124b818f4449552f960d815cfba83a53d"
[[package]]
name = "futures-executor"
version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "45025be030969d763025784f7f355043dc6bc74093e4ecc5000ca4dc50d8745c"
dependencies = [
"futures-core",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-io"
version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "522de2a0fe3e380f1bc577ba0474108faf3f6b18321dbf60b3b9c39a75073377"
[[package]]
name = "futures-macro"
version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "18e4a4b95cea4b4ccbcf1c5675ca7c4ee4e9e75eb79944d07defde18068f79bb"
dependencies = [
"autocfg",
"proc-macro-hack",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "futures-sink"
version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36ea153c13024fe480590b3e3d4cad89a0cfacecc24577b68f86c6ced9c2bc11"
[[package]]
name = "futures-task"
version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d3d00f4eddb73e498a54394f228cd55853bdf059259e8e7bc6e69d408892e99"
[[package]]
name = "futures-util"
version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36568465210a3a6ee45e1f165136d68671471a501e632e9a98d96872222b5481"
dependencies = [
"autocfg",
"futures-channel",
"futures-core",
"futures-io",
"futures-macro",
"futures-sink",
"futures-task",
"memchr",
"pin-project-lite",
"pin-utils",
"proc-macro-hack",
"proc-macro-nested",
"slab",
]
[[package]]
name = "getrandom"
version = "0.2.3"
@ -741,6 +895,7 @@ dependencies = [
name = "gmaildb"
version = "0.1.0"
dependencies = [
"cached",
"chrono",
"crossbeam-channel",
"eframe",
@ -763,6 +918,12 @@ dependencies = [
"treemap",
]
[[package]]
name = "hashbrown"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04"
[[package]]
name = "hashbrown"
version = "0.11.2"
@ -778,7 +939,7 @@ version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7249a3129cbc1ffccd74857f81464a323a152173cdb134e0fd81bc803b29facf"
dependencies = [
"hashbrown",
"hashbrown 0.11.2",
]
[[package]]
@ -1331,6 +1492,12 @@ version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d31d11c69a6b52a174b42bdc0c30e5e11670f90788b2c471c31c1d17d449443"
[[package]]
name = "pin-utils"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "pkg-config"
version = "0.3.20"
@ -1368,6 +1535,12 @@ version = "0.5.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5"
[[package]]
name = "proc-macro-nested"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc881b2c22681370c6a780e47af9840ef841837bc98118431d4e1868bd0c1086"
[[package]]
name = "proc-macro2"
version = "1.0.29"
@ -1621,6 +1794,12 @@ version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "533494a8f9b724d33625ab53c6c4800f7cc445895924a8ef649222dcb76e938b"
[[package]]
name = "slab"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c307a32c1c5c437f38c7fd45d753050587732ba8628319fbdf12a7e289ccc590"
[[package]]
name = "smallvec"
version = "1.7.0"

@ -26,6 +26,7 @@ treemap = "0.3.2"
num-format = "0.4.0"
strum = "0.21"
strum_macros = "0.21"
cached = { version = "0.25.0", features = [] }
[features]
default = ["gui"]

@ -12,8 +12,8 @@ use crossbeam_channel::{unbounded, Receiver, Sender};
use eyre::{Report, Result};
use crate::database::{
query::{Field, Filter, Query},
query_result::QueryResult,
query::{Field, Filter, Query, ValueField},
query_result::{QueryResult, QueryRow},
Database,
};
use crate::types::Config;
@ -27,8 +27,14 @@ use super::partitions::{Partition, Partitions};
// - give query a range
// - use strum
#[derive(Debug)]
pub enum Response<Context: Send + 'static> {
Grouped(Query, Context, Partitions),
Normal(Query, Context, Vec<QueryRow>),
}
pub type InputSender<Context> = Sender<(Query, Context)>;
pub type OutputReciever<Context> = Receiver<Result<(Partitions, Context)>>;
pub type OutputReciever<Context> = Receiver<Result<Response<Context>>>;
pub type Handle = JoinHandle<Result<(), Report>>;
pub struct Link<Context: Send + 'static> {
@ -52,17 +58,26 @@ pub fn run<Context: Send + Sync + 'static>(config: &Config) -> Result<Link<Conte
fn inner_loop<Context: Send + Sync + 'static>(
database: Database,
input_receiver: Receiver<(Query, Context)>,
output_sender: Sender<Result<(Partitions, Context)>>,
output_sender: Sender<Result<Response<Context>>>,
) -> Result<()> {
loop {
let (query, context) = input_receiver.recv()?;
let result = database.query(&query)?;
let partitions = calculate_partitions(&result)?;
output_sender.send(Ok((Partitions::new(partitions), context)))?
let response = match query {
Query::Grouped { .. } => {
let partitions = calculate_partitions(&result)?;
Response::Grouped(query, context, Partitions::new(partitions))
}
Query::Normal { .. } => {
let converted = calculate_rows(&result)?;
Response::Normal(query, context, converted)
}
};
output_sender.send(Ok(response))?;
}
}
fn calculate_partitions<'a>(result: &[QueryResult]) -> Result<Vec<Partition>> {
fn calculate_partitions(result: &[QueryResult]) -> Result<Vec<Partition>> {
let mut partitions = Vec::new();
for r in result.iter() {
let partition = r.try_into()?;
@ -71,3 +86,18 @@ fn calculate_partitions<'a>(result: &[QueryResult]) -> Result<Vec<Partition>> {
Ok(partitions)
}
fn calculate_rows(result: &[QueryResult]) -> Result<Vec<QueryRow>> {
Ok(result
.iter()
.map(|r| {
let values = match r {
QueryResult::Normal(values) => values,
_ => {
panic!("Invalid result type, expected `Normal`")
}
};
values.clone()
})
.collect())
}

@ -1,9 +1,12 @@
use std::ops::RangeInclusive;
use std::ops::{Range, RangeInclusive};
use cached::{Cached, SizedCache};
use eframe::egui::Rect;
use eyre::{eyre, Result};
use eyre::{bail, eyre, Result};
use crate::cluster_engine::calc::Response;
use crate::database::query::{Field, Filter, Query, ValueField};
use crate::database::query_result::QueryRow;
use crate::types::Config;
use super::calc::Link;
@ -34,9 +37,16 @@ impl Grouping {
/// It is used for sending requests and receiving responses
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum Action {
/// Recalculate the current partition based on a changed grouping
Recalculate,
/// Select a new partition
Select,
Wait,
/// Load the mails for the current partition
Mails,
/// Waiting for the Partition query to finish
WaitPartition,
/// Waiting for the query to finish
WaitMails,
}
pub struct Engine {
@ -45,6 +55,10 @@ pub struct Engine {
link: Link<Action>,
partitions: Vec<Partitions>,
action: Option<Action>,
/// This is a very simple cache from ranges to rows.
/// It doesn't account for overlapping ranges.
/// There's a lot of room for improvement here.
row_cache: SizedCache<Range<usize>, Vec<QueryRow>>,
}
impl Engine {
@ -56,6 +70,7 @@ impl Engine {
group_by_stack: vec![default_group_by_stack(0)],
partitions: Vec::new(),
action: None,
row_cache: SizedCache::with_size(10000),
};
Ok(engine)
}
@ -72,6 +87,15 @@ impl Engine {
Some(partition.items())
}
/// The total amount of elements in all the partitions
pub fn current_element_count(&self) -> usize {
let partitions = match self.partitions.last() {
Some(n) => n,
None => return 0,
};
partitions.element_count()
}
/// Retrieve the min and max amount of items. The range that should be displayed.
/// Per default, it is the whole range of the partition
pub fn current_range(&self) -> Option<(RangeInclusive<usize>, usize)> {
@ -128,6 +152,8 @@ impl Engine {
.get_mut(grouping.index)
.map(|e| *e = field.clone());
self.action = Some(Action::Recalculate);
// Remove any rows that were cached for this partition
self.row_cache.cache_clear();
self.update()
}
@ -178,6 +204,9 @@ impl Engine {
// Remove the selection in the last partition
self.partitions.last_mut().map(|e| e.selected = None);
// Remove any rows that were cached for this partition
self.row_cache.cache_clear();
}
// Send the last action over the wire to be calculated
@ -188,30 +217,34 @@ impl Engine {
};
let request = self.make_group_query().ok_or(eyre!("Invalid State."))?;
self.link.input_sender.send((request, action))?;
self.action = Some(Action::Wait);
self.action = Some(Action::WaitPartition);
Ok(())
}
/// Fetch the channels to see if there're any updates
pub fn process(&mut self) -> Result<()> {
match self.link.output_receiver.try_recv() {
let response = match self.link.output_receiver.try_recv() {
// We received something
Ok(Ok((p, action))) => {
match action {
Action::Select => self.partitions.push(p),
Action::Recalculate => {
let len = self.partitions.len();
self.partitions[len - 1] = p;
}
Action::Wait => panic!("Should never send a wait action into the other thread"),
}
self.action = None;
}
Ok(Ok(response)) => response,
// We received nothing
Err(_) => (),
Err(_) => return Ok(()),
// There was an error, we forward it
Ok(Err(e)) => return Err(e),
};
match response {
Response::Grouped(_, Action::Select, p) => self.partitions.push(p),
Response::Grouped(_, Action::Recalculate, p) => {
let len = self.partitions.len();
self.partitions[len - 1] = p;
}
Response::Normal(Query::Normal { range, .. }, Action::Mails, r) => {
self.row_cache.cache_set(range.clone(), r.clone());
}
_ => bail!("Invalid Query / Response combination"),
}
self.action = None;
Ok(())
}
@ -233,16 +266,36 @@ impl Engine {
.collect()
}
pub fn request_contents(&mut self, range: &Range<usize>) -> Result<()> {
let request = self
.make_normal_query(range.clone())
.ok_or(eyre!("Invalid State."))?;
self.link
.input_sender
.send((request.clone(), Action::Mails))?;
self.action = Some(Action::WaitMails);
Ok(())
}
/// Query the contents for the current filter settings
/// This is a blocking call to simplify things a great deal
pub fn current_contents(&mut self, range: std::ops::Range<usize>) -> Result<Vec<ValueField>> {
todo!()
pub fn current_contents(&mut self, range: &Range<usize>) -> Result<Option<&Vec<QueryRow>>> {
Ok(self.row_cache.cache_get(range))
}
pub fn is_busy(&self) -> bool {
self.is_partitions_busy() || self.is_mail_busy()
}
/// When we don't have partitions loaded yet, or
/// when we're currently querying / loading new partitions
pub fn is_busy(&self) -> bool {
self.partitions.is_empty() || self.action.is_some()
pub fn is_partitions_busy(&self) -> bool {
self.partitions.is_empty() || self.action == Some(Action::WaitPartition)
}
/// If we're loading mails
pub fn is_mail_busy(&self) -> bool {
self.action == Some(Action::WaitMails)
}
fn make_group_query(&self) -> Option<Query> {
@ -255,6 +308,18 @@ impl Engine {
group_by: self.group_by_stack.last()?.clone(),
})
}
fn make_normal_query(&self, range: Range<usize>) -> Option<Query> {
let mut filters = Vec::new();
for entry in &self.search_stack {
filters.push(Filter::Like(entry.clone()));
}
Some(Query::Normal {
filters,
fields: vec![Field::SenderDomain, Field::SenderLocalPart, Field::Subject],
range,
})
}
}
/// Return the default group by fields index for each stack entry

@ -4,7 +4,10 @@ use eframe::egui::Rect as EguiRect;
use eyre::{Report, Result};
use treemap::{Mappable, Rect, TreemapLayout};
use crate::database::{query::ValueField, query_result::QueryResult};
use crate::database::{
query::{Field, ValueField},
query_result::QueryResult,
};
#[derive(Debug, Clone)]
pub struct Partition {
@ -63,6 +66,12 @@ impl Partitions {
layout.layout_items(&mut self.items(), bounds);
}
/// The total amount of items in all the partitions.
/// E.g. the sum of the count of the partitions
pub fn element_count(&self) -> usize {
self.items.iter().map(|e| e.count).sum::<usize>()
}
/// The items in this partition, with range applied
pub fn items(&mut self) -> &mut [Partition] {
match &self.range {
@ -93,15 +102,12 @@ impl Mappable for Partition {
impl<'a> TryFrom<&'a QueryResult> for Partition {
type Error = Report;
fn try_from(result: &'a QueryResult) -> Result<Self> {
let (count, values) = match result {
QueryResult::Grouped { count, values } => (count, values),
let (count, field) = match result {
QueryResult::Grouped { count, value } => (count, value),
_ => return Err(eyre::eyre!("Invalid result type, expected `Grouped`")),
};
// so far we can only support one group by at a time.
// at least in here. The queries support it
let field = values
.first()
.ok_or(eyre::eyre!("No group by fields available"))?;
Ok(Partition {
field: field.clone(),

@ -1,3 +1,4 @@
use std::collections::HashMap;
use std::convert::TryInto;
use std::str::FromStr;
@ -44,7 +45,7 @@ impl<'a> RowConversion<'a> for QueryResult {
Ok(QueryResult::Grouped {
count: amount,
values,
value: values[field].clone(),
})
}
fn from_row<'stmt>(fields: &'a [Field], row: &Row<'stmt>) -> Result<Self> {
@ -53,27 +54,31 @@ impl<'a> RowConversion<'a> for QueryResult {
}
}
fn values_from_fields<'stmt>(fields: &[Field], row: &Row<'stmt>) -> Result<Vec<ValueField>> {
let mut values = vec![];
fn values_from_fields<'stmt>(
fields: &[Field],
row: &Row<'stmt>,
) -> Result<HashMap<Field, ValueField>> {
let mut values: HashMap<Field, ValueField> = HashMap::default();
for field in fields {
use Field::*;
// Use type safety when unpacking
match field {
SenderDomain | SenderLocalPart | SenderName | ToGroup | ToName | ToAddress => {
SenderDomain | SenderLocalPart | SenderName | ToGroup | ToName | ToAddress
| Subject => {
let string: String = row.get::<&str, String>(field.as_str())?.into();
values.push(ValueField::string(&field, &string));
values.insert(*field, ValueField::string(&field, &string));
}
Year | Month | Day => {
values.push(ValueField::usize(
&field,
row.get::<&str, usize>(field.as_str())?.into(),
));
values.insert(
*field,
ValueField::usize(&field, row.get::<&str, usize>(field.as_str())?.into()),
);
}
IsReply | IsSend => {
values.push(ValueField::bool(
&field,
row.get::<&str, bool>(field.as_str())?.into(),
));
values.insert(
*field,
ValueField::bool(&field, row.get::<&str, bool>(field.as_str())?.into()),
);
}
}
}

@ -1,10 +1,14 @@
use rsql_builder;
use serde_json::{self, Value};
use serde_json;
pub use serde_json::Value;
use strum::{self, IntoEnumIterator};
use strum_macros::{EnumIter, IntoStaticStr};
use std::ops::Range;
pub const AMOUNT_FIELD_NAME: &str = "amount";
#[derive(Clone, Debug)]
pub enum Filter {
Like(ValueField),
NotLike(ValueField),
@ -25,6 +29,7 @@ pub enum Field {
ToAddress,
IsReply,
IsSend,
Subject,
}
impl Field {
@ -76,6 +81,7 @@ impl ValueField {
}
}
#[derive(Clone, Debug)]
pub enum Query {
Grouped {
filters: Vec<Filter>,
@ -84,6 +90,7 @@ pub enum Query {
Normal {
fields: Vec<Field>,
filters: Vec<Filter>,
range: Range<usize>,
},
}
@ -119,11 +126,11 @@ impl Query {
),
format!("GROUP BY {}", group_by.as_str()),
),
Query::Normal { fields, .. } => {
Query::Normal { fields, range, .. } => {
let fields: Vec<&str> = fields.iter().map(|e| e.into()).collect();
(
format!("SELECT {} FROM emails", fields.join(", ")),
"".to_owned(),
format!("LIMIT {}, {}", range.start, range.end - range.start),
)
}
};

@ -1,4 +1,7 @@
use super::query::ValueField;
use super::query::{Field, Value, ValueField};
use std::collections::HashMap;
pub type QueryRow = HashMap<Field, ValueField>;
#[derive(Debug)]
pub enum QueryResult {
@ -7,7 +10,7 @@ pub enum QueryResult {
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.
values: Vec<ValueField>,
value: ValueField,
},
Normal(Vec<ValueField>),
Normal(QueryRow),
}

@ -41,7 +41,10 @@ impl epi::App for GmailDBApp {
}
fn update(&mut self, ctx: &egui::CtxRef, frame: &mut epi::Frame<'_>) {
self.error = self.engine.process().err();
// Avoid any processing if there is an unhandled error.
if self.error.is_none() {
self.error = self.engine.process().err();
}
let Self {
engine,
@ -51,23 +54,21 @@ impl epi::App for GmailDBApp {
} = self;
if let Some(error) = error {
dbg!(&error);
egui::CentralPanel::default().show(ctx, |ui| ui.add(widgets::ErrorBox(&error)));
} else {
if *show_emails {
egui::SidePanel::right("my_left_panel").show(ctx, |ui| {
ui.heading("GMail DB laskjf aslkfjlkajsf");
ui.horizontal(|ui| {
ui.label("Search");
});
ui.add(super::mail_panel::MailPanel::new(engine, error));
});
}
egui::TopBottomPanel::top("my_panel").show(ctx, |ui| {
ui.add(super::widgets::TopBar::new(engine, error));
ui.add(super::top_bar::TopBar::new(engine, error));
});
egui::CentralPanel::default().show(ctx, |ui| {
if engine.is_busy() {
if engine.is_partitions_busy() {
ui.centered_and_justified(|ui| {
ui.add(Spinner::new(egui::vec2(50.0, 50.0)));
});

@ -0,0 +1,54 @@
use crate::cluster_engine::Engine;
use crate::database::query::{Field, Value};
use eframe::egui::{self, Widget};
use eyre::Report;
use super::widgets::Table;
pub struct MailPanel<'a> {
engine: &'a mut Engine,
error: &'a mut Option<Report>,
}
impl<'a> MailPanel<'a> {
pub fn new(engine: &'a mut Engine, error: &'a mut Option<Report>) -> Self {
MailPanel { engine, error }
}
}
impl<'a> Widget for MailPanel<'a> {
fn ui(self, ui: &mut egui::Ui) -> egui::Response {
let empty_vec = Vec::new();
let mut selected_row: Option<usize> = None;
ui.vertical(|ui| {
ui.add(
Table::new_selectable(
"mail_list",
&mut selected_row,
self.engine.current_element_count(),
|range| match self.engine.current_contents(&range) {
Ok(Some(n)) => n.clone(),
Ok(None) => {
*self.error = self.engine.request_contents(&range).err();
empty_vec.clone()
}
Err(e) => {
*self.error = Some(e);
empty_vec.clone()
}
},
)
.column("Sender", |sample| {
format!(
"{}@{}",
sample[&Field::SenderLocalPart].value().as_str().unwrap(),
sample[&Field::SenderDomain].value().as_str().unwrap()
)
})
.column("Subject", |sample| {
sample[&Field::Subject].value().to_string()
}),
)
})
.response
}
}

@ -2,6 +2,8 @@ use crate::types::Config;
use eframe::{self, egui, epi};
mod app;
mod mail_panel;
mod top_bar;
pub(crate) mod widgets;
pub fn run_gui(config: Config) {

@ -1,9 +1,9 @@
mod error_box;
mod rectangles;
mod spinner;
mod top_bar;
mod table;
pub use error_box::ErrorBox;
pub use rectangles::Rectangles;
pub use spinner::Spinner;
pub use top_bar::TopBar;
pub use table::Table;

@ -0,0 +1,259 @@
//! This is a fork of `https://raw.githubusercontent.com/sagebind/smplinfo/master/src/ui/widgets/table.rs` with
//! some modifications.
use std::ops::Range;
use std::{fmt::Display, hash::Hash};
use eframe::egui::{vec2, Id, Key, Response, ScrollArea, Sense, TextStyle, Ui, Vec2, Widget};
const DEFAULT_COLUMN_WIDTH: f32 = 200.0;
/// An ordinary table. Feature set is currently pretty limited.
///
/// - `R`: The data type of a single row displayed.
/// - `C`: The type of collection holding the rows to display. Any collection
/// implementing `AsRef<[R]>` can be used.
pub struct Table<'selection, R, C: AsRef<[R]>, RowMaker: FnMut(Range<usize>) -> C> {
id_source: Id,
columns: Vec<Column<R>>,
selected_row: Option<&'selection mut Option<usize>>,
header_height: f32,
row_height: f32,
cell_padding: Vec2,
row_maker: RowMaker,
num_rows: usize,
}
/// Table column definition.
struct Column<R> {
name: String,
value_mapper: Box<dyn FnMut(&R) -> String>,
max_width: Option<f32>,
}
impl<R, C: AsRef<[R]>, RowMaker: FnMut(Range<usize>) -> C> Table<'static, R, C, RowMaker> {
pub fn new(id_source: impl Hash, num_rows: usize, row_maker: RowMaker) -> Self {
Self {
id_source: Id::new(id_source),
columns: Vec::new(),
selected_row: None,
header_height: 28.0,
row_height: 24.0,
cell_padding: vec2(8.0, 4.0),
row_maker,
num_rows,
}
}
}
impl<'s, R, C: AsRef<[R]>, RowMaker: FnMut(Range<usize>) -> C> Table<'s, R, C, RowMaker> {
pub fn new_selectable(
id_source: impl Hash,
selected_row: &'s mut Option<usize>,
num_rows: usize,
row_maker: RowMaker,
) -> Self {
Self {
id_source: Id::new(id_source),
columns: Vec::new(),
selected_row: Some(selected_row),
header_height: 28.0,
row_height: 24.0,
cell_padding: vec2(8.0, 4.0),
row_maker,
num_rows,
}
}
pub fn column(
mut self,
name: impl Display,
value_mapper: impl FnMut(&R) -> String + 'static,
) -> Self {
self.columns.push(Column {
name: name.to_string(),
value_mapper: Box::new(value_mapper),
max_width: None,
});
self
}
fn supports_selection(&self) -> bool {
self.selected_row.is_some()
}
fn header_ui(&mut self, ui: &mut Ui, state: &mut State) {
let header_text_style = TextStyle::Body;
// Table always grows as wide as available, so the header should too.
let (_, rect) = ui.allocate_space(vec2(ui.available_width(), self.header_height));
// Header background
let painter = ui.painter_at(rect);
painter.rect_filled(
rect,
ui.visuals().widgets.inactive.corner_radius,
ui.visuals().widgets.inactive.bg_fill,
);
let mut column_offset = 0.0;
for (i, column) in self.columns.iter().enumerate() {
let column_id = self.id_source.with("_column_").with(i);
let desired_column_width = state.column_width(i);
let galley = ui
.fonts()
.layout_single_line(header_text_style, column.name.clone());
let mut column_rect = rect;
column_rect.min.x += column_offset;
if column_rect.width() > desired_column_width {
column_rect.set_width(desired_column_width);
}
let response = ui.interact(column_rect, column_id, Sense::hover());
if response.hovered() {
ui.painter().rect_stroke(
column_rect,
ui.visuals().widgets.hovered.corner_radius,
ui.visuals().widgets.hovered.bg_stroke,
);
}
let mut text_pos = column_rect.left_center();
text_pos.x += self.cell_padding.x;
text_pos.y -= galley.size.y / 2.0;
ui.painter_at(column_rect).galley(
text_pos,
galley,
if response.hovered() {
ui.style().visuals.widgets.hovered.fg_stroke.color
} else {
ui.style().visuals.widgets.inactive.fg_stroke.color
},
);
column_offset += column_rect.width();
}
}
}
impl<'s, R, C: AsRef<[R]>, RowMaker: FnMut(Range<usize>) -> C> Widget
for Table<'s, R, C, RowMaker>
{
fn ui(mut self, ui: &mut Ui) -> Response {
if self.columns.is_empty() {
panic!("uh, what do I do if no columns are defined?");
}
let mut state = ui
.memory()
.id_data_temp
.get_or_default::<State>(self.id_source)
.clone();
// First step: compute some sizes used during rendering. Since this is a
// homogenous table, we can figure out its exact sizes based on the
// number of rows and columns.
let table_rect = ui.available_rect_before_wrap_finite();
let response = ui.interact(table_rect, self.id_source, Sense::hover());
self.header_ui(ui, &mut state);
// Now render the table body, which is inside an independently
// scrollable area.
ScrollArea::auto_sized().show_rows(ui, self.row_height, self.num_rows, |ui, row_range| {
ui.scope(|ui| {
let maker = &mut self.row_maker;
let rows = maker(row_range);
// When laying out the table, don't allocate any spacing between the
// rows.
ui.spacing_mut().item_spacing.y = 0.0;
// TODO: Decide row height more intelligently...
let row_size = vec2(ui.available_width(), self.row_height);
let cell_text_style = ui.style().body_text_style;
for (row_idx, row) in rows.as_ref().into_iter().enumerate() {
let (row_id, row_rect) = ui.allocate_space(row_size);
// let row_id = self.id_source.with("_row_").with(row_idx);
let row_response = ui.interact(row_rect, row_id, Sense::click());
let mut cell_text_color = ui.style().visuals.text_color();
// If this row is currently selected, make it look like it is.
if self.selected_row == Some(&mut Some(row_idx)) {
cell_text_color = ui.visuals().strong_text_color();
ui.painter().rect_filled(
row_rect,
0.0,
ui.style().visuals.selection.bg_fill,
);
} else if row_response.hovered() {
ui.painter().rect_filled(
row_rect,
0.0,
ui.visuals().widgets.hovered.bg_fill,
);
} else if row_idx % 2 > 0 {
ui.painter()
.rect_filled(row_rect, 0.0, ui.visuals().faint_bg_color);
}
let mut column_offset = 0.0;
for (col_idx, column) in self.columns.iter_mut().enumerate() {
let desired_column_width = state.column_width(col_idx);
let cell_text = (column.value_mapper)(row);
let mut column_rect = row_rect;
column_rect.min.x += column_offset;
if column_rect.width() > desired_column_width {
column_rect.set_width(desired_column_width);
}
let painter = ui.painter_at(column_rect);
let galley = ui.fonts().layout_single_line(cell_text_style, cell_text);
let mut text_pos = column_rect.left_center();
text_pos.x += self.cell_padding.x;
text_pos.y -= galley.size.y / 2.0;
painter.galley(text_pos, galley, cell_text_color);
column_offset += column_rect.width();
}
// INTERACTION
//if let Some(selected_row) = self.selected_row.as_mut() {
// if row_response.clicked() {
// **selected_row = Some(row_idx);
// }
//}
}
});
});
response
}
}
/// Persistent table UI state.
#[derive(Clone, Default)]
struct State {
/// Current width of each column. This is updated when a column is resized.
column_widths: Vec<f32>,
}
impl State {
fn column_width(&self, column: usize) -> f32 {
self.column_widths
.get(column)
.cloned()
.unwrap_or(DEFAULT_COLUMN_WIDTH)
}
}
Loading…
Cancel
Save