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