From 618c19116699bf70b3debce8a3ed11c707323bc8 Mon Sep 17 00:00:00 2001 From: AlPaka1234 <356241429+AlPaka1234@users.noreply.github.com> Date: Fri, 13 Nov 2026 20:58:53 +0100 Subject: [PATCH] Window: restore hidden panels when selecting panels, control bar or workspaces (#2742) (#2746) Fixes #2742. Make UiState::hidden_panels session-only (#[serde(skip)]) and clear hidden_panels when opening or revealing panels, toggling the Control bar while hidden, or loading/resetting workspaces. Treat hidden panels and flyouts as not visible in checked/toggle checks so Window commands unhide them instead of toggling them off invisibly. --- crates/ui-egui/src/menus/run.rs | 19 ++++++- crates/ui-egui/src/menus/state.rs | 2 +- crates/ui-egui/src/menus/tests.rs | 88 +++++++++++++++++++++++++++++ crates/ui-egui/src/panel_docking.rs | 6 +- crates/ui-egui/src/prefs/tests.rs | 11 ++++ crates/ui-egui/src/state.rs | 2 + 6 files changed, 123 insertions(+), 5 deletions(-) diff --git a/crates/ui-egui/src/menus/run.rs b/crates/ui-egui/src/menus/run.rs index 4180eca6b..d8274129b 100644 --- a/crates/ui-egui/src/menus/run.rs +++ b/crates/ui-egui/src/menus/run.rs @@ -9,8 +9,14 @@ use crate::app::file_dialogs::{Bound, with_path}; /// Type › Tabs: float the Tabs panel (placed above the selected text frame) or close it. fn toggle_tabs_panel(app: &mut DesignApp) { if app.ui.floating.iter().any(|(p, _)| p == "tabs") { - app.ui.floating.retain(|(p, _)| p != "tabs"); + if app.ui.hidden_panels != 0 { + app.ui.hidden_panels = 0; + app.ui.tabs_panel.snap_request = 2; + } else { + app.ui.floating.retain(|(p, _)| p != "tabs"); + } } else { + app.ui.hidden_panels = 0; let at = app.canvas_rect.map_or(egui::pos2(300.0, 140.0), |r| r.min + egui::vec2(80.0, 60.0)); app.ui.floating.push(("tabs".into(), [at.x, at.y])); app.ui.tabs_panel.snap_request = 2; @@ -984,7 +990,15 @@ fn run_ui_part5(app: &mut DesignApp, id: &str, p: &Value) -> Option flag(&mut app.ui.control_bar), + "window.controlBar" => { + app.ui.control_bar = if app.ui.hidden_panels != 0 { + app.ui.hidden_panels = 0; + true + } else { + !app.ui.control_bar + }; + Ok(json!(app.ui.control_bar)) + } "view.flattenerPreview" => flag(&mut app.ui.flattener_preview), "view.tagMarkers" => { app.canvas.shown = None; @@ -1115,6 +1129,7 @@ fn run_ui_part5(app: &mut DesignApp, id: &str, p: &Value) -> Option { app.ui.docking_generation = app.ui.docking_generation.wrapping_add(1); crate::panel_docking::forget_panels(app); + app.ui.hidden_panels = 0; let name = p.get("name").and_then(Value::as_str).unwrap_or("Essentials"); if let Some(w) = app.ui.custom_workspaces.iter().find(|w| w.name == name).cloned() { if let Some(hidden) = w.hidden_menu_items { diff --git a/crates/ui-egui/src/menus/state.rs b/crates/ui-egui/src/menus/state.rs index e8c142e25..671140c81 100644 --- a/crates/ui-egui/src/menus/state.rs +++ b/crates/ui-egui/src/menus/state.rs @@ -110,7 +110,7 @@ pub fn checked(app: &DesignApp, id: &str, params: &Value) -> Option { "list.bullets" | "list.numbers" => { app.session.selection_has_list(if id == "list.bullets" { designcraft_doc::ListType::Bullets } else { designcraft_doc::ListType::Numbers }) } - "window.controlBar" => app.ui.control_bar, + "window.controlBar" => app.ui.control_bar && app.ui.hidden_panels == 0, "window.split" => app.split, "window.newWindow" => app.second_window, "view.proofColors" => app.ui.proof_colors, diff --git a/crates/ui-egui/src/menus/tests.rs b/crates/ui-egui/src/menus/tests.rs index 0b24dcaca..0e00214b3 100644 --- a/crates/ui-egui/src/menus/tests.rs +++ b/crates/ui-egui/src/menus/tests.rs @@ -3519,3 +3519,91 @@ fn zoom_commands_zoom_around_the_pointer_when_it_is_over_the_canvas() { let kept = under(&app, rect.center()); assert!((kept.x - centre.x).abs() < 1e-3 && (kept.y - centre.y).abs() < 1e-3); } + +#[test] +fn window_menu_commands_restore_hidden_panels() { + let mut app = crate::DesignApp::new(designcraft_engine::Session::new(), crate::Services::default()); + app.session.execute("file.new", &json!({})).unwrap(); + let ctx = egui::Context::default(); + let frame = |app: &mut crate::DesignApp| { + for _ in 0..2 { + let input = + egui::RawInput { screen_rect: Some(egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(1400.0, 900.0))), ..Default::default() }; + let mut out = ctx.run_ui(input, |ui| { + app.logic(&ui.ctx().clone()); + app.ui(ui); + }); + out.textures_delta.clear(); + } + }; + let props_tab = crate::panel_docking::tab_id("properties"); + frame(&mut app); + assert!(ctx.read_response(props_tab).is_some(), "properties panel starts visible"); + + // Window › Properties unhides panels hidden by Tab. + run_ui(&mut app, "window.hidePanels", &json!({})).unwrap().unwrap(); + assert_eq!(app.ui.hidden_panels, 1); + frame(&mut app); + assert!(ctx.read_response(props_tab).is_none()); + run_ui(&mut app, "window.panel", &json!({"panel": "properties"})).unwrap().unwrap(); + assert_eq!(app.ui.hidden_panels, 0); + frame(&mut app); + assert!(ctx.read_response(props_tab).is_some(), "Window > Properties restores hidden panels"); + + // Shift+Tab + Type/Window panel commands unhide and reveal the requested panel. + run_ui(&mut app, "window.hidePanelsExceptTools", &json!({})).unwrap().unwrap(); + assert_eq!(app.ui.hidden_panels, 2); + run_ui(&mut app, "app.characterPanel", &json!({})).unwrap().unwrap(); + assert_eq!(app.ui.hidden_panels, 0); + assert!(crate::panel_docking::showing(&app, "character")); + + run_ui(&mut app, "window.hidePanels", &json!({})).unwrap().unwrap(); + run_ui(&mut app, "app.paragraphStylesPanel", &json!({})).unwrap().unwrap(); + assert_eq!(app.ui.hidden_panels, 0); + assert!(crate::panel_docking::showing(&app, "paragraphStyles")); + + // Float-only Tabs: opening when hidden reveals it; choosing it while already open but hidden + // reveals it instead of closing it invisibly. + run_ui(&mut app, "window.hidePanels", &json!({})).unwrap().unwrap(); + run_ui(&mut app, "window.panel", &json!({"panel": "tabs"})).unwrap().unwrap(); + assert_eq!(app.ui.hidden_panels, 0); + assert!(app.ui.floating.iter().any(|(p, _)| p == "tabs")); + + run_ui(&mut app, "window.hidePanels", &json!({})).unwrap().unwrap(); + run_ui(&mut app, "window.panel", &json!({"panel": "tabs"})).unwrap().unwrap(); + assert_eq!(app.ui.hidden_panels, 0); + assert!(app.ui.floating.iter().any(|(p, _)| p == "tabs"), "Tabs stays open when unhidden"); + + // Table toggle activates instead of closing while hidden. + run_ui(&mut app, "window.panel", &json!({"panel": "table"})).unwrap().unwrap(); + assert!(crate::panel_docking::showing(&app, "table")); + run_ui(&mut app, "window.hidePanels", &json!({})).unwrap().unwrap(); + run_ui(&mut app, "app.tablePanel", &json!({})).unwrap().unwrap(); + assert_eq!(app.ui.hidden_panels, 0); + assert!(crate::panel_docking::showing(&app, "table")); + + // An open flyout in an iconic column isn't closed by Window › while hidden. + run_ui(&mut app, "window.workspace", &json!({"name": "Typography"})).unwrap().unwrap(); + run_ui(&mut app, "window.panel", &json!({"panel": "character"})).unwrap().unwrap(); + assert_eq!(app.ui.docking_flyout.as_deref(), Some("character")); + run_ui(&mut app, "window.hidePanels", &json!({})).unwrap().unwrap(); + run_ui(&mut app, "window.panel", &json!({"panel": "character"})).unwrap().unwrap(); + assert_eq!(app.ui.hidden_panels, 0); + assert!(crate::panel_docking::showing(&app, "character")); + + // Window › Workspace › Reset and Control restore hidden panels. + run_ui(&mut app, "window.workspace", &json!({"name": "Essentials"})).unwrap().unwrap(); + run_ui(&mut app, "window.hidePanels", &json!({})).unwrap().unwrap(); + run_ui(&mut app, "window.resetWorkspace", &json!({})).unwrap().unwrap(); + assert_eq!(app.ui.hidden_panels, 0); + frame(&mut app); + assert!(ctx.read_response(props_tab).is_some(), "Reset Workspace restores hidden panels"); + + app.ui.control_bar = true; + run_ui(&mut app, "window.hidePanels", &json!({})).unwrap().unwrap(); + assert_eq!(checked(&app, "window.controlBar", &Value::Null), Some(false)); + run_ui(&mut app, "window.controlBar", &json!({})).unwrap().unwrap(); + assert_eq!(app.ui.hidden_panels, 0); + assert!(app.ui.control_bar); + assert_eq!(checked(&app, "window.controlBar", &Value::Null), Some(true)); +} diff --git a/crates/ui-egui/src/panel_docking.rs b/crates/ui-egui/src/panel_docking.rs index 31366dfb5..847a3de33 100644 --- a/crates/ui-egui/src/panel_docking.rs +++ b/crates/ui-egui/src/panel_docking.rs @@ -517,7 +517,7 @@ pub(crate) fn command(app: &mut DesignApp, id: &str, params: &Value) -> Option Option bool { - app.ui.docking_flyout.as_deref() == Some(panel) && in_iconic_column(&layout_of(app), panel) + app.ui.hidden_panels == 0 && app.ui.docking_flyout.as_deref() == Some(panel) && in_iconic_column(&layout_of(app), panel) } /// Close the open flyout; the layout is unchanged. @@ -826,6 +826,7 @@ impl Transaction { release_closed(app, &closed); keep_float_only(app); if let Some(panel) = reveal { + app.ui.hidden_panels = 0; app.ui.docking_opened = Some(panel.clone()); app.ui.docking_raise = Some(panel); app.ui.docking_flyout = flyout; @@ -905,6 +906,7 @@ fn float_only_panel(app: &mut DesignApp, id: &str, panel: &str, params: &Value) }; app.ui.floating.retain(|(p, _)| p != panel); if let Some(at) = at { + app.ui.hidden_panels = 0; app.ui.floating.push((panel.to_string(), at)); } Ok(json!({"panel": panel, "floating": floating_panels(app)})) diff --git a/crates/ui-egui/src/prefs/tests.rs b/crates/ui-egui/src/prefs/tests.rs index c897b05ab..8738f34c7 100644 --- a/crates/ui-egui/src/prefs/tests.rs +++ b/crates/ui-egui/src/prefs/tests.rs @@ -310,3 +310,14 @@ fn unrelated_malformed_preferences_are_still_rejected() { assert!(UiState::from_preferences_json(br#"{"uiScale":"invalid","docking":false}"#).is_err()); assert!(UiState::from_preferences_json(br#"{"customWorkspaces":false}"#).is_err()); } + +#[test] +fn hidden_panels_is_not_persisted_across_restarts() { + let read = UiState::read_preferences(br#"{"hiddenPanels":1}"#).unwrap(); + assert!(read.intact); + assert_eq!(read.ui.hidden_panels, 0); + let mut ui = read.ui; + ui.hidden_panels = 2; + let saved = serde_json::to_value(&ui).unwrap(); + assert!(saved.get("hiddenPanels").is_none()); +} diff --git a/crates/ui-egui/src/state.rs b/crates/ui-egui/src/state.rs index 0c41a4817..e05686103 100644 --- a/crates/ui-egui/src/state.rs +++ b/crates/ui-egui/src/state.rs @@ -204,6 +204,8 @@ pub struct UiState { #[serde(skip)] pub flattener_preview: bool, /// Tab / Shift+Tab: 1 = every panel hidden, 2 = all but the Tools panel (0 = shown). + /// Session-only so panels never stay hidden across restarts. + #[serde(skip)] pub hidden_panels: u8, /// "Don't show again" in the warning before deleting pages that contain objects. pub skip_delete_pages_warning: bool,