From a75d6b432fca8013e0d1e14d1643ed6d14f6e082 Mon Sep 17 00:00:00 2001 From: alemidev Date: Mon, 13 Jun 2022 02:37:20 +0200 Subject: [PATCH] feat: made plots collapsable, moved log in footer footer is also collapsing --- src/app/mod.rs | 329 ++++++++++++++++++++++++++++++------------------ src/app/util.rs | 63 ++++++---- 2 files changed, 250 insertions(+), 142 deletions(-) diff --git a/src/app/mod.rs b/src/app/mod.rs index 9aedce2..3bd7803 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -1,17 +1,20 @@ pub mod data; -pub mod worker; pub mod util; +pub mod worker; -use std::sync::Arc; -use chrono::{Utc, Local}; -use tracing::error; +use chrono::{Local, Utc}; use eframe::egui; use eframe::egui::plot::GridMark; -use eframe::egui::{RichText, plot::{Line, Plot}, Color32}; +use eframe::egui::{ + plot::{Line, Plot}, + Color32, +}; +use std::sync::Arc; +use tracing::error; use self::data::ApplicationState; -use self::worker::native_save; use self::util::{human_size, timestamp_to_str}; +use self::worker::native_save; struct InputBuffer { panel_name: String, @@ -37,7 +40,7 @@ impl Default for InputBuffer { color: Color32::TRANSPARENT, visible: true, panel_id: 0, - } + } } } @@ -45,12 +48,15 @@ pub struct App { data: Arc, input: InputBuffer, edit: bool, - show_log: bool, } impl App { pub fn new(_cc: &eframe::CreationContext, data: Arc) -> Self { - Self { data, input: InputBuffer::default(), edit: false, show_log: false } + Self { + data, + input: InputBuffer::default(), + edit: false, + } } } @@ -69,7 +75,10 @@ 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); + eframe::egui::TextEdit::singleline(&mut self.input.panel_name) + .hint_text("name") + .desired_width(50.0) + .show(ui); if ui.button("add").clicked() { if let Err(e) = self.data.add_panel(self.input.panel_name.as_str()) { error!(target: "ui", "Failed to add panel: {:?}", e); @@ -77,20 +86,35 @@ impl eframe::App for App { } 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); + 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.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); @@ -114,29 +138,53 @@ impl eframe::App for App { if ui.small_button("×").clicked() { frame.quit(); } - ui.checkbox(&mut self.show_log, "log"); }); }); }); }); egui::TopBottomPanel::bottom("footer").show(ctx, |ui| { - ui.horizontal(|ui|{ - ui.label(self.data.file_path.to_str().unwrap()); // TODO maybe calculate it just once? - ui.separator(); - ui.label(human_size(*self.data.file_size.read().expect("Filesize RwLock poisoned"))); - let diags = self.data.diagnostics.read().expect("Diagnostics RwLock poisoned"); - if diags.len() > 0 { + egui::collapsing_header::CollapsingState::load_with_default_open( + ctx, + ui.make_persistent_id("footer-logs"), + false, + ) + .show_header(ui, |ui| { + ui.horizontal(|ui| { + ui.label(self.data.file_path.to_str().unwrap()); // TODO maybe calculate it just once? ui.separator(); - ui.label(diags.last().unwrap_or(&"".to_string())); - } - ui.with_layout(egui::Layout::top_down(egui::Align::RIGHT), |ui| { - ui.horizontal(|ui| { - ui.label(format!("v{}-{}", env!("CARGO_PKG_VERSION"), git_version::git_version!())); - ui.separator(); - ui.hyperlink_to("", "mailto:me@alemi.dev"); - ui.label("alemi"); + ui.label(human_size( + *self + .data + .file_size + .read() + .expect("Filesize RwLock poisoned"), + )); + ui.with_layout(egui::Layout::top_down(egui::Align::RIGHT), |ui| { + ui.horizontal(|ui| { + ui.label(format!( + "v{}-{}", + env!("CARGO_PKG_VERSION"), + git_version::git_version!() + )); + ui.separator(); + ui.hyperlink_to("", "mailto:me@alemi.dev"); + ui.label("alemi"); + }); }); }); + }) + .body(|ui| { + ui.set_height(200.0); + egui::ScrollArea::vertical().show(ui, |ui| { + let msgs = self + .data + .diagnostics + .read() + .expect("Diagnostics RwLock poisoned"); + for msg in msgs.iter() { + ui.label(msg); + } + }); }); }); if self.edit { @@ -147,20 +195,40 @@ impl eframe::App for App { 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); + 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); + 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"); + 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.selectable_value( + &mut source.panel_id, + p.id, + p.name.as_str(), + ); } }); ui.color_edit_button_srgba(&mut source.color); @@ -174,120 +242,143 @@ impl eframe::App for App { }); }); } - if self.show_log { - egui::SidePanel::right("logs-panel").show(ctx, |ui| { - egui::ScrollArea::vertical().show(ui, |ui| { - ui.heading("logs"); - ui.separator(); - let msgs = self.data.diagnostics.read().expect("Diagnostics RwLock poisoned"); - ui.group(|ui| { - for msg in msgs.iter() { - ui.label(msg); - } - }); - }); - }); - } - let mut to_swap : Vec = Vec::new(); + let mut to_swap: Vec = Vec::new(); 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 sources = self.data.sources.read().expect("Sources RwLock poisoned"); // TODO only lock as write when editing for (index, panel) in panels.iter_mut().enumerate() { - ui.group(|ui| { - ui.vertical(|ui| { - ui.horizontal(|ui| { - ui.heading(panel.name.as_str()); - ui.separator(); - if self.edit && index > 0 { - if ui.small_button("up").clicked() { + if index > 0 { + ui.separator(); + } + egui::collapsing_header::CollapsingState::load_with_default_open( + ctx, + ui.make_persistent_id(format!("panel-{}-compressable", panel.id)), + true, + ) + .show_header(ui, |ui| { + ui.horizontal(|ui| { + if self.edit { + if ui.small_button(" ^ ").clicked() { + if index > 0 { to_swap.push(index); // TODO kinda jank but is there a better way? } - ui.separator(); } - for source in &*sources { - if source.panel_id == panel.id { - if source.visible { - ui.label( - RichText::new(source.name.as_str()) - .color(if source.color == Color32::TRANSPARENT { Color32::GRAY } else { source.color }) - ); - } else { - ui.label(RichText::new(source.name.as_str()).color(Color32::BLACK)); - } - ui.separator(); - } - } - ui.add(egui::Slider::new(&mut panel.height, 0..=500).text("height")); ui.separator(); - ui.checkbox(&mut panel.limit, "limit"); - ui.add(egui::DragValue::new(&mut panel.view_size).speed(10).clamp_range(0..=2147483647)); - ui.label("mins"); + } + 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.checkbox(&mut panel.view_scroll, "autoscroll"); - ui.separator(); - }); - - let mut p = Plot::new(format!("plot-{}", panel.name)) - .height(panel.height as f32) - .allow_scroll(false); - - if panel.view_scroll { - p = p.include_x(Utc::now().timestamp() 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)); - p = p.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); - } + 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"); }); - p = p.x_grid_spacer(|grid| { + }); + }); + }) + .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.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(); + let mut out: Vec = Vec::new(); loop { counter += 3600; - if counter > end as i64 { break; } + if counter > end as i64 { + break; + } if (counter + offset) % 86400 == 0 { - out.push(GridMark { value: counter as f64, step_size: 86400 as f64 }) + 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 }); + 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)); - } + 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)); } - }); + } }); }); } }); }); - if !to_swap.is_empty() { // TODO can this be done in background? idk + if !to_swap.is_empty() { + // TODO can this be done in background? idk let mut panels = self.data.panels.write().expect("Panels RwLock poisoned"); for index in to_swap { - panels.swap(index-1, index); + panels.swap(index - 1, index); } } } diff --git a/src/app/util.rs b/src/app/util.rs index df05abf..cc3922a 100644 --- a/src/app/util.rs +++ b/src/app/util.rs @@ -1,7 +1,7 @@ -use std::sync::Arc; -use chrono::{DateTime, NaiveDateTime, Utc, Local}; -use tracing_subscriber::Layer; +use chrono::{DateTime, Local, NaiveDateTime, Utc}; use eframe::egui::Color32; +use std::sync::Arc; +use tracing_subscriber::Layer; use super::data::ApplicationState; @@ -22,17 +22,19 @@ pub fn human_size(size: u64) -> String { pub fn timestamp_to_str(t: i64, date: bool, time: bool) -> String { format!( "{}", - DateTime::::from(DateTime::::from_utc(NaiveDateTime::from_timestamp(t, 0), Utc)).format( - if date && time { - "%Y/%m/%d %H:%M:%S" - } else if date { - "%Y/%m/%d" - } else if time { - "%H:%M:%S" - } else { - "%s" - } - ) + DateTime::::from(DateTime::::from_utc( + NaiveDateTime::from_timestamp(t, 0), + Utc + )) + .format(if date && time { + "%Y/%m/%d %H:%M:%S" + } else if date { + "%Y/%m/%d" + } else if time { + "%H:%M:%S" + } else { + "%s" + }) ) } @@ -64,21 +66,36 @@ impl InternalLogger { } } -impl Layer for InternalLogger where S: tracing::Subscriber { +impl Layer for InternalLogger +where + S: tracing::Subscriber, +{ fn on_event( - &self, - event: &tracing::Event<'_>, - _ctx: tracing_subscriber::layer::Context<'_, S>, + &self, + event: &tracing::Event<'_>, + _ctx: tracing_subscriber::layer::Context<'_, S>, ) { - let mut msg_visitor = LogMessageVisitor { msg: "".to_string() }; + let mut msg_visitor = LogMessageVisitor { + msg: "".to_string(), + }; event.record(&mut msg_visitor); - let out = format!("{} [{}] {}: {}", Local::now().format("%H:%M:%S"), event.metadata().level(), event.metadata().target(), msg_visitor.msg); - self.state.diagnostics.write().expect("Diagnostics RwLock poisoned").push(out); + let out = format!( + "{} [{}] {}: {}", + Local::now().format("%H:%M:%S"), + event.metadata().level(), + event.metadata().target(), + msg_visitor.msg + ); + self.state + .diagnostics + .write() + .expect("Diagnostics RwLock poisoned") + .push(out); } } struct LogMessageVisitor { - msg : String, + msg: String, } impl tracing::field::Visit for LogMessageVisitor { @@ -93,4 +110,4 @@ impl tracing::field::Visit for LogMessageVisitor { self.msg = value.to_string(); } } -} \ No newline at end of file +}