From b71afe6cf2a9f10c2174bfae228ef00dfb865fe7 Mon Sep 17 00:00:00 2001 From: Andrew Collins Date: Sat, 10 Aug 2024 13:29:08 +1000 Subject: [PATCH] Save zoom and focused window from layout so that it can be restored (#143) --- Cargo.lock | 15 ++- crates/modalkit-ratatui/Cargo.toml | 1 + crates/modalkit-ratatui/src/screen.rs | 27 ++++-- crates/modalkit-ratatui/src/windows/layout.rs | 54 ++++++++++- crates/modalkit-ratatui/src/windows/mod.rs | 7 +- .../modalkit-ratatui/tests/window-layout.json | 91 +++++++++++++++++++ 6 files changed, 181 insertions(+), 14 deletions(-) create mode 100644 crates/modalkit-ratatui/tests/window-layout.json diff --git a/Cargo.lock b/Cargo.lock index ba480f1..1288e31 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -503,7 +503,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c2a198fb6b0eada2a8df47933734e6d35d350665a33a3593d7164fa52c75c19" dependencies = [ "cfg-if", - "windows-targets 0.48.5", + "windows-targets 0.52.5", ] [[package]] @@ -614,6 +614,7 @@ dependencies = [ "ratatui", "regex", "serde", + "serde_json", ] [[package]] @@ -983,6 +984,18 @@ dependencies = [ "syn 2.0.52", ] +[[package]] +name = "serde_json" +version = "1.0.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784b6203951c57ff748476b126ccb5e8e2959a5c19e5c617ab1956be3dbc68da" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + [[package]] name = "signal-hook" version = "0.3.17" diff --git a/crates/modalkit-ratatui/Cargo.toml b/crates/modalkit-ratatui/Cargo.toml index 7991515..2d9a84c 100644 --- a/crates/modalkit-ratatui/Cargo.toml +++ b/crates/modalkit-ratatui/Cargo.toml @@ -22,3 +22,4 @@ serde = { version = "^1.0", features = ["derive"] } [dev-dependencies] rand = { workspace = true } +serde_json = "1.0.122" diff --git a/crates/modalkit-ratatui/src/screen.rs b/crates/modalkit-ratatui/src/screen.rs index 033662d..d4bfce3 100644 --- a/crates/modalkit-ratatui/src/screen.rs +++ b/crates/modalkit-ratatui/src/screen.rs @@ -22,7 +22,7 @@ use ratatui::{ use super::{ cmdbar::{CommandBar, CommandBarState}, util::{rect_down, rect_zero_height}, - windows::{WindowActions, WindowLayout, WindowLayoutDescription, WindowLayoutState}, + windows::{WindowActions, WindowLayout, WindowLayoutRoot, WindowLayoutState}, TerminalCursor, Window, WindowOps, @@ -244,23 +244,33 @@ fn bold<'a>(s: String) -> Span<'a> { #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(bound(deserialize = "I::WindowId: Deserialize<'de>"))] #[serde(bound(serialize = "I::WindowId: Serialize"))] -pub struct TabLayoutDescription { +pub struct TabbedLayoutDescription { /// The description of the window layout for each tab. - pub tabs: Vec>, + pub tabs: Vec>, + /// The index of the last focused tab + pub focused: usize, } -impl TabLayoutDescription { +impl TabbedLayoutDescription { /// Create a new collection of tabs from this description. pub fn to_layout>( self, area: Option, store: &mut Store, ) -> UIResult>, I> { - self.tabs + let mut tabs = self + .tabs .into_iter() .map(|desc| desc.to_layout(area, store)) .collect::, I>>() - .map(FocusList::new) + .map(FocusList::new)?; + + // Count starts at 1 + let change = FocusChange::Offset(Count::Exact(self.focused + 1), true); + let ctx = EditContext::default(); + tabs.focus(&change, &ctx); + + Ok(tabs) } } @@ -314,9 +324,10 @@ where } /// Get a description of the open tabs and their window layouts. - pub fn as_description(&self) -> TabLayoutDescription { - TabLayoutDescription { + pub fn as_description(&self) -> TabbedLayoutDescription { + TabbedLayoutDescription { tabs: self.tabs.iter().map(WindowLayoutState::as_description).collect(), + focused: self.tabs.pos(), } } diff --git a/crates/modalkit-ratatui/src/windows/layout.rs b/crates/modalkit-ratatui/src/windows/layout.rs index f99653f..dfad9e7 100644 --- a/crates/modalkit-ratatui/src/windows/layout.rs +++ b/crates/modalkit-ratatui/src/windows/layout.rs @@ -1102,6 +1102,34 @@ where } } +/// Data structure holding layout description and state +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[serde(bound(deserialize = "I::WindowId: Deserialize<'de>"))] +#[serde(bound(serialize = "I::WindowId: Serialize"))] +#[serde(rename_all = "lowercase")] +pub struct WindowLayoutRoot { + layout: WindowLayoutDescription, + focused: usize, + zoomed: bool, +} + +impl WindowLayoutRoot +where + I: ApplicationInfo, +{ + /// Restore a layout from a description of windows and splits. + pub fn to_layout>( + self, + area: Option, + store: &mut Store, + ) -> UIResult, I> { + let mut layout = self.layout.to_layout(area, store)?; + layout._focus(self.focused); + layout.zoom = self.zoomed; + Ok(layout) + } +} + /// A description of a window layout. #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] #[serde(bound(deserialize = "I::WindowId: Deserialize<'de>"))] @@ -1282,18 +1310,28 @@ where } /// Convert this layout to a serializable summary of its windows and splits. - pub fn as_description(&self) -> WindowLayoutDescription { + pub fn as_description(&self) -> WindowLayoutRoot { let mut children = vec![]; + let focused = self.focused; + let zoomed = self.zoom; let Some(root) = &self.root else { - return WindowLayoutDescription::Split { children, length: None }; + return WindowLayoutRoot { + layout: WindowLayoutDescription::Split { children, length: None }, + focused, + zoomed, + }; }; for w in root.iter() { children.push(w.into()); } - return WindowLayoutDescription::Split { children, length: None }; + return WindowLayoutRoot { + layout: WindowLayoutDescription::Split { children, length: None }, + focused, + zoomed, + }; } /// Create a new instance containing a single [Window] displaying some content. @@ -2808,6 +2846,7 @@ mod tests { let (mut tree, mut store, _) = three_by_three(); let mut buffer = Buffer::empty(Rect::new(0, 0, 60, 60)); let area = Rect::new(0, 0, 60, 60); + tree._focus(3); // Draw so that everything gets an initial area. WindowLayout::new(&mut store).render(area, &mut buffer, &mut tree); @@ -2855,7 +2894,9 @@ mod tests { }], length: None, }; - assert_eq!(desc1, exp); + assert_eq!(desc1.layout, exp); + assert_eq!(desc1.focused, 3); + assert_eq!(desc1.zoomed, false); // Turn back into a layout, and then generate a new description to show it's the same. let tree = desc1 @@ -2863,5 +2904,10 @@ mod tests { .to_layout::(tree.info.area.into(), &mut store) .unwrap(); assert_eq!(tree.as_description(), desc1); + + // Test against an example JSON serialization to test naming. + let serialized = serde_json::to_string_pretty(&desc1).unwrap(); + let exp = include_str!("../../tests/window-layout.json"); + assert_eq!(serialized, exp.trim_end()); } } diff --git a/crates/modalkit-ratatui/src/windows/mod.rs b/crates/modalkit-ratatui/src/windows/mod.rs index fbf2683..8d0c2a5 100644 --- a/crates/modalkit-ratatui/src/windows/mod.rs +++ b/crates/modalkit-ratatui/src/windows/mod.rs @@ -30,7 +30,12 @@ mod size; mod slot; mod tree; -pub use self::layout::{WindowLayout, WindowLayoutDescription, WindowLayoutState}; +pub use self::layout::{ + WindowLayout, + WindowLayoutDescription, + WindowLayoutRoot, + WindowLayoutState, +}; struct AxisTreeNode { value: Value, diff --git a/crates/modalkit-ratatui/tests/window-layout.json b/crates/modalkit-ratatui/tests/window-layout.json new file mode 100644 index 0000000..da6d716 --- /dev/null +++ b/crates/modalkit-ratatui/tests/window-layout.json @@ -0,0 +1,91 @@ +{ + "layout": { + "type": "split", + "children": [ + { + "type": "split", + "children": [ + { + "type": "split", + "children": [ + { + "type": "split", + "children": [ + { + "type": "window", + "window": 0, + "length": 20 + }, + { + "type": "window", + "window": 1, + "length": 20 + } + ], + "length": 20 + }, + { + "type": "split", + "children": [ + { + "type": "window", + "window": 2, + "length": 20 + }, + { + "type": "window", + "window": 3, + "length": 20 + } + ], + "length": 20 + }, + { + "type": "split", + "children": [ + { + "type": "window", + "window": 4, + "length": 20 + }, + { + "type": "window", + "window": 5, + "length": 20 + } + ], + "length": 20 + } + ], + "length": 40 + }, + { + "type": "split", + "children": [ + { + "type": "window", + "window": 6, + "length": 20 + }, + { + "type": "window", + "window": 7, + "length": 20 + }, + { + "type": "window", + "window": 8, + "length": 20 + } + ], + "length": 20 + } + ], + "length": 60 + } + ], + "length": null + }, + "focused": 3, + "zoomed": false +}