diff --git a/crates/ui/src/docking/native_tests.rs b/crates/ui/src/docking/native_tests.rs index 63bd9f9e..98a53295 100644 --- a/crates/ui/src/docking/native_tests.rs +++ b/crates/ui/src/docking/native_tests.rs @@ -328,6 +328,62 @@ fn a_restored_window_opens_on_a_connected_monitor() { assert_eq!(window(&d, 5).monitor.as_ref().and_then(|m| m.name.as_deref()), Some("left")); } +/// The last `InnerSize`, `MinInnerSize` and `MaxInnerSize` sent to `viewport`. +fn size_commands(d: &DockDriver, viewport: egui::ViewportId) -> [Option; 3] { + let mut sizes = [None; 3]; + for (_, c) in d.host().commands.iter().filter(|(v, _)| *v == viewport) { + match c { + ViewportCommand::InnerSize(s) => sizes[0] = Some(*s), + ViewportCommand::MinInnerSize(s) => sizes[1] = Some(*s), + ViewportCommand::MaxInnerSize(s) => sizes[2] = Some(*s), + _ => {} + } + } + sizes +} + +#[test] +fn a_minimized_native_window_shrinks_to_its_bar_and_keeps_its_expanded_size() { + let mut d = driver(layout(true)); + let viewport = d.viewport(window(&d, 5).id); + let open = os_window(&d, 5).unwrap().0; + let (native, rect) = (window(&d, 5).native, window(&d, 5).rect); + assert_eq!(native, Some([open.left(), open.top(), 240.0, 300.0])); + + // Its bar and its group's tab strip, at the same width and top-left corner; only the width + // resizes. + let collapsed = BAR + super::DockStyle::default().tab_height; + d.layout.apply(Action::SetMinimized { panel: 5, minimized: true }).unwrap(); + d.settle(); + assert_eq!(os_window(&d, 5).unwrap().0, Rect::from_min_size(open.min, vec2(240.0, collapsed))); + let [_, min, max] = size_commands(&d, viewport); + assert_eq!((min.map(|s| s.y), max.map(|s| s.y)), (Some(collapsed), Some(collapsed))); + + // The layout keeps the expanded size after the window reports the collapsed one, past the + // wait for commanded moves, and takes a new width. + for _ in 0..40 { + d.step(vec![]); + } + assert_eq!((window(&d, 5).native, window(&d, 5).rect), (native, rect)); + d.host().windows.get_mut(&viewport).unwrap().rect = Rect::from_min_size(open.min, vec2(300.0, collapsed)); + for _ in 0..40 { + d.step(vec![]); + } + assert_eq!(window(&d, 5).native, Some([open.left(), open.top(), 300.0, 300.0])); + assert_eq!(window(&d, 5).rect[2..], [300.0, 300.0]); + + // Expanded: the saved height comes back, and the height resizes again. + d.layout.apply(Action::SetMinimized { panel: 5, minimized: false }).unwrap(); + d.settle(); + assert_eq!(os_window(&d, 5).unwrap().0, Rect::from_min_size(open.min, vec2(300.0, 300.0))); + assert_eq!(size_commands(&d, viewport), [Some(vec2(300.0, 300.0)), Some(vec2(160.0, 100.0)), Some(egui::Vec2::INFINITY)]); + // The limits change before the size: the OS clamps a new size to the limits it has. + let commands: Vec<_> = d.host().commands.iter().filter(|(v, _)| *v == viewport).map(|(_, c)| c.clone()).collect(); + let last = |f: fn(&ViewportCommand) -> bool| commands.iter().rposition(f); + assert!(last(|c| matches!(c, ViewportCommand::MaxInnerSize(_))) < last(|c| matches!(c, ViewportCommand::InnerSize(_))), "{commands:?}"); + assert_eq!(window(&d, 5).native, Some([open.left(), open.top(), 300.0, 300.0])); +} + #[test] fn a_locked_area_moves_no_native_window_and_records_where_the_os_put_it() { let mut d = driver(layout(true)); diff --git a/crates/ui/src/docking/render/floating.rs b/crates/ui/src/docking/render/floating.rs index fded5aff..ceab8df8 100644 --- a/crates/ui/src/docking/render/floating.rs +++ b/crates/ui/src/docking/render/floating.rs @@ -39,15 +39,19 @@ impl Renderer<'_, P> { pub(super) fn window_limits(&self, window: &Floating

) -> PanelLimits { let chrome = self.window_chrome(window); let tree = node_limits(&window.root, self.style, &*self.content); - let body = match (&window.root, window.minimized) { - (Node::Tabs { .. }, true) => (self.style.tab_height, self.style.tab_height), - (_, true) => (0.0, 0.0), - (_, false) => (tree.min.y, tree.max.y), + let body = match window.minimized { + true => (self.minimized_body(window), self.minimized_body(window)), + false => (tree.min.y, tree.max.y), }; let min = vec2(tree.min.x, chrome + body.0).min(vec2(1_000_000.0, 1_000_000.0)); PanelLimits { min, max: vec2(tree.max.x, chrome + body.1).max(min).min(vec2(1_000_000.0, 1_000_000.0)) } } + /// The height of a minimized window's body: the tab strip of its only group, or nothing. + pub(super) fn minimized_body(&self, window: &Floating

) -> f32 { + if matches!(window.root, Node::Tabs { .. }) { self.style.tab_height } else { 0.0 } + } + /// The title bar and, for a movable (resizable) window, the resize band above it. fn window_chrome(&self, window: &Floating

) -> f32 { let band = if window.panels().all(|panel| self.permissions(panel).movable) { RESIZE_BAND } else { 0.0 }; diff --git a/crates/ui/src/docking/render/native.rs b/crates/ui/src/docking/render/native.rs index 1dd96e49..06839e87 100644 --- a/crates/ui/src/docking/render/native.rs +++ b/crates/ui/src/docking/render/native.rs @@ -164,6 +164,9 @@ struct Place { corrected: bool, /// Shown, as it opened or was last told. shown: bool, + /// The height of its bar while minimized, as it was last told. `known` keeps the expanded + /// size meanwhile. + collapsed: Option, } /// Whether two rects differ by more than rounding. @@ -448,6 +451,9 @@ impl Renderer<'_, P> { let moving = |ctx: &Context| ctx.data(|d| d.get_temp::(drag_key)).is_some_and(|d| d.window == window.id); let info = ctx.input(|i| i.raw.viewports.get(&viewport).cloned()); let frame = info.as_ref().and_then(content_of).map_or(Vec2::ZERO, |(_, frame)| frame); + // A minimized window shows its bar and what `floating_body` draws below it. + let collapsed = window.minimized.then(|| BAR_HEIGHT + self.minimized_body(window)); + let shown = |[x, y, w, h]: [f32; 4]| [x, y, w, collapsed.unwrap_or(h)]; let mut place = match ctx.data(|d| d.get_temp::(key)) { Some(mut place) => { // The application changed the window's place (undo, reset, a reloaded layout): @@ -456,7 +462,7 @@ impl Renderer<'_, P> { && let Some(native) = window.native.filter(|native| differs(*native, place.known)) { let target = kept_reachable(native, &self.monitors); - place_window(ctx, viewport, target, frame); + place_window(ctx, viewport, shown(target), frame); (place.known, place.pending) = (target, Some(now + MOVE_WAIT)); } place @@ -474,9 +480,22 @@ impl Renderer<'_, P> { }; seen.insert(window.id); ctx.data_mut(|d| d.insert_temp(seen_key, seen)); - Place { spawn, known: spawn, pending: Some(now + MOVE_WAIT), corrected: false, shown: true } + Place { spawn, known: spawn, pending: Some(now + MOVE_WAIT), corrected: false, shown: true, collapsed: None } } }; + // Minimized or expanded: the window keeps its top-left corner and width, and its + // height is fixed while it shows only its bar. + // The limits go first: the OS clamps the new size to the limits it has. + if place.collapsed != collapsed { + let (min, max) = match collapsed { + Some(height) => (vec2(MIN_SIZE[0], height), vec2(1_000_000.0, height)), + None => (Vec2::from(MIN_SIZE), Vec2::INFINITY), + }; + ctx.send_viewport_cmd_to(viewport, ViewportCommand::MinInnerSize(min)); + ctx.send_viewport_cmd_to(viewport, ViewportCommand::MaxInnerSize(max)); + place_window(ctx, viewport, shown(place.known), frame); + (place.collapsed, place.pending) = (collapsed, Some(now + MOVE_WAIT)); + } let spawn = place.spawn; let title = crate::docking::dock_model::first_active(&window.root).map(|p| self.label_of(p)).unwrap_or_default(); let builder = ViewportBuilder::default() @@ -599,7 +618,9 @@ impl Renderer<'_, P> { let opacity = if this { drag_opacity(drag.as_ref(), float_only, self.style.title_bar_docks) } else { 1.0 }; set_window_opacity(&title, main.as_deref(), opacity); } - if let Some(placed) = report.placed.filter(|_| !moving) { + // The collapsed height a minimized window reports is not the size to keep. + let reported = report.placed.map(|[x, y, w, h]| [x, y, w, if place.collapsed.is_some() { place.known[3] } else { h }]); + if let Some(placed) = reported.filter(|_| !moving) { let waiting = place.pending.is_some_and(|until| now < until); if !differs(placed, place.known) { place.pending = None;