From c53abb2eedfafca4c72929a7d2c3f3167ac60eca Mon Sep 17 00:00:00 2001 From: alemi Date: Tue, 14 Jun 2022 00:51:30 +0200 Subject: [PATCH] chore: modularized gui code --- src/app/data/mod.rs | 22 ++-- src/app/data/source.rs | 35 +++++- src/app/gui/mod.rs | 2 + src/app/gui/panel.rs | 125 ++++++++++++++++++++ src/app/gui/source.rs | 79 +++++++++++++ src/app/mod.rs | 254 ++++------------------------------------- 6 files changed, 275 insertions(+), 242 deletions(-) create mode 100644 src/app/gui/mod.rs create mode 100644 src/app/gui/panel.rs create mode 100644 src/app/gui/source.rs diff --git a/src/app/data/mod.rs b/src/app/data/mod.rs index adf5cfe..a76a2e9 100644 --- a/src/app/data/mod.rs +++ b/src/app/data/mod.rs @@ -62,23 +62,23 @@ impl ApplicationState { }); } - pub fn add_panel(&self, name:&str) -> Result<(), FetchError> { - let panel = self.storage.lock().expect("Storage Mutex poisoned") + pub fn add_panel(&self, panel: &Panel) -> Result<(), FetchError> { + let verified_panel = self.storage.lock().expect("Storage Mutex poisoned") .new_panel( - name, - 100, - 200, - 280, + panel.name.as_str(), + panel.view_size, + panel.width, + panel.height, self.panels.read().expect("Panels RwLock poisoned").len() as i32 // todo can this be made more compact and without acquisition? )?; // TODO make values customizable and useful - self.panels.write().expect("Panels RwLock poisoned").push(panel); + self.panels.write().expect("Panels RwLock poisoned").push(verified_panel); Ok(()) } - pub fn add_source(&self, panel_id:i32, name:&str, url:&str, query_x:&str, query_y:&str, color:Color32, visible:bool) -> Result<(), FetchError> { - let source = self.storage.lock().expect("Storage Mutex poisoned") - .new_source(panel_id, name, url, query_x, query_y, color, visible)?; - self.sources.write().expect("Sources RwLock poisoned").push(source); + pub fn add_source(&self, source: &Source) -> Result<(), FetchError> { + let verified_source = self.storage.lock().expect("Storage Mutex poisoned") + .new_source(source.panel_id, source.name.as_str(), source.url.as_str(), source.query_x.as_str(), source.query_y.as_str(), source.color, source.visible)?; + self.sources.write().expect("Sources RwLock poisoned").push(verified_source); return Ok(()); } } diff --git a/src/app/data/source.rs b/src/app/data/source.rs index b942f77..0ad119e 100644 --- a/src/app/data/source.rs +++ b/src/app/data/source.rs @@ -15,6 +15,21 @@ pub struct Panel { pub limit: bool, } +impl Default for Panel { + fn default() -> Self { + Panel { + id: -1, + name: "".to_string(), + view_scroll: true, + view_size: 300, + timeserie: true, + width: 100, + height: 200, + limit: false, + } + } +} + pub struct Source { pub(crate) id: i32, pub name: String, @@ -31,6 +46,24 @@ pub struct Source { pub(crate) data: RwLock>, } +impl Default for Source { + fn default() -> Self { + Source { + id: -1, + name: "".to_string(), + url: "".to_string(), + interval: 60, + color: Color32::TRANSPARENT, + visible: false, + last_fetch: RwLock::new(Utc::now()), + query_x: "".to_string(), + query_y: "".to_string(), + panel_id: -1, + data: RwLock::new(Vec::new()) + } + } +} + impl Source { pub fn valid(&self) -> bool { let last_fetch = self.last_fetch.read().expect("LastFetch RwLock poisoned"); @@ -58,4 +91,4 @@ pub fn fetch(url:&str, query_x:&str, query_y:&str) -> Result } let y = jql::walker(&res, query_y)?.as_f64().ok_or(FetchError::JQLError("Y query is null".to_string()))?; return Ok( Value { x, y } ); -} \ No newline at end of file +} diff --git a/src/app/gui/mod.rs b/src/app/gui/mod.rs new file mode 100644 index 0000000..4f2bb82 --- /dev/null +++ b/src/app/gui/mod.rs @@ -0,0 +1,2 @@ +pub mod source; +pub mod panel; diff --git a/src/app/gui/panel.rs b/src/app/gui/panel.rs new file mode 100644 index 0000000..62ca023 --- /dev/null +++ b/src/app/gui/panel.rs @@ -0,0 +1,125 @@ +use chrono::{Utc, Local}; +use eframe::egui::{Ui, Layout, Align, plot::{Plot, Legend, Corner, Line, GridMark}, Slider, DragValue}; + +use crate::app::{data::source::{Panel, Source}, util::timestamp_to_str}; + +pub fn panel_edit_inline_ui(ui: &mut Ui, panel: &mut Panel) { + eframe::egui::TextEdit::singleline(&mut panel.name) + .hint_text("name") + .desired_width(50.0) + .show(ui); +} + +pub fn panel_title_ui(ui: &mut Ui, panel: &mut Panel) { + ui.horizontal(|ui| { + ui.heading(panel.name.as_str()); + ui.with_layout(Layout::right_to_left(), |ui| { + ui.horizontal(|ui| { + ui.toggle_value(&mut panel.view_scroll, " • "); + ui.separator(); + ui.label("m"); + ui.add( + DragValue::new(&mut panel.view_size) + .speed(10) + .clamp_range(0..=2147483647i32), + ); + ui.checkbox(&mut panel.limit, "limit"); + ui.separator(); + ui.checkbox(&mut panel.timeserie, "timeserie"); + ui.separator(); + ui.add( + Slider::new(&mut panel.height, 0..=500).text("height"), + ); + ui.separator(); + }); + }); + }); +} + + +pub fn panel_body_ui(ui: &mut Ui, panel: &mut Panel, sources: &Vec) { + let mut p = Plot::new(format!("plot-{}", panel.name)) + .height(panel.height as f32) + .allow_scroll(false) + .legend(Legend::default().position(Corner::LeftTop)); + + if panel.view_scroll { + p = p + .include_x(Utc::now().timestamp() as f64); + if panel.limit { + p = p + .set_margin_fraction(eframe::emath::Vec2{x:0.0, y:0.1}) + .include_x((Utc::now().timestamp() + ( panel.view_size as i64 * 3)) as f64); + } + if panel.limit { + p = p.include_x( + (Utc::now().timestamp() - (panel.view_size as i64 * 60)) + as f64, + ); + } + } + + if panel.timeserie { + p = p + .x_axis_formatter(|x, _range| { + timestamp_to_str(x as i64, true, false) + }) + .label_formatter(|name, value| { + if !name.is_empty() { + return format!( + "{}\nx = {}\ny = {:.1}", + name, + timestamp_to_str(value.x as i64, false, true), + value.y + ); + } else { + return format!( + "x = {}\ny = {:.1}", + timestamp_to_str(value.x as i64, false, true), + value.y + ); + } + }) + .x_grid_spacer(|grid| { + let offset = Local::now().offset().local_minus_utc() as i64; + let (start, end) = grid.bounds; + let mut counter = (start as i64) - ((start as i64) % 3600); + let mut out: Vec = Vec::new(); + loop { + counter += 3600; + if counter > end as i64 { + break; + } + if (counter + offset) % 86400 == 0 { + out.push(GridMark { + value: counter as f64, + step_size: 86400 as f64, + }) + } else if counter % 3600 == 0 { + out.push(GridMark { + value: counter as f64, + step_size: 3600 as f64, + }); + } + } + return out; + }); + } + + p.show(ui, |plot_ui| { + for source in &*sources { + if source.visible && source.panel_id == panel.id { + let line = if panel.limit { + Line::new(source.values_filter( + (Utc::now().timestamp() + - (panel.view_size as i64 * 60)) as f64, + )) + .name(source.name.as_str()) + } else { + Line::new(source.values()).name(source.name.as_str()) + }; + plot_ui.line(line.color(source.color)); + } + } + }); +} diff --git a/src/app/gui/source.rs b/src/app/gui/source.rs new file mode 100644 index 0000000..c9db406 --- /dev/null +++ b/src/app/gui/source.rs @@ -0,0 +1,79 @@ +use eframe::egui; +use eframe::egui::Ui; + +use crate::app::data::source::{Source, Panel}; + +pub fn source_edit_inline_ui(ui: &mut Ui, source: &mut Source, panels: &Vec) { + eframe::egui::TextEdit::singleline(&mut source.name) + .hint_text("name") + .desired_width(50.0) + .show(ui); + eframe::egui::TextEdit::singleline(&mut source.url) + .hint_text("url") + .desired_width(160.0) + .show(ui); + eframe::egui::TextEdit::singleline(&mut source.query_x) + .hint_text("x") + .desired_width(30.0) + .show(ui); + eframe::egui::TextEdit::singleline(&mut source.query_y) + .hint_text("y") + .desired_width(30.0) + .show(ui); + egui::ComboBox::from_id_source("panel") + .selected_text(format!("panel [{}]", source.panel_id)) + .width(70.0) + .show_ui(ui, |ui| { + for p in panels { + ui.selectable_value( + &mut source.panel_id, + p.id, + p.name.as_str(), + ); + } + }); + ui.checkbox(&mut source.visible, "visible"); + ui.add(egui::Slider::new(&mut source.interval, 1..=60)); + ui.color_edit_button_srgba(&mut source.color); +} + + +pub fn source_ui(ui: &mut Ui, source: &mut Source, panels: &Vec) { + ui.group(|ui| { + ui.horizontal(|ui| { + ui.checkbox(&mut source.visible, ""); + eframe::egui::TextEdit::singleline(&mut source.name) + .hint_text("name") + .desired_width(80.0) + .show(ui); + eframe::egui::TextEdit::singleline(&mut source.url) + .hint_text("url") + .desired_width(300.0) + .show(ui); + }); + ui.horizontal(|ui| { + ui.add(egui::Slider::new(&mut source.interval, 1..=60)); + eframe::egui::TextEdit::singleline(&mut source.query_x) + .hint_text("x") + .desired_width(50.0) + .show(ui); + eframe::egui::TextEdit::singleline(&mut source.query_y) + .hint_text("y") + .desired_width(50.0) + .show(ui); + egui::ComboBox::from_id_source(format!("panel-{}", source.id)) + .selected_text(format!("panel [{}]", source.panel_id)) + .width(70.0) + .show_ui(ui, |ui| { + for p in panels { + ui.selectable_value( + &mut source.panel_id, + p.id, + p.name.as_str(), + ); + } + }); + ui.color_edit_button_srgba(&mut source.color); + }); + }); +} diff --git a/src/app/mod.rs b/src/app/mod.rs index 368d0aa..b736e3b 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -1,4 +1,5 @@ pub mod data; +pub mod gui; pub mod util; pub mod worker; @@ -13,40 +14,16 @@ use std::sync::Arc; use tracing::error; use self::data::ApplicationState; +use self::data::source::{Panel,Source}; +use self::gui::panel::{panel_edit_inline_ui, panel_title_ui, panel_body_ui}; +use self::gui::source::{source_ui, source_edit_inline_ui}; use self::util::{human_size, timestamp_to_str}; use self::worker::native_save; -struct InputBuffer { - panel_name: String, - name: String, - url: String, - interval: i32, - query_x: String, - query_y: String, - color: Color32, - visible: bool, - panel_id: i32, -} - -impl Default for InputBuffer { - fn default() -> Self { - InputBuffer { - panel_name: "".to_string(), - name: "".to_string(), - url: "".to_string(), - interval: 60, - query_x: "".to_string(), - query_y: "".to_string(), - color: Color32::TRANSPARENT, - visible: true, - panel_id: 0, - } - } -} - pub struct App { data: Arc, - input: InputBuffer, + input_source: Source, + input_panel: Panel, edit: bool, } @@ -54,7 +31,8 @@ impl App { pub fn new(_cc: &eframe::CreationContext, data: Arc) -> Self { Self { data, - input: InputBuffer::default(), + input_panel: Panel::default(), + input_source: Source::default(), edit: false, } } @@ -75,59 +53,17 @@ impl eframe::App for App { } ui.separator(); ui.label("+ panel"); - eframe::egui::TextEdit::singleline(&mut self.input.panel_name) - .hint_text("name") - .desired_width(50.0) - .show(ui); + panel_edit_inline_ui(ui, &mut self.input_panel); if ui.button("add").clicked() { - if let Err(e) = self.data.add_panel(self.input.panel_name.as_str()) { + if let Err(e) = self.data.add_panel(&self.input_panel) { error!(target: "ui", "Failed to add panel: {:?}", e); }; } ui.separator(); ui.label("+ source"); - eframe::egui::TextEdit::singleline(&mut self.input.name) - .hint_text("name") - .desired_width(50.0) - .show(ui); - eframe::egui::TextEdit::singleline(&mut self.input.url) - .hint_text("url") - .desired_width(160.0) - .show(ui); - eframe::egui::TextEdit::singleline(&mut self.input.query_x) - .hint_text("x") - .desired_width(30.0) - .show(ui); - eframe::egui::TextEdit::singleline(&mut self.input.query_y) - .hint_text("y") - .desired_width(30.0) - .show(ui); - egui::ComboBox::from_id_source("panel") - .selected_text(format!("panel [{}]", self.input.panel_id)) - .width(70.0) - .show_ui(ui, |ui| { - let pnls = self.data.panels.write().expect("Panels RwLock poisoned"); - for p in &*pnls { - ui.selectable_value( - &mut self.input.panel_id, - p.id, - p.name.as_str(), - ); - } - }); - ui.checkbox(&mut self.input.visible, "visible"); - ui.add(egui::Slider::new(&mut self.input.interval, 1..=60)); - ui.color_edit_button_srgba(&mut self.input.color); + source_edit_inline_ui(ui, &mut self.input_source, &self.data.panels.read().expect("Panels RwLock poisoned")); if ui.button("add").clicked() { - if let Err(e) = self.data.add_source( - self.input.panel_id, - self.input.name.as_str(), - self.input.url.as_str(), - self.input.query_x.as_str(), - self.input.query_y.as_str(), - self.input.color, - self.input.visible, - ) { + if let Err(e) = self.data.add_source(&self.input_source) { error!(target: "ui", "Error adding souce : {:?}", e); } } @@ -190,51 +126,12 @@ impl eframe::App for App { if self.edit { egui::SidePanel::left("sources-bar").show(ctx, |ui| { let mut sources = self.data.sources.write().expect("Sources RwLock poisoned"); + let panels = self.data.panels.read().expect("Panels RwLock poisoned"); egui::ScrollArea::vertical().show(ui, |ui| { for source in &mut *sources { - ui.group(|ui| { - ui.horizontal(|ui| { - ui.checkbox(&mut source.visible, ""); - eframe::egui::TextEdit::singleline(&mut source.name) - .hint_text("name") - .desired_width(80.0) - .show(ui); - eframe::egui::TextEdit::singleline(&mut source.url) - .hint_text("url") - .desired_width(300.0) - .show(ui); - }); - ui.horizontal(|ui| { - ui.add(egui::Slider::new(&mut source.interval, 1..=60)); - eframe::egui::TextEdit::singleline(&mut source.query_x) - .hint_text("x") - .desired_width(50.0) - .show(ui); - eframe::egui::TextEdit::singleline(&mut source.query_y) - .hint_text("y") - .desired_width(50.0) - .show(ui); - egui::ComboBox::from_id_source(format!("panel-{}", source.id)) - .selected_text(format!("panel [{}]", source.panel_id)) - .width(70.0) - .show_ui(ui, |ui| { - let pnls = self - .data - .panels - .read() - .expect("Panels RwLock poisoned"); - for p in &*pnls { - ui.selectable_value( - &mut source.panel_id, - p.id, - p.name.as_str(), - ); - } - }); - ui.color_edit_button_srgba(&mut source.color); - }); - }); + source_ui(ui, source, &panels); } + // TODO make this not necessary ui.collapsing("extra space", |ui| { ui.add_space(300.0); ui.separator(); @@ -246,6 +143,7 @@ impl eframe::App for App { egui::CentralPanel::default().show(ctx, |ui| { egui::ScrollArea::vertical().show(ui, |ui| { let mut panels = self.data.panels.write().expect("Panels RwLock poisoned"); // TODO only lock as write when editing + let panels_count = panels.len(); let sources = self.data.sources.read().expect("Sources RwLock poisoned"); // TODO only lock as write when editing for (index, panel) in panels.iter_mut().enumerate() { if index > 0 { @@ -256,126 +154,22 @@ impl eframe::App for App { ui.make_persistent_id(format!("panel-{}-compressable", panel.id)), true, ) - .show_header(ui, |ui| { - ui.horizontal(|ui| { + .show_header(ui, |ui| { if self.edit { - if ui.small_button(" ^ ").clicked() { + if ui.small_button(" + ").clicked() { if index > 0 { to_swap.push(index); // TODO kinda jank but is there a better way? } } - ui.separator(); - } - ui.heading(panel.name.as_str()); - if self.edit { - ui.separator(); - ui.add( - egui::Slider::new(&mut panel.height, 0..=500).text("height"), - ); - ui.separator(); - ui.checkbox(&mut panel.timeserie, "timeserie"); - } - ui.with_layout(egui::Layout::top_down(egui::Align::RIGHT), |ui| { - ui.horizontal(|ui| { - ui.toggle_value(&mut panel.view_scroll, " • "); - ui.separator(); - ui.label("m"); - ui.add( - egui::DragValue::new(&mut panel.view_size) - .speed(10) - .clamp_range(0..=2147483647i32), - ); - ui.checkbox(&mut panel.limit, "limit"); - }); - }); - }); - }) - .body(|ui| { - let mut p = Plot::new(format!("plot-{}", panel.name)) - .height(panel.height as f32) - .allow_scroll(false) - .legend(egui::plot::Legend::default().position(egui::plot::Corner::LeftTop)); - - if panel.view_scroll { - p = p - .include_x(Utc::now().timestamp() as f64); - if panel.limit { - p = p - .set_margin_fraction(eframe::emath::Vec2{x:0.0, y:0.1}) - .include_x((Utc::now().timestamp() + ( panel.view_size as i64 * 3)) as f64); - } - if panel.limit { - p = p.include_x( - (Utc::now().timestamp() - (panel.view_size as i64 * 60)) - as f64, - ); - } - } - - if panel.timeserie { - p = p - .x_axis_formatter(|x, _range| { - timestamp_to_str(x as i64, true, false) - }) - .label_formatter(|name, value| { - if !name.is_empty() { - return format!( - "{}\nx = {}\ny = {:.1}", - name, - timestamp_to_str(value.x as i64, false, true), - value.y - ); - } else { - return format!( - "x = {}\ny = {:.1}", - timestamp_to_str(value.x as i64, false, true), - value.y - ); + if ui.small_button(" - ").clicked() { + if index < panels_count - 1 { + to_swap.push(index + 1); // TODO kinda jank but is there a better way? } - }) - .x_grid_spacer(|grid| { - let offset = Local::now().offset().local_minus_utc() as i64; - let (start, end) = grid.bounds; - let mut counter = (start as i64) - ((start as i64) % 3600); - let mut out: Vec = Vec::new(); - loop { - counter += 3600; - if counter > end as i64 { - break; - } - if (counter + offset) % 86400 == 0 { - out.push(GridMark { - value: counter as f64, - step_size: 86400 as f64, - }) - } else if counter % 3600 == 0 { - out.push(GridMark { - value: counter as f64, - step_size: 3600 as f64, - }); - } - } - return out; - }); - } - - p.show(ui, |plot_ui| { - for source in &*sources { - if source.visible && source.panel_id == panel.id { - let line = if panel.limit { - Line::new(source.values_filter( - (Utc::now().timestamp() - - (panel.view_size as i64 * 60)) as f64, - )) - .name(source.name.as_str()) - } else { - Line::new(source.values()).name(source.name.as_str()) - }; - plot_ui.line(line.color(source.color)); } } - }); - }); + panel_title_ui(ui, panel); + }) + .body(|ui| panel_body_ui(ui, panel, &sources)); } }); });