UI: drop panels anywhere in a set or dock column, and on its tabs

A group's upper half inserts the dropped groups right above it and its
lower half right below it, in floating sets and dock columns alike, and a
dock column's room under its groups puts them at its bottom; the edge
bands were too thin to hit, and the one above a dock column's top group
lay outside the column. The tabs a drop stacks panels with now light up
over the panels.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CC5DiGJEZ5ADaQMBEQk4W1
This commit is contained in:
redrun6767 2026-10-09 18:48:40 +00:00 committed by bflatastic
parent 77667945b1
commit a7279231cb
3 changed files with 75 additions and 15 deletions

View File

@ -9,9 +9,10 @@
//! × of a group puts its panels back in the dock too. The Tools panel floats the same way by its
//! title bar ([`crate::toolbar`]) and docks again on the window's left edge.
//!
//! Dropped on the top or bottom edge of another group (a line shows where), a group joins it in a
//! set: the groups stack top to bottom, each showing one of its panels, and move together by the
//! top group's title bar, whose × docks them all. A group dragged by its tab strip (a lone panel's
//! Dropped on the upper or lower half of another group (a line shows where), a group joins it in
//! a set right above or below it: the groups stack top to bottom, each showing one of its panels,
//! and move together by the top group's title bar, whose × docks them all; dropped on its tabs (lit
//! up), its panels join that group as tabs. A group dragged by its tab strip (a lone panel's
//! tab, or the strip right of the tabs) leaves its set and floats on its own. A double-click on a
//! tab collapses a group to its tabs, and another expands it.
//!
@ -317,23 +318,35 @@ fn drop_target(app: &VectorcraftApp, ctx: &egui::Context, what: Moving, at: Pos2
}
}
}
// Another group's top or bottom edge puts the moved groups in a set with it; its title bar and
// tabs stack them as tabs.
// Another group (floating or docked): its tabs (and a set's title bar) stack the moved groups
// with it as tabs; the upper half of the group (and just above a set) puts them in its set
// right above it, the lower half (and just below it) right below it.
for (i, g) in app.ui.floating_panels.iter().enumerate() {
let Some((first, _)) = g.panels.first().and_then(|p| panel(p)).filter(|_| !moving.contains(&i)) else { continue };
let Some(r) = ctx.data(|d| d.get_temp::<Rect>(group_rect_id(first))) else { continue };
let line = |y: f32| Rect::from_x_y_ranges(r.x_range(), (y - 2.0)..=(y + 2.0));
if Rect::from_x_y_ranges(r.x_range(), (r.bottom() - EDGE)..=(r.bottom() + EDGE)).contains(at) {
return Some((Drop::Below(first), line(r.bottom())));
}
let lead = leads(&app.ui, i);
if lead && Rect::from_x_y_ranges(r.x_range(), (r.top() - EDGE)..=r.top()).contains(at) {
return Some((Drop::Above(first), line(r.top())));
}
let head = Rect::from_min_size(r.min, vec2(r.width(), if lead { 1.0 + TITLE + STRIP } else { GAP + STRIP }));
if head.contains(at) {
return Some((Drop::Stack(first), head));
}
let above = if lead { EDGE } else { 0.0 };
if Rect::from_x_y_ranges(r.x_range(), (r.top() - above)..=(r.bottom() + EDGE)).contains(at) {
let mid = (head.bottom() + r.bottom()) / 2.0;
return Some(if at.y < mid { (Drop::Above(first), line(r.top())) } else { (Drop::Below(first), line(r.bottom())) });
}
}
// A dock column's room under its groups puts them at its bottom.
let columns = ctx.data(|d| d.get_temp::<Vec<Rect>>(columns_id())).unwrap_or_default();
for ((_, set), col) in docked_sets(&app.ui).into_iter().zip(columns) {
if !col.contains(at) || set.iter().any(|i| moving.contains(i)) {
continue;
}
let Some(last) = set.last().and_then(|&i| app.ui.floating_panels.get(i)).and_then(|g| g.panels.first()).and_then(|p| panel(p)) else {
continue;
};
let y = ctx.data(|d| d.get_temp::<Rect>(group_rect_id(last.0))).map_or(col.bottom(), |r| r.bottom());
return Some((Drop::Below(last.0), Rect::from_x_y_ranges(col.x_range(), (y - 2.0)..=(y + 2.0))));
}
let dock = zone(dock_rect_id()).filter(|_| app.ui.dock && app.ui.screen_mode < 3)?;
// Panels that aren't tabs of the tabbed group (or all of them, with it collapsed) go back to
@ -351,6 +364,13 @@ fn highlight(ctx: &egui::Context, zone: Rect) {
painter.rect(zone.shrink(1.0), CornerRadius::same(2), t.accent.gamma_multiply(0.2), Stroke::new(2.0, t.accent), egui::StrokeKind::Inside);
}
/// Light up the tabs a drop stacks the moved panels with, over the floating and docked groups.
fn tab_zone(ctx: &egui::Context, tabs: Rect) {
let t = Tokens::get(ctx);
let painter = ctx.layer_painter(egui::LayerId::new(egui::Order::Foreground, Id::new("floating-drop-tabs")));
painter.rect(tabs.shrink(1.0), CornerRadius::same(2), t.accent.gamma_multiply(0.25), Stroke::new(2.0, t.accent), egui::StrokeKind::Inside);
}
/// Show where a drop puts the moved groups in a set: a line over the floating groups.
fn insertion_line(ctx: &egui::Context, line: Rect) {
let t = Tokens::get(ctx);
@ -381,6 +401,7 @@ pub fn track(app: &mut VectorcraftApp, ctx: &egui::Context) {
}
match target {
Some((Drop::Below(_) | Drop::Above(_) | Drop::Column(_), line)) => insertion_line(ctx, line),
Some((Drop::Stack(_), tabs)) => tab_zone(ctx, tabs),
Some((_, zone)) => highlight(ctx, zone),
None => {}
}
@ -757,7 +778,8 @@ fn draw_groups(
if !g.collapsed {
dock::panel_body(app, ui, g.active, width, tall);
}
let bottom = ui.min_rect().bottom().max(strip.bottom());
// Where the next widget would go: in a dock column, the ui's own bounds reach its bottom.
let bottom = ui.cursor().top().max(strip.bottom());
if let Some(&id) = g.ids.first() {
let rect = Rect::from_min_max(pos2(strip.left(), top), pos2(strip.left() + width, bottom));
ui.ctx().data_mut(|d| d.insert_temp(group_rect_id(id), rect));
@ -1496,6 +1518,43 @@ mod tests {
assert_eq!(d, [(vec!["layers"], Some(2)), (vec!["color"], Some(1)), (vec!["stroke"], Some(1))]);
}
#[test]
fn a_dock_column_takes_groups_anywhere_in_it_and_as_tabs() {
let mut h = Harness::new();
h.app.run("window.panel.dock", json!({"panel": "color", "column": true})).unwrap();
h.app.run("window.panel.float", json!({"panel": "swatches", "below": "color"})).unwrap();
h.app.run("window.panel.float", json!({"panel": "stroke", "below": "swatches"})).unwrap();
h.settle();
let order = |h: &Harness| h.app.ui.floating_panels.iter().map(|g| g.panels.join("+")).collect::<Vec<_>>();
// Dropped on the upper half of Swatches, Gradient goes in right above it.
h.app.run("window.panel.float", json!({"panel": "gradient", "x": 300, "y": 200})).unwrap();
h.settle();
let r = h.temp_rect(group_rect_id("swatches"));
let tabs_bottom = r.top() + GAP + STRIP;
let upper = pos2(r.center().x, tabs_bottom + (r.bottom() - tabs_bottom) / 4.0);
h.hold(h.title("gradient"), upper);
assert_eq!(drop_target(&h.app, &h.ctx, Moving::Panel("gradient"), upper).map(|t| t.0), Some(Drop::Above("swatches")));
h.release(upper);
assert_eq!(order(&h), ["color", "gradient", "swatches", "stroke"]);
assert!(h.app.ui.floating_panels.iter().all(|g| g.docked == Some(1)), "all in the column");
// Dropped on Stroke's tabs, Transparency joins Stroke's group as a tab.
h.app.run("window.panel.float", json!({"panel": "transparency", "x": 300, "y": 200})).unwrap();
h.settle();
let r = h.temp_rect(group_rect_id("stroke"));
let tabs = pos2(r.left() + 120.0, r.top() + GAP + STRIP / 2.0);
h.hold(h.title("transparency"), tabs);
assert_eq!(drop_target(&h.app, &h.ctx, Moving::Panel("transparency"), tabs).map(|t| t.0), Some(Drop::Stack("stroke")));
h.release(tabs);
assert_eq!(order(&h), ["color", "gradient", "swatches", "stroke+transparency"]);
// Dropped low in the column, Align goes at its bottom.
h.app.run("window.panel.float", json!({"panel": "align", "x": 300, "y": 200})).unwrap();
h.settle();
let col = columns(&h)[0];
h.drag(h.title("align"), pos2(col.center().x, col.bottom() - 6.0));
assert_eq!(order(&h), ["color", "gradient", "swatches", "stroke+transparency", "align"]);
assert!(h.app.ui.floating_panels.iter().all(|g| g.docked == Some(1)));
}
fn floating_pos(app: &VectorcraftApp, id: &str) -> [f32; 2] {
group_of(&app.ui, id).map(|i| app.ui.floating_panels[i].pos).unwrap()
}

View File

@ -260,7 +260,7 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[
"window.panel.float",
"Float Panel",
"",
"{panel: id or \"tools\", x?, y?, onto?: id, below?: id, above?: id, group?: bool, collapsed?: bool} float a panel out of the dock (or out of its floating group) as its own floating group with its top-left corner at x, y in window points (default: cascaded), as dragging its tab out of the dock does; `onto` stacks it as a tab with the floating group holding that panel instead; `below` / `above` puts its group in a set with that group, right below or above it, as dropping it on that group's bottom or top edge does (the set's groups stack top to bottom and move together); `group` takes its whole group (its floating group, or the Properties | Layers | Libraries tabs left in the dock); a panel alone in its group is moved (out of its set, given x, y); `collapsed` shows its group's tabs only, as a double-click on a tab does; a docked group floats its dock column. \"tools\" floats the Tools panel. Returns {panel, floating, group, set, pos, collapsed, column}",
"{panel: id or \"tools\", x?, y?, onto?: id, below?: id, above?: id, group?: bool, collapsed?: bool} float a panel out of the dock (or out of its floating group) as its own floating group with its top-left corner at x, y in window points (default: cascaded), as dragging its tab out of the dock does; `onto` stacks it as a tab with the floating group holding that panel instead; `below` / `above` puts its group in a set with that group, right below or above it, as dropping it on that group's lower or upper half does (the set's groups stack top to bottom and move together); `group` takes its whole group (its floating group, or the Properties | Layers | Libraries tabs left in the dock); a panel alone in its group is moved (out of its set, given x, y); `collapsed` shows its group's tabs only, as a double-click on a tab does; a docked group floats its dock column. \"tools\" floats the Tools panel. Returns {panel, floating, group, set, pos, collapsed, column}",
),
(
"window.panel.dock",

View File

@ -260,8 +260,9 @@ Floating panels: dragging a dock tab, a panel icon or a popped-out panel's title
inside the window, and the strip right of the dock's tabs floats the whole Properties | Layers | Libraries group. A
floating group moves by its title bar (or the strip right of its tabs); a tab dragged out of it floats on its own;
dropped on another group's title bar or tabs it stacks with that group, and dropped on the dock (lit up while the
pointer is over it) or closed with its × its panels go back to the tabbed group or the icon column. Dropped on another
group's bottom or top edge (a line shows where), a group joins it in a set: the groups stack top to bottom, each
pointer is over it) or closed with its × its panels go back to the tabbed group or the icon column. Dropped on the upper or
lower half of another group, floating or docked (a line shows where), a group joins it in a set right above or below
it: the groups stack top to bottom, each
showing one of its panels, and move together by the top group's title bar, whose × docks them all; a group dragged by
its tab strip (a lone panel's tab or the strip right of the tabs) leaves the set. A double-click on a tab collapses a
group to its tabs, and another expands it. Dropped on the left edge of the icon column or of a dock column, a set