diff --git a/crates/ui/src/docking/model.rs b/crates/ui/src/docking/model.rs index be2ae3a3..360724ab 100644 --- a/crates/ui/src/docking/model.rs +++ b/crates/ui/src/docking/model.rs @@ -61,15 +61,16 @@ pub struct StackEntry

{ /// Serde reads the v1 shape `{panels, active, rect}` as one group, and a stack of groups /// `{groups, rect}` as a vertical split of them, sized by their heights. A window with one /// group (not minimized, no native rect) writes the v1 shape, so older builds can read files -/// that use no newer feature. A native window also writes its `id`; any other window gets a -/// fresh one on load. +/// that use no newer feature. Every window writes its `id`; a window read without one gets a +/// fresh one. /// /// Equality ignores `id`: two windows with the same contents and place are the same layout. #[derive(Clone, Debug)] pub struct Floating

{ /// Names the window across panel moves, and its OS window with feature `native-windows`. - /// Unique within a layout: [`Self::new`] and loading draw fresh ids, [`Self::with_id`] - /// sets one, and merging windows keeps the receiving window's. + /// Unique within a layout: [`Self::new`] draws a fresh id, [`Self::with_id`] sets one, + /// loading keeps the saved id (or draws one when it is missing or taken), and merging + /// windows keeps the receiving window's. pub id: u64, /// `Split`, `Tabs` and `Stack` nodes; a `Dock` is invalid here. pub root: Node

, @@ -181,6 +182,7 @@ mod floating_serde { #[derive(Serialize)] struct V1<'a, P> { + id: u64, panels: &'a [P], active: usize, rect: [f32; 4], @@ -188,8 +190,7 @@ mod floating_serde { #[derive(Serialize)] struct Tree<'a, P> { - #[serde(skip_serializing_if = "Option::is_none")] - id: Option, + id: u64, root: &'a Node

, rect: [f32; 4], #[serde(skip_serializing_if = "Option::is_none")] @@ -206,10 +207,10 @@ mod floating_serde { fn serialize(&self, serializer: S) -> Result { match &self.root { Node::Tabs { panels, active } if !self.minimized && self.native.is_none() => { - V1 { panels, active: *active, rect: self.rect }.serialize(serializer) + V1 { id: self.id, panels, active: *active, rect: self.rect }.serialize(serializer) } root => Tree { - id: self.native.map(|_| self.id), + id: self.id, root, rect: self.rect, native: self.native, diff --git a/crates/ui/src/docking/model_tests.rs b/crates/ui/src/docking/model_tests.rs index 9327fd4d..c315cdf2 100644 --- a/crates/ui/src/docking/model_tests.rs +++ b/crates/ui/src/docking/model_tests.rs @@ -576,18 +576,32 @@ mod serde_tests { fn single_plain_floating_window_writes_the_v1_shape() { let rect = [1.0, 2.0, 300.0, 200.0]; let plain = Floating::single(vec![4u32, 5], 1, rect); - assert_eq!(serde_json::to_value(&plain).unwrap(), json!({"panels": [4, 5], "active": 1, "rect": rect})); + assert_eq!(serde_json::to_value(&plain).unwrap(), json!({"id": plain.id, "panels": [4, 5], "active": 1, "rect": rect})); let minimized = Floating { minimized: true, ..Floating::single(vec![4u32], 0, rect) }; let stacked = Floating::stack(vec![g(&[4], 0), g(&[5], 0)], rect).unwrap(); let native = Floating { native: Some(rect), ..Floating::single(vec![4u32], 0, rect) }; for window in [minimized, stacked, native] { let value = serde_json::to_value(&window).unwrap(); assert!(value.get("root").is_some() && value.get("panels").is_none(), "{value}"); + assert_eq!(value["id"], json!(window.id), "{value}"); assert_eq!(serde_json::from_value::>(value).unwrap(), window); } assert!(serde_json::from_value::>(json!({"rect": rect})).is_err()); } + #[test] + fn in_window_windows_keep_their_ids_through_a_save() { + let rect = [1.0, 2.0, 300.0, 200.0]; + let layout = Layout { + root: None, + floating: vec![Floating::single(vec![4u32], 0, rect).with_id(7), Floating::stack(vec![g(&[5], 0), g(&[6], 0)], rect).unwrap().with_id(8)], + }; + let saved = serde_json::to_string(&layout).unwrap(); + let loaded: Layout = serde_json::from_str(&saved).unwrap(); + assert_eq!(loaded, layout); + assert_eq!(loaded.floating.iter().map(|w| w.id).collect::>(), [7, 8]); + } + #[test] fn v2_round_trips() { let mut workspace = Workspace::new(docked()); @@ -668,8 +682,13 @@ mod serde_tests { } let layout: super::Layout = serde_json::from_value(layout_value.clone()).unwrap_or_else(|e| panic!("{app}{pointer}: {e}")); layout.validate().unwrap_or_else(|e| panic!("{app}{pointer}: {e}")); - // Nothing new is in use, so the layout writes back exactly as read. - assert_eq!(serde_json::to_value(&layout).unwrap(), layout_value, "{app}{pointer}"); + // Nothing new is in use, so the layout writes back exactly as read, plus the + // fresh id of each floating window. + let mut written = serde_json::to_value(&layout).unwrap(); + for (window, value) in layout.floating.iter().zip(written["floating"].as_array_mut().unwrap()) { + assert_eq!(value.as_object_mut().unwrap().remove("id"), Some(json!(window.id)), "{app}{pointer}"); + } + assert_eq!(written, layout_value, "{app}{pointer}"); } } } diff --git a/crates/ui/src/docking/native_tests.rs b/crates/ui/src/docking/native_tests.rs index 258af673..d8711abc 100644 --- a/crates/ui/src/docking/native_tests.rs +++ b/crates/ui/src/docking/native_tests.rs @@ -581,8 +581,9 @@ fn a_native_window_saves_its_id_place_and_monitor() { let copies: Layout = serde_json::from_str(&serde_json::to_string(&copies).unwrap()).unwrap(); assert_eq!(copies.floating[0].id, layout.floating[0].id); assert_ne!(copies.floating[1].id, copies.floating[0].id); - // An in-window window writes no id, as older builds did. - assert!(!serde_json::to_string(&Floating::single(vec![1u32], 0, [0.0, 0.0, 9.0, 9.0])).unwrap().contains("id")); + // An in-window window writes its id too. + let in_window = Floating::single(vec![1u32], 0, [0.0, 0.0, 9.0, 9.0]).with_id(3); + assert!(serde_json::to_string(&in_window).unwrap().contains(r#""id":3"#)); } /// Height of the bar at the top of a native window. diff --git a/docs/ui-docking.md b/docs/ui-docking.md index 39152ee6..c47357e4 100644 --- a/docs/ui-docking.md +++ b/docs/ui-docking.md @@ -413,10 +413,11 @@ the main window has never reported its `outer_rect`; an app that forces X11 in c windows from the first frame. `DockArea::native_windows(bool)` overrides the choice either way. - **Identity.** `Floating::id` names the OS window, so a window keeps it when its panels change, - including when its first panel moves out. `Floating::new` and loading draw fresh ids, and - `Floating::with_id` sets one; a layout - loads with unique ids; a window that merges into another takes the receiver's id, and a whole - window that moves (floated again, or its only group dragged) keeps its own. + including when its first panel moves out. `Floating::new` draws a fresh id and + `Floating::with_id` sets one. A saved layout stores every window's id and loads with unique + ids: a window saved without an id, or with one an earlier window took, gets a fresh one. A + window that merges into another takes the receiver's id, and a whole window that moves + (floated again, or its only group dragged) keeps its own. - **Kind and level.** `DockContent::window_kind` sorts panels into palettes (`WindowKind::Palette`, the default) and viewers (`WindowKind::Normal`), such as a video monitor on a second display. A window holding any `Normal` panel is a normal window, whichever