From 59bd4ec2d7b9d10a053bc350d28668a2f6a1bb7d Mon Sep 17 00:00:00 2001
From: Alberto Nidasio <alberto.nidasio@skywarder.eu>
Date: Mon, 9 Dec 2024 18:45:05 +0000
Subject: [PATCH] Implemented basic layout persistance

---
 Cargo.lock                      |  93 ++++++++++
 Cargo.toml                      |   4 +-
 src/main.rs                     |  12 +-
 src/ui.rs                       |   1 +
 src/ui/composable_view.rs       | 145 +++++++++++----
 src/ui/layout_manager.rs        | 307 ++++++++++++++++++++++++++++++++
 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, 557 insertions(+), 43 deletions(-)
 create mode 100644 src/ui/layout_manager.rs

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