From ea4c823abb46fa9d21361604ed1c6d21c54e9ca8 Mon Sep 17 00:00:00 2001
From: aiongg <67543387+aiongg@users.noreply.github.com>
Date: Sun, 25 Oct 2026 09:50:13 -0400
Subject: [PATCH] docking: save every floating window's id (#317)
---
crates/ui/src/docking/model.rs | 17 +++++++++--------
crates/ui/src/docking/model_tests.rs | 25 ++++++++++++++++++++++---
crates/ui/src/docking/native_tests.rs | 5 +++--
docs/ui-docking.md | 9 +++++----
4 files changed, 39 insertions(+), 17 deletions(-)
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