//! A panel to edit filters use eframe::egui::{self, vec2, Color32, Response, Widget}; use eyre::Report; use ps_core::{ model::{segmentations, Engine}, Field, Filter, ValueField, }; /// Filter values for the UI. /// All values are mapped as `Option` /// as we have three states for each of them: yes, no, and all /// Say for `is_seen1: Only seen Some(yes), Only not seen Some(no), any None #[derive(Default)] pub struct FilterState { /// Yes: All I've send, No: All I've received, None: Any is_send: Option, is_reply: Option, is_seen: Option, subject_contains: Option, tags_contains: Option, } impl FilterState { pub fn new() -> Self { FilterState { is_send: Some(false), ..Default::default() } } fn apply(&self, engine: &mut Engine, error: &mut Option) { // FIXME: In principle this could rather be logic for the `engine`, but I'd like to have a generic engine at some point. let mut filters = Vec::new(); if let Some(val) = self.is_send { filters.push(Filter::Is(ValueField::bool(&Field::IsSend, val))); } if let Some(val) = self.is_seen { filters.push(Filter::Is(ValueField::bool(&Field::MetaIsSeen, val))); } if let Some(val) = self.is_reply { filters.push(Filter::Is(ValueField::bool(&Field::IsReply, val))); } // FIXME: The system currently doesn't allow searching for multiple tags // (e.g. (x like tag1 or x like tag2)) // this would require a `Filter::Expression` that is just added verbatim if let Some(n) = &self.tags_contains { filters.push(Filter::Like(ValueField::string( &Field::MetaTags, n.clone(), ))); } if let Some(n) = &self.subject_contains { filters.push(Filter::Contains(ValueField::string( &Field::Subject, n.clone(), ))); } *error = segmentations::set_filters(engine, &filters).err(); } fn clear(&mut self) { self.is_send = None; self.is_reply = None; self.is_seen = None; self.subject_contains = None; self.tags_contains = None; } } pub struct FilterPanel<'a> { engine: &'a mut Engine, state: &'a mut FilterState, error: &'a mut Option, } impl<'a> FilterPanel<'a> { pub fn new( engine: &'a mut Engine, state: &'a mut FilterState, error: &'a mut Option, ) -> Self { Self { engine, state, error, } } } impl<'a> Widget for FilterPanel<'a> { fn ui(self, ui: &mut egui::Ui) -> Response { let Self { engine, state, error, } = self; egui::Frame::none() .margin(vec2(15.0, 10.5)) .show(ui, |ui| { FilterPanel::filter_panel_contents(ui, engine, state); ui.add_space(5.0); ui.horizontal(|ui| { if ui.button("Apply").clicked() { state.apply(engine, error); } ui.add_space(10.0); if ui.button("Clear").clicked() { state.clear(); state.apply(engine, error); } }); }) .response } } impl FilterPanel<'_> { fn filter_panel_contents( ui: &mut egui::Ui, engine: &mut Engine, state: &mut FilterState, ) -> Response { egui::ScrollArea::vertical() .max_height(330.0) .show(ui, |ui| { egui::Grid::new("filter_grid") .spacing(vec2(15.0, 15.0)) .show(ui, |ui| { // We want to have aligned labels for our input fields egui::Grid::new("filter_text_grid") .spacing(vec2(5.0, 15.0)) .show(ui, |ui| { if let Some((range, total)) = segmentations::segments_range(engine) { ui.label("Segment 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.end_row(); } input_block(ui, "Subject", &mut state.subject_contains); ui.end_row(); }); ui.end_row(); if engine.format_has_tags() { input_tags( ui, "Labels / Tags", &mut state.tags_contains, engine.known_tags(), ); ui.end_row(); } radio_group( ui, "Inbox", &["Only Send Mails", "Only Received Mails", "All"], &mut state.is_send, ); ui.end_row(); radio_group( ui, "Only Replies", &["Yes", "No", "All"], &mut state.is_reply, ); ui.end_row(); if engine.format_has_seen() { radio_group(ui, "Only Seen", &["Yes", "No", "All"], &mut state.is_seen); ui.end_row(); } ui.end_row(); }) }) .response } } fn radio_group(ui: &mut egui::Ui, title: &str, names: &[&str; 3], value: &mut Option) { let mut radio_value = match value { Some(true) => 0, Some(false) => 1, None => 2, }; ui.vertical(|ui| { ui.add(egui::Label::new(title)); //ui.end_row(); ui.indent(&title, |ui| { ui.radio_value(&mut radio_value, 0, names[0]); ui.radio_value(&mut radio_value, 1, names[1]); ui.radio_value(&mut radio_value, 2, names[2]); }); }); let output_value = match radio_value { 0 => Some(true), 1 => Some(false), _ => None, }; *value = output_value; } fn input_block(ui: &mut egui::Ui, title: &str, value: &mut Option) { let mut text_value = value.clone().unwrap_or_else(|| "".to_string()); ui.label(title); ui.text_edit_singleline(&mut text_value); match text_value.as_str() { "" => *value = None, _ => *value = Some(text_value), } } fn input_tags( ui: &mut egui::Ui, title: &str, selection: &mut Option, available: &[String], ) { ui.vertical(|ui| { ui.add(egui::Label::new(title)); egui::Frame::none() .margin((5.0, 5.0)) .corner_radius(8.0) .fill(Color32::BLACK) .show(ui, |ui| { ui.horizontal_wrapped(|ui| { // Overly complicated, but can later on easily be extended for multi selection for (index, tag) in available.iter().enumerate() { let was_selected = if let Some(n) = selection { n == tag } else { false }; let mut selected = if was_selected { index } else { 9999 }; if ui.selectable_value(&mut selected, index, tag).clicked() { if was_selected { *selection = None; } else { *selection = Some(tag.clone()); } } } }); }); // if ui.button("Clear").clicked() { // selection.clear(); // } }); }