docking: save every floating window's id (#317)

This commit is contained in:
aiongg authored and GitHub committed 2026-10-25 09:50:13 -04:00
1 parent e9824219c7
commit ea4c823abb
4 files changed
+39 -17

No files matched your search

+9 -8
View File
@@ -61,15 +61,16 @@ pub struct StackEntry<P> {
/// 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<P> {
/// 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<P>,
@@ -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<u64>,
id: u64,
root: &'a Node<P>,
rect: [f32; 4],
#[serde(skip_serializing_if = "Option::is_none")]
@@ -206,10 +207,10 @@ mod floating_serde {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
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,
+22 -3
View File
@@ -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::<Floating<u32>>(value).unwrap(), window);
}
assert!(serde_json::from_value::<Floating<u32>>(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<u32> = serde_json::from_str(&saved).unwrap();
assert_eq!(loaded, layout);
assert_eq!(loaded.floating.iter().map(|w| w.id).collect::<Vec<_>>(), [7, 8]);
}
#[test]
fn v2_round_trips() {
let mut workspace = Workspace::new(docked());
@@ -668,8 +682,13 @@ mod serde_tests {
}
let layout: super::Layout<String> = 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}");
}
}
}
+3 -2
View File
@@ -581,8 +581,9 @@ fn a_native_window_saves_its_id_place_and_monitor() {
let copies: Layout<u32> = 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.
+5 -4
View File
@@ -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