mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-13 00:17:46 +00:00
fix(ui): omit stock body bands for host-owned dock groups
This commit is contained in:
+3
-1
@@ -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
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user