From 24ad840ecb762429f2d2287cae4d03ac7b9b0f4a Mon Sep 17 00:00:00 2001 From: Alberto Nidasio <alberto.nidasio@skywarder.eu> Date: Sat, 16 Nov 2024 14:10:48 +0100 Subject: [PATCH 01/16] Implemented basic layout persistance --- src/main.rs | 5 ++++- src/ui/composable_view.rs | 42 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/src/main.rs b/src/main.rs index 64e7c45..caebba7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -22,6 +22,9 @@ fn main() -> Result<(), eframe::Error> { eframe::run_native( "segs", // This is the app id, used for example by Wayland native_options, - Box::new(|_| Ok(Box::<ComposableView>::default())), + Box::new(|_| { + let app = ComposableView::from_file("layout.json"); + Ok(Box::new(app)) + }), ) } diff --git a/src/ui/composable_view.rs b/src/ui/composable_view.rs index db58813..79f5bb6 100644 --- a/src/ui/composable_view.rs +++ b/src/ui/composable_view.rs @@ -1,3 +1,5 @@ +use std::fs; + use super::{ panes::{Pane, PaneBehavior}, shortcuts, @@ -5,9 +7,13 @@ use super::{ use egui::{Key, Modifiers}; use egui_tiles::{Behavior, Container, Linear, LinearDir, Tile, TileId, Tiles, Tree}; +use serde::{Deserialize, Serialize}; +#[derive(Serialize, Deserialize)] pub struct ComposableView { panes_tree: Tree<Pane>, + + #[serde(skip)] behavior: ComposableBehavior, } @@ -25,6 +31,27 @@ impl Default for ComposableView { } } +impl ComposableView { + pub fn from_file(file_name: &str) -> Self { + match fs::read_to_string(file_name) { + Ok(serialized_layout) => match serde_json::from_str(&serialized_layout) { + Ok(layout) => { + println!("Loaded layout from file"); + layout + } + Err(e) => { + println!("Error deserializing layout: {}", e); + Self::default() + } + }, + Err(e) => { + println!("Error reading layout file: {}", e); + Self::default() + } + } + } +} + // 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! @@ -97,7 +124,20 @@ impl eframe::App for ComposableView { // 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("Save layout").clicked() { + match serde_json::to_string_pretty(self) { + Ok(serialized_layout) => { + fs::write("layout.json", serialized_layout).unwrap(); + } + Err(e) => { + eprintln!("Error serializing layout: {}", e); + } + } + } + }) }); // A central panel covers the remainder of the screen, i.e. whatever area is left after adding other panels. -- GitLab From c6d5aa79b0b07c7e9f118ab38742e858569c494d Mon Sep 17 00:00:00 2001 From: Alberto Nidasio <alberto.nidasio@skywarder.eu> Date: Thu, 21 Nov 2024 20:48:29 +0100 Subject: [PATCH 02/16] Layout manager can now load layouts from disk --- Cargo.lock | 103 ++++++++++++++++++++ Cargo.toml | 3 +- src/main.rs | 3 +- src/ui.rs | 1 + src/ui/composable_view.rs | 135 ++++++++++++-------------- src/ui/layout_manager.rs | 165 ++++++++++++++++++++++++++++++++ src/ui/panes.rs | 4 +- src/ui/panes/default.rs | 8 ++ src/ui/panes/messages_viewer.rs | 6 ++ src/ui/panes/plot_2d.rs | 20 +++- 10 files changed, 364 insertions(+), 84 deletions(-) create mode 100644 src/ui/layout_manager.rs diff --git a/Cargo.lock b/Cargo.lock index 315eaa6..4d50cf0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -112,6 +112,15 @@ dependencies = [ "winit", ] +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + [[package]] name = "adler2" version = "2.0.0" @@ -480,6 +489,27 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +[[package]] +name = "backtrace" +version = "0.3.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets 0.52.6", +] + +[[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 +536,9 @@ name = "bitflags" version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +dependencies = [ + "serde", +] [[package]] name = "block" @@ -938,6 +971,7 @@ dependencies = [ "epaint", "log", "nohash-hasher", + "ron", "serde", ] @@ -1332,6 +1366,12 @@ dependencies = [ "wasi", ] +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + [[package]] name = "gl_generator" version = "0.14.0" @@ -2237,6 +2277,15 @@ dependencies = [ "objc2-foundation", ] +[[package]] +name = "object" +version = "0.36.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" +dependencies = [ + "memchr", +] + [[package]] name = "once_cell" version = "1.20.2" @@ -2545,6 +2594,24 @@ 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-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + [[package]] name = "rustc-hash" version = "1.1.0" @@ -2617,6 +2684,7 @@ dependencies = [ "log", "serde", "serde_json", + "sha256", ] [[package]] @@ -2673,6 +2741,30 @@ dependencies = [ "digest", ] +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha256" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18278f6a914fa3070aa316493f7d2ddfb9ac86ebc06fa3b83bffda487e9065b0" +dependencies = [ + "async-trait", + "bytes", + "hex", + "sha2", + "tokio", +] + [[package]] name = "shlex" version = "1.3.0" @@ -2900,6 +2992,17 @@ dependencies = [ "zerovec", ] +[[package]] +name = "tokio" +version = "1.41.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cfb5bee7a6a52939ca9224d6ac897bb669134078daa8735560897f69de4d33" +dependencies = [ + "backtrace", + "bytes", + "pin-project-lite", +] + [[package]] name = "toml_datetime" version = "0.6.8" diff --git a/Cargo.toml b/Cargo.toml index 6a26a05..e2e26d4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,7 @@ license = "MIT" # ======= GUI & Rendering ======= egui_tiles = "0.10" eframe = "0.29" -egui = { version = "0.29", features = ["log"] } +egui = { version = "0.29", features = ["log", "persistence"] } egui_plot = "0.29" # ========= Persistency ========= serde = { version = "1.0", features = ["derive"] } @@ -22,3 +22,4 @@ log = "0.4" # =========== Utility =========== # for dynamic dispatch enum_dispatch = "0.3" +sha256 = "1.5.0" diff --git a/src/main.rs b/src/main.rs index caebba7..c9367e8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -23,7 +23,8 @@ fn main() -> Result<(), eframe::Error> { "segs", // This is the app id, used for example by Wayland native_options, Box::new(|_| { - let app = ComposableView::from_file("layout.json"); + // let app = ComposableView::from_file("layout.json"); + let app = ComposableView::default(); Ok(Box::new(app)) }), ) diff --git a/src/ui.rs b/src/ui.rs index ff73104..ea8dbb2 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 79f5bb6..04a5bef 100644 --- a/src/ui/composable_view.rs +++ b/src/ui/composable_view.rs @@ -1,64 +1,34 @@ -use std::fs; +use std::path::PathBuf; use super::{ + layout_manager::{layout_manager_ui, load_layouts, ComposableViewLayout}, panes::{Pane, PaneBehavior}, shortcuts, }; -use egui::{Key, Modifiers}; +use egui::{ahash::HashMap, Key, Modifiers}; use egui_tiles::{Behavior, Container, Linear, LinearDir, Tile, TileId, Tiles, Tree}; use serde::{Deserialize, Serialize}; -#[derive(Serialize, Deserialize)] +#[derive(Default)] pub struct ComposableView { - panes_tree: Tree<Pane>, + /// Persistent state of the app + pub state: ComposableViewState, - #[serde(skip)] 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); - - Self { - panes_tree, - behavior: Default::default(), - } - } -} - -impl ComposableView { - pub fn from_file(file_name: &str) -> Self { - match fs::read_to_string(file_name) { - Ok(serialized_layout) => match serde_json::from_str(&serialized_layout) { - Ok(layout) => { - println!("Loaded layout from file"); - layout - } - Err(e) => { - println!("Error deserializing layout: {}", e); - Self::default() - } - }, - Err(e) => { - println!("Error reading layout file: {}", e); - Self::default() - } - } - } + pub layout_manager_open: bool, + pub layout_manager_selection: Option<PathBuf>, + pub layouts: HashMap<PathBuf, ComposableViewLayout>, } // 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) { + 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 = self - .panes_tree + let hovered_pane = panes_tree .tiles .iter() .find(|(_, tile)| matches!(tile, Tile::Pane(pane) if pane.contains_pointer())) @@ -83,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, @@ -96,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, @@ -110,40 +80,55 @@ 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)); } } } + // 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| { + panes_tree.ui(&mut self.behavior, ui); + }); + // Show a panel at the bottom of the screen with few global controls egui::TopBottomPanel::bottom("bottom_control").show(ctx, |ui| { ui.horizontal(|ui| { egui::global_theme_preference_switch(ui); - if ui.button("Save layout").clicked() { - match serde_json::to_string_pretty(self) { - Ok(serialized_layout) => { - fs::write("layout.json", serialized_layout).unwrap(); - } - Err(e) => { - eprintln!("Error serializing layout: {}", e); - } - } + if ui.button("Layout Manager").clicked() { + self.layout_manager_open = !self.layout_manager_open; + load_layouts(&mut self.layouts); } }) }); - // 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); - }); + let mut window_visible = self.layout_manager_open; + egui::Window::new("Layouts Manager") + .collapsible(false) + // .resizable(false) + .open(&mut window_visible) + .show(ctx, |ui| layout_manager_ui(ui, self)); + self.layout_manager_open = window_visible; + } +} + +#[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 } } } @@ -176,6 +161,14 @@ impl Behavior<Pane> for ComposableBehavior { } } +#[derive(Clone, Debug)] +pub enum PaneAction { + SplitH, + SplitV, + Close, + Replace(Box<Pane>), +} + #[derive(Clone, Debug)] pub struct PaneResponse { pub action_called: Option<PaneAction>, @@ -200,11 +193,3 @@ impl Default for PaneResponse { } } } - -#[derive(Clone, Debug)] -pub enum PaneAction { - SplitH, - SplitV, - Close, - Replace(Box<Pane>), -} diff --git a/src/ui/layout_manager.rs b/src/ui/layout_manager.rs new file mode 100644 index 0000000..6b330e7 --- /dev/null +++ b/src/ui/layout_manager.rs @@ -0,0 +1,165 @@ +use std::{fs, path::PathBuf}; + +use egui::{ + ahash::{HashMap, HashSet}, + Button, Color32, ScrollArea, Stroke, Vec2, Vec2b, Widget, +}; +use serde::{Deserialize, Serialize}; + +use super::{composable_view::ComposableViewState, ComposableView}; + +pub fn layout_manager_ui(ui: &mut egui::Ui, state: &mut ComposableView) { + let layout_changed = + state + .layout_manager_selection + .clone() + .map_or(false, |layout_manager_selection| { + let selected_layout = state.layouts.get(&layout_manager_selection).unwrap(); + let current_layout = selected_layout.swap_state(&state.state); + + *selected_layout != current_layout + }); + + let non_flex_id = egui::Id::new("non_flex"); + let available_height = ui.available_height(); + let non_flex_height = ui.data(|data| data.get_temp(non_flex_id).unwrap_or(available_height)); + let flex_height = (available_height - non_flex_height).max(100.0); + + ui.set_min_size(Vec2::new(400.0, 200.0)); + ui.horizontal(|ui| { + ui.vertical(|ui| { + ui.vertical(|ui| { + ui.set_height(flex_height); + ui.set_width(150.0); + ScrollArea::vertical() + .auto_shrink(Vec2b::FALSE) + .show(ui, |ui| { + for (key, _) in state.layouts.iter() { + let mut button = + Button::new(key.to_str().unwrap()).fill(Color32::TRANSPARENT); + + if state + .layout_manager_selection + .as_ref() + .map_or_else(|| false, |selected_key| selected_key == key) + { + if layout_changed { + button = button + .stroke(Stroke::new(1.0, Color32::BROWN)) + .fill(Color32::YELLOW) + } else { + button = button + .stroke(Stroke::new(1.0, Color32::GREEN)) + .fill(Color32::LIGHT_GREEN); + } + } + + if button.ui(ui).clicked() { + println!("Selected path {}", key.to_str().unwrap()); + state.layout_manager_selection.replace(key.clone()); + } + } + }); + }); + + ui.add_sized(Vec2::new(150.0, 0.0), Button::new("Open an empty layout!")); + }); + ui.vertical(|ui| { + ui.vertical(|ui| { + ui.set_height(flex_height); + if let Some(key) = state.layout_manager_selection.as_ref() { + if let Some(layout) = state.layouts.get(key) { + ui.label(&layout.title); + ui.label(&layout.description); + } + } else { + ui.label( + "Select a layout from the ones available on the left, or start a new one!", + ); + } + }); + + ui.add_enabled_ui(state.layout_manager_selection.is_some(), |ui| { + if ui + .add_sized(Vec2::new(ui.available_width(), 0.0), Button::new("Open")) + .clicked() + { + if let Some(selected_layout) = state + .layouts + .get(state.layout_manager_selection.as_ref().unwrap()) + { + state.state = selected_layout.state.clone(); + } + } + }) + }) + }); + + ui.data_mut(|data| data.insert_temp(non_flex_id, ui.min_rect().height() - flex_height)); +} + +#[derive(Serialize, Deserialize, Clone, PartialEq)] +pub struct ComposableViewLayout { + title: String, + description: String, + state: ComposableViewState, +} + +impl ComposableViewLayout { + fn from_file(path: &PathBuf) -> Option<Self> { + match fs::read_to_string(path) { + Ok(json) => match serde_json::from_str::<ComposableViewLayout>(&json) { + Ok(layout) => Some(layout), + Err(e) => { + eprintln!("Error deserializing layout: {}", e); + None + } + }, + Err(e) => { + eprintln!("Error reading file: {}", e); + None + } + } + } + + fn to_file(&self, path: &PathBuf) { + match serde_json::to_string_pretty(self) { + Ok(serialized_layout) => { + fs::write(path, serialized_layout).unwrap(); + } + Err(e) => { + eprintln!("Error serializing layout: {}", e); + } + } + } + + fn swap_state(&self, state: &ComposableViewState) -> Self { + Self { + state: state.clone(), + ..self.clone() + } + } +} + +pub fn load_layouts(layouts: &mut HashMap<PathBuf, ComposableViewLayout>) { + // We can't save the hash into the file, if the user manually changes the file, + // the hash almost certainly won't me recomputed. Better to compute the hasH every time. + + let layout_files: HashSet<PathBuf> = fs::read_dir("layouts") + .unwrap() + .flat_map(|x| x) + .map(|path| path.path()) + .collect(); + + // Remove the layouts which are not in the layouts folder + layouts.retain(|k, _| layout_files.contains(k)); + + // Load new layouts which we don't already have + layout_files.iter().for_each(|path| { + if !layouts.contains_key(path) { + if let Some(layout) = ComposableViewLayout::from_file(path) { + layouts.insert(path.clone(), layout); + } + } + }); +} diff --git a/src/ui/panes.rs b/src/ui/panes.rs index 9aebb54..50c4f90 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 780dfa7..2d1b51a 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 cca2803..af279bc 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 a3ad807..6ac0658 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(); -- GitLab From 7d2b298c3c1a0c7a591e33298193521c9d3ae1fb Mon Sep 17 00:00:00 2001 From: Alberto Nidasio <alberto.nidasio@skywarder.eu> Date: Mon, 25 Nov 2024 12:33:29 +0100 Subject: [PATCH 03/16] Updated layout manger's UI and implemented layout save --- Cargo.lock | 103 ----------------- Cargo.toml | 3 +- src/main.rs | 1 - src/ui/composable_view.rs | 30 +++-- src/ui/layout_manager.rs | 234 ++++++++++++++++++++------------------ 5 files changed, 140 insertions(+), 231 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4d50cf0..315eaa6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -112,15 +112,6 @@ dependencies = [ "winit", ] -[[package]] -name = "addr2line" -version = "0.24.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" -dependencies = [ - "gimli", -] - [[package]] name = "adler2" version = "2.0.0" @@ -489,27 +480,6 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" -[[package]] -name = "backtrace" -version = "0.3.74" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" -dependencies = [ - "addr2line", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", - "windows-targets 0.52.6", -] - -[[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" @@ -536,9 +506,6 @@ name = "bitflags" version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" -dependencies = [ - "serde", -] [[package]] name = "block" @@ -971,7 +938,6 @@ dependencies = [ "epaint", "log", "nohash-hasher", - "ron", "serde", ] @@ -1366,12 +1332,6 @@ dependencies = [ "wasi", ] -[[package]] -name = "gimli" -version = "0.31.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" - [[package]] name = "gl_generator" version = "0.14.0" @@ -2277,15 +2237,6 @@ dependencies = [ "objc2-foundation", ] -[[package]] -name = "object" -version = "0.36.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" -dependencies = [ - "memchr", -] - [[package]] name = "once_cell" version = "1.20.2" @@ -2594,24 +2545,6 @@ 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-demangle" -version = "0.1.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" - [[package]] name = "rustc-hash" version = "1.1.0" @@ -2684,7 +2617,6 @@ dependencies = [ "log", "serde", "serde_json", - "sha256", ] [[package]] @@ -2741,30 +2673,6 @@ dependencies = [ "digest", ] -[[package]] -name = "sha2" -version = "0.10.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "sha256" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18278f6a914fa3070aa316493f7d2ddfb9ac86ebc06fa3b83bffda487e9065b0" -dependencies = [ - "async-trait", - "bytes", - "hex", - "sha2", - "tokio", -] - [[package]] name = "shlex" version = "1.3.0" @@ -2992,17 +2900,6 @@ dependencies = [ "zerovec", ] -[[package]] -name = "tokio" -version = "1.41.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cfb5bee7a6a52939ca9224d6ac897bb669134078daa8735560897f69de4d33" -dependencies = [ - "backtrace", - "bytes", - "pin-project-lite", -] - [[package]] name = "toml_datetime" version = "0.6.8" diff --git a/Cargo.toml b/Cargo.toml index e2e26d4..6a26a05 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,7 @@ license = "MIT" # ======= GUI & Rendering ======= egui_tiles = "0.10" eframe = "0.29" -egui = { version = "0.29", features = ["log", "persistence"] } +egui = { version = "0.29", features = ["log"] } egui_plot = "0.29" # ========= Persistency ========= serde = { version = "1.0", features = ["derive"] } @@ -22,4 +22,3 @@ log = "0.4" # =========== Utility =========== # for dynamic dispatch enum_dispatch = "0.3" -sha256 = "1.5.0" diff --git a/src/main.rs b/src/main.rs index c9367e8..cd3cb13 100644 --- a/src/main.rs +++ b/src/main.rs @@ -23,7 +23,6 @@ fn main() -> Result<(), eframe::Error> { "segs", // This is the app id, used for example by Wayland native_options, Box::new(|_| { - // let app = ComposableView::from_file("layout.json"); let app = ComposableView::default(); Ok(Box::new(app)) }), diff --git a/src/ui/composable_view.rs b/src/ui/composable_view.rs index 04a5bef..7805306 100644 --- a/src/ui/composable_view.rs +++ b/src/ui/composable_view.rs @@ -1,7 +1,7 @@ use std::path::PathBuf; use super::{ - layout_manager::{layout_manager_ui, load_layouts, ComposableViewLayout}, + layout_manager::{find_current_layout, layout_manager_ui}, panes::{Pane, PaneBehavior}, shortcuts, }; @@ -19,7 +19,8 @@ pub struct ComposableView { pub layout_manager_open: bool, pub layout_manager_selection: Option<PathBuf>, - pub layouts: HashMap<PathBuf, ComposableViewLayout>, + pub layout_manager_text_input: String, + pub layouts: HashMap<PathBuf, ComposableViewState>, } // An app must implement the `App` trait to define how the ui is built @@ -102,7 +103,13 @@ impl eframe::App for ComposableView { if ui.button("Layout Manager").clicked() { self.layout_manager_open = !self.layout_manager_open; - load_layouts(&mut self.layouts); + + // When we open the layout manger, try to match the current layout to the ones available. + // This way the user will see the current layout selected + if self.layout_manager_open { + self.layout_manager_selection = + find_current_layout(&self.layouts, &self.state); + } } }) }); @@ -110,7 +117,6 @@ impl eframe::App for ComposableView { let mut window_visible = self.layout_manager_open; egui::Window::new("Layouts Manager") .collapsible(false) - // .resizable(false) .open(&mut window_visible) .show(ctx, |ui| layout_manager_ui(ui, self)); self.layout_manager_open = window_visible; @@ -161,14 +167,6 @@ impl Behavior<Pane> for ComposableBehavior { } } -#[derive(Clone, Debug)] -pub enum PaneAction { - SplitH, - SplitV, - Close, - Replace(Box<Pane>), -} - #[derive(Clone, Debug)] pub struct PaneResponse { pub action_called: Option<PaneAction>, @@ -193,3 +191,11 @@ impl Default for PaneResponse { } } } + +#[derive(Clone, Debug)] +pub enum PaneAction { + SplitH, + SplitV, + Close, + Replace(Box<Pane>), +} diff --git a/src/ui/layout_manager.rs b/src/ui/layout_manager.rs index 6b330e7..a1b78db 100644 --- a/src/ui/layout_manager.rs +++ b/src/ui/layout_manager.rs @@ -1,114 +1,19 @@ -use std::{fs, path::PathBuf}; +use std::{ + fs, + path::{Path, PathBuf}, +}; use egui::{ ahash::{HashMap, HashSet}, - Button, Color32, ScrollArea, Stroke, Vec2, Vec2b, Widget, + Button, Color32, ScrollArea, Stroke, TextEdit, Vec2, Vec2b, Widget, }; -use serde::{Deserialize, Serialize}; use super::{composable_view::ComposableViewState, ComposableView}; -pub fn layout_manager_ui(ui: &mut egui::Ui, state: &mut ComposableView) { - let layout_changed = - state - .layout_manager_selection - .clone() - .map_or(false, |layout_manager_selection| { - let selected_layout = state.layouts.get(&layout_manager_selection).unwrap(); - let current_layout = selected_layout.swap_state(&state.state); - - *selected_layout != current_layout - }); - - let non_flex_id = egui::Id::new("non_flex"); - let available_height = ui.available_height(); - let non_flex_height = ui.data(|data| data.get_temp(non_flex_id).unwrap_or(available_height)); - let flex_height = (available_height - non_flex_height).max(100.0); - - ui.set_min_size(Vec2::new(400.0, 200.0)); - ui.horizontal(|ui| { - ui.vertical(|ui| { - ui.vertical(|ui| { - ui.set_height(flex_height); - ui.set_width(150.0); - ScrollArea::vertical() - .auto_shrink(Vec2b::FALSE) - .show(ui, |ui| { - for (key, _) in state.layouts.iter() { - let mut button = - Button::new(key.to_str().unwrap()).fill(Color32::TRANSPARENT); - - if state - .layout_manager_selection - .as_ref() - .map_or_else(|| false, |selected_key| selected_key == key) - { - if layout_changed { - button = button - .stroke(Stroke::new(1.0, Color32::BROWN)) - .fill(Color32::YELLOW) - } else { - button = button - .stroke(Stroke::new(1.0, Color32::GREEN)) - .fill(Color32::LIGHT_GREEN); - } - } - - if button.ui(ui).clicked() { - println!("Selected path {}", key.to_str().unwrap()); - state.layout_manager_selection.replace(key.clone()); - } - } - }); - }); - - ui.add_sized(Vec2::new(150.0, 0.0), Button::new("Open an empty layout!")); - }); - ui.vertical(|ui| { - ui.vertical(|ui| { - ui.set_height(flex_height); - if let Some(key) = state.layout_manager_selection.as_ref() { - if let Some(layout) = state.layouts.get(key) { - ui.label(&layout.title); - ui.label(&layout.description); - } - } else { - ui.label( - "Select a layout from the ones available on the left, or start a new one!", - ); - } - }); - - ui.add_enabled_ui(state.layout_manager_selection.is_some(), |ui| { - if ui - .add_sized(Vec2::new(ui.available_width(), 0.0), Button::new("Open")) - .clicked() - { - if let Some(selected_layout) = state - .layouts - .get(state.layout_manager_selection.as_ref().unwrap()) - { - state.state = selected_layout.state.clone(); - } - } - }) - }) - }); - - ui.data_mut(|data| data.insert_temp(non_flex_id, ui.min_rect().height() - flex_height)); -} - -#[derive(Serialize, Deserialize, Clone, PartialEq)] -pub struct ComposableViewLayout { - title: String, - description: String, - state: ComposableViewState, -} - -impl ComposableViewLayout { +impl ComposableViewState { fn from_file(path: &PathBuf) -> Option<Self> { match fs::read_to_string(path) { - Ok(json) => match serde_json::from_str::<ComposableViewLayout>(&json) { + Ok(json) => match serde_json::from_str::<ComposableViewState>(&json) { Ok(layout) => Some(layout), Err(e) => { eprintln!("Error deserializing layout: {}", e); @@ -122,7 +27,7 @@ impl ComposableViewLayout { } } - fn to_file(&self, path: &PathBuf) { + fn to_file(&self, path: &Path) { match serde_json::to_string_pretty(self) { Ok(serialized_layout) => { fs::write(path, serialized_layout).unwrap(); @@ -132,19 +37,112 @@ impl ComposableViewLayout { } } } +} + +pub fn layout_manager_ui(ui: &mut egui::Ui, state: &mut ComposableView) { + load_layouts(&mut state.layouts); + + let layout_changed = + state + .layout_manager_selection + .clone() + .map_or(true, |layout_manager_selection| { + let selected_layout = state.layouts.get(&layout_manager_selection).unwrap(); + *selected_layout != state.state + }); - fn swap_state(&self, state: &ComposableViewState) -> Self { - Self { - state: state.clone(), - ..self.clone() + let non_flex_id = egui::Id::new("non_flex"); + let available_height = ui.available_height(); + let non_flex_height = ui.data(|data| data.get_temp(non_flex_id).unwrap_or(available_height)); + let flex_height = (available_height - non_flex_height).max(100.0); + + // ui.set_min_size(Vec2::new(400.0, 200.0)); + ui.vertical(|ui| { + // Layouts scroll area + ui.vertical(|ui| { + ui.set_height(flex_height); + ui.set_width(ui.available_width()); + ScrollArea::vertical() + .auto_shrink(Vec2b::FALSE) + .show(ui, |ui| { + for (key, _) in state.layouts.iter() { + let mut button = + Button::new(key.to_str().unwrap()).fill(Color32::TRANSPARENT); + + if state + .layout_manager_selection + .as_ref() + .map_or_else(|| false, |selected_key| selected_key == key) + { + if layout_changed { + button = button + .stroke(Stroke::new(1.0, Color32::BROWN)) + .fill(Color32::YELLOW) + } else { + button = button + .stroke(Stroke::new(1.0, Color32::GREEN)) + .fill(Color32::LIGHT_GREEN); + } + } + + if button.ui(ui).clicked() { + state.layout_manager_selection.replace(key.clone()); + } + } + }); + }); + + // "Open empty layout" button + let open_empty_resp = ui.add_sized( + Vec2::new(ui.available_width(), 0.0), + Button::new("Open an empty layout!"), + ); + if open_empty_resp.clicked() { + state.state = ComposableViewState::default(); + state.layout_manager_selection.take(); } - } -} -pub fn load_layouts(layouts: &mut HashMap<PathBuf, ComposableViewLayout>) { - // We can't save the hash into the file, if the user manually changes the file, - // the hash almost certainly won't me recomputed. Better to compute the hasH every time. + // "Open slected layout" button + ui.add_enabled_ui(state.layout_manager_selection.is_some(), |ui| { + let open_selected_resp = ui.add_sized( + Vec2::new(ui.available_width(), 0.0), + Button::new("Open selected layout"), + ); + if open_selected_resp.clicked() { + if let Some(selected_layout) = state + .layouts + .get(state.layout_manager_selection.as_ref().unwrap()) + { + state.state = selected_layout.clone(); + } + } + }); + // Layout save ui + ui.add_enabled_ui(layout_changed, |ui| { + ui.add_sized( + Vec2::new(ui.available_width(), 0.0), + TextEdit::singleline(&mut state.layout_manager_text_input), + ); + ui.add_enabled_ui(!state.layout_manager_text_input.is_empty(), |ui| { + let save_resp = ui.add_sized( + Vec2::new(ui.available_width(), 0.0), + Button::new("Save layout"), + ); + if save_resp.clicked() { + let path = Path::new("layouts") + .join(state.layout_manager_text_input.clone()) + .with_extension("json"); + state.state.to_file(&path); + } + }); + }) + }); + + ui.data_mut(|data| data.insert_temp(non_flex_id, ui.min_rect().height() - flex_height)); +} + +pub fn load_layouts(layouts: &mut HashMap<PathBuf, ComposableViewState>) { let layout_files: HashSet<PathBuf> = fs::read_dir("layouts") .unwrap() .flat_map(|x| x) @@ -157,9 +155,19 @@ pub fn load_layouts(layouts: &mut HashMap<PathBuf, ComposableViewLayout>) { // Load new layouts which we don't already have layout_files.iter().for_each(|path| { if !layouts.contains_key(path) { - if let Some(layout) = ComposableViewLayout::from_file(path) { + if let Some(layout) = ComposableViewState::from_file(path) { layouts.insert(path.clone(), layout); } } }); } + +pub fn find_current_layout( + layouts: &HashMap<PathBuf, ComposableViewState>, + state: &ComposableViewState, +) -> Option<PathBuf> { + layouts + .iter() + .find(|(_, v)| *v == state) + .map(|(k, _)| k.clone()) +} -- GitLab From d60495d27772320d542420bcad3b730ab65a92b6 Mon Sep 17 00:00:00 2001 From: Alberto Nidasio <alberto.nidasio@skywarder.eu> Date: Tue, 26 Nov 2024 22:35:50 +0100 Subject: [PATCH 04/16] Now the last selected layout is peristed between runs --- Cargo.lock | 26 ++++++++++++++++ Cargo.toml | 2 +- src/main.rs | 7 +++-- src/ui/composable_view.rs | 64 +++++++++++++++++++++++++++++++++++++-- src/ui/layout_manager.rs | 29 ------------------ 5 files changed, 94 insertions(+), 34 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 315eaa6..e1aed5a 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,6 +985,7 @@ dependencies = [ "egui", "log", "raw-window-handle", + "serde", "smithay-clipboard", "web-time", "webbrowser", @@ -2545,6 +2559,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" diff --git a/Cargo.toml b/Cargo.toml index 6a26a05..6fa09fc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,7 @@ 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" # ========= Persistency ========= diff --git a/src/main.rs b/src/main.rs index cd3cb13..5208728 100644 --- a/src/main.rs +++ b/src/main.rs @@ -22,8 +22,11 @@ fn main() -> Result<(), eframe::Error> { eframe::run_native( "segs", // This is the app id, used for example by Wayland native_options, - Box::new(|_| { - let app = ComposableView::default(); + Box::new(|ctx| { + let app = ctx + .storage + .map(|storage| ComposableView::new(storage)) + .unwrap_or_default(); Ok(Box::new(app)) }), ) diff --git a/src/ui/composable_view.rs b/src/ui/composable_view.rs index 7805306..aa4e213 100644 --- a/src/ui/composable_view.rs +++ b/src/ui/composable_view.rs @@ -1,10 +1,13 @@ -use std::path::PathBuf; - use super::{ layout_manager::{find_current_layout, layout_manager_ui}, panes::{Pane, PaneBehavior}, shortcuts, }; +use std::{ + fs, + path::{Path, PathBuf}, + str::FromStr, +}; use egui::{ahash::HashMap, Key, Modifiers}; use egui_tiles::{Behavior, Container, Linear, LinearDir, Tile, TileId, Tiles, Tree}; @@ -121,6 +124,34 @@ impl eframe::App for ComposableView { .show(ctx, |ui| layout_manager_ui(ui, self)); self.layout_manager_open = window_visible; } + + fn save(&mut self, storage: &mut dyn eframe::Storage) { + let selected_layout = self + .layout_manager_selection + .as_ref() + .map(|path| { + path.to_str() + .map(|s| s.to_string()) + .unwrap_or("".to_string()) + }) + .unwrap_or("".to_string()); + storage.set_string("selected_layout", selected_layout); + } +} + +impl ComposableView { + pub fn new(storage: &dyn eframe::Storage) -> Self { + let selected_layout = storage + .get_string("selected_layout") + .map(|path| PathBuf::from_str(path.as_str()).unwrap()) + .map(|path| ComposableViewState::from_file(&path)) + .flatten(); + + Self { + state: selected_layout.unwrap_or_default(), + ..Self::default() + } + } } #[derive(Serialize, Deserialize, Clone, PartialEq)] @@ -138,6 +169,35 @@ impl Default for ComposableViewState { } } +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) { + match serde_json::to_string_pretty(self) { + Ok(serialized_layout) => { + fs::write(path, serialized_layout).unwrap(); + } + Err(e) => { + eprintln!("Error serializing layout: {}", e); + } + } + } +} + /// Behavior for the tree of panes in the composable view #[derive(Default)] pub struct ComposableBehavior { diff --git a/src/ui/layout_manager.rs b/src/ui/layout_manager.rs index a1b78db..923776f 100644 --- a/src/ui/layout_manager.rs +++ b/src/ui/layout_manager.rs @@ -10,35 +10,6 @@ use egui::{ use super::{composable_view::ComposableViewState, ComposableView}; -impl ComposableViewState { - 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 - } - } - } - - fn to_file(&self, path: &Path) { - match serde_json::to_string_pretty(self) { - Ok(serialized_layout) => { - fs::write(path, serialized_layout).unwrap(); - } - Err(e) => { - eprintln!("Error serializing layout: {}", e); - } - } - } -} - pub fn layout_manager_ui(ui: &mut egui::Ui, state: &mut ComposableView) { load_layouts(&mut state.layouts); -- GitLab From 011b7b3effad9f50568846cf0d21a97d6466c584 Mon Sep 17 00:00:00 2001 From: Alberto Nidasio <alberto.nidasio@skywarder.eu> Date: Tue, 26 Nov 2024 23:07:44 +0100 Subject: [PATCH 05/16] Preliminary implementation of layout import into native path --- Cargo.lock | 10 ++++++++++ Cargo.toml | 1 + src/main.rs | 3 ++- src/ui/composable_view.rs | 9 ++++++++- src/ui/layout_manager.rs | 29 ++++++++++++++++++++++++++++- 5 files changed, 49 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e1aed5a..ca9a473 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -992,6 +992,15 @@ dependencies = [ "winit", ] +[[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" @@ -2636,6 +2645,7 @@ version = "0.1.0" dependencies = [ "eframe", "egui", + "egui_file", "egui_plot", "egui_tiles", "enum_dispatch", diff --git a/Cargo.toml b/Cargo.toml index 6fa09fc..565c09c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ egui_tiles = "0.10" 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" diff --git a/src/main.rs b/src/main.rs index 5208728..9f132ad 100644 --- a/src/main.rs +++ b/src/main.rs @@ -23,9 +23,10 @@ fn main() -> Result<(), eframe::Error> { "segs", // This is the app id, used for example by Wayland native_options, Box::new(|ctx| { + let persistence_path = eframe::storage_dir("segs").unwrap(); let app = ctx .storage - .map(|storage| ComposableView::new(storage)) + .map(|storage| ComposableView::new(storage, persistence_path)) .unwrap_or_default(); Ok(Box::new(app)) }), diff --git a/src/ui/composable_view.rs b/src/ui/composable_view.rs index aa4e213..0103034 100644 --- a/src/ui/composable_view.rs +++ b/src/ui/composable_view.rs @@ -10,6 +10,7 @@ use std::{ }; use egui::{ahash::HashMap, Key, Modifiers}; +use egui_file::FileDialog; use egui_tiles::{Behavior, Container, Linear, LinearDir, Tile, TileId, Tiles, Tree}; use serde::{Deserialize, Serialize}; @@ -23,7 +24,10 @@ pub struct ComposableView { pub layout_manager_open: bool, pub layout_manager_selection: Option<PathBuf>, pub layout_manager_text_input: String, + pub layout_manager_file_dialog: Option<FileDialog>, pub layouts: HashMap<PathBuf, ComposableViewState>, + + pub persistence_path: PathBuf, } // An app must implement the `App` trait to define how the ui is built @@ -112,6 +116,8 @@ impl eframe::App for ComposableView { if self.layout_manager_open { self.layout_manager_selection = find_current_layout(&self.layouts, &self.state); + } else { + self.layout_manager_file_dialog.take(); } } }) @@ -140,7 +146,7 @@ impl eframe::App for ComposableView { } impl ComposableView { - pub fn new(storage: &dyn eframe::Storage) -> Self { + pub fn new(storage: &dyn eframe::Storage, persistence_path: PathBuf) -> Self { let selected_layout = storage .get_string("selected_layout") .map(|path| PathBuf::from_str(path.as_str()).unwrap()) @@ -149,6 +155,7 @@ impl ComposableView { Self { state: selected_layout.unwrap_or_default(), + persistence_path, ..Self::default() } } diff --git a/src/ui/layout_manager.rs b/src/ui/layout_manager.rs index 923776f..691897b 100644 --- a/src/ui/layout_manager.rs +++ b/src/ui/layout_manager.rs @@ -7,6 +7,7 @@ use egui::{ ahash::{HashMap, HashSet}, Button, Color32, ScrollArea, Stroke, TextEdit, Vec2, Vec2b, Widget, }; +use egui_file::FileDialog; use super::{composable_view::ComposableViewState, ComposableView}; @@ -107,7 +108,33 @@ pub fn layout_manager_ui(ui: &mut egui::Ui, state: &mut ComposableView) { state.state.to_file(&path); } }); - }) + }); + + // Import layout ui + let import_layout_resp = ui.add_sized( + Vec2::new(ui.available_width(), 0.0), + Button::new("Import layout"), + ); + if import_layout_resp.clicked() { + let mut file_dialog = FileDialog::open_file(None); + file_dialog.open(); + state.layout_manager_file_dialog = Some(file_dialog); + } + if let Some(file_dialog) = &mut state.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 = state.persistence_path.join(file_name); + + match fs::copy(file, destination.clone()) { + Ok(_) => println!("Layout imported in {}", destination.to_str().unwrap()), + Err(e) => println!("Error importing layout: {:?}", e), + } + } + } + } }); ui.data_mut(|data| data.insert_temp(non_flex_id, ui.min_rect().height() - flex_height)); -- GitLab From 8a05cc15b053a47787885e28201b8cdc56b2ad9a Mon Sep 17 00:00:00 2001 From: Alberto Nidasio <alberto.nidasio@skywarder.eu> Date: Wed, 27 Nov 2024 16:51:27 +0100 Subject: [PATCH 06/16] Moved layouts path to native path --- src/main.rs | 7 +- src/ui/composable_view.rs | 51 ++++--------- src/ui/layout_manager.rs | 151 +++++++++++++++++++++++++------------- 3 files changed, 119 insertions(+), 90 deletions(-) diff --git a/src/main.rs b/src/main.rs index 9f132ad..c988cd7 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,13 +22,12 @@ 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(|ctx| { - let persistence_path = eframe::storage_dir("segs").unwrap(); let app = ctx .storage - .map(|storage| ComposableView::new(storage, persistence_path)) + .map(|storage| ComposableView::new(APP_NAME, storage)) .unwrap_or_default(); Ok(Box::new(app)) }), diff --git a/src/ui/composable_view.rs b/src/ui/composable_view.rs index 0103034..f66d1aa 100644 --- a/src/ui/composable_view.rs +++ b/src/ui/composable_view.rs @@ -1,16 +1,14 @@ use super::{ - layout_manager::{find_current_layout, layout_manager_ui}, + layout_manager::{find_current_layout, layout_manager_ui, LayoutManager}, panes::{Pane, PaneBehavior}, shortcuts, }; use std::{ fs, path::{Path, PathBuf}, - str::FromStr, }; -use egui::{ahash::HashMap, Key, Modifiers}; -use egui_file::FileDialog; +use egui::{Key, Modifiers}; use egui_tiles::{Behavior, Container, Linear, LinearDir, Tile, TileId, Tiles, Tree}; use serde::{Deserialize, Serialize}; @@ -21,13 +19,7 @@ pub struct ComposableView { behavior: ComposableBehavior, - pub layout_manager_open: bool, - pub layout_manager_selection: Option<PathBuf>, - pub layout_manager_text_input: String, - pub layout_manager_file_dialog: Option<FileDialog>, - pub layouts: HashMap<PathBuf, ComposableViewState>, - - pub persistence_path: PathBuf, + pub layout_manager: LayoutManager, } // An app must implement the `App` trait to define how the ui is built @@ -109,53 +101,40 @@ impl eframe::App for ComposableView { egui::global_theme_preference_switch(ui); if ui.button("Layout Manager").clicked() { - self.layout_manager_open = !self.layout_manager_open; + self.layout_manager.open = !self.layout_manager.open; // When we open the layout manger, try to match the current layout to the ones available. // This way the user will see the current layout selected - if self.layout_manager_open { - self.layout_manager_selection = - find_current_layout(&self.layouts, &self.state); + if self.layout_manager.open { + self.layout_manager.selection = + find_current_layout(&self.layout_manager.layouts, &self.state); } else { - self.layout_manager_file_dialog.take(); + self.layout_manager.file_dialog.take(); } } }) }); - let mut window_visible = self.layout_manager_open; + let mut window_visible = self.layout_manager.open; egui::Window::new("Layouts Manager") .collapsible(false) .open(&mut window_visible) .show(ctx, |ui| layout_manager_ui(ui, self)); - self.layout_manager_open = window_visible; + self.layout_manager.open = window_visible; } fn save(&mut self, storage: &mut dyn eframe::Storage) { - let selected_layout = self - .layout_manager_selection - .as_ref() - .map(|path| { - path.to_str() - .map(|s| s.to_string()) - .unwrap_or("".to_string()) - }) - .unwrap_or("".to_string()); - storage.set_string("selected_layout", selected_layout); + self.layout_manager.save_selection(storage); } } impl ComposableView { - pub fn new(storage: &dyn eframe::Storage, persistence_path: PathBuf) -> Self { - let selected_layout = storage - .get_string("selected_layout") - .map(|path| PathBuf::from_str(path.as_str()).unwrap()) - .map(|path| ComposableViewState::from_file(&path)) - .flatten(); + pub fn new(app_name: &str, storage: &dyn eframe::Storage) -> Self { + let layout_manager = LayoutManager::new(app_name, storage); Self { - state: selected_layout.unwrap_or_default(), - persistence_path, + state: layout_manager.read_selected_layout().unwrap_or_default(), + layout_manager, ..Self::default() } } diff --git a/src/ui/layout_manager.rs b/src/ui/layout_manager.rs index 691897b..b4265a0 100644 --- a/src/ui/layout_manager.rs +++ b/src/ui/layout_manager.rs @@ -1,34 +1,68 @@ -use std::{ - fs, - path::{Path, PathBuf}, -}; - -use egui::{ - ahash::{HashMap, HashSet}, - Button, Color32, ScrollArea, Stroke, TextEdit, Vec2, Vec2b, Widget, -}; +use std::{collections::BTreeMap, fs, path::PathBuf, str::FromStr}; + +use egui::{ahash::HashSet, Button, Color32, ScrollArea, Stroke, TextEdit, Vec2, Vec2b, Widget}; 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 { + pub open: bool, + pub selection: Option<PathBuf>, + pub text_input: String, + pub file_dialog: Option<FileDialog>, + pub layouts: BTreeMap<PathBuf, ComposableViewState>, + pub layouts_path: PathBuf, +} + +impl LayoutManager { + pub fn new(app_name: &str, storage: &dyn eframe::Storage) -> Self { + 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() + } + } + + pub fn read_selected_layout(&self) -> Option<ComposableViewState> { + self.selection + .as_ref() + .map(|name| { + let selected_layout_path = self.layouts_path.join(name); + ComposableViewState::from_file(&selected_layout_path) + }) + .flatten() + } + + pub fn save_selection(&self, storage: &mut dyn eframe::Storage) { + if let Some(selection) = self.selection.as_ref().map(|s| s.to_str()).flatten() { + storage.set_string(SELECTED_LAYOUT_KEY, selection.to_string()); + } + } +} + pub fn layout_manager_ui(ui: &mut egui::Ui, state: &mut ComposableView) { - load_layouts(&mut state.layouts); - - let layout_changed = - state - .layout_manager_selection - .clone() - .map_or(true, |layout_manager_selection| { - let selected_layout = state.layouts.get(&layout_manager_selection).unwrap(); - *selected_layout != state.state - }); + load_layouts(&mut state.layout_manager); + + let layout_changed = state + .layout_manager + .selection + .clone() + .map_or(true, |selection| { + let selected_layout = state.layout_manager.layouts.get(&selection).unwrap(); + *selected_layout != state.state + }); let non_flex_id = egui::Id::new("non_flex"); let available_height = ui.available_height(); let non_flex_height = ui.data(|data| data.get_temp(non_flex_id).unwrap_or(available_height)); let flex_height = (available_height - non_flex_height).max(100.0); - // ui.set_min_size(Vec2::new(400.0, 200.0)); ui.vertical(|ui| { // Layouts scroll area ui.vertical(|ui| { @@ -37,12 +71,13 @@ pub fn layout_manager_ui(ui: &mut egui::Ui, state: &mut ComposableView) { ScrollArea::vertical() .auto_shrink(Vec2b::FALSE) .show(ui, |ui| { - for (key, _) in state.layouts.iter() { + for (key, _) in state.layout_manager.layouts.iter() { let mut button = Button::new(key.to_str().unwrap()).fill(Color32::TRANSPARENT); if state - .layout_manager_selection + .layout_manager + .selection .as_ref() .map_or_else(|| false, |selected_key| selected_key == key) { @@ -58,7 +93,7 @@ pub fn layout_manager_ui(ui: &mut egui::Ui, state: &mut ComposableView) { } if button.ui(ui).clicked() { - state.layout_manager_selection.replace(key.clone()); + state.layout_manager.selection.replace(key.clone()); } } }); @@ -71,19 +106,20 @@ pub fn layout_manager_ui(ui: &mut egui::Ui, state: &mut ComposableView) { ); if open_empty_resp.clicked() { state.state = ComposableViewState::default(); - state.layout_manager_selection.take(); + state.layout_manager.selection.take(); } // "Open slected layout" button - ui.add_enabled_ui(state.layout_manager_selection.is_some(), |ui| { + ui.add_enabled_ui(state.layout_manager.selection.is_some(), |ui| { let open_selected_resp = ui.add_sized( Vec2::new(ui.available_width(), 0.0), Button::new("Open selected layout"), ); if open_selected_resp.clicked() { if let Some(selected_layout) = state + .layout_manager .layouts - .get(state.layout_manager_selection.as_ref().unwrap()) + .get(state.layout_manager.selection.as_ref().unwrap()) { state.state = selected_layout.clone(); } @@ -94,16 +130,18 @@ pub fn layout_manager_ui(ui: &mut egui::Ui, state: &mut ComposableView) { ui.add_enabled_ui(layout_changed, |ui| { ui.add_sized( Vec2::new(ui.available_width(), 0.0), - TextEdit::singleline(&mut state.layout_manager_text_input), + TextEdit::singleline(&mut state.layout_manager.text_input), ); - ui.add_enabled_ui(!state.layout_manager_text_input.is_empty(), |ui| { + ui.add_enabled_ui(!state.layout_manager.text_input.is_empty(), |ui| { let save_resp = ui.add_sized( Vec2::new(ui.available_width(), 0.0), Button::new("Save layout"), ); if save_resp.clicked() { - let path = Path::new("layouts") - .join(state.layout_manager_text_input.clone()) + let path = state + .layout_manager + .layouts_path + .join(state.layout_manager.text_input.clone()) .with_extension("json"); state.state.to_file(&path); } @@ -118,15 +156,23 @@ pub fn layout_manager_ui(ui: &mut egui::Ui, state: &mut ComposableView) { if import_layout_resp.clicked() { let mut file_dialog = FileDialog::open_file(None); file_dialog.open(); - state.layout_manager_file_dialog = Some(file_dialog); + state.layout_manager.file_dialog = Some(file_dialog); } - if let Some(file_dialog) = &mut state.layout_manager_file_dialog { + if let Some(file_dialog) = &mut state.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 = state.persistence_path.join(file_name); + let destination = state.layout_manager.layouts_path.join(file_name); + + // First check if the layouts folder exists + if !state.layout_manager.layouts_path.exists() { + match fs::create_dir_all(&state.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()), @@ -140,28 +186,31 @@ pub fn layout_manager_ui(ui: &mut egui::Ui, state: &mut ComposableView) { ui.data_mut(|data| data.insert_temp(non_flex_id, ui.min_rect().height() - flex_height)); } -pub fn load_layouts(layouts: &mut HashMap<PathBuf, ComposableViewState>) { - let layout_files: HashSet<PathBuf> = fs::read_dir("layouts") - .unwrap() - .flat_map(|x| x) - .map(|path| path.path()) - .collect(); - - // Remove the layouts which are not in the layouts folder - layouts.retain(|k, _| layout_files.contains(k)); - - // Load new layouts which we don't already have - layout_files.iter().for_each(|path| { - if !layouts.contains_key(path) { - if let Some(layout) = ComposableViewState::from_file(path) { - layouts.insert(path.clone(), layout); +pub fn load_layouts(layout_manager: &mut LayoutManager) { + if let Ok(files) = layout_manager.layouts_path.read_dir() { + let layout_files: HashSet<PathBuf> = + files.flat_map(|x| x).map(|path| path.path()).collect(); + + // Remove the layouts which are not in the layouts folder + layout_manager + .layouts + .retain(|k, _| layout_files.contains(k)); + + // Load new layouts which we don't already have + layout_files.iter().for_each(|path| { + if !layout_manager.layouts.contains_key(path) { + if let Some(layout) = ComposableViewState::from_file(path) { + layout_manager + .layouts + .insert(path.file_name().unwrap().into(), layout); + } } - } - }); + }); + } } pub fn find_current_layout( - layouts: &HashMap<PathBuf, ComposableViewState>, + layouts: &BTreeMap<PathBuf, ComposableViewState>, state: &ComposableViewState, ) -> Option<PathBuf> { layouts -- GitLab From 5bb19df3fcb30c02910f4b5e1783c5a111181c95 Mon Sep 17 00:00:00 2001 From: Alberto Nidasio <alberto.nidasio@skywarder.eu> Date: Wed, 27 Nov 2024 17:07:31 +0100 Subject: [PATCH 07/16] Importing a layout now also opens it --- src/ui/layout_manager.rs | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/src/ui/layout_manager.rs b/src/ui/layout_manager.rs index b4265a0..901c8a6 100644 --- a/src/ui/layout_manager.rs +++ b/src/ui/layout_manager.rs @@ -44,6 +44,16 @@ impl LayoutManager { storage.set_string(SELECTED_LAYOUT_KEY, selection.to_string()); } } + + pub fn try_open_selection(state: &mut ComposableView) { + if let Some(selected_layout) = state + .layout_manager + .layouts + .get(state.layout_manager.selection.as_ref().unwrap()) + { + state.state = selected_layout.clone(); + } + } } pub fn layout_manager_ui(ui: &mut egui::Ui, state: &mut ComposableView) { @@ -109,20 +119,14 @@ pub fn layout_manager_ui(ui: &mut egui::Ui, state: &mut ComposableView) { state.layout_manager.selection.take(); } - // "Open slected layout" button + // "Open selected layout" button ui.add_enabled_ui(state.layout_manager.selection.is_some(), |ui| { let open_selected_resp = ui.add_sized( Vec2::new(ui.available_width(), 0.0), Button::new("Open selected layout"), ); if open_selected_resp.clicked() { - if let Some(selected_layout) = state - .layout_manager - .layouts - .get(state.layout_manager.selection.as_ref().unwrap()) - { - state.state = selected_layout.clone(); - } + LayoutManager::try_open_selection(state); } }); @@ -175,7 +179,12 @@ pub fn layout_manager_ui(ui: &mut egui::Ui, state: &mut ComposableView) { } match fs::copy(file, destination.clone()) { - Ok(_) => println!("Layout imported in {}", destination.to_str().unwrap()), + Ok(_) => { + println!("Layout imported in {}", destination.to_str().unwrap()); + state.layout_manager.selection.replace(file_name.into()); + load_layouts(&mut state.layout_manager); + LayoutManager::try_open_selection(state); + } Err(e) => println!("Error importing layout: {:?}", e), } } -- GitLab From d1b984ce9bd399b92a6c8abcda25ea79c578c379 Mon Sep 17 00:00:00 2001 From: Alberto Nidasio <alberto.nidasio@skywarder.eu> Date: Thu, 28 Nov 2024 13:13:18 +0100 Subject: [PATCH 08/16] Now the displayed layout is saved instead of the selected one. Also other minor code refactoring --- src/ui/composable_view.rs | 50 ++++++++++++------ src/ui/layout_manager.rs | 107 +++++++++++++++----------------------- 2 files changed, 75 insertions(+), 82 deletions(-) diff --git a/src/ui/composable_view.rs b/src/ui/composable_view.rs index f66d1aa..2f099a5 100644 --- a/src/ui/composable_view.rs +++ b/src/ui/composable_view.rs @@ -1,5 +1,5 @@ use super::{ - layout_manager::{find_current_layout, layout_manager_ui, LayoutManager}, + layout_manager::{layout_manager_ui, LayoutManager}, panes::{Pane, PaneBehavior}, shortcuts, }; @@ -17,9 +17,8 @@ pub struct ComposableView { /// Persistent state of the app pub state: ComposableViewState, - behavior: ComposableBehavior, - pub layout_manager: LayoutManager, + behavior: ComposableBehavior, } // An app must implement the `App` trait to define how the ui is built @@ -27,7 +26,8 @@ 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) { let panes_tree = &mut self.state.panes_tree; - // get the id of the hovered pane, in order to apply actions to it + + // Get the id of the hovered pane, in order to apply actions to it let hovered_pane = panes_tree .tiles .iter() @@ -90,11 +90,6 @@ impl eframe::App for ComposableView { } } - // 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| { - panes_tree.ui(&mut self.behavior, ui); - }); - // Show a panel at the bottom of the screen with few global controls egui::TopBottomPanel::bottom("bottom_control").show(ctx, |ui| { ui.horizontal(|ui| { @@ -103,18 +98,22 @@ impl eframe::App for ComposableView { if ui.button("Layout Manager").clicked() { self.layout_manager.open = !self.layout_manager.open; - // When we open the layout manger, try to match the current layout to the ones available. - // This way the user will see the current layout selected if self.layout_manager.open { - self.layout_manager.selection = - find_current_layout(&self.layout_manager.layouts, &self.state); + // When opening, we set the selection to the current layout + self.layout_manager.selection = self.layout_manager.displayed.clone(); } else { + // When closing, we delete also the file dialog self.layout_manager.file_dialog.take(); } } }) }); + // 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| { + panes_tree.ui(&mut self.behavior, ui); + }); + let mut window_visible = self.layout_manager.open; egui::Window::new("Layouts Manager") .collapsible(false) @@ -124,20 +123,37 @@ impl eframe::App for ComposableView { } fn save(&mut self, storage: &mut dyn eframe::Storage) { - self.layout_manager.save_selection(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); - - Self { - state: layout_manager.read_selected_layout().unwrap_or_default(), + let mut composable_view = Self { layout_manager, ..Self::default() + }; + composable_view.try_display_selected_layout(); + composable_view + } + + pub fn try_display_selected_layout(&mut self) { + if let Some(slection) = self.layout_manager.selection.as_ref() { + if let Some(selected_layout) = self.layout_manager.layouts.get(slection) { + self.state = selected_layout.clone(); + self.layout_manager.displayed = Some(slection.clone()); + } } } + + pub fn save_new_layout(&mut self, name: &String) { + let layouts_path = &self.layout_manager.layouts_path; + let path = layouts_path.join(name).with_extension("json"); + self.state.to_file(&path); + self.layout_manager.selection.replace(name.into()); + self.layout_manager.displayed.replace(name.into()); + } } #[derive(Serialize, Deserialize, Clone, PartialEq)] diff --git a/src/ui/layout_manager.rs b/src/ui/layout_manager.rs index 901c8a6..1d56b0e 100644 --- a/src/ui/layout_manager.rs +++ b/src/ui/layout_manager.rs @@ -11,7 +11,13 @@ static SELECTED_LAYOUT_KEY: &str = "selected_layout"; #[derive(Default)] pub struct LayoutManager { pub open: bool, + + /// Currently dislayed layout in the ui + pub displayed: Option<PathBuf>, + + /// Currently selected layout in the list, gets reset to the displayed layout when the dialog is opened pub selection: Option<PathBuf>, + pub text_input: String, pub file_dialog: Option<FileDialog>, pub layouts: BTreeMap<PathBuf, ComposableViewState>, @@ -19,45 +25,53 @@ pub struct LayoutManager { } 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 { - 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() - } - } - - pub fn read_selected_layout(&self) -> Option<ComposableViewState> { - self.selection - .as_ref() - .map(|name| { - let selected_layout_path = self.layouts_path.join(name); - ComposableViewState::from_file(&selected_layout_path) - }) - .flatten() + }; + layout_manager.reload_layouts(); + layout_manager } - pub fn save_selection(&self, storage: &mut dyn eframe::Storage) { - if let Some(selection) = self.selection.as_ref().map(|s| s.to_str()).flatten() { - storage.set_string(SELECTED_LAYOUT_KEY, selection.to_string()); + // 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) } } - pub fn try_open_selection(state: &mut ComposableView) { - if let Some(selected_layout) = state - .layout_manager - .layouts - .get(state.layout_manager.selection.as_ref().unwrap()) - { - state.state = selected_layout.clone(); + /// Scans the layout directory and reloads the layouts + pub fn reload_layouts(&mut self) { + // TODO: Do a simlier complete reload + if let Ok(files) = self.layouts_path.read_dir() { + let layout_files: HashSet<PathBuf> = + files.flat_map(|x| x).map(|path| path.path()).collect(); + + // Remove the layouts which are not in the layouts folder + self.layouts.retain(|k, _| layout_files.contains(k)); + + // Load new layouts which we don't already have + layout_files.iter().for_each(|path| { + if !self.layouts.contains_key(path) { + if let Some(layout) = ComposableViewState::from_file(path) { + self.layouts + .insert(path.file_stem().unwrap().into(), layout); + } + } + }); } } } pub fn layout_manager_ui(ui: &mut egui::Ui, state: &mut ComposableView) { - load_layouts(&mut state.layout_manager); + // TODO: Replace with a reload button instead of reloading at every frame + state.layout_manager.reload_layouts(); let layout_changed = state .layout_manager @@ -126,7 +140,7 @@ pub fn layout_manager_ui(ui: &mut egui::Ui, state: &mut ComposableView) { Button::new("Open selected layout"), ); if open_selected_resp.clicked() { - LayoutManager::try_open_selection(state); + state.try_display_selected_layout(); } }); @@ -142,12 +156,8 @@ pub fn layout_manager_ui(ui: &mut egui::Ui, state: &mut ComposableView) { Button::new("Save layout"), ); if save_resp.clicked() { - let path = state - .layout_manager - .layouts_path - .join(state.layout_manager.text_input.clone()) - .with_extension("json"); - state.state.to_file(&path); + let name = state.layout_manager.text_input.clone(); + state.save_new_layout(&name); } }); }); @@ -182,8 +192,8 @@ pub fn layout_manager_ui(ui: &mut egui::Ui, state: &mut ComposableView) { Ok(_) => { println!("Layout imported in {}", destination.to_str().unwrap()); state.layout_manager.selection.replace(file_name.into()); - load_layouts(&mut state.layout_manager); - LayoutManager::try_open_selection(state); + state.layout_manager.reload_layouts(); + state.try_display_selected_layout(); } Err(e) => println!("Error importing layout: {:?}", e), } @@ -194,36 +204,3 @@ pub fn layout_manager_ui(ui: &mut egui::Ui, state: &mut ComposableView) { ui.data_mut(|data| data.insert_temp(non_flex_id, ui.min_rect().height() - flex_height)); } - -pub fn load_layouts(layout_manager: &mut LayoutManager) { - if let Ok(files) = layout_manager.layouts_path.read_dir() { - let layout_files: HashSet<PathBuf> = - files.flat_map(|x| x).map(|path| path.path()).collect(); - - // Remove the layouts which are not in the layouts folder - layout_manager - .layouts - .retain(|k, _| layout_files.contains(k)); - - // Load new layouts which we don't already have - layout_files.iter().for_each(|path| { - if !layout_manager.layouts.contains_key(path) { - if let Some(layout) = ComposableViewState::from_file(path) { - layout_manager - .layouts - .insert(path.file_name().unwrap().into(), layout); - } - } - }); - } -} - -pub fn find_current_layout( - layouts: &BTreeMap<PathBuf, ComposableViewState>, - state: &ComposableViewState, -) -> Option<PathBuf> { - layouts - .iter() - .find(|(_, v)| *v == state) - .map(|(k, _)| k.clone()) -} -- GitLab From 2720a80c7a2745fa045caa6655bcd78c7aa262d9 Mon Sep 17 00:00:00 2001 From: Alberto Nidasio <alberto.nidasio@skywarder.eu> Date: Thu, 28 Nov 2024 14:30:37 +0100 Subject: [PATCH 09/16] Changed text color of selected layout --- src/ui/layout_manager.rs | 35 ++++++++++++++++++----------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/src/ui/layout_manager.rs b/src/ui/layout_manager.rs index 1d56b0e..60f8c63 100644 --- a/src/ui/layout_manager.rs +++ b/src/ui/layout_manager.rs @@ -1,6 +1,8 @@ use std::{collections::BTreeMap, fs, path::PathBuf, str::FromStr}; -use egui::{ahash::HashSet, Button, Color32, ScrollArea, Stroke, TextEdit, Vec2, Vec2b, Widget}; +use egui::{ + ahash::HashSet, Button, Color32, RichText, ScrollArea, Stroke, TextEdit, Vec2, Vec2b, Widget, +}; use egui_file::FileDialog; use super::{composable_view::ComposableViewState, ComposableView}; @@ -96,25 +98,24 @@ pub fn layout_manager_ui(ui: &mut egui::Ui, state: &mut ComposableView) { .auto_shrink(Vec2b::FALSE) .show(ui, |ui| { for (key, _) in state.layout_manager.layouts.iter() { - let mut button = - Button::new(key.to_str().unwrap()).fill(Color32::TRANSPARENT); - - if state + let is_selected = state .layout_manager .selection .as_ref() - .map_or_else(|| false, |selected_key| selected_key == key) - { - if layout_changed { - button = button - .stroke(Stroke::new(1.0, Color32::BROWN)) - .fill(Color32::YELLOW) - } else { - button = button - .stroke(Stroke::new(1.0, Color32::GREEN)) - .fill(Color32::LIGHT_GREEN); - } - } + .map_or_else(|| false, |selected_key| selected_key == key); + let name = key.to_str().unwrap(); + + let button = if is_selected && layout_changed { + Button::new(RichText::new(name).color(Color32::BLACK)) + .stroke(Stroke::new(1.0, Color32::BROWN)) + .fill(Color32::YELLOW) + } else if is_selected && !layout_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) + }; if button.ui(ui).clicked() { state.layout_manager.selection.replace(key.clone()); -- GitLab From b44a15d96baa4a1a17bbc75f9136b5c865dccb66 Mon Sep 17 00:00:00 2001 From: Alberto Nidasio <alberto.nidasio@skywarder.eu> Date: Thu, 28 Nov 2024 19:26:54 +0100 Subject: [PATCH 10/16] Reorganized ui elements and implemented double click to open a layout --- Cargo.lock | 57 +++++++++ Cargo.toml | 1 + src/ui/layout_manager.rs | 260 +++++++++++++++++++++------------------ 3 files changed, 201 insertions(+), 117 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ca9a473..a6a4c77 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -992,6 +992,19 @@ dependencies = [ "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" @@ -1064,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" @@ -1929,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" @@ -2645,6 +2695,7 @@ version = "0.1.0" dependencies = [ "eframe", "egui", + "egui_extras", "egui_file", "egui_plot", "egui_tiles", @@ -3016,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 565c09c..81b1769 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,3 +23,4 @@ log = "0.4" # =========== Utility =========== # for dynamic dispatch enum_dispatch = "0.3" +egui_extras = "0.29.1" diff --git a/src/ui/layout_manager.rs b/src/ui/layout_manager.rs index 60f8c63..e6e890c 100644 --- a/src/ui/layout_manager.rs +++ b/src/ui/layout_manager.rs @@ -1,8 +1,7 @@ use std::{collections::BTreeMap, fs, path::PathBuf, str::FromStr}; -use egui::{ - ahash::HashSet, Button, Color32, RichText, ScrollArea, Stroke, TextEdit, Vec2, Vec2b, Widget, -}; +use egui::{ahash::HashSet, Button, Color32, RichText, Separator, Stroke, TextEdit, Vec2, Widget}; +use egui_extras::{Column, Size, StripBuilder, TableBuilder}; use egui_file::FileDialog; use super::{composable_view::ComposableViewState, ComposableView}; @@ -84,124 +83,151 @@ pub fn layout_manager_ui(ui: &mut egui::Ui, state: &mut ComposableView) { *selected_layout != state.state }); - let non_flex_id = egui::Id::new("non_flex"); - let available_height = ui.available_height(); - let non_flex_height = ui.data(|data| data.get_temp(non_flex_id).unwrap_or(available_height)); - let flex_height = (available_height - non_flex_height).max(100.0); - - ui.vertical(|ui| { - // Layouts scroll area - ui.vertical(|ui| { - ui.set_height(flex_height); - ui.set_width(ui.available_width()); - ScrollArea::vertical() - .auto_shrink(Vec2b::FALSE) - .show(ui, |ui| { - for (key, _) in state.layout_manager.layouts.iter() { - let is_selected = state - .layout_manager - .selection - .as_ref() - .map_or_else(|| false, |selected_key| selected_key == key); - let name = key.to_str().unwrap(); - - let button = if is_selected && layout_changed { - Button::new(RichText::new(name).color(Color32::BLACK)) - .stroke(Stroke::new(1.0, Color32::BROWN)) - .fill(Color32::YELLOW) - } else if is_selected && !layout_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) - }; - - if button.ui(ui).clicked() { - state.layout_manager.selection.replace(key.clone()); + // 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| { + let available_height = ui.available_height(); + TableBuilder::new(ui) + .column(Column::remainder()) + .column(Column::auto()) + .min_scrolled_height(0.0) + .max_scroll_height(available_height) + .body(|mut body| { + let mut open_selected = false; + + for key in state.layout_manager.layouts.keys() { + let name = key.to_str().unwrap(); + let is_selected = state + .layout_manager + .selection + .as_ref() + .map_or_else(|| false, |selected_key| selected_key == key); + + let name_button = if is_selected && layout_changed { + Button::new(RichText::new(name).color(Color32::BLACK)) + .stroke(Stroke::new(1.0, Color32::BROWN)) + .fill(Color32::YELLOW) + } else if is_selected && !layout_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("↗"); + + body.row(20.0, |mut row| { + row.col(|ui| { + let name_button_resp = name_button.ui(ui); + if name_button_resp.clicked() { + state.layout_manager.selection.replace(key.clone()); + } + if name_button_resp.double_clicked() { + state.layout_manager.selection.replace(key.clone()); + open_selected = true; + } + }); + row.col(|ui| { + if open_button.ui(ui).clicked() { + state.layout_manager.selection.replace(key.clone()); + open_selected = true; + } + }); + }); } - } - }); - }); - - // "Open empty layout" button - let open_empty_resp = ui.add_sized( - Vec2::new(ui.available_width(), 0.0), - Button::new("Open an empty layout!"), - ); - if open_empty_resp.clicked() { - state.state = ComposableViewState::default(); - state.layout_manager.selection.take(); - } - - // "Open selected layout" button - ui.add_enabled_ui(state.layout_manager.selection.is_some(), |ui| { - let open_selected_resp = ui.add_sized( - Vec2::new(ui.available_width(), 0.0), - Button::new("Open selected layout"), - ); - if open_selected_resp.clicked() { - state.try_display_selected_layout(); - } - }); - // Layout save ui - ui.add_enabled_ui(layout_changed, |ui| { - ui.add_sized( - Vec2::new(ui.available_width(), 0.0), - TextEdit::singleline(&mut state.layout_manager.text_input), - ); - ui.add_enabled_ui(!state.layout_manager.text_input.is_empty(), |ui| { - let save_resp = ui.add_sized( - Vec2::new(ui.available_width(), 0.0), - Button::new("Save layout"), - ); - if save_resp.clicked() { - let name = state.layout_manager.text_input.clone(); - state.save_new_layout(&name); - } + if open_selected { + state.try_display_selected_layout(); + } + }); }); - }); - - // Import layout ui - let import_layout_resp = ui.add_sized( - Vec2::new(ui.available_width(), 0.0), - Button::new("Import layout"), - ); - if import_layout_resp.clicked() { - let mut file_dialog = FileDialog::open_file(None); - file_dialog.open(); - state.layout_manager.file_dialog = Some(file_dialog); - } - if let Some(file_dialog) = &mut state.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 = state.layout_manager.layouts_path.join(file_name); - - // First check if the layouts folder exists - if !state.layout_manager.layouts_path.exists() { - match fs::create_dir_all(&state.layout_manager.layouts_path) { - Ok(_) => println!("Created layouts folder"), - Err(e) => println!("Error creating layouts folder: {:?}", e), + strip.cell(|ui| { + ui.add(Separator::default().spacing(7.0)); + }); + strip.strip(|builder| { + builder.sizes(Size::remainder(), 2).horizontal(|mut strip| { + strip.cell(|ui| { + // "Open empty layout" button + let open_empty_resp = ui.add_sized( + Vec2::new(ui.available_width(), 0.0), + Button::new("Load empty"), + ); + if open_empty_resp.clicked() { + state.state = ComposableViewState::default(); + state.layout_manager.selection.take(); } - } - match fs::copy(file, destination.clone()) { - Ok(_) => { - println!("Layout imported in {}", destination.to_str().unwrap()); - state.layout_manager.selection.replace(file_name.into()); - state.layout_manager.reload_layouts(); - state.try_display_selected_layout(); + // Import layout ui + 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(); + state.layout_manager.file_dialog = Some(file_dialog); } - Err(e) => println!("Error importing layout: {:?}", e), - } - } - } - } - }); - - ui.data_mut(|data| data.insert_temp(non_flex_id, ui.min_rect().height() - flex_height)); + if let Some(file_dialog) = &mut state.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 = + state.layout_manager.layouts_path.join(file_name); + + // First check if the layouts folder exists + if !state.layout_manager.layouts_path.exists() { + match fs::create_dir_all(&state.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() + ); + state + .layout_manager + .selection + .replace(file_name.into()); + state.layout_manager.reload_layouts(); + state.try_display_selected_layout(); + } + Err(e) => println!("Error importing layout: {:?}", e), + } + } + } + } + }); + strip.cell(|ui| { + // Layout save ui + ui.add_enabled_ui(layout_changed, |ui| { + ui.add_sized( + Vec2::new(ui.available_width(), 0.0), + TextEdit::singleline(&mut state.layout_manager.text_input), + ); + ui.add_enabled_ui(!state.layout_manager.text_input.is_empty(), |ui| { + let save_resp = ui.add_sized( + Vec2::new(ui.available_width(), 0.0), + Button::new("Save layout"), + ); + if save_resp.clicked() { + let name = state.layout_manager.text_input.clone(); + state.save_new_layout(&name); + } + }); + }); + }); + }); + }); + }); } -- GitLab From 9a06ad3c084afb677303c71750630f0d13ddf2aa Mon Sep 17 00:00:00 2001 From: Alberto Nidasio <alberto.nidasio@skywarder.eu> Date: Thu, 28 Nov 2024 20:20:20 +0100 Subject: [PATCH 11/16] Implemented delete button for layouts and enter shortcut to save new layouts --- src/ui/layout_manager.rs | 100 ++++++++++++++++++++++++++++----------- 1 file changed, 73 insertions(+), 27 deletions(-) diff --git a/src/ui/layout_manager.rs b/src/ui/layout_manager.rs index e6e890c..b60157f 100644 --- a/src/ui/layout_manager.rs +++ b/src/ui/layout_manager.rs @@ -1,6 +1,9 @@ use std::{collections::BTreeMap, fs, path::PathBuf, str::FromStr}; -use egui::{ahash::HashSet, Button, Color32, RichText, Separator, Stroke, TextEdit, Vec2, Widget}; +use egui::{ + ahash::HashSet, Button, Color32, InnerResponse, RichText, Separator, Stroke, TextEdit, Vec2, + Widget, +}; use egui_extras::{Column, Size, StripBuilder, TableBuilder}; use egui_file::FileDialog; @@ -68,6 +71,20 @@ impl LayoutManager { }); } } + + 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) { + // TODO: When addressing live realod issue add `self.layouts.remove(key);` + let _ = fs::remove_file(self.layouts_path.join(key).with_extension("json")); + } + } } pub fn layout_manager_ui(ui: &mut egui::Ui, state: &mut ComposableView) { @@ -76,10 +93,9 @@ pub fn layout_manager_ui(ui: &mut egui::Ui, state: &mut ComposableView) { let layout_changed = state .layout_manager - .selection - .clone() + .get_selected() .map_or(true, |selection| { - let selected_layout = state.layout_manager.layouts.get(&selection).unwrap(); + let selected_layout = selection; *selected_layout != state.state }); @@ -94,10 +110,13 @@ pub fn layout_manager_ui(ui: &mut egui::Ui, state: &mut ComposableView) { 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 open_selected = false; + let mut to_select: Option<PathBuf> = None; + let mut to_open: Option<PathBuf> = None; + let mut to_delete: Option<PathBuf> = None; for key in state.layout_manager.layouts.keys() { let name = key.to_str().unwrap(); @@ -119,30 +138,41 @@ pub fn layout_manager_ui(ui: &mut egui::Ui, state: &mut ComposableView) { 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() { - state.layout_manager.selection.replace(key.clone()); + to_select = Some(key.clone()); } if name_button_resp.double_clicked() { - state.layout_manager.selection.replace(key.clone()); - open_selected = true; + to_open = Some(key.clone()); } }); row.col(|ui| { if open_button.ui(ui).clicked() { - state.layout_manager.selection.replace(key.clone()); - open_selected = true; + to_open = Some(key.clone()); + } + }); + row.col(|ui| { + if delete_button.ui(ui).clicked() { + to_delete = Some(key.clone()); } }); }); } - if open_selected { + if let Some(to_select) = to_select { + state.layout_manager.selection.replace(to_select); + } + if let Some(to_open) = to_open { + state.layout_manager.selection.replace(to_open); state.try_display_selected_layout(); } + if let Some(to_delete) = to_delete { + state.layout_manager.delete(&to_delete); + } }); }); strip.cell(|ui| { @@ -150,8 +180,9 @@ pub fn layout_manager_ui(ui: &mut egui::Ui, state: &mut ComposableView) { }); strip.strip(|builder| { builder.sizes(Size::remainder(), 2).horizontal(|mut strip| { + // Load empty and import buttons strip.cell(|ui| { - // "Open empty layout" button + // Load empty button let open_empty_resp = ui.add_sized( Vec2::new(ui.available_width(), 0.0), Button::new("Load empty"), @@ -161,7 +192,7 @@ pub fn layout_manager_ui(ui: &mut egui::Ui, state: &mut ComposableView) { state.layout_manager.selection.take(); } - // Import layout ui + // Import button let import_layout_resp = ui .add_sized(Vec2::new(ui.available_width(), 0.0), Button::new("Import")); if import_layout_resp.clicked() { @@ -208,24 +239,39 @@ pub fn layout_manager_ui(ui: &mut egui::Ui, state: &mut ComposableView) { } } }); + // Layout save ui strip.cell(|ui| { - // Layout save ui - ui.add_enabled_ui(layout_changed, |ui| { - ui.add_sized( - Vec2::new(ui.available_width(), 0.0), - TextEdit::singleline(&mut state.layout_manager.text_input), - ); - ui.add_enabled_ui(!state.layout_manager.text_input.is_empty(), |ui| { - let save_resp = ui.add_sized( + let InnerResponse { inner: to_save, .. } = + ui.add_enabled_ui(layout_changed, |ui| { + // Text edit + let text_edit_resp = ui.add_sized( Vec2::new(ui.available_width(), 0.0), - Button::new("Save layout"), + TextEdit::singleline(&mut state.layout_manager.text_input), ); - if save_resp.clicked() { - let name = state.layout_manager.text_input.clone(); - state.save_new_layout(&name); - } + + // Save button + let InnerResponse { + inner: save_button_resp, + .. + } = ui.add_enabled_ui( + !state.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 { + state.save_new_layout(&state.layout_manager.text_input.clone()); + } }); }); }); -- GitLab From ac18f34ea8f733d11e6f0dd6b5a1e7629d48acd5 Mon Sep 17 00:00:00 2001 From: Alberto Nidasio <alberto.nidasio@skywarder.eu> Date: Thu, 28 Nov 2024 21:26:38 +0100 Subject: [PATCH 12/16] Simplified layouts loading from disk --- src/ui/layout_manager.rs | 33 +++++++++++++-------------------- 1 file changed, 13 insertions(+), 20 deletions(-) diff --git a/src/ui/layout_manager.rs b/src/ui/layout_manager.rs index b60157f..d418620 100644 --- a/src/ui/layout_manager.rs +++ b/src/ui/layout_manager.rs @@ -1,9 +1,6 @@ use std::{collections::BTreeMap, fs, path::PathBuf, str::FromStr}; -use egui::{ - ahash::HashSet, Button, Color32, InnerResponse, RichText, Separator, Stroke, TextEdit, Vec2, - Widget, -}; +use egui::{Button, Color32, InnerResponse, RichText, Separator, Stroke, TextEdit, Vec2, Widget}; use egui_extras::{Column, Size, StripBuilder, TableBuilder}; use egui_file::FileDialog; @@ -52,23 +49,20 @@ impl LayoutManager { /// Scans the layout directory and reloads the layouts pub fn reload_layouts(&mut self) { - // TODO: Do a simlier complete reload if let Ok(files) = self.layouts_path.read_dir() { - let layout_files: HashSet<PathBuf> = - files.flat_map(|x| x).map(|path| path.path()).collect(); - - // Remove the layouts which are not in the layouts folder - self.layouts.retain(|k, _| layout_files.contains(k)); - - // Load new layouts which we don't already have - layout_files.iter().for_each(|path| { - if !self.layouts.contains_key(path) { - if let Some(layout) = ComposableViewState::from_file(path) { - self.layouts - .insert(path.file_stem().unwrap().into(), layout); + 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(); } } @@ -81,7 +75,6 @@ impl LayoutManager { pub fn delete(&mut self, key: &PathBuf) { if self.layouts.contains_key(key) { - // TODO: When addressing live realod issue add `self.layouts.remove(key);` let _ = fs::remove_file(self.layouts_path.join(key).with_extension("json")); } } -- GitLab From ba2b7976d4803cf28c0c8207ac5796aafcf0a5b7 Mon Sep 17 00:00:00 2001 From: Alberto Nidasio <alberto.nidasio@skywarder.eu> Date: Tue, 3 Dec 2024 15:07:31 +0100 Subject: [PATCH 13/16] Fixed typos --- src/ui/composable_view.rs | 6 +++--- src/ui/layout_manager.rs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/ui/composable_view.rs b/src/ui/composable_view.rs index 2f099a5..7145bb7 100644 --- a/src/ui/composable_view.rs +++ b/src/ui/composable_view.rs @@ -139,10 +139,10 @@ impl ComposableView { } pub fn try_display_selected_layout(&mut self) { - if let Some(slection) = self.layout_manager.selection.as_ref() { - if let Some(selected_layout) = self.layout_manager.layouts.get(slection) { + if let Some(selection) = self.layout_manager.selection.as_ref() { + if let Some(selected_layout) = self.layout_manager.layouts.get(selection) { self.state = selected_layout.clone(); - self.layout_manager.displayed = Some(slection.clone()); + self.layout_manager.displayed = Some(selection.clone()); } } } diff --git a/src/ui/layout_manager.rs b/src/ui/layout_manager.rs index d418620..c79fed8 100644 --- a/src/ui/layout_manager.rs +++ b/src/ui/layout_manager.rs @@ -39,7 +39,7 @@ impl LayoutManager { layout_manager } - // Saves in permanent storage the file name of the currently displayed layout + /// 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()); -- GitLab From 8755dd1b913d3a593ea572eeccb3b8efcd7ee401 Mon Sep 17 00:00:00 2001 From: Alberto Nidasio <alberto.nidasio@skywarder.eu> Date: Thu, 5 Dec 2024 20:54:45 +0100 Subject: [PATCH 14/16] Moved LayoutManager related functions from ComposibleView --- src/ui/composable_view.rs | 19 +------------------ src/ui/layout_manager.rs | 24 +++++++++++++++++++++--- 2 files changed, 22 insertions(+), 21 deletions(-) diff --git a/src/ui/composable_view.rs b/src/ui/composable_view.rs index 7145bb7..f53b630 100644 --- a/src/ui/composable_view.rs +++ b/src/ui/composable_view.rs @@ -134,26 +134,9 @@ impl ComposableView { layout_manager, ..Self::default() }; - composable_view.try_display_selected_layout(); + LayoutManager::try_display_selected_layout(&mut composable_view); composable_view } - - pub fn try_display_selected_layout(&mut self) { - if let Some(selection) = self.layout_manager.selection.as_ref() { - if let Some(selected_layout) = self.layout_manager.layouts.get(selection) { - self.state = selected_layout.clone(); - self.layout_manager.displayed = Some(selection.clone()); - } - } - } - - pub fn save_new_layout(&mut self, name: &String) { - let layouts_path = &self.layout_manager.layouts_path; - let path = layouts_path.join(name).with_extension("json"); - self.state.to_file(&path); - self.layout_manager.selection.replace(name.into()); - self.layout_manager.displayed.replace(name.into()); - } } #[derive(Serialize, Deserialize, Clone, PartialEq)] diff --git a/src/ui/layout_manager.rs b/src/ui/layout_manager.rs index c79fed8..63b8c08 100644 --- a/src/ui/layout_manager.rs +++ b/src/ui/layout_manager.rs @@ -78,6 +78,23 @@ impl LayoutManager { 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 layout_manager_ui(ui: &mut egui::Ui, state: &mut ComposableView) { @@ -161,7 +178,7 @@ pub fn layout_manager_ui(ui: &mut egui::Ui, state: &mut ComposableView) { } if let Some(to_open) = to_open { state.layout_manager.selection.replace(to_open); - state.try_display_selected_layout(); + LayoutManager::try_display_selected_layout(state); } if let Some(to_delete) = to_delete { state.layout_manager.delete(&to_delete); @@ -224,7 +241,7 @@ pub fn layout_manager_ui(ui: &mut egui::Ui, state: &mut ComposableView) { .selection .replace(file_name.into()); state.layout_manager.reload_layouts(); - state.try_display_selected_layout(); + LayoutManager::try_display_selected_layout(state); } Err(e) => println!("Error importing layout: {:?}", e), } @@ -263,7 +280,8 @@ pub fn layout_manager_ui(ui: &mut egui::Ui, state: &mut ComposableView) { }); if to_save { - state.save_new_layout(&state.layout_manager.text_input.clone()); + let name = state.layout_manager.text_input.clone(); + LayoutManager::save_current_layout(state, &name); } }); }); -- GitLab From e8cf89e35f02d16307ee65669bf7083e17419d5a Mon Sep 17 00:00:00 2001 From: Alberto Nidasio <alberto.nidasio@skywarder.eu> Date: Thu, 5 Dec 2024 21:29:56 +0100 Subject: [PATCH 15/16] Improved LayoutManager functionalities encapsulation --- src/ui/composable_view.rs | 19 +- src/ui/layout_manager.rs | 383 ++++++++++++++++++++------------------ 2 files changed, 203 insertions(+), 199 deletions(-) diff --git a/src/ui/composable_view.rs b/src/ui/composable_view.rs index f53b630..370c01c 100644 --- a/src/ui/composable_view.rs +++ b/src/ui/composable_view.rs @@ -1,5 +1,5 @@ use super::{ - layout_manager::{layout_manager_ui, LayoutManager}, + layout_manager::LayoutManager, panes::{Pane, PaneBehavior}, shortcuts, }; @@ -96,15 +96,7 @@ impl eframe::App for ComposableView { egui::global_theme_preference_switch(ui); if ui.button("Layout Manager").clicked() { - self.layout_manager.open = !self.layout_manager.open; - - if self.layout_manager.open { - // When opening, we set the selection to the current layout - self.layout_manager.selection = self.layout_manager.displayed.clone(); - } else { - // When closing, we delete also the file dialog - self.layout_manager.file_dialog.take(); - } + self.layout_manager.toggle_open_state(); } }) }); @@ -114,12 +106,7 @@ impl eframe::App for ComposableView { panes_tree.ui(&mut self.behavior, ui); }); - let mut window_visible = self.layout_manager.open; - egui::Window::new("Layouts Manager") - .collapsible(false) - .open(&mut window_visible) - .show(ctx, |ui| layout_manager_ui(ui, self)); - self.layout_manager.open = window_visible; + LayoutManager::show(self, ctx); } fn save(&mut self, storage: &mut dyn eframe::Storage) { diff --git a/src/ui/layout_manager.rs b/src/ui/layout_manager.rs index 63b8c08..526505f 100644 --- a/src/ui/layout_manager.rs +++ b/src/ui/layout_manager.rs @@ -1,6 +1,9 @@ use std::{collections::BTreeMap, fs, path::PathBuf, str::FromStr}; -use egui::{Button, Color32, InnerResponse, RichText, Separator, Stroke, TextEdit, Vec2, Widget}; +use egui::{ + Button, Color32, Context, InnerResponse, RichText, Separator, Stroke, TextEdit, Ui, Vec2, + Widget, +}; use egui_extras::{Column, Size, StripBuilder, TableBuilder}; use egui_file::FileDialog; @@ -11,18 +14,18 @@ static SELECTED_LAYOUT_KEY: &str = "selected_layout"; #[derive(Default)] pub struct LayoutManager { - pub open: bool, + open: bool, /// Currently dislayed layout in the ui - pub displayed: Option<PathBuf>, + displayed: Option<PathBuf>, /// Currently selected layout in the list, gets reset to the displayed layout when the dialog is opened - pub selection: Option<PathBuf>, + selection: Option<PathBuf>, - pub text_input: String, - pub file_dialog: Option<FileDialog>, - pub layouts: BTreeMap<PathBuf, ComposableViewState>, - pub layouts_path: PathBuf, + text_input: String, + file_dialog: Option<FileDialog>, + layouts: BTreeMap<PathBuf, ComposableViewState>, + layouts_path: PathBuf, } impl LayoutManager { @@ -95,196 +98,210 @@ impl LayoutManager { cv.layout_manager.selection.replace(name.into()); cv.layout_manager.displayed.replace(name.into()); } -} -pub fn layout_manager_ui(ui: &mut egui::Ui, state: &mut ComposableView) { - // TODO: Replace with a reload button instead of reloading at every frame - state.layout_manager.reload_layouts(); + pub fn toggle_open_state(&mut self) { + self.open = !self.open; - let layout_changed = state - .layout_manager - .get_selected() - .map_or(true, |selection| { - let selected_layout = selection; - *selected_layout != state.state - }); + 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(); + } + } - // 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| { - 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 state.layout_manager.layouts.keys() { - let name = key.to_str().unwrap(); - let is_selected = state - .layout_manager - .selection - .as_ref() - .map_or_else(|| false, |selected_key| selected_key == key); - - let name_button = if is_selected && layout_changed { - Button::new(RichText::new(name).color(Color32::BLACK)) - .stroke(Stroke::new(1.0, Color32::BROWN)) - .fill(Color32::YELLOW) - } else if is_selected && !layout_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()); - } - }); - }); - } + 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(); - if let Some(to_select) = to_select { - state.layout_manager.selection.replace(to_select); - } - if let Some(to_open) = to_open { - state.layout_manager.selection.replace(to_open); - LayoutManager::try_display_selected_layout(state); - } - if let Some(to_delete) = to_delete { - state.layout_manager.delete(&to_delete); - } + 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) + }); }); }); - strip.cell(|ui| { - ui.add(Separator::default().spacing(7.0)); + 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); + } }); - strip.strip(|builder| { - 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() { - state.state = ComposableViewState::default(); - state.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(); - state.layout_manager.file_dialog = Some(file_dialog); - } - if let Some(file_dialog) = &mut state.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 = - state.layout_manager.layouts_path.join(file_name); - - // First check if the layouts folder exists - if !state.layout_manager.layouts_path.exists() { - match fs::create_dir_all(&state.layout_manager.layouts_path) - { - Ok(_) => println!("Created layouts folder"), - Err(e) => { - println!("Error creating layouts folder: {:?}", e) - } - } - } + 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(); + } - match fs::copy(file, destination.clone()) { - Ok(_) => { - println!( - "Layout imported in {}", - destination.to_str().unwrap() - ); - state - .layout_manager - .selection - .replace(file_name.into()); - state.layout_manager.reload_layouts(); - LayoutManager::try_display_selected_layout(state); - } - Err(e) => println!("Error importing layout: {:?}", e), + // 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"), + ) }); - // Layout save ui - strip.cell(|ui| { - let InnerResponse { inner: to_save, .. } = - ui.add_enabled_ui(layout_changed, |ui| { - // Text edit - let text_edit_resp = ui.add_sized( - Vec2::new(ui.available_width(), 0.0), - TextEdit::singleline(&mut state.layout_manager.text_input), - ); - - // Save button - let InnerResponse { - inner: save_button_resp, - .. - } = ui.add_enabled_ui( - !state.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 = state.layout_manager.text_input.clone(); - LayoutManager::save_current_layout(state, &name); - } - }); + + 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); + } }); }); + } } -- GitLab From b8940580270f59b6ef65afb8e59affdb59e42827 Mon Sep 17 00:00:00 2001 From: Alberto Nidasio <alberto.nidasio@skywarder.eu> Date: Thu, 5 Dec 2024 21:43:33 +0100 Subject: [PATCH 16/16] Layouts directory is now automatically created if missing --- src/ui/composable_view.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/ui/composable_view.rs b/src/ui/composable_view.rs index 370c01c..b2a0c5d 100644 --- a/src/ui/composable_view.rs +++ b/src/ui/composable_view.rs @@ -159,8 +159,24 @@ impl ComposableViewState { } 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) => { -- GitLab