fix(ui): omit stock body bands for host-owned dock groups

This commit is contained in:
KotovGGG
2026-11-01 01:31:53 +03:00
parent 235fffbdbe
commit 4f263edcdd
5 changed files with 81 additions and 14 deletions
+3 -1
View File
@@ -16,7 +16,9 @@ and command handling remain in each application. No icons, fonts or application
`DockContent` can replace an iconic column or a complete tab group without a
second layout tree. `column_rect` reports the fitted column before painting;
`owns_iconic_column`/`iconic_column` and `custom_group` suppress the corresponding
stock chrome, body and input when the host takes ownership. These hooks are also
stock chrome, body and input when the host takes ownership. Docked custom groups
also skip stock body top/bottom drop bands; column-edge targets remain available.
These hooks are also
forwarded by `show_customized`'s label/permissions/limits adapter.
`resize_handle_style` and `split_separator` let the host paint one seam while
+61 -2
View File
@@ -58,9 +58,11 @@ fn the_top_gutter_is_reserved_by_default_and_can_be_owned_by_the_contents() {
#[derive(Default)]
struct Host {
exclusive: bool,
stock_group: Option<u32>,
bodies: usize,
columns: Vec<Rect>,
iconic: Vec<Rect>,
groups: std::collections::HashMap<u32, Rect>,
collapsed: bool,
protected: bool,
frame_width: bool,
@@ -92,8 +94,9 @@ impl DockContent<u32> for Host {
fn body(&mut self, _: &mut egui::Ui, _: &u32) {
self.bodies += 1;
}
fn custom_group(&mut self, _: &mut egui::Ui, _: &[u32], _: &u32, _: Rect, _: bool) -> bool {
self.exclusive
fn custom_group(&mut self, _: &mut egui::Ui, _: &[u32], selected: &u32, rect: Rect, _: bool) -> bool {
self.groups.insert(*selected, rect);
self.exclusive && self.stock_group.as_ref() != Some(selected)
}
fn owns_iconic_column(&self, _: &u32) -> bool {
self.exclusive
@@ -259,3 +262,59 @@ fn internal_width_input_is_suppressed_only_when_explicitly_owned_through_overrid
assert_eq!(hits.iter().any(|(hit, _)| *hit == Hit::Divider(Divider::WindowSplit(1, vec![]))), !owned);
}
}
#[test]
fn exclusive_docked_groups_skip_stock_body_bands_but_keep_column_edges() {
for owned in [false, true] {
for zone in [Zone::Top, Zone::Bottom, Zone::Left, Zone::Right] {
let ctx = Context::default();
let area = Id::unique("owned-dock-targets");
let layout = Layout {
root: Some(Node::Dock { side: Side::Start, columns: vec![Column::new(vec![Group::new(vec![1, 2], 0)], 240.0)] }),
floating: vec![Floating::single(vec![99], 0, [500.0, 100.0, 200.0, 150.0])],
};
let mut host = Host { exclusive: owned, stock_group: Some(99), ..Default::default() };
let mut step = |events, start: bool| {
let mut out = DockOutput::default();
ctx.run_ui(input(events), |ui| {
let mut style = DockStyle::from_ui(ui);
super::floating_tests::frame_style(&mut style);
out = DockArea::new(area).show_content(ui, &layout, &style, &mut host);
if start {
assert!(begin_drag(ui.ctx(), area, 99, Rect::from_min_size(pos2(500.0, 100.0), vec2(200.0, 150.0))));
}
})
.drop_without_applying_deltas();
(out, host.groups.get(&1).copied())
};
step(vec![], false);
step(vec![], false);
let (_, target) = step(vec![], false);
let target = target.expect("the docked group is drawn");
let from = pos2(550.0, 140.0);
step(vec![Event::PointerMoved(from), button(from, true)], true);
let to = match zone {
Zone::Top => pos2(target.center().x, target.top() + 50.0),
Zone::Bottom => pos2(target.center().x, target.bottom() - 20.0),
Zone::Left => pos2(target.left() + 20.0, target.center().y),
Zone::Right => pos2(target.right() - 20.0, target.center().y),
Zone::Center => unreachable!(),
};
step(vec![Event::PointerMoved(to)], false);
let (out, _) = step(vec![Event::PointerMoved(to), button(to, false)], false);
let group_drop = out.actions.iter().any(|a| {
matches!(
a,
Action::MovePanel { to: Target::At { placement: Placement::Group { .. }, .. }, .. }
| Action::MoveGroup { to: Target::At { placement: Placement::Group { .. }, .. }, .. }
)
});
if matches!(zone, Zone::Top | Zone::Bottom) {
assert_eq!(group_drop, !owned, "{owned} {zone:?}: {:?}", out.actions);
} else {
assert!(!out.actions.is_empty(), "column edges still work: {owned} {zone:?}");
assert!(!group_drop, "column edges must not become group bands");
}
}
}
}
+8 -5
View File
@@ -509,9 +509,12 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
/// seams and bands clipped to the visible `view`, and the height dividers.
fn column_groups(&mut self, ui: &mut Ui, column: &Column<P>, heights: &[f32], area: Rect, view: Rect) {
let rects = stacked(area, heights);
for (group, rect) in column.groups.iter().zip(&rects) {
self.group(ui, &group.panels, group.active, group.minimized, *rect, GroupKind::Dock);
}
let owned: Vec<_> = column
.groups
.iter()
.zip(&rects)
.map(|(group, rect)| self.group(ui, &group.panels, group.active, group.minimized, *rect, GroupKind::Dock))
.collect();
let top = rects.last().map_or(area.top(), |r| r.bottom());
let xs = view.x_range();
let half = self.style.seam_band * 0.5;
@@ -525,7 +528,7 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
}
};
let mut previous: Option<&P> = None;
for (group, rect) in column.groups.iter().zip(&rects) {
for ((group, rect), owned) in column.groups.iter().zip(&rects).zip(owned) {
let Some(anchor) = group.panels.first() else { continue };
if let Some(above) = previous {
let band = Rect::from_x_y_ranges(xs, (rect.top() - half)..=(rect.top() + half));
@@ -541,7 +544,7 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
self.dock_groups.push(DockGroup {
body,
center,
sides: [None, None, Some((above, line(rect.top()))), Some((below, line(rect.bottom())))],
sides: [None, None, (!owned).then(|| (above, line(rect.top()))), (!owned).then(|| (below, line(rect.bottom())))],
});
}
previous = Some(anchor);
+3 -1
View File
@@ -135,7 +135,9 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
pub(super) fn floating_body(&mut self, ui: &mut Ui, window: &Floating<P>, body: Rect, frame: Rect) {
let Some(key) = window.panels().next().cloned() else { return };
match (&window.root, window.minimized) {
(Node::Tabs { panels, active }, true) => self.group(ui, panels, *active, true, body, GroupKind::Floating { window: frame }),
(Node::Tabs { panels, active }, true) => {
self.group(ui, panels, *active, true, body, GroupKind::Floating { window: frame });
}
(_, true) => {}
(root, false) => {
let outer = self.tree.replace((key, frame));
+6 -5
View File
@@ -36,7 +36,7 @@ const OVERFLOW_WIDTH: f32 = 22.0;
const WHEEL_NOTCH: f32 = 40.0;
impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
/// Draw one tab group at `rect`.
/// Draw one tab group at `rect`, returning whether the host took exclusive ownership.
///
/// An `active` index outside `panels` is a no-op. A lone protected panel is drawn stripless:
/// its whole rect is the body and its only tab has `Rect::NOTHING`. Side splits are offered
@@ -45,15 +45,15 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
/// hidden overflow tabs still appear in the drop target with `Rect::NOTHING`. The host's extra
/// tabs follow the panel tabs and fit with them ([`fit_with_extra`]). `minimized` hides the body
/// but keeps the strip. Tabs, strip, header and drag bar are mirrored in right-to-left layout.
pub(super) fn group(&mut self, ui: &mut Ui, panels: &[P], active: usize, minimized: bool, rect: Rect, kind: GroupKind) {
let Some(selected) = panels.get(active) else { return };
pub(super) fn group(&mut self, ui: &mut Ui, panels: &[P], active: usize, minimized: bool, rect: Rect, kind: GroupKind) -> bool {
let Some(selected) = panels.get(active) else { return false };
let host_collapsed = self.content.group_collapsed(panels);
let minimized = minimized || host_collapsed;
if !host_collapsed {
self.record_float(panels, rect.size(), rect.size());
}
if self.content.custom_group(ui, panels, selected, rect, matches!(kind, GroupKind::Floating { .. })) {
return;
return true;
}
let policy = self.permissions(selected);
let splits = matches!(kind, GroupKind::Frame | GroupKind::Floating { .. }) && policy.accepts_splits;
@@ -71,7 +71,7 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
accepts_tabs: policy.accepts_tabs,
right_to_left: rtl,
});
return;
return false;
}
let movable = panels.iter().all(|p| self.permissions(p).movable);
let strip = Rect::from_min_max(rect.min, pos2(rect.right(), (rect.top() + self.style.tab_height).min(rect.bottom())));
@@ -175,6 +175,7 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
self.contents(ui, selected, Rect::from_min_max(pos2(rect.left(), strip.bottom()), rect.max));
}
ui.painter().rect_stroke(rect, 0.0, self.style.border, StrokeKind::Inside);
false
}
/// A part of the strip the host draws (`header` or `extra-tabs`) at `rect`. A press in it