diff --git a/Cargo.lock b/Cargo.lock index e5ce4aa..17881fe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1203,6 +1203,19 @@ dependencies = [ "egui", ] +[[package]] +name = "egui_tiles" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bb10cef7bdbd1adb158aec9cca20f34779fd40ea126e02662ab558189f1c435" +dependencies = [ + "ahash", + "egui", + "itertools", + "log", + "serde", +] + [[package]] name = "either" version = "1.10.0" @@ -2440,6 +2453,15 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.10" @@ -3809,6 +3831,7 @@ dependencies = [ "egui-gizmo", "egui_extras", "egui_plot", + "egui_tiles", "env_logger", "futures", "home", diff --git a/Cargo.toml b/Cargo.toml index 45b4420..d45d047 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,7 @@ eframe = { version = "0.26", default-features = false, features = ["glow", "wayl egui_plot = "0.26" egui_extras = { version = "0.26", features = ["image"] } image = { version = "0.24", default-features = false, features = ["jpeg", "png"] } +egui_tiles = "0.7.2" egui-gizmo = "0.16" walkers = { git = "https://github.com/podusowski/walkers" } # serialization & communication @@ -41,7 +42,6 @@ reqwest = { version = "0.11", default-features = false, features = ["rustls-tls" # Used for profiling puffin = { version = "0.19", optional = true } puffin_egui = { version = "0.26", optional = true } -#egui_tiles = "0.6.0" # X86 dependencies [target.'cfg(target_arch = "x86_64")'.dependencies] diff --git a/src/gui.rs b/src/gui.rs index 25aff0b..a355cc8 100644 --- a/src/gui.rs +++ b/src/gui.rs @@ -13,11 +13,9 @@ use mithril::telemetry::*; mod panels; mod fc_settings; mod map; -mod maxi_grid; -mod misc; mod plot; mod simulation_settings; -mod tabs; +pub mod tabs; mod theme; mod top_bar; pub mod windows; // TODO: make this private (it is public because it has ARCHIVE) @@ -151,19 +149,21 @@ impl Sam { self.open_data_source(Box::new(log)); } + let enabled = !self.archive_window.open; + // Top menu bar // TODO: avoid passing in self here - MenuBarPanel::show(ctx, self, !self.archive_window.open); + MenuBarPanel::show(ctx, self, enabled); let data_source = self.data_sources.last_mut().unwrap(); // If our current data source is a simulation, show a config panel to the left if let Some(sim) = data_source.as_any_mut().downcast_mut::() { - SimulationPanel::show(ctx, sim, !self.archive_window.open); + SimulationPanel::show(ctx, sim, enabled); } // Header containing text indicators and flight mode buttons - HeaderPanel::show(ctx, data_source.deref_mut(), !self.archive_window.open); + HeaderPanel::show(ctx, data_source.deref_mut(), enabled); // Bottom status bar egui::TopBottomPanel::bottom("bottombar").min_height(30.0).show(ctx, |ui| { @@ -187,19 +187,20 @@ impl Sam { }); // Everything else. This has to be called after all the other panels are created. - egui::CentralPanel::default().show(ctx, |ui| { - ui.set_enabled(!self.archive_window.open); - match self.tab { - GuiTab::Launch => {} - GuiTab::Plot => self.plot_tab.main_ui(ui, data_source.deref_mut()), - GuiTab::Configure => { - let changed = self.configure_tab.main_ui(ui, data_source.deref_mut(), &mut self.settings); - if changed { - data_source.apply_settings(&self.settings); - } + match self.tab { + GuiTab::Launch => { + egui::CentralPanel::default().show(ctx, |ui| { + ui.set_enabled(enabled) + }).inner + } + GuiTab::Plot => self.plot_tab.main_ui(ctx, data_source.deref_mut(), &mut self.settings, enabled), + GuiTab::Configure => { + let changed = self.configure_tab.main_ui(ctx, data_source.deref_mut(), &mut self.settings, enabled); + if changed { + data_source.apply_settings(&self.settings); } } - }); + } } } diff --git a/src/gui/maxi_grid.rs b/src/gui/maxi_grid.rs deleted file mode 100644 index f3056db..0000000 --- a/src/gui/maxi_grid.rs +++ /dev/null @@ -1,101 +0,0 @@ -//! Contains a widget to display a grid with maximizable cells. - -use std::cell::RefCell; -use std::rc::Rc; - -use eframe::egui; -use egui::{Align, Layout, Rect, Ui, Vec2}; - -/// State object held by the application, storing the currently maximized cell. -#[derive(Default, Clone)] -pub struct MaxiGridState { - maximized: Rc>>, -} - -impl MaxiGridState { - fn maximize(&self, cell: (usize, usize)) { - self.maximized.borrow_mut().replace(cell); - } - - fn minimize(&self) { - *self.maximized.borrow_mut() = None; - } - - fn maximized(&self) -> Option<(usize, usize)> { - *self.maximized.borrow() - } -} - -/// Grid widget. Created and destroyed during draw. -pub struct MaxiGrid<'a> { - cells: (usize, usize), - available_rect: Rect, - ui: &'a mut Ui, - state: MaxiGridState, - current_cell: (usize, usize), -} - -impl<'a> MaxiGrid<'a> { - pub fn new(cells: (usize, usize), ui: &'a mut Ui, state: MaxiGridState) -> Self { - let available_rect = ui.available_rect_before_wrap(); - - Self { - cells, - available_rect, - ui, - state, - current_cell: (0, 0), - } - } - - fn draw(&mut self, title: &'static str, cb: impl FnOnce(&mut Ui), maximized: bool) { - let rect = if maximized { - self.ui.available_rect_before_wrap() - } else { - let top_left = self.available_rect.left_top(); - let cell_size = self.available_rect.size() / Vec2::new(self.cells.0 as f32, self.cells.1 as f32); - - let cell_top_left = top_left + (cell_size * Vec2::new(self.current_cell.0 as f32, self.current_cell.1 as f32)); - let cell_bottom_right = cell_top_left + cell_size; - Rect::from_min_max(cell_top_left, cell_bottom_right).shrink2(self.ui.style().spacing.item_spacing / 2.0) - }; - - self.ui.put(rect, |ui: &mut Ui| { - ui.vertical(|ui| { - ui.add_space(3.0); - ui.horizontal(|ui| { - ui.heading(title); - ui.with_layout(Layout::right_to_left(Align::Center), |ui| { - if maximized { - if ui.button("๐Ÿ—•").clicked() { - self.state.minimize(); - } - } else if ui.button("๐Ÿ—–").clicked() { - self.state.maximize(self.current_cell); - } - }); - }); - - (cb)(ui); - }) - .response - }); - } - - pub fn cell(mut self, title: &'static str, cb: impl FnOnce(&mut Ui)) -> Self { - let maximized = self.state.maximized(); - if maximized.map(|c| c == self.current_cell).unwrap_or(false) { - self.draw(title, cb, true); - } else if maximized.is_none() { - self.draw(title, cb, false); - } - - self.current_cell.0 += 1; - if self.current_cell.0 >= self.cells.0 { - self.current_cell.0 = 0; - self.current_cell.1 += 1; - } - - self - } -} diff --git a/src/gui/misc.rs b/src/gui/misc.rs deleted file mode 100644 index 24cbbc6..0000000 --- a/src/gui/misc.rs +++ /dev/null @@ -1,15 +0,0 @@ -use eframe::egui; - -pub trait MiscUiExt { - fn toggle_button(&mut self, value: &mut bool, text_false: &str, text_true: &str); -} - -impl MiscUiExt for egui::Ui { - fn toggle_button(&mut self, value: &mut bool, text_false: &str, text_true: &str) { - let text = if *value { text_true } else { text_false }; - - if self.button(text).clicked() { - *value = !(*value); - } - } -} diff --git a/src/gui/tabs.rs b/src/gui/tabs.rs index dc8f523..0a26c35 100644 --- a/src/gui/tabs.rs +++ b/src/gui/tabs.rs @@ -1,5 +1,5 @@ -mod configure; -mod plot; +pub mod configure; +pub mod plot; pub use configure::*; pub use plot::*; diff --git a/src/gui/tabs/configure.rs b/src/gui/tabs/configure.rs index c6068df..0acda04 100644 --- a/src/gui/tabs/configure.rs +++ b/src/gui/tabs/configure.rs @@ -17,191 +17,206 @@ impl ConfigureTab { Self {} } - pub fn main_ui(&mut self, ui: &mut egui::Ui, data_source: &mut dyn DataSource, settings: &mut AppSettings) -> bool { + fn app_settings_ui(&mut self, ui: &mut egui::Ui, data_source: &mut dyn DataSource, settings: &mut AppSettings) -> bool { let mut changed = false; - ui.horizontal(|ui| { - ui.set_width(ui.available_width()); - ui.set_height(ui.available_height()); - - ui.vertical(|ui| { - ui.set_width(ui.available_width() / 2.0); - ui.set_height(ui.available_height()); - - ui.add_space(10.0); - ui.heading("GCS Settings"); - ui.add_space(10.0); - - egui::Grid::new("app_settings_grid") - .num_columns(2) - .spacing([40.0, 4.0]) - .striped(true) - .show(ui, |ui| { - ui.label("MapBox Access Token"); - ui.add_sized(ui.available_size(), TextEdit::singleline(&mut settings.mapbox_access_token)); - ui.end_row(); - - ui.label("LoRa channel selection (500kHz BW)"); - ui.vertical(|ui| { - ui.horizontal(|ui| { - ui.toggle_value( - &mut settings.lora.channels[0], - RichText::new("863.25").monospace().size(10.0), - ); - ui.toggle_value( - &mut settings.lora.channels[1], - RichText::new("863.75").monospace().size(10.0), - ); - ui.toggle_value( - &mut settings.lora.channels[2], - RichText::new("864.25").monospace().size(10.0), - ); - ui.toggle_value( - &mut settings.lora.channels[3], - RichText::new("864.75").monospace().size(10.0), - ); - ui.toggle_value( - &mut settings.lora.channels[4], - RichText::new("865.25").monospace().size(10.0), - ); - ui.toggle_value( - &mut settings.lora.channels[5], - RichText::new("865.75").monospace().size(10.0), - ); - ui.toggle_value( - &mut settings.lora.channels[6], - RichText::new("866.25").monospace().size(10.0), - ); - ui.label(RichText::new("MHz").weak().size(10.0)); - }); - ui.horizontal(|ui| { - ui.toggle_value( - &mut settings.lora.channels[7], - RichText::new("866.75").monospace().size(10.0), - ); - ui.toggle_value( - &mut settings.lora.channels[8], - RichText::new("867.25").monospace().size(10.0), - ); - ui.toggle_value( - &mut settings.lora.channels[9], - RichText::new("867.75").monospace().size(10.0), - ); - ui.toggle_value( - &mut settings.lora.channels[10], - RichText::new("868.25").monospace().size(10.0), - ); - ui.toggle_value( - &mut settings.lora.channels[11], - RichText::new("868.75").monospace().size(10.0), - ); - ui.toggle_value( - &mut settings.lora.channels[12], - RichText::new("869.25").monospace().size(10.0), - ); - ui.toggle_value( - &mut settings.lora.channels[13], - RichText::new("869.75").monospace().size(10.0), - ); - ui.label(RichText::new("MHz").weak().size(10.0)); - }); - }); - ui.end_row(); - - ui.label("LoRa binding phrase"); - ui.with_layout(Layout::right_to_left(Align::Center), |ui| { - if ui.button("โฌ… Copy from FC").clicked() { - settings.lora.binding_phrase = data_source - .fc_settings() - .map(|s| s.lora.binding_phrase.clone()) - .unwrap_or(settings.lora.binding_phrase.clone()); - } - - ui.add_sized(ui.available_size(), TextEdit::singleline(&mut settings.lora.binding_phrase)); - }); - ui.end_row(); - - ui.label("LoRa uplink key"); - ui.with_layout(Layout::right_to_left(Align::Center), |ui| { - if ui.button("โฌ… Copy from FC").clicked() { - settings.lora.authentication_key = data_source - .fc_settings() - .map(|s| s.lora.authentication_key) - .unwrap_or(settings.lora.authentication_key); - } - - #[cfg(not(target_arch = "wasm32"))] - if ui.button("๐Ÿ”ƒRekey").clicked() { - settings.lora.authentication_key = rand::random(); - } - - ui.with_layout(Layout::left_to_right(Align::Center), |ui| { - ui.monospace(format!("{:032x}", settings.lora.authentication_key)); - }) - }); - ui.end_row(); + ui.add_space(10.0); + ui.heading("GCS Settings"); + ui.add_space(10.0); + + egui::Grid::new("app_settings_grid") + .num_columns(2) + .spacing([40.0, 4.0]) + .striped(true) + .show(ui, |ui| { + ui.label("MapBox Access Token"); + ui.add_sized(ui.available_size(), TextEdit::singleline(&mut settings.mapbox_access_token)); + ui.end_row(); + + ui.label("LoRa channel selection (500kHz BW)"); + ui.vertical(|ui| { + ui.horizontal(|ui| { + ui.toggle_value( + &mut settings.lora.channels[0], + RichText::new("863.25").monospace().size(10.0), + ); + ui.toggle_value( + &mut settings.lora.channels[1], + RichText::new("863.75").monospace().size(10.0), + ); + ui.toggle_value( + &mut settings.lora.channels[2], + RichText::new("864.25").monospace().size(10.0), + ); + ui.toggle_value( + &mut settings.lora.channels[3], + RichText::new("864.75").monospace().size(10.0), + ); + ui.toggle_value( + &mut settings.lora.channels[4], + RichText::new("865.25").monospace().size(10.0), + ); + ui.toggle_value( + &mut settings.lora.channels[5], + RichText::new("865.75").monospace().size(10.0), + ); + ui.toggle_value( + &mut settings.lora.channels[6], + RichText::new("866.25").monospace().size(10.0), + ); + ui.label(RichText::new("MHz").weak().size(10.0)); + }); + ui.horizontal(|ui| { + ui.toggle_value( + &mut settings.lora.channels[7], + RichText::new("866.75").monospace().size(10.0), + ); + ui.toggle_value( + &mut settings.lora.channels[8], + RichText::new("867.25").monospace().size(10.0), + ); + ui.toggle_value( + &mut settings.lora.channels[9], + RichText::new("867.75").monospace().size(10.0), + ); + ui.toggle_value( + &mut settings.lora.channels[10], + RichText::new("868.25").monospace().size(10.0), + ); + ui.toggle_value( + &mut settings.lora.channels[11], + RichText::new("868.75").monospace().size(10.0), + ); + ui.toggle_value( + &mut settings.lora.channels[12], + RichText::new("869.25").monospace().size(10.0), + ); + ui.toggle_value( + &mut settings.lora.channels[13], + RichText::new("869.75").monospace().size(10.0), + ); + ui.label(RichText::new("MHz").weak().size(10.0)); }); + }); + ui.end_row(); - ui.add_space(20.0); + ui.label("LoRa binding phrase"); + ui.with_layout(Layout::right_to_left(Align::Center), |ui| { + if ui.button("โฌ… Copy from FC").clicked() { + settings.lora.binding_phrase = data_source + .fc_settings() + .map(|s| s.lora.binding_phrase.clone()) + .unwrap_or(settings.lora.binding_phrase.clone()); + } - ui.horizontal_centered(|ui| { - ui.set_width(ui.available_width()); + ui.add_sized(ui.available_size(), TextEdit::singleline(&mut settings.lora.binding_phrase)); + }); + ui.end_row(); - if ui.button("๐Ÿ”ƒReload").clicked() { - *settings = AppSettings::load().unwrap_or_default(); - changed = true; + ui.label("LoRa uplink key"); + ui.with_layout(Layout::right_to_left(Align::Center), |ui| { + if ui.button("โฌ… Copy from FC").clicked() { + settings.lora.authentication_key = data_source + .fc_settings() + .map(|s| s.lora.authentication_key) + .unwrap_or(settings.lora.authentication_key); } - if ui.button("๐Ÿ’พ Save Settings").clicked() { - settings.save().unwrap(); - changed = true; + #[cfg(not(target_arch = "wasm32"))] + if ui.button("๐Ÿ”ƒRekey").clicked() { + settings.lora.authentication_key = rand::random(); } + + ui.with_layout(Layout::left_to_right(Align::Center), |ui| { + ui.monospace(format!("{:032x}", settings.lora.authentication_key)); + }) }); + ui.end_row(); }); - ui.separator(); + ui.add_space(20.0); - ui.vertical(|ui| { - ui.set_width(ui.available_width()); + ui.horizontal_centered(|ui| { + ui.set_width(ui.available_width()); + + if ui.button("๐Ÿ”ƒReload").clicked() { + *settings = AppSettings::load().unwrap_or_default(); + changed = true; + } + + if ui.button("๐Ÿ’พ Save Settings").clicked() { + settings.save().unwrap(); + changed = true; + } + }); - ui.add_space(10.0); - ui.heading("FC Settings"); - ui.add_space(10.0); + changed + } - if let Some(fc_settings) = data_source.fc_settings_mut() { - fc_settings.ui(ui, Some(settings)); - } else { - ui.colored_label(Color32::GRAY, "Not connected."); + fn fc_settings_ui(&mut self, ui: &mut egui::Ui, data_source: &mut dyn DataSource, settings: &mut AppSettings) { + ui.add_space(10.0); + ui.heading("FC Settings"); + ui.add_space(10.0); + + if let Some(fc_settings) = data_source.fc_settings_mut() { + fc_settings.ui(ui, Some(settings)); + } else { + ui.colored_label(Color32::GRAY, "Not connected."); + } + + ui.add_space(20.0); + + ui.with_layout(Layout::right_to_left(Align::Center), |ui| { + if ui + .add_enabled(data_source.fc_settings().is_some(), Button::new("๐Ÿ’พ Write Settings & Reboot")) + .clicked() + { + let settings = data_source.fc_settings().cloned().unwrap(); + data_source.send(UplinkMessage::WriteSettings(settings)).unwrap(); + } + + #[cfg(not(any(target_arch = "wasm32", target_os="android")))] + if ui.add_enabled(data_source.fc_settings().is_some(), Button::new("๐Ÿ–น Save to File")).clicked() { + save_fc_settings_file(data_source.fc_settings().unwrap()); + } + + #[cfg(not(any(target_arch = "wasm32", target_os="android")))] + if ui.add_enabled(data_source.fc_settings().is_some(), Button::new("๐Ÿ–น Load from File")).clicked() + { + if let Some(settings) = open_fc_settings_file() { + info!("Loaded settings: {:?}", settings); + *data_source.fc_settings_mut().unwrap() = settings; } + } - ui.add_space(20.0); + if ui.button("๐Ÿ”ƒReload").clicked() { + data_source.send(UplinkMessage::ReadSettings).unwrap(); + } + }); + } - ui.with_layout(Layout::right_to_left(Align::Center), |ui| { - if ui - .add_enabled(data_source.fc_settings().is_some(), Button::new("๐Ÿ’พ Write Settings & Reboot")) - .clicked() - { - let settings = data_source.fc_settings().cloned().unwrap(); - data_source.send(UplinkMessage::WriteSettings(settings)).unwrap(); - } + pub fn main_ui(&mut self, ctx: &egui::Context, data_source: &mut dyn DataSource, settings: &mut AppSettings, enabled: bool) -> bool { + let mut changed = false; - #[cfg(not(any(target_arch = "wasm32", target_os="android")))] - if ui.add_enabled(data_source.fc_settings().is_some(), Button::new("๐Ÿ–น Save to File")).clicked() { - save_fc_settings_file(data_source.fc_settings().unwrap()); - } + egui::CentralPanel::default().show(ctx, |ui| { + ui.set_enabled(enabled); + ui.horizontal(|ui| { + ui.set_width(ui.available_width()); + ui.set_height(ui.available_height()); - #[cfg(not(any(target_arch = "wasm32", target_os="android")))] - if ui.add_enabled(data_source.fc_settings().is_some(), Button::new("๐Ÿ–น Load from File")).clicked() - { - if let Some(settings) = open_fc_settings_file() { - info!("Loaded settings: {:?}", settings); - *data_source.fc_settings_mut().unwrap() = settings; - } - } + ui.vertical(|ui| { + ui.set_width(ui.available_width() / 2.0); + ui.set_height(ui.available_height()); - if ui.button("๐Ÿ”ƒReload").clicked() { - data_source.send(UplinkMessage::ReadSettings).unwrap(); - } + changed = self.app_settings_ui(ui, data_source, settings); + }); + + ui.separator(); + + ui.vertical(|ui| { + ui.set_width(ui.available_width()); + + self.fc_settings_ui(ui, data_source, settings); }); }); }); diff --git a/src/gui/tabs/plot.rs b/src/gui/tabs/plot.rs index bb7e55e..62b6b05 100644 --- a/src/gui/tabs/plot.rs +++ b/src/gui/tabs/plot.rs @@ -1,12 +1,22 @@ use std::cell::RefCell; use std::rc::Rc; +use egui::Align; +use egui::Layout; +use egui::SelectableLabel; +use egui::TextBuffer; +use egui::TextEdit; +use serde::{Deserialize, Serialize}; +use egui::CentralPanel; use egui::Color32; use egui::Rect; +use egui::SidePanel; +use egui::Stroke; use egui::Vec2; use egui_gizmo::Gizmo; use egui_gizmo::GizmoMode; use egui_gizmo::GizmoVisuals; +use egui_tiles::SimplificationOptions; use nalgebra::UnitQuaternion; use nalgebra::Vector3; @@ -14,8 +24,6 @@ use crate::data_source::DataSource; use crate::settings::AppSettings; use crate::gui::map::*; -use crate::gui::maxi_grid::*; -use crate::gui::misc::*; use crate::gui::plot::*; const R: Color32 = Color32::from_rgb(0xfb, 0x49, 0x34); @@ -30,8 +38,8 @@ const BR: Color32 = Color32::from_rgb(0x61, 0x48, 0x1c); const P: Color32 = Color32::from_rgb(0xb1, 0x62, 0x86); const C: Color32 = Color32::from_rgb(0x68, 0x9d, 0x6a); -#[derive(Debug, Clone, Copy, PartialEq)] -enum SelectedPlot { +#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] +pub enum PlotCell { Orientation, VerticalSpeed, Altitude, @@ -46,28 +54,198 @@ enum SelectedPlot { Map, } -impl std::fmt::Display for SelectedPlot { +impl PlotCell { + fn all() -> Vec { + vec![ + PlotCell::Orientation, + PlotCell::VerticalSpeed, + PlotCell::Altitude, + PlotCell::Map, + PlotCell::Gyroscope, + PlotCell::Accelerometers, + PlotCell::Magnetometer, + PlotCell::Pressures, + PlotCell::Temperatures, + PlotCell::Power, + PlotCell::Runtime, + PlotCell::Signal, + ] + } +} + +impl std::fmt::Display for PlotCell { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { match self { - SelectedPlot::Orientation => write!(f, "Orientation"), - SelectedPlot::VerticalSpeed => write!(f, "Vertical Speed & Accel."), - SelectedPlot::Altitude => write!(f, "Altitude"), - SelectedPlot::Gyroscope => write!(f, "Gyroscope"), - SelectedPlot::Accelerometers => write!(f, "Accelerometers"), - SelectedPlot::Magnetometer => write!(f, "Magnetometer"), - SelectedPlot::Pressures => write!(f, "Pressures"), - SelectedPlot::Temperatures => write!(f, "Temperatures"), - SelectedPlot::Power => write!(f, "Power"), - SelectedPlot::Runtime => write!(f, "Runtime"), - SelectedPlot::Signal => write!(f, "Signal"), - SelectedPlot::Map => write!(f, "Map"), + PlotCell::Orientation => write!(f, "Orientation"), + PlotCell::VerticalSpeed => write!(f, "Vertical Speed & Accel."), + PlotCell::Altitude => write!(f, "Altitude (ASL)"), + PlotCell::Gyroscope => write!(f, "Gyroscope"), + PlotCell::Accelerometers => write!(f, "Accelerometers"), + PlotCell::Magnetometer => write!(f, "Magnetometer"), + PlotCell::Pressures => write!(f, "Pressures"), + PlotCell::Temperatures => write!(f, "Temperatures"), + PlotCell::Power => write!(f, "Power"), + PlotCell::Runtime => write!(f, "Runtime"), + PlotCell::Signal => write!(f, "Signal"), + PlotCell::Map => write!(f, "Map"), + } + } +} + +struct TileBehavior<'a> { + data_source: &'a mut dyn DataSource, + orientation_plot: &'a mut PlotState, + vertical_speed_plot: &'a mut PlotState, + altitude_plot: &'a mut PlotState, + gyroscope_plot: &'a mut PlotState, + accelerometer_plot: &'a mut PlotState, + magnetometer_plot: &'a mut PlotState, + barometer_plot: &'a mut PlotState, + temperature_plot: &'a mut PlotState, + power_plot: &'a mut PlotState, + runtime_plot: &'a mut PlotState, + signal_plot: &'a mut PlotState, + map: &'a mut MapState, +} + +// TODO: move these somewhere else +impl<'a> TileBehavior<'a> { + fn plot_gizmo( + &mut self, + ui: &mut egui::Ui, + viewport: Rect, + orientation: UnitQuaternion, + colors: (Color32, Color32, Color32) + ) { + // We can't disable interaction with the gizmo, so we disable the entire UI + // when the user gets too close. TODO: upstream way to disable interaction? + let enabled = !ui.rect_contains_pointer(viewport); + + // use top right of plot for indicator, space below for plot + let viewport = Rect::from_two_pos(viewport.lerp_inside(Vec2::new(0.55, 0.55)), viewport.right_top()); + + let fade_to_color = Color32::BLACK; + ui.visuals_mut().widgets.noninteractive.weak_bg_fill = fade_to_color; + + // square viewport + let viewport_square_side = f32::min(viewport.width(), viewport.height()); + let viewport = viewport.shrink2((viewport.size() - Vec2::splat(viewport_square_side))*0.5); + + let view = UnitQuaternion::from_euler_angles(-90.0f32.to_radians(), 180f32.to_radians(), 0.0f32.to_radians()); + + let visuals = GizmoVisuals { + x_color: colors.0, + y_color: colors.1, + z_color: colors.2, + inactive_alpha: 1.0, + highlight_alpha: 1.0, + gizmo_size: viewport_square_side * 0.4, + ..Default::default() + }; + + let gizmo = Gizmo::new("My gizmo") + .mode(GizmoMode::Translate) + .viewport(viewport) + .orientation(egui_gizmo::GizmoOrientation::Local) + .model_matrix(orientation.to_homogeneous().into()) + .view_matrix(view.to_homogeneous().into()) + .visuals(visuals); + + ui.add_enabled_ui(enabled, |ui| { + gizmo.interact(ui); + }); + } + + fn plot_orientation(&mut self, ui: &mut egui::Ui) { + #[cfg(feature = "profiling")] + puffin::profile_function!(); + + let mut viewport = ui.cursor(); + viewport.set_width(ui.available_width()); + viewport.set_height(ui.available_height()); + + let orientation = self.data_source.vehicle_states() + .rev() + .find_map(|(_, vs)| vs.orientation) + .unwrap_or(UnitQuaternion::new(Vector3::new(0.0, 0.0, 0.0))); + let true_orientation = self.data_source.vehicle_states() + .rev() + .find_map(|(_, vs)| vs.true_orientation); + + ui.plot_telemetry(&self.orientation_plot, self.data_source); + + if let Some(orientation) = true_orientation { + self.plot_gizmo(ui, viewport, orientation, (R1, G1, B1)); + } + + self.plot_gizmo(ui, viewport, orientation, (R, G, B)); + } +} + +impl<'a> egui_tiles::Behavior for TileBehavior<'a> { + fn tab_title_for_pane(&mut self, pane: &PlotCell) -> egui::WidgetText { + format!("{}", pane).into() + } + + fn pane_ui(&mut self, ui: &mut egui::Ui, _tile_id: egui_tiles::TileId, pane: &mut PlotCell) -> egui_tiles::UiResponse { + match pane { + PlotCell::Orientation => self.plot_orientation(ui), + PlotCell::VerticalSpeed => ui.plot_telemetry(&self.vertical_speed_plot, self.data_source), + PlotCell::Altitude => ui.plot_telemetry(&self.altitude_plot, self.data_source), + PlotCell::Gyroscope => ui.plot_telemetry(&self.gyroscope_plot, self.data_source), + PlotCell::Accelerometers => ui.plot_telemetry(&self.accelerometer_plot, self.data_source), + PlotCell::Magnetometer => ui.plot_telemetry(&self.magnetometer_plot, self.data_source), + PlotCell::Pressures => ui.plot_telemetry(&self.barometer_plot, self.data_source), + PlotCell::Temperatures => ui.plot_telemetry(&self.temperature_plot, self.data_source), + PlotCell::Power => ui.plot_telemetry(&self.power_plot, self.data_source), + PlotCell::Runtime => ui.plot_telemetry(&self.runtime_plot, self.data_source), + PlotCell::Signal => ui.plot_telemetry(&self.signal_plot, self.data_source), + PlotCell::Map => { ui.add(Map::new(&mut self.map, self.data_source)); }, + } + + egui_tiles::UiResponse::None + } + + fn dragged_overlay_color(&self, _visuals: &egui::Visuals) -> Color32 { + Color32::from_rgb(0xb8, 0xbb, 0x26).gamma_multiply(0.5) + } + + fn drag_preview_stroke(&self, _visuals: &egui::Visuals) -> egui::Stroke { + Stroke { width: 1.0, color: Color32::from_rgb(0xb8, 0xbb, 0x26) } + } + + fn drag_preview_color(&self, _visuals: &egui::Visuals) -> Color32 { + Color32::from_rgb(0xb8, 0xbb, 0x26).gamma_multiply(0.5) + } + + fn tab_bar_color(&self, visuals: &egui::Visuals) -> Color32 { + visuals.widgets.noninteractive.bg_fill + } + + fn tab_bg_color(&self, visuals: &egui::Visuals, _tiles: &egui_tiles::Tiles, _tile_id: egui_tiles::TileId, active: bool) -> Color32 { + if active { self.tab_bar_color(visuals) } else { visuals.extreme_bg_color } + } + + fn tab_text_color(&self, visuals: &egui::Visuals, _tiles: &egui_tiles::Tiles, _tile_id: egui_tiles::TileId, active: bool) -> Color32 { + if active { visuals.widgets.active.fg_stroke.color } else { visuals.widgets.hovered.fg_stroke.color } + } + + fn simplification_options(&self) -> egui_tiles::SimplificationOptions { + SimplificationOptions { + prune_empty_tabs: true, + prune_empty_containers: true, + prune_single_child_containers: true, + all_panes_must_have_tabs: true, + join_nested_linear_containers: true, + ..SimplificationOptions::OFF } } } pub struct PlotTab { - maxi_grid_state: MaxiGridState, - dropdown_selected_plot: SelectedPlot, + tile_tree: egui_tiles::Tree, + show_view_settings: bool, + new_preset_name: String, shared_plot: Rc>, orientation_plot: PlotState, @@ -81,7 +259,6 @@ pub struct PlotTab { power_plot: PlotState, runtime_plot: PlotState, signal_plot: PlotState, - map: MapState, } @@ -156,8 +333,9 @@ impl PlotTab { let map = MapState::new(ctx, (!settings.mapbox_access_token.is_empty()).then_some(settings.mapbox_access_token.clone())); Self { - maxi_grid_state: MaxiGridState::default(), - dropdown_selected_plot: SelectedPlot::Orientation, + tile_tree: Self::tree_grid(), + show_view_settings: false, + new_preset_name: String::new(), shared_plot, orientation_plot, vertical_speed_plot, @@ -174,143 +352,117 @@ impl PlotTab { } } - fn plot_gizmo( - &mut self, - ui: &mut egui::Ui, - viewport: Rect, - orientation: UnitQuaternion, - colors: (Color32, Color32, Color32) - ) { - // We can't disable interaction with the gizmo, so we disable the entire UI - // when the user gets too close. TODO: upstream way to disable interaction? - let enabled = !ui.rect_contains_pointer(viewport); - - // use top right of plot for indicator, space below for plot - let viewport = Rect::from_two_pos(viewport.lerp_inside(Vec2::new(0.55, 0.55)), viewport.right_top()); - - let fade_to_color = Color32::BLACK; - ui.visuals_mut().widgets.noninteractive.weak_bg_fill = fade_to_color; - - // square viewport - let viewport_square_side = f32::min(viewport.width(), viewport.height()); - let viewport = viewport.shrink2((viewport.size() - Vec2::splat(viewport_square_side))*0.5); - - let view = UnitQuaternion::from_euler_angles(-90.0f32.to_radians(), 180f32.to_radians(), 0.0f32.to_radians()); + fn tree_grid() -> egui_tiles::Tree { + egui_tiles::Tree::new_grid("plot_tree", PlotCell::all()) + } - let visuals = GizmoVisuals { - x_color: colors.0, - y_color: colors.1, - z_color: colors.2, - inactive_alpha: 1.0, - highlight_alpha: 1.0, - gizmo_size: viewport_square_side * 0.4, - ..Default::default() - }; + fn tree_from_tiles(tiles: egui_tiles::Tiles) -> egui_tiles::Tree { + let root = tiles.iter() + .filter(|(id, _tile)| tiles.is_root(**id)) + .next() + .unwrap().0; + egui_tiles::Tree::new("plot_tree", *root, tiles) + } - let gizmo = Gizmo::new("My gizmo") - .mode(GizmoMode::Translate) - .viewport(viewport) - .orientation(egui_gizmo::GizmoOrientation::Local) - .model_matrix(orientation.to_homogeneous().into()) - .view_matrix(view.to_homogeneous().into()) - .visuals(visuals); + fn tree_presets() -> Vec<(&'static str, egui_tiles::Tree)> { + vec![ + ("Grid", egui_tiles::Tree::new_grid("plot_tree", PlotCell::all())), + ("Tabs", egui_tiles::Tree::new_tabs("plot_tree", PlotCell::all())), + ] + } - ui.add_enabled_ui(enabled, |ui| { - gizmo.interact(ui); - }); + fn same_topology(&self, tiles: &egui_tiles::Tiles) -> bool { + // remove all the additional info to allow comparison + // TODO: find a better way + let serialized = serde_json::to_string(&self.tile_tree.tiles).unwrap(); + let deserialized: egui_tiles::Tiles = serde_json::from_str(&serialized).unwrap(); + deserialized == *tiles } - fn plot_orientation(&mut self, ui: &mut egui::Ui, data_source: &mut dyn DataSource) { + pub fn main_ui(&mut self, ctx: &egui::Context, data_source: &mut dyn DataSource, settings: &mut AppSettings, enabled: bool) { #[cfg(feature = "profiling")] puffin::profile_function!(); - let mut viewport = ui.cursor(); - viewport.set_width(ui.available_width()); - viewport.set_height(ui.available_height()); - - let orientation = data_source.vehicle_states() - .rev() - .find_map(|(_, vs)| vs.orientation) - .unwrap_or(UnitQuaternion::new(Vector3::new(0.0, 0.0, 0.0))); - let true_orientation = data_source.vehicle_states() - .rev() - .find_map(|(_, vs)| vs.true_orientation); - - ui.plot_telemetry(&self.orientation_plot, data_source); - - if let Some(orientation) = true_orientation { - self.plot_gizmo(ui, viewport, orientation, (R1, G1, B1)); + if self.show_view_settings { + SidePanel::right("dock_view_settings").show(ctx, |ui| { + ui.set_enabled(enabled); + + ui.vertical(|ui| { + ui.set_width(ui.available_width()); + ui.checkbox(&mut self.shared_plot.borrow_mut().show_stats, "Show Stats"); + ui.separator(); + ui.weak("Presets"); + + for (name, tree) in Self::tree_presets() { + if ui.add_sized(Vec2::new(ui.available_width(), 1.0), SelectableLabel::new(self.same_topology(&tree.tiles), name)).clicked() { + self.tile_tree = tree; + } + } + + ui.separator(); + ui.weak("Custom"); + + let mut changed = false; + if let Some(saved) = settings.tile_presets.as_mut() { + for (name, tiles) in saved.clone() { + ui.with_layout(Layout::right_to_left(Align::TOP), |ui| { + if ui.button("๐Ÿ—‘").clicked() { + saved.remove(&name); + changed = true; + } + + if ui.add_sized(Vec2::new(ui.available_width(), 1.0), SelectableLabel::new(self.same_topology(&tiles), name)).clicked() { + self.tile_tree = Self::tree_from_tiles(tiles.clone()); + } + }); + } + } + + ui.with_layout(Layout::right_to_left(Align::TOP), |ui| { + if ui.button("Save").clicked() { + settings.tile_presets + .get_or_insert(std::collections::HashMap::new()) + .insert(self.new_preset_name.take(), self.tile_tree.tiles.clone()); + changed = true; + } + + ui.add(TextEdit::singleline(&mut self.new_preset_name)); + }); + + if changed { + if let Err(e) = settings.save() { + log::error!("Failed to save settings: {:?}", e); + } + } + }) + }); } - self.plot_gizmo(ui, viewport, orientation, (R, G, B)); - } - - pub fn main_ui(&mut self, ui: &mut egui::Ui, data_source: &mut dyn DataSource) { - #[cfg(feature = "profiling")] - puffin::profile_function!(); - self.shared_plot.borrow_mut().set_end(data_source.end()); - if ui.available_width() > 1000.0 { - MaxiGrid::new((4, 3), ui, self.maxi_grid_state.clone()) - .cell("Orientation", |ui| self.plot_orientation(ui, data_source)) - .cell("Vert. Speed & Accel", |ui| ui.plot_telemetry(&self.vertical_speed_plot, data_source)) - .cell("Altitude (ASL)", |ui| ui.plot_telemetry(&self.altitude_plot, data_source)) - .cell("Position", |ui| { ui.add(Map::new(&mut self.map, data_source)); }) - .cell("Gyroscope", |ui| ui.plot_telemetry(&self.gyroscope_plot, data_source)) - .cell("Accelerometers", |ui| ui.plot_telemetry(&self.accelerometer_plot, data_source)) - .cell("Magnetometer", |ui| ui.plot_telemetry(&self.magnetometer_plot, data_source)) - .cell("Pressures", |ui| ui.plot_telemetry(&self.barometer_plot, data_source)) - .cell("Temperature", |ui| ui.plot_telemetry(&self.temperature_plot, data_source)) - .cell("Power", |ui| ui.plot_telemetry(&self.power_plot, data_source)) - .cell("Runtime", |ui| ui.plot_telemetry(&self.runtime_plot, data_source)) - .cell("Signal", |ui| ui.plot_telemetry(&self.signal_plot, data_source)); - } else { - ui.vertical(|ui| { - ui.horizontal(|ui| { - ui.spacing_mut().combo_width = ui.available_width(); - egui::ComboBox::from_id_source("plot_selector") - .selected_text(format!("{}", self.dropdown_selected_plot)) - .show_ui(ui, |ui| { - ui.set_width(ui.available_width()); - for p in [ - SelectedPlot::Orientation, - SelectedPlot::VerticalSpeed, - SelectedPlot::Altitude, - SelectedPlot::Gyroscope, - SelectedPlot::Accelerometers, - SelectedPlot::Magnetometer, - SelectedPlot::Pressures, - SelectedPlot::Temperatures, - SelectedPlot::Power, - SelectedPlot::Runtime, - SelectedPlot::Signal, - SelectedPlot::Map - ] { - ui.selectable_value(&mut self.dropdown_selected_plot, p, format!("{}", p)); - } - }); - }); - - match self.dropdown_selected_plot { - SelectedPlot::Orientation => self.plot_orientation(ui, data_source), - SelectedPlot::VerticalSpeed => ui.plot_telemetry(&self.vertical_speed_plot, data_source), - SelectedPlot::Altitude => ui.plot_telemetry(&self.altitude_plot, data_source), - SelectedPlot::Gyroscope => ui.plot_telemetry(&self.gyroscope_plot, data_source), - SelectedPlot::Accelerometers => ui.plot_telemetry(&self.accelerometer_plot, data_source), - SelectedPlot::Magnetometer => ui.plot_telemetry(&self.magnetometer_plot, data_source), - SelectedPlot::Pressures => ui.plot_telemetry(&self.barometer_plot, data_source), - SelectedPlot::Temperatures => ui.plot_telemetry(&self.temperature_plot, data_source), - SelectedPlot::Power => ui.plot_telemetry(&self.power_plot, data_source), - SelectedPlot::Runtime => ui.plot_telemetry(&self.runtime_plot, data_source), - SelectedPlot::Signal => ui.plot_telemetry(&self.signal_plot, data_source), - SelectedPlot::Map => { ui.add(Map::new(&mut self.map, data_source)); }, - } - }); - } + CentralPanel::default().show(ctx, |ui| { + ui.set_enabled(enabled); + + let mut behavior = TileBehavior { + data_source, + orientation_plot: &mut self.orientation_plot, + vertical_speed_plot: &mut self.vertical_speed_plot, + altitude_plot: &mut self.altitude_plot, + gyroscope_plot: &mut self.gyroscope_plot, + accelerometer_plot: &mut self.accelerometer_plot, + magnetometer_plot: &mut self.magnetometer_plot, + barometer_plot: &mut self.barometer_plot, + temperature_plot: &mut self.temperature_plot, + power_plot: &mut self.power_plot, + runtime_plot: &mut self.runtime_plot, + signal_plot: &mut self.signal_plot, + map: &mut self.map, + }; + self.tile_tree.ui(&mut behavior, ui); + }); } pub fn bottom_bar_ui(&mut self, ui: &mut egui::Ui, _data_source: &mut dyn DataSource) { - ui.toggle_button(&mut self.shared_plot.borrow_mut().show_stats, "๐Ÿ“ˆ Show Stats", "๐Ÿ“‰ Hide Stats"); + ui.toggle_value(&mut self.show_view_settings, "โš™ View Settings"); } } diff --git a/src/settings.rs b/src/settings.rs index 92c8bff..2bbc638 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -1,4 +1,5 @@ use std::fs::File; +use std::collections::HashMap; use serde::{Deserialize, Serialize}; @@ -8,6 +9,7 @@ use mithril::settings::LoRaSettings; pub struct AppSettings { pub mapbox_access_token: String, pub lora: LoRaSettings, + pub tile_presets: Option>>, } impl AppSettings {