diff --git a/Cargo.lock b/Cargo.lock index 315eaa6e6c6b9903edf25c6cb9a2c0a3630a206c..a6a4c77effac769fbbc29724e165f0eca851eeb6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -480,6 +480,12 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + [[package]] name = "bit-set" version = "0.6.0" @@ -506,6 +512,9 @@ name = "bitflags" version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +dependencies = [ + "serde", +] [[package]] name = "block" @@ -907,6 +916,7 @@ dependencies = [ "glow 0.14.2", "glutin", "glutin-winit", + "home", "image", "js-sys", "log", @@ -916,6 +926,8 @@ dependencies = [ "parking_lot", "percent-encoding", "raw-window-handle", + "ron", + "serde", "static_assertions", "wasm-bindgen", "wasm-bindgen-futures", @@ -938,6 +950,7 @@ dependencies = [ "epaint", "log", "nohash-hasher", + "ron", "serde", ] @@ -972,12 +985,35 @@ dependencies = [ "egui", "log", "raw-window-handle", + "serde", "smithay-clipboard", "web-time", "webbrowser", "winit", ] +[[package]] +name = "egui_extras" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf3c1f5cd8dfe2ade470a218696c66cf556fcfd701e7830fa2e9f4428292a2a1" +dependencies = [ + "ahash", + "egui", + "enum-map", + "log", + "mime_guess2", +] + +[[package]] +name = "egui_file" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31e8280f9aea0f814013815071aebcf5c341e1d5420b381d3b0c1e3c42604d2" +dependencies = [ + "egui", +] + [[package]] name = "egui_glow" version = "0.29.1" @@ -1041,6 +1077,27 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf" +[[package]] +name = "enum-map" +version = "2.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6866f3bfdf8207509a033af1a75a7b08abda06bbaaeae6669323fd5a097df2e9" +dependencies = [ + "enum-map-derive", + "serde", +] + +[[package]] +name = "enum-map-derive" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f282cfdfe92516eb26c2af8589c274c7c17681f5ecc03c18255fe741c6aa64eb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + [[package]] name = "enum_dispatch" version = "0.3.13" @@ -1906,6 +1963,22 @@ dependencies = [ "paste", ] +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess2" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25a3333bb1609500601edc766a39b4c1772874a4ce26022f4d866854dc020c41" +dependencies = [ + "mime", + "unicase", +] + [[package]] name = "miniz_oxide" version = "0.8.0" @@ -2545,6 +2618,18 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" +[[package]] +name = "ron" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94" +dependencies = [ + "base64", + "bitflags 2.6.0", + "serde", + "serde_derive", +] + [[package]] name = "rustc-hash" version = "1.1.0" @@ -2610,6 +2695,8 @@ version = "0.1.0" dependencies = [ "eframe", "egui", + "egui_extras", + "egui_file", "egui_plot", "egui_tiles", "enum_dispatch", @@ -2980,6 +3067,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "unicase" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e51b68083f157f853b6379db119d1c1be0e6e4dec98101079dec41f6f5cf6df" + [[package]] name = "unicode-ident" version = "1.0.13" diff --git a/Cargo.toml b/Cargo.toml index 6a26a05776fdee570e1ffe58d58df61c6d1dc6cb..81b176910971b7c5dc22c8bbf27c9baa6cc8a199 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,9 +10,10 @@ license = "MIT" [dependencies] # ======= GUI & Rendering ======= egui_tiles = "0.10" -eframe = "0.29" +eframe = { version = "0.29", features = ["persistence"] } egui = { version = "0.29", features = ["log"] } egui_plot = "0.29" +egui_file = "0.19" # ========= Persistency ========= serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" @@ -22,3 +23,4 @@ log = "0.4" # =========== Utility =========== # for dynamic dispatch enum_dispatch = "0.3" +egui_extras = "0.29.1" diff --git a/src/main.rs b/src/main.rs index 64e7c45fc68288774b9c7da110acf79273a06718..c988cd73ecab35c754722869bba9d6b72dc8df98 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,6 +2,8 @@ use ui::ComposableView; mod ui; +static APP_NAME: &str = "segs"; + fn main() -> Result<(), eframe::Error> { // set up logging (USE RUST_LOG=debug to see logs) env_logger::init(); @@ -20,8 +22,14 @@ fn main() -> Result<(), eframe::Error> { // CreationContext constains information useful to initilize our app, like storage. // Storage allows to store custom data in a way that persist whan you restart the app. eframe::run_native( - "segs", // This is the app id, used for example by Wayland + APP_NAME, // This is the app id, used for example by Wayland native_options, - Box::new(|_| Ok(Box::<ComposableView>::default())), + Box::new(|ctx| { + let app = ctx + .storage + .map(|storage| ComposableView::new(APP_NAME, storage)) + .unwrap_or_default(); + Ok(Box::new(app)) + }), ) } diff --git a/src/ui.rs b/src/ui.rs index ff73104d0525856ecd195db04506e3906352ee23..ea8dbb2fd3ac5a4c9c74de336cf88039d953c413 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -1,4 +1,5 @@ mod composable_view; +mod layout_manager; mod panes; mod shortcuts; diff --git a/src/ui/composable_view.rs b/src/ui/composable_view.rs index db58813273f2e34aa1368fe12487da795d7a0a3c..b2a0c5d8ce5f398c43e0c27081f9e71d063ba49b 100644 --- a/src/ui/composable_view.rs +++ b/src/ui/composable_view.rs @@ -1,37 +1,34 @@ use super::{ + layout_manager::LayoutManager, panes::{Pane, PaneBehavior}, shortcuts, }; +use std::{ + fs, + path::{Path, PathBuf}, +}; use egui::{Key, Modifiers}; use egui_tiles::{Behavior, Container, Linear, LinearDir, Tile, TileId, Tiles, Tree}; +use serde::{Deserialize, Serialize}; +#[derive(Default)] pub struct ComposableView { - panes_tree: Tree<Pane>, - behavior: ComposableBehavior, -} - -// Implementing the default trait allows us to define a default configuration for our app -impl Default for ComposableView { - fn default() -> Self { - let mut tiles = Tiles::default(); - let root = tiles.insert_pane(Pane::default()); - let panes_tree = egui_tiles::Tree::new("my_tree", root, tiles); + /// Persistent state of the app + pub state: ComposableViewState, - Self { - panes_tree, - behavior: Default::default(), - } - } + pub layout_manager: LayoutManager, + behavior: ComposableBehavior, } // An app must implement the `App` trait to define how the ui is built impl eframe::App for ComposableView { // The update function is called each time the UI needs repainting! fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { - // get the id of the hovered pane, in order to apply actions to it - let hovered_pane = self - .panes_tree + let panes_tree = &mut self.state.panes_tree; + + // Get the id of the hovered pane, in order to apply actions to it + let hovered_pane = panes_tree .tiles .iter() .find(|(_, tile)| matches!(tile, Tile::Pane(pane) if pane.contains_pointer())) @@ -56,10 +53,10 @@ impl eframe::App for ComposableView { if let Some((action, hovered_tile)) = pane_action.take() { match action { PaneAction::SplitH => { - let hovered_tile_pane = self.panes_tree.tiles.remove(hovered_tile).unwrap(); - let left_pane = self.panes_tree.tiles.insert_new(hovered_tile_pane); - let right_pane = self.panes_tree.tiles.insert_pane(Pane::default()); - self.panes_tree.tiles.insert( + let hovered_tile_pane = panes_tree.tiles.remove(hovered_tile).unwrap(); + let left_pane = panes_tree.tiles.insert_new(hovered_tile_pane); + let right_pane = panes_tree.tiles.insert_pane(Pane::default()); + panes_tree.tiles.insert( hovered_tile, Tile::Container(Container::Linear(Linear::new_binary( LinearDir::Horizontal, @@ -69,10 +66,10 @@ impl eframe::App for ComposableView { ); } PaneAction::SplitV => { - let hovered_tile_pane = self.panes_tree.tiles.remove(hovered_tile).unwrap(); - let replaced = self.panes_tree.tiles.insert_new(hovered_tile_pane); - let lower_pane = self.panes_tree.tiles.insert_pane(Pane::default()); - self.panes_tree.tiles.insert( + let hovered_tile_pane = panes_tree.tiles.remove(hovered_tile).unwrap(); + let replaced = panes_tree.tiles.insert_new(hovered_tile_pane); + let lower_pane = panes_tree.tiles.insert_pane(Pane::default()); + panes_tree.tiles.insert( hovered_tile, Tile::Container(Container::Linear(Linear::new_binary( LinearDir::Vertical, @@ -83,27 +80,109 @@ impl eframe::App for ComposableView { } PaneAction::Close => { // Ignore if the root pane is the only one - if self.panes_tree.tiles.len() != 1 { - self.panes_tree.remove_recursively(hovered_tile); + if panes_tree.tiles.len() != 1 { + panes_tree.remove_recursively(hovered_tile); } } PaneAction::Replace(new_pane) => { - self.panes_tree - .tiles - .insert(hovered_tile, Tile::Pane(*new_pane)); + panes_tree.tiles.insert(hovered_tile, Tile::Pane(*new_pane)); } } } // Show a panel at the bottom of the screen with few global controls egui::TopBottomPanel::bottom("bottom_control").show(ctx, |ui| { - egui::global_theme_preference_switch(ui); + ui.horizontal(|ui| { + egui::global_theme_preference_switch(ui); + + if ui.button("Layout Manager").clicked() { + self.layout_manager.toggle_open_state(); + } + }) }); // A central panel covers the remainder of the screen, i.e. whatever area is left after adding other panels. egui::CentralPanel::default().show(ctx, |ui| { - self.panes_tree.ui(&mut self.behavior, ui); + panes_tree.ui(&mut self.behavior, ui); }); + + LayoutManager::show(self, ctx); + } + + fn save(&mut self, storage: &mut dyn eframe::Storage) { + self.layout_manager.save_displayed(storage); + } +} + +impl ComposableView { + pub fn new(app_name: &str, storage: &dyn eframe::Storage) -> Self { + let layout_manager = LayoutManager::new(app_name, storage); + let mut composable_view = Self { + layout_manager, + ..Self::default() + }; + LayoutManager::try_display_selected_layout(&mut composable_view); + composable_view + } +} + +#[derive(Serialize, Deserialize, Clone, PartialEq)] +pub struct ComposableViewState { + pub panes_tree: Tree<Pane>, +} + +impl Default for ComposableViewState { + fn default() -> Self { + let mut tiles = Tiles::default(); + let root = tiles.insert_pane(Pane::default()); + let panes_tree = egui_tiles::Tree::new("main_tree", root, tiles); + + Self { panes_tree } + } +} + +impl ComposableViewState { + pub fn from_file(path: &PathBuf) -> Option<Self> { + match fs::read_to_string(path) { + Ok(json) => match serde_json::from_str::<ComposableViewState>(&json) { + Ok(layout) => Some(layout), + Err(e) => { + eprintln!("Error deserializing layout: {}", e); + None + } + }, + Err(e) => { + eprintln!("Error reading file: {}", e); + None + } + } + } + + pub fn to_file(&self, path: &Path) { + // Check if the parent path exists, if not create it + if let Some(parent) = path.parent() { + if !parent.exists() { + match fs::create_dir_all(parent) { + Ok(_) => { + println!("Created directory {:?}", parent); + } + Err(e) => { + eprintln!("Error creating directory: {}", e); + return; + } + } + } + } + + match serde_json::to_string_pretty(self) { + Ok(serialized_layout) => { + println!("Saving layout into {:?}", path); + fs::write(path, serialized_layout).unwrap(); + } + Err(e) => { + eprintln!("Error serializing layout: {}", e); + } + } } } diff --git a/src/ui/layout_manager.rs b/src/ui/layout_manager.rs new file mode 100644 index 0000000000000000000000000000000000000000..526505f8b600e7607b3b20d4627365746d4727b0 --- /dev/null +++ b/src/ui/layout_manager.rs @@ -0,0 +1,307 @@ +use std::{collections::BTreeMap, fs, path::PathBuf, str::FromStr}; + +use egui::{ + Button, Color32, Context, InnerResponse, RichText, Separator, Stroke, TextEdit, Ui, Vec2, + Widget, +}; +use egui_extras::{Column, Size, StripBuilder, TableBuilder}; +use egui_file::FileDialog; + +use super::{composable_view::ComposableViewState, ComposableView}; + +static LAYOUTS_DIR: &str = "layouts"; +static SELECTED_LAYOUT_KEY: &str = "selected_layout"; + +#[derive(Default)] +pub struct LayoutManager { + open: bool, + + /// Currently dislayed layout in the ui + displayed: Option<PathBuf>, + + /// Currently selected layout in the list, gets reset to the displayed layout when the dialog is opened + selection: Option<PathBuf>, + + text_input: String, + file_dialog: Option<FileDialog>, + layouts: BTreeMap<PathBuf, ComposableViewState>, + layouts_path: PathBuf, +} + +impl LayoutManager { + /// Chooses the layouts path and gets the previously selected layout from storage + pub fn new(app_name: &str, storage: &dyn eframe::Storage) -> Self { + let mut layout_manager = Self { + layouts_path: eframe::storage_dir(app_name).unwrap().join(LAYOUTS_DIR), + selection: storage + .get_string(SELECTED_LAYOUT_KEY) + .map(|path| PathBuf::from_str(path.as_str()).unwrap()), + ..Self::default() + }; + layout_manager.reload_layouts(); + layout_manager + } + + /// Saves in permanent storage the file name of the currently displayed layout + pub fn save_displayed(&self, storage: &mut dyn eframe::Storage) { + if let Some(displayed) = self.displayed.as_ref().map(|s| s.to_str()).flatten() { + storage.set_string(SELECTED_LAYOUT_KEY, displayed.to_string()); + println!("Layout \"{}\" will be displayed next time", displayed) + } + } + + /// Scans the layout directory and reloads the layouts + pub fn reload_layouts(&mut self) { + if let Ok(files) = self.layouts_path.read_dir() { + self.layouts = files + .flat_map(|x| x) + .map(|path| path.path()) + .map(|path| { + if let Some(layout) = ComposableViewState::from_file(&path) { + let path: PathBuf = path.file_stem().unwrap().into(); + Some((path, layout)) + } else { + None + } + }) + .flatten() + .collect(); + } + } + + pub fn get_selected(&self) -> Option<&ComposableViewState> { + self.selection + .as_ref() + .map(|selection| self.layouts.get(selection)) + .flatten() + } + + pub fn delete(&mut self, key: &PathBuf) { + if self.layouts.contains_key(key) { + let _ = fs::remove_file(self.layouts_path.join(key).with_extension("json")); + } + } + + pub fn try_display_selected_layout(cv: &mut ComposableView) { + if let Some(selection) = cv.layout_manager.selection.as_ref() { + if let Some(selected_layout) = cv.layout_manager.layouts.get(selection) { + cv.state = selected_layout.clone(); + cv.layout_manager.displayed = Some(selection.clone()); + } + } + } + + pub fn save_current_layout(cv: &mut ComposableView, name: &String) { + let layouts_path = &cv.layout_manager.layouts_path; + let path = layouts_path.join(name).with_extension("json"); + cv.state.to_file(&path); + cv.layout_manager.selection.replace(name.into()); + cv.layout_manager.displayed.replace(name.into()); + } + + pub fn toggle_open_state(&mut self) { + self.open = !self.open; + + if self.open { + // When opening, we set the selection to the current layout + self.selection = self.displayed.clone(); + } else { + // When closing, we delete also the file dialog + self.file_dialog.take(); + } + } + + pub fn show(cv: &mut ComposableView, ctx: &Context) { + let mut window_visible = cv.layout_manager.open; + egui::Window::new("Layouts Manager") + .collapsible(false) + .open(&mut window_visible) + .show(ctx, |ui| { + // Make sure to reload the layots, this ways the user sees always + // the current content of the layouts folder + cv.layout_manager.reload_layouts(); + + let changed = match cv.layout_manager.get_selected() { + Some(selected_layout) => *selected_layout != cv.state, + None => true, + }; + + // Layouts table + StripBuilder::new(ui) + .size(Size::remainder().at_least(100.0)) + .size(Size::exact(7.0)) + .size(Size::exact(40.0)) + .vertical(|mut strip| { + strip.cell(|ui| LayoutManager::show_layouts_table(ui, cv, changed)); + strip.cell(|ui| { + ui.add(Separator::default().spacing(7.0)); + }); + strip.strip(|builder| { + LayoutManager::show_action_buttons(builder, cv, changed) + }); + }); + }); + cv.layout_manager.open = window_visible; + } + + fn show_layouts_table(ui: &mut Ui, cv: &mut ComposableView, changed: bool) { + let available_height = ui.available_height(); + TableBuilder::new(ui) + .column(Column::remainder()) + .column(Column::auto()) + .column(Column::auto()) + .min_scrolled_height(0.0) + .max_scroll_height(available_height) + .body(|mut body| { + let mut to_select: Option<PathBuf> = None; + let mut to_open: Option<PathBuf> = None; + let mut to_delete: Option<PathBuf> = None; + + for key in cv.layout_manager.layouts.keys() { + let name = key.to_str().unwrap(); + let is_selected = cv + .layout_manager + .selection + .as_ref() + .map_or_else(|| false, |selected_key| selected_key == key); + + let name_button = if is_selected && changed { + Button::new(RichText::new(name).color(Color32::BLACK)) + .stroke(Stroke::new(1.0, Color32::BROWN)) + .fill(Color32::YELLOW) + } else if is_selected && !changed { + Button::new(RichText::new(name).color(Color32::BLACK)) + .stroke(Stroke::new(1.0, Color32::GREEN)) + .fill(Color32::LIGHT_GREEN) + } else { + Button::new(name).fill(Color32::TRANSPARENT) + }; + let open_button = Button::new("↗"); + let delete_button = Button::new("🗑"); + + body.row(20.0, |mut row| { + row.col(|ui| { + let name_button_resp = name_button.ui(ui); + if name_button_resp.clicked() { + to_select = Some(key.clone()); + } + if name_button_resp.double_clicked() { + to_open = Some(key.clone()); + } + }); + row.col(|ui| { + if open_button.ui(ui).clicked() { + to_open = Some(key.clone()); + } + }); + row.col(|ui| { + if delete_button.ui(ui).clicked() { + to_delete = Some(key.clone()); + } + }); + }); + } + + if let Some(to_select) = to_select { + cv.layout_manager.selection.replace(to_select); + } + if let Some(to_open) = to_open { + cv.layout_manager.selection.replace(to_open); + LayoutManager::try_display_selected_layout(cv); + } + if let Some(to_delete) = to_delete { + cv.layout_manager.delete(&to_delete); + } + }); + } + + fn show_action_buttons(builder: StripBuilder, cv: &mut ComposableView, changed: bool) { + builder.sizes(Size::remainder(), 2).horizontal(|mut strip| { + // Load empty and import buttons + strip.cell(|ui| { + // Load empty button + let open_empty_resp = ui.add_sized( + Vec2::new(ui.available_width(), 0.0), + Button::new("Load empty"), + ); + if open_empty_resp.clicked() { + cv.state = ComposableViewState::default(); + cv.layout_manager.selection.take(); + } + + // Import button + let import_layout_resp = + ui.add_sized(Vec2::new(ui.available_width(), 0.0), Button::new("Import")); + if import_layout_resp.clicked() { + let mut file_dialog = FileDialog::open_file(None); + file_dialog.open(); + cv.layout_manager.file_dialog = Some(file_dialog); + } + if let Some(file_dialog) = &mut cv.layout_manager.file_dialog { + if file_dialog.show(ui.ctx()).selected() { + if let Some(file) = file_dialog.path() { + println!("Selected layout to import: {:?}", file); + + let file_name = file.file_name().unwrap(); + let destination = cv.layout_manager.layouts_path.join(file_name); + + // First check if the layouts folder exists + if !cv.layout_manager.layouts_path.exists() { + match fs::create_dir_all(&cv.layout_manager.layouts_path) { + Ok(_) => println!("Created layouts folder"), + Err(e) => { + println!("Error creating layouts folder: {:?}", e) + } + } + } + + match fs::copy(file, destination.clone()) { + Ok(_) => { + println!( + "Layout imported in {}", + destination.to_str().unwrap() + ); + cv.layout_manager.selection.replace(file_name.into()); + cv.layout_manager.reload_layouts(); + LayoutManager::try_display_selected_layout(cv); + } + Err(e) => println!("Error importing layout: {:?}", e), + } + } + } + } + }); + // Layout save ui + strip.cell(|ui| { + let InnerResponse { inner: to_save, .. } = ui.add_enabled_ui(changed, |ui| { + // Text edit + let text_edit_resp = ui.add_sized( + Vec2::new(ui.available_width(), 0.0), + TextEdit::singleline(&mut cv.layout_manager.text_input), + ); + + // Save button + let InnerResponse { + inner: save_button_resp, + .. + } = ui.add_enabled_ui(!cv.layout_manager.text_input.is_empty(), |ui| { + ui.add_sized( + Vec2::new(ui.available_width(), 0.0), + Button::new("Save layout"), + ) + }); + + let to_save = text_edit_resp.lost_focus() + && ui.input(|i| i.key_pressed(egui::Key::Enter)); + let to_save = to_save || save_button_resp.clicked(); + to_save + }); + + if to_save { + let name = cv.layout_manager.text_input.clone(); + LayoutManager::save_current_layout(cv, &name); + } + }); + }); + } +} diff --git a/src/ui/panes.rs b/src/ui/panes.rs index 9aebb54415524b2339a11c5abafdb58cc802b5b4..50c4f9007cbb314cceb2d684fc068feaf6714e52 100644 --- a/src/ui/panes.rs +++ b/src/ui/panes.rs @@ -7,7 +7,7 @@ use serde::{Deserialize, Serialize}; use super::composable_view::PaneResponse; -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct Pane { pub pane: PaneKind, } @@ -43,7 +43,7 @@ impl PaneBehavior for Pane { } // An enum to represent the diffent kinds of widget available to the user. -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[enum_dispatch] pub enum PaneKind { Default(default::DefaultPane), diff --git a/src/ui/panes/default.rs b/src/ui/panes/default.rs index 780dfa7df8975e87d5671aac368e798167643108..2d1b51aeed68fff6033806ca74eed0e3106705e5 100644 --- a/src/ui/panes/default.rs +++ b/src/ui/panes/default.rs @@ -7,6 +7,8 @@ use crate::ui::composable_view::{PaneAction, PaneResponse}; pub struct DefaultPane { occupied: f32, fixed: bool, + + #[serde(skip)] contains_pointer: bool, } @@ -20,6 +22,12 @@ impl Default for DefaultPane { } } +impl PartialEq for DefaultPane { + fn eq(&self, other: &Self) -> bool { + self.occupied == other.occupied && self.fixed == other.fixed + } +} + impl PaneBehavior for DefaultPane { fn ui(&mut self, ui: &mut egui::Ui) -> PaneResponse { let mut response = PaneResponse::default(); diff --git a/src/ui/panes/messages_viewer.rs b/src/ui/panes/messages_viewer.rs index cca280353f2e9b6b870d2297eeb59a5185fe178c..af279bc80da8100c4a044116fe6477b3ed2ef9d9 100644 --- a/src/ui/panes/messages_viewer.rs +++ b/src/ui/panes/messages_viewer.rs @@ -11,6 +11,12 @@ pub struct MessagesViewerPane { contains_pointer: bool, } +impl PartialEq for MessagesViewerPane { + fn eq(&self, _other: &Self) -> bool { + true + } +} + impl PaneBehavior for MessagesViewerPane { fn ui(&mut self, ui: &mut egui::Ui) -> PaneResponse { let mut response = PaneResponse::default(); diff --git a/src/ui/panes/plot_2d.rs b/src/ui/panes/plot_2d.rs index a3ad8079da0f8a0c36444ed70ade6d5b06e4e680..6ac065822b6fdf0711e8cac9f229f2f8ea87d196 100644 --- a/src/ui/panes/plot_2d.rs +++ b/src/ui/panes/plot_2d.rs @@ -7,14 +7,16 @@ use serde::{Deserialize, Serialize}; #[derive(Clone, Debug, Serialize, Deserialize)] pub struct Plot2DPane { - #[serde(skip)] - pub contains_pointer: bool, - settings_visible: bool, n_points: u32, frequency: f64, width: f32, color: egui::Color32, - open: bool, + + #[serde(skip)] + pub contains_pointer: bool, + + #[serde(skip)] + settings_visible: bool, } impl Default for Plot2DPane { @@ -26,11 +28,19 @@ impl Default for Plot2DPane { frequency: 1.0, width: 1.0, color: egui::Color32::from_rgb(0, 120, 240), - open: false, } } } +impl PartialEq for Plot2DPane { + fn eq(&self, other: &Self) -> bool { + self.n_points == other.n_points + && self.frequency == other.frequency + && self.width == other.width + && self.color == other.color + } +} + impl PaneBehavior for Plot2DPane { fn ui(&mut self, ui: &mut egui::Ui) -> PaneResponse { let mut response = PaneResponse::default();