From 57ddb71d971294137884f76f274b986134775db3 Mon Sep 17 00:00:00 2001 From: nrq6993-a11y Date: Sun, 18 Oct 2026 19:34:09 +0300 Subject: [PATCH 1/2] #1753: saved workspaces restore dock panel width Persist the main dock column width in workspace snapshots and reapply it when switching layouts. Co-authored-by: Cursor --- crates/ui-egui/src/dock.rs | 57 +++++++++++++++++++++++++++-- crates/ui-egui/src/panel_docking.rs | 9 ++++- crates/ui-egui/src/state.rs | 8 ++++ crates/ui-egui/src/workspaces.rs | 24 ++++++++++++ 4 files changed, 94 insertions(+), 4 deletions(-) diff --git a/crates/ui-egui/src/dock.rs b/crates/ui-egui/src/dock.rs index 561d67b02..e881559d1 100644 --- a/crates/ui-egui/src/dock.rs +++ b/crates/ui-egui/src/dock.rs @@ -16,6 +16,55 @@ const ICON_COL: f32 = 38.0; const HEADER: f32 = 14.0; /// The tabbed group's width until the dock has been laid out (its default size). const DOCK_WIDTH: f32 = 300.0; +pub(crate) const LEGACY_DOCK_PANEL: &str = "dock"; +pub(crate) const SHARED_DOCK_PANEL: &str = "shared-panel-dock"; +const LEGACY_DOCK_MIN: f32 = 230.0; +const LEGACY_DOCK_MAX: f32 = 520.0; +const SHARED_DOCK_MIN: f32 = 220.0; +const SHARED_DOCK_MAX: f32 = 720.0; + +pub(crate) fn clamp_legacy_dock_width(width: f32) -> f32 { + width.clamp(LEGACY_DOCK_MIN, LEGACY_DOCK_MAX) +} + +pub(crate) fn clamp_shared_dock_width(width: f32) -> f32 { + width.clamp(SHARED_DOCK_MIN, SHARED_DOCK_MAX) +} + +fn seed_dock_panel_width(ctx: &egui::Context, panel: &str, width: f32) { + let screen = ctx.content_rect(); + let w = width.max(1.0); + let outer_rect = egui::Rect::from_min_size(egui::pos2(screen.right() - w, screen.top()), egui::vec2(w, screen.height())); + let state = egui::containers::panel::PanelState { outer_rect }; + ctx.data_mut(|data| data.insert_persisted(egui::Id::new(panel), state)); +} + +pub(crate) fn apply_pending_dock_width(app: &mut VectorcraftApp, ctx: &egui::Context, panel: &str, clamp: fn(f32) -> f32) { + if !app.ui.dock_width_pending { + return; + } + if let Some(width) = app.ui.dock_width.filter(|w| w.is_finite() && *w > 0.0) { + seed_dock_panel_width(ctx, panel, clamp(width)); + } + app.ui.dock_width_pending = false; +} + +pub(crate) fn note_dock_width(app: &mut VectorcraftApp, ctx: &egui::Context, panel: &str) { + if let Some(state) = egui::containers::panel::PanelState::load(ctx, egui::Id::new(panel)) { + let width = state.size().x; + if width.is_finite() && width > 0.0 { + app.ui.dock_width = Some(width); + } + } +} + +pub(crate) fn legacy_dock_default_width(ui: &crate::state::UiState) -> f32 { + ui.dock_width.filter(|w| w.is_finite()).map(clamp_legacy_dock_width).unwrap_or(DOCK_WIDTH) +} + +pub(crate) fn shared_dock_default_width(ui: &crate::state::UiState) -> f32 { + ui.dock_width.filter(|w| w.is_finite()).map(clamp_shared_dock_width).unwrap_or(310.0) +} /// Top of a popped-out panel. const FLYOUT_TOP: f32 = 110.0; @@ -81,14 +130,15 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { if crate::panel_docking::show(app, ui) { return; } + apply_pending_dock_width(app, ui.ctx(), LEGACY_DOCK_PANEL, clamp_legacy_dock_width); let t = Tokens::get(ui.ctx()); let shown = floating::shown_tab(&app.ui); // Main tabbed group, unless it is collapsed to icons or all its panels float. let group = shown.filter(|_| !app.ui.dock_collapsed).map(|active| { - egui::Panel::right("dock") + egui::Panel::right(LEGACY_DOCK_PANEL) .resizable(true) - .default_size(DOCK_WIDTH) - .size_range(230.0..=520.0) + .default_size(legacy_dock_default_width(&app.ui)) + .size_range(LEGACY_DOCK_MIN..=LEGACY_DOCK_MAX) .frame(egui::Frame::NONE) .show(ui, |ui| { crate::theme::card(ui, &t, egui::Margin { left: 0, right: 6, top: 6, bottom: 6 }, |ui| { @@ -114,6 +164,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { .response .rect }); + note_dock_width(app, ui.ctx(), LEGACY_DOCK_PANEL); // Collapsed icon-panel strip, left of the expanded panel group (like Illustrator's dock). A // collapsed group's panels head it, under the « that expands them again. Floating panels leave // it. diff --git a/crates/ui-egui/src/panel_docking.rs b/crates/ui-egui/src/panel_docking.rs index 003175462..a351a982e 100644 --- a/crates/ui-egui/src/panel_docking.rs +++ b/crates/ui-egui/src/panel_docking.rs @@ -651,6 +651,8 @@ pub(crate) fn show(app: &mut VectorcraftApp, ui: &mut egui::Ui) -> bool { Some((layer, layout_rect, fill)) }) .collect(); + crate::dock::apply_pending_dock_width(app, ui.ctx(), crate::dock::SHARED_DOCK_PANEL, crate::dock::clamp_shared_dock_width); + let shared_dock_default = crate::dock::shared_dock_default_width(&app.ui); let mut draw = |ui: &mut egui::Ui| { if layout.root.is_some() && crate::dock::collapse_header(ui, collapsed) { let _ = app.run("window.collapseDock", json!({"collapsed":!collapsed})); @@ -717,10 +719,15 @@ pub(crate) fn show(app: &mut VectorcraftApp, ui: &mut egui::Ui) -> bool { output }; let output = if layout.root.is_some() { - let panel = egui::Panel::right("shared-panel-dock").default_size(310.0).size_range(220.0..=720.0).resizable(true).frame(egui::Frame::NONE); + let panel = egui::Panel::right(crate::dock::SHARED_DOCK_PANEL) + .default_size(shared_dock_default) + .size_range(220.0..=720.0) + .resizable(true) + .frame(egui::Frame::NONE); let panel = if collapsed { panel.exact_size(38.0).resizable(false) } else { panel }; let shown = panel.show(ui, |ui| crate::theme::card(ui, &t, egui::Margin { left: 0, right: 6, top: 6, bottom: 6 }, &mut draw)); ui.ctx().data_mut(|data| data.insert_temp(crate::floating::dock_rect_id(), shown.response.rect)); + crate::dock::note_dock_width(app, ui.ctx(), crate::dock::SHARED_DOCK_PANEL); shown.inner } else { draw(ui) diff --git a/crates/ui-egui/src/state.rs b/crates/ui-egui/src/state.rs index 4fdfd0ce5..a27c7ba5f 100644 --- a/crates/ui-egui/src/state.rs +++ b/crates/ui-egui/src/state.rs @@ -354,6 +354,12 @@ pub struct UiState { /// Invalidates in-flight gestures when a workspace is replaced. #[serde(skip)] pub docking_generation: u64, + /// The main dock column's outer width in screen points (legacy or shared docking). + #[serde(default, skip_serializing_if = "Option::is_none")] + pub dock_width: Option, + /// Next frame should seed egui [`PanelState`] from [`Self::dock_width`] (workspace switch). + #[serde(skip)] + pub dock_width_pending: bool, /// The Tools panel floats with its top-left corner here (dragged out by its title bar); `None`: /// docked at the window's left edge. #[serde(default)] @@ -772,6 +778,8 @@ impl Default for UiState { docking_hidden: Default::default(), docking_icons: Default::default(), docking_generation: 0, + dock_width: None, + dock_width_pending: false, toolbar_pos: None, status_bar: true, dock: true, diff --git a/crates/ui-egui/src/workspaces.rs b/crates/ui-egui/src/workspaces.rs index bcc459a34..56a48e15b 100644 --- a/crates/ui-egui/src/workspaces.rs +++ b/crates/ui-egui/src/workspaces.rs @@ -41,6 +41,9 @@ pub struct Workspace { pub docking_icons: std::collections::BTreeSet, /// Where the Tools panel floats (built-in workspaces: docked). pub toolbar_pos: Option<[f32; 2]>, + /// Main dock column width in screen points (#1753). + #[serde(default, skip_serializing_if = "Option::is_none")] + pub dock_width: Option, } impl Default for Workspace { @@ -140,6 +143,7 @@ pub fn capture(ui: &UiState, name: &str) -> Workspace { docking_hidden: ui.docking_hidden.clone(), docking_icons: ui.docking_icons.clone(), toolbar_pos: ui.toolbar_pos, + dock_width: ui.dock_width, } } @@ -165,6 +169,8 @@ pub fn apply(ui: &mut UiState, w: &Workspace) { ui.docking_icons = w.docking_icons.iter().filter(|id| crate::state::ICON_PANELS.iter().any(|entry| entry.0 == id.as_str())).take(64).cloned().collect(); ui.toolbar_pos = w.toolbar_pos; + ui.dock_width = w.dock_width; + ui.dock_width_pending = true; FloatingPanels::sanitize(&mut ui.floating_panels, &mut ui.toolbar_pos); ui.flyout = None; ui.workspace = w.name.clone(); @@ -491,4 +497,22 @@ mod tests { assert!(find(&ui, n).is_some(), "{n}"); } } + + #[test] + fn saved_workspaces_restore_dock_width() { + let mut ui = UiState::default(); + ui.dock_width = Some(420.0); + save_as(&mut ui, "Wide").unwrap(); + ui.dock_width = Some(260.0); + save_as(&mut ui, "Narrow").unwrap(); + let wide = find(&ui, "Wide").unwrap(); + apply(&mut ui, &wide); + assert_eq!(ui.dock_width, Some(420.0)); + assert!(ui.dock_width_pending); + let narrow = find(&ui, "Narrow").unwrap(); + apply(&mut ui, &narrow); + assert_eq!(ui.dock_width, Some(260.0)); + let back: UiState = serde_json::from_value(serde_json::to_value(&ui).unwrap()).unwrap(); + assert_eq!(back.custom_workspaces[1].dock_width, Some(260.0)); + } } From b529f356cc375a0e1cd332714adefda600890f70 Mon Sep 17 00:00:00 2001 From: bflatastic Date: Mon, 19 Oct 2026 00:28:48 +0700 Subject: [PATCH 2/2] Tests: the dock-width test builds its UiState with the width, as clippy asks (#2953) --- crates/ui-egui/src/workspaces.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/ui-egui/src/workspaces.rs b/crates/ui-egui/src/workspaces.rs index 56a48e15b..055860213 100644 --- a/crates/ui-egui/src/workspaces.rs +++ b/crates/ui-egui/src/workspaces.rs @@ -500,8 +500,7 @@ mod tests { #[test] fn saved_workspaces_restore_dock_width() { - let mut ui = UiState::default(); - ui.dock_width = Some(420.0); + let mut ui = UiState { dock_width: Some(420.0), ..Default::default() }; save_as(&mut ui, "Wide").unwrap(); ui.dock_width = Some(260.0); save_as(&mut ui, "Narrow").unwrap();