diff --git a/crates/ui/src/docking/group_tests.rs b/crates/ui/src/docking/group_tests.rs index cad73cfc..62d6013f 100644 --- a/crates/ui/src/docking/group_tests.rs +++ b/crates/ui/src/docking/group_tests.rs @@ -54,10 +54,16 @@ struct TestContent { whole_group: bool, /// Height every body takes; `None` draws a one-line label. body_height: Option, + /// Every body takes its whole rect. + fill: bool, } impl DockContent for TestContent { fn body(&mut self, ui: &mut egui::Ui, _: &u32) { + if self.fill { + ui.allocate_rect(ui.max_rect(), egui::Sense::hover()); + return; + } match self.body_height { Some(height) => _ = ui.allocate_exact_size(vec2(10.0, height), egui::Sense::hover()), None => _ = ui.label("Body"), @@ -593,6 +599,27 @@ fn a_short_docked_panel_floats_at_its_content_height_up_to_its_floating_size() { assert_eq!(*height, title + strip + 150.0); } +#[test] +fn a_panel_that_fills_its_frame_floats_at_its_floating_size_or_320() { + let float = |floating_size: Option| { + let content = TestContent { fill: true, floating_size, ..Default::default() }; + let mut d = Driver::with(frames(), content, Rect::from_min_size(pos2(0.0, 0.0), vec2(600.0, 600.0))); + let target = pos2(400.0, 300.0); + d.hold(d.tab(3).center(), target); + d.modifiers = Modifiers::COMMAND; + let out = d.step(vec![button(target, false)]); + out.actions + .iter() + .find_map(|a| match a { + Action::Float { panel: 3, rect } => Some(rect[3]), + _ => None, + }) + .unwrap_or_else(|| panic!("{:?}", out.actions)) + }; + assert_eq!(float(None), 320.0); + assert_eq!(float(Some(vec2(300.0, 400.0))), 400.0); +} + #[test] fn tab_focus_follows_only_the_users_input() { let mut d = Driver::new(docked()); diff --git a/crates/ui/src/docking/render/mod.rs b/crates/ui/src/docking/render/mod.rs index d099edeb..167f68d9 100644 --- a/crates/ui/src/docking/render/mod.rs +++ b/crates/ui/src/docking/render/mod.rs @@ -218,7 +218,7 @@ impl Renderer<'_, P> { ui.new_child(UiBuilder::new().id(self.id.with(("body", panel))).max_rect(rect).layout(egui::Layout::top_down(egui::Align::Min))); child.set_clip_rect(rect.intersect(ui.clip_rect())); self.content.body(&mut child, panel); - self.float_space.record_content(panel.clone(), child.min_rect().height()); + self.float_space.record_content(panel.clone(), child.min_rect().height(), rect.height()); // Any button: a canvas pans with the middle button and opens menus with the secondary. if child.rect_contains_pointer(rect) && child.input(|i| i.pointer.any_pressed()) { self.output.focused = Some(panel.clone()); diff --git a/crates/ui/src/docking/render/preview.rs b/crates/ui/src/docking/render/preview.rs index 13f36e5a..861e9afd 100644 --- a/crates/ui/src/docking/render/preview.rs +++ b/crates/ui/src/docking/render/preview.rs @@ -279,16 +279,21 @@ impl Renderer<'_, P> { } /// Size of a panel floated by a drag or the Float menu: the host's floating size, else the - /// source size within the panel limits. Out of a dock column, or with a floating size, the - /// height fits the content the panel last drew, up to that height (320 in a dock). + /// source size within the panel limits. Out of a dock or frame, or with a floating size, the + /// height fits the content the panel last drew, up to that height (else 320). Content that + /// filled its body floats at that height. pub(super) fn float_size(&self, panel: &P, source: Vec2) -> Vec2 { let chrome = self.style.title_height + self.style.tab_height; if let Some(size) = self.content.floating_size(panel).filter(|s| s.is_finite() && s.x > 0.0 && s.y > 0.0) { return vec2(size.x, self.fitted_height(panel, chrome, size.y).unwrap_or(size.y)); } let mut size = self.iconic.float_width(panel).map_or(source, |width| vec2(width, DOCKED_FLOAT_HEIGHT)); - if matches!(self.layout.place(panel), Some(Place::Dock { .. })) { - size.y = self.fitted_height(panel, chrome, DOCKED_FLOAT_HEIGHT).unwrap_or(size.y.min(DOCKED_FLOAT_HEIGHT)); + if matches!(self.layout.place(panel), Some(Place::Dock { .. } | Place::Node(_))) { + size.y = match self.fitted_height(panel, chrome, DOCKED_FLOAT_HEIGHT) { + Some(height) => height, + None if self.float_space.filled(panel) => DOCKED_FLOAT_HEIGHT, + None => size.y.min(DOCKED_FLOAT_HEIGHT), + }; } let limits = self.limits(panel); let header = vec2(0.0, self.style.tab_height); diff --git a/crates/ui/src/docking/surface.rs b/crates/ui/src/docking/surface.rs index 99a8e4b9..9c1dc6b3 100644 --- a/crates/ui/src/docking/surface.rs +++ b/crates/ui/src/docking/surface.rs @@ -255,8 +255,8 @@ pub struct FloatSpace

{ pub(super) groups: Vec<(P, Vec2)>, pub(super) columns: Vec<(P, Vec2)>, /// The height each panel's content used when it was last drawn, which fits a floated group - /// to its content. - pub(super) contents: Vec<(P, f32)>, + /// to its content; `None` for content that filled its body. + pub(super) contents: Vec<(P, Option)>, } impl FloatSpace

{ @@ -275,9 +275,16 @@ impl FloatSpace

{ } impl FloatSpace

{ - pub(super) fn record_content(&mut self, panel: P, height: f32) { - let height = if height.is_finite() { height.max(0.0) } else { 0.0 }; - match self.contents.iter_mut().find(|(p, _)| *p == panel) { + /// Record the height `panel`'s content used in a body `available` tall. Content that + /// reaches the body's bottom filled it and has no height of its own, unless the body was + /// sized to the height recorded before (a fitted window). + pub(super) fn record_content(&mut self, panel: P, used: f32, available: f32) { + const TOLERANCE: f32 = 1.0; + let used = if used.is_finite() { used.max(0.0) } else { 0.0 }; + let recorded = self.contents.iter_mut().find(|(p, _)| *p == panel); + let fitted = recorded.as_ref().and_then(|(_, height)| *height).is_some_and(|height| height >= available - TOLERANCE); + let height = (used < available - TOLERANCE || fitted).then_some(used); + match recorded { Some((_, recorded)) => *recorded = height, None => self.contents.push((panel, height)), } @@ -299,8 +306,13 @@ impl FloatSpace

{ Self::find(&self.columns, panel) } + /// Whether `panel`'s content filled its body when it was last drawn. + pub(super) fn filled(&self, panel: &P) -> bool { + self.contents.iter().any(|(p, height)| p == panel && height.is_none()) + } + pub(super) fn content(&self, panel: &P) -> Option { - self.contents.iter().find(|(p, _)| p == panel).map(|(_, height)| *height) + self.contents.iter().find(|(p, _)| p == panel).and_then(|(_, height)| *height) } /// The first record wins: an iconic panel is recorded at its icon before its flyout. @@ -513,8 +525,9 @@ pub trait DockContent

{ /// Preferred floating size, independent of the current dock geometry: the width of a /// floated panel and the most height it takes. A floated panel or group, and a flyout, fit /// their height to the content the active panel last drew (at least 120 points), up to this - /// height, or up to 320 out of a dock when this is `None`. A rect a command gives and a - /// window the user resized keep their size. + /// height, or up to 320 out of a dock when this is `None`. Content that filled its body + /// takes this height, else 320. A rect a command gives and a window the user resized keep + /// their size. fn floating_size(&self, _panel: &P) -> Option { None } diff --git a/docs/ui-docking.md b/docs/ui-docking.md index d159c887..86d92121 100644 --- a/docs/ui-docking.md +++ b/docs/ui-docking.md @@ -156,9 +156,11 @@ the bar itself shrinks it. A tab dropped on the "…" button goes to the end of A panel or group floated by a drag, a menu or a command without a rect takes its content height: the title bar, the tab strip and the height the active panel's content used when it was last drawn (the content ui's `min_rect`), at least 120 points. The most it takes is the height of -`DockContent::floating_size`, or 320 points for a panel or group leaving a dock. A short panel -therefore floats as a short window. A panel's width is its `floating_size` width, else its docked -width; a group keeps its docked width. +`DockContent::floating_size`, or 320 points for a panel leaving a dock or frame and a group +leaving a dock. A short panel therefore floats as a short window. Content that reaches the bottom +of its body (a panel that allocates its whole rect) has no height of its own: it floats at the +`floating_size` height, else at 320 points. A panel's width is its `floating_size` width, else +its docked width; a group keeps its docked width. A flyout fits its content the same way, without the title bar. - A panel the area has not drawn floats at the `floating_size` height, else the docked height