fix(ui): preserve host docking geometry and split ownership

This commit is contained in:
KotovGGG committed 2026-11-01 00:02:55 +03:00
1 parent dcd94c27e7
commit 235fffbdbe
6 files changed
+141 -10

No files matched your search

+3 -1
View File
@@ -21,7 +21,9 @@ forwarded by `show_customized`'s label/permissions/limits adapter.
`resize_handle_style` and `split_separator` let the host paint one seam while
retaining stock resize input. `owns_floating_width` delegates horizontal floating
handles to the host; stock vertical handles remain available. Presentation-only
frame-edge handles to the host; internal dividers and vertical handles remain available.
`owns_floating_split_width` separately delegates internal horizontal dividers when
the host supplies their replacement input. Presentation-only
`group_collapsed` does not change the selected tab in the shared model.
All ownership hooks default to false/no-op. `DockStyle::dock_gap` defaults to
+115 -1
View File
@@ -61,8 +61,34 @@ struct Host {
bodies: usize,
columns: Vec<Rect>,
iconic: Vec<Rect>,
collapsed: bool,
protected: bool,
frame_width: bool,
split_width: bool,
space: Option<FloatSpace<u32>>,
style_calls: std::cell::Cell<usize>,
}
impl DockContent<u32> for Host {
fn group_collapsed(&self, _: &[u32]) -> bool {
self.collapsed
}
fn permissions(&self, _: &u32) -> Permissions {
if self.protected { Permissions::PROTECTED } else { Permissions::default() }
}
fn owns_floating_split_width(&self, _: &u32) -> bool {
self.split_width
}
fn owns_floating_width(&self, _: &u32) -> bool {
self.frame_width
}
fn resize_handle_style(&self, _: crate::layout::ResizeHandleStyle) -> crate::layout::ResizeHandleStyle {
self.style_calls.set(self.style_calls.get() + 1);
crate::layout::ResizeHandleStyle {
hovered: egui::Color32::TRANSPARENT,
dragged: egui::Color32::TRANSPARENT,
focused: egui::Color32::TRANSPARENT,
}
}
fn body(&mut self, _: &mut egui::Ui, _: &u32) {
self.bodies += 1;
}
@@ -88,12 +114,13 @@ fn render(layout: &Layout<u32>, host: &mut Host) -> Vec<(Hit<u32>, Rect)> {
host.iconic.clear();
let mut style = DockStyle::from_ui(ui);
super::floating_tests::frame_style(&mut style);
let protected = host.protected;
let shown = DockArea::new(Id::unique("host")).show_customized(
ui,
layout,
&style,
|id| format!("Pane {id}"),
|_| Permissions::default(),
|_| if protected { Permissions::PROTECTED } else { Permissions::default() },
|_| PanelLimits::default(),
host,
);
@@ -101,6 +128,7 @@ fn render(layout: &Layout<u32>, host: &mut Host) -> Vec<(Hit<u32>, Rect)> {
})
.drop_without_applying_deltas();
}
host.space = float_space(&ctx, Id::unique("host"));
DockArea::hit_rects(&ctx, Id::unique("host"))
}
@@ -145,3 +173,89 @@ fn invalid_side_dock_gaps_are_rejected_before_host_painting() {
.drop_without_applying_deltas();
}
}
#[test]
fn host_owned_surfaces_record_fitted_float_geometry() {
let mut host = Host { exclusive: true, ..Default::default() };
let layout = Layout { root: Some(Node::Tabs { panels: vec![1, 2], active: 0 }), floating: vec![] };
render(&layout, &mut host);
let space = host.space.as_ref().unwrap();
assert!(space.panel(&1).is_some_and(|s| s.x > 700.0 && s.y > 0.0));
assert!(space.group(&2).is_some_and(|s| s.x > 700.0 && s.y > 0.0));
}
#[test]
fn host_owned_iconic_columns_record_fitted_float_geometry() {
let mut host = Host { exclusive: true, ..Default::default() };
let mut column = Column::new(vec![Group::new(vec![1, 2], 0)], 90.0);
column.iconic = true;
let layout = Layout { root: Some(Node::Dock { side: Side::Start, columns: vec![column] }), floating: vec![] };
render(&layout, &mut host);
let size = host.space.as_ref().unwrap().column(&1).expect("host column is recorded");
assert_eq!(size.x, host.columns[0].width());
assert!(size.y > 0.0);
}
#[test]
fn a_host_collapsed_protected_group_keeps_its_selection_without_drawing_a_body() {
let layout = Layout { root: Some(Node::Tabs { panels: vec![1], active: 0 }), floating: vec![] };
let mut host = Host { protected: true, collapsed: true, ..Default::default() };
render(&layout, &mut host);
assert_eq!(host.bodies, 0);
assert!(matches!(layout.root, Some(Node::Tabs { active: 0, .. })));
assert!(host.space.as_ref().unwrap().panel(&1).is_none());
}
#[test]
fn frame_width_ownership_keeps_internal_split_input_and_host_style() {
use crate::layout::{SplitAxis, SplitSize};
let ctx = Context::default();
let mut window = Floating::single(vec![1], 0, [100.0, 100.0, 500.0, 350.0]);
window.root = Node::Split {
axis: SplitAxis::Horizontal,
size: SplitSize::Ratio(0.5),
first: Box::new(Node::Tabs { panels: vec![1], active: 0 }),
second: Box::new(Node::Tabs { panels: vec![2], active: 0 }),
};
let layout = Layout { root: None, floating: vec![window] };
let mut host = Host { frame_width: true, ..Default::default() };
let mut step = |events| {
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(Id::unique("owned-width")).show_content(ui, &layout, &style, &mut host);
})
.drop_without_applying_deltas();
out
};
step(vec![]);
step(vec![]);
step(vec![]);
let hit = Hit::Divider(Divider::WindowSplit(1, vec![]));
let hits = DockArea::<u32>::hit_rects(&ctx, Id::unique("owned-width"));
let at = hits.iter().find(|(h, _)| *h == hit).expect("internal divider remains interactive").1.center();
step(vec![Event::PointerMoved(at), button(at, true)]);
step(vec![Event::PointerMoved(at + vec2(10.0, 0.0))]);
let out = step(vec![Event::PointerMoved(at + vec2(40.0, 0.0))]);
assert!(out.actions.iter().any(|a| matches!(a, Action::ResizeWindowSplit { .. })), "{:?}", out.actions);
assert!(host.style_calls.get() > 0);
}
#[test]
fn internal_width_input_is_suppressed_only_when_explicitly_owned_through_overrides() {
use crate::layout::{SplitAxis, SplitSize};
let mut window = Floating::single(vec![1], 0, [100.0, 100.0, 500.0, 350.0]);
window.root = Node::Split {
axis: SplitAxis::Horizontal,
size: SplitSize::Ratio(0.5),
first: Box::new(Node::Tabs { panels: vec![1], active: 0 }),
second: Box::new(Node::Tabs { panels: vec![2], active: 0 }),
};
let layout = Layout { root: None, floating: vec![window] };
for owned in [false, true] {
let mut host = Host { frame_width: true, split_width: owned, ..Default::default() };
let hits = render(&layout, &mut host);
assert_eq!(hits.iter().any(|(hit, _)| *hit == Hit::Divider(Divider::WindowSplit(1, vec![]))), !owned);
}
}
+1 -1
View File
@@ -459,13 +459,13 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
}
let side = side_at(edge, self.style.right_to_left);
self.content.column_rect(ui.ctx(), column, rect, side);
self.record_column_float(column.groups.iter().flat_map(|g| g.panels.iter().cloned()).collect(), rect);
if column.iconic && self.content.owns_iconic_column(first) {
let mut child = ui.new_child(UiBuilder::new().id_salt(("host-iconic", first)).max_rect(rect));
child.set_clip_rect(rect.intersect(ui.clip_rect()));
self.content.iconic_column(&mut child, column, rect, side);
return;
}
self.record_column_float(column.groups.iter().flat_map(|g| g.panels.iter().cloned()).collect(), rect);
let (bar, body, heights) = self.column_layout(column, rect);
self.column_bar(ui, column, first, bar, rect, edge);
if column.iconic && usable(body) {
+6 -4
View File
@@ -47,19 +47,21 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
/// 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 };
if self.content.custom_group(ui, panels, selected, rect, matches!(kind, GroupKind::Floating { .. })) {
return;
}
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;
}
let policy = self.permissions(selected);
let splits = matches!(kind, GroupKind::Frame | GroupKind::Floating { .. }) && policy.accepts_splits;
let rtl = self.style.right_to_left;
if stripless(panels, &*self.content) {
self.contents(ui, selected, rect);
if !minimized {
self.contents(ui, selected, rect);
}
self.targets.push(DropTarget {
anchor: selected.clone(),
rect,
+6 -2
View File
@@ -214,7 +214,7 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
// Named by its layout path, which `ResizeSplit` takes and hits report.
self.content.split_separator(ui, split.divider);
let host_width =
*axis == SplitAxis::Horizontal && self.tree.as_ref().is_some_and(|(panel, _)| self.content.owns_floating_width(panel));
*axis == SplitAxis::Horizontal && self.tree.as_ref().is_some_and(|(panel, _)| self.content.owns_floating_split_width(panel));
if host_width {
return;
}
@@ -224,7 +224,11 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
*axis,
split.divider,
&self.style.resize_label,
ResizeHandleStyle { hovered: self.style.accent, dragged: self.style.accent, focused: self.style.accent },
self.content.resize_handle_style(ResizeHandleStyle {
hovered: self.style.accent,
dragged: self.style.accent,
focused: self.style.accent,
}),
)
.value(split.first_size(), (split.first_size() + lo)..=(split.first_size() + hi))
.show(ui);
+10 -1
View File
@@ -865,10 +865,16 @@ pub trait DockContent<P> {
}
/// Paint a fitted split gap in the owning surface, without another resize target.
fn split_separator(&mut self, _ui: &mut Ui, _rect: Rect) {}
/// The host supplies exclusive horizontal resize input for this floating tree.
/// The host supplies horizontal resize input at the floating frame edges.
/// Internal split dividers retain stock input unless `owns_floating_split_width` is true.
fn owns_floating_width(&self, _panel: &P) -> bool {
false
}
/// The host supplies horizontal resize input for every internal split in this floating tree.
/// This is separate from frame-edge ownership; only return true with replacement input.
fn owns_floating_split_width(&self, _panel: &P) -> bool {
false
}
/// Plain text name for accessibility, menus and the default visual label.
fn logical_label(&self, panel: &P) -> String
where
@@ -1029,6 +1035,9 @@ impl<P, C: DockContent<P> + ?Sized> DockContent<P> for Overrides<'_, P, C> {
fn owns_floating_width(&self, panel: &P) -> bool {
self.inner.owns_floating_width(panel)
}
fn owns_floating_split_width(&self, panel: &P) -> bool {
self.inner.owns_floating_split_width(panel)
}
fn logical_label(&self, panel: &P) -> String {
(self.label)(panel)
}