Panels: a double-click on the tab strip right of the tabs collapses the group (fixes #3620)

This commit is contained in:
Marco Arnulfo
2026-10-24 02:05:18 +07:00
committed by bflatastic
parent 35104e10ed
commit 0666e8e978
4 changed files with 135 additions and 12 deletions
+10 -5
View File
@@ -234,9 +234,10 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
});
}
/// The tabbed group's tab strip, showing `active`: a click on a tab shows it, a double-click
/// collapses the group to its tabs or expands it, a tab dragged out of the strip floats on its own,
/// and the strip right of the tabs floats the whole group. True when the shown panel floats now.
/// The tabbed group's tab strip, showing `active`: a click on a tab shows it, a double-click on a
/// tab or on the strip right of the tabs collapses the group to its tabs or expands it, a tab
/// dragged out of the strip floats on its own, and the strip right of the tabs floats the whole
/// group. True when the shown panel floats now.
fn tab_strip(app: &mut VectorcraftApp, ui: &mut Ui, active: DockTab) -> bool {
let t = Tokens::get(ui.ctx());
let (strip, _) = ui.allocate_exact_size(vec2(ui.available_width(), 33.0), Sense::hover());
@@ -276,8 +277,12 @@ fn tab_strip(app: &mut VectorcraftApp, ui: &mut Ui, active: DockTab) -> bool {
}
let rest = Rect::from_min_max(egui::pos2(out.right, strip.top()), egui::pos2(menu.left() - 4.0, strip.bottom()));
if rest.width() > 0.0 {
let resp = ui.interact(rest, ui.id().with("dock-group-bar"), Sense::drag());
if resp.dragged() && outside {
let resp = ui.interact(rest, ui.id().with("dock-group-bar"), Sense::click_and_drag());
if resp.double_clicked() {
if let Err(error) = app.run("window.panel.collapse", json!({ "panel": active.info().0 })) {
app.ui.status = error;
}
} else if resp.dragged() && outside {
tear = Some((floating::docked_tabs(&app.ui).map(|t| t.info().0).collect(), active.info().0, strip.left()));
}
resp.on_hover_cursor(egui::CursorIcon::Grab).on_hover_text(tl!("Drag to float the panel group"));
+40 -6
View File
@@ -678,7 +678,8 @@ pub fn track(app: &mut VectorcraftApp, ctx: &egui::Context) {
enum Action {
/// Show the group's `k`th tab.
Activate(usize, usize),
/// A double-click on a tab collapses the group to its tabs, or expands it.
/// A double-click on a tab or the strip right of the tabs collapses the group to its tabs, or
/// expands it.
Collapse(usize),
/// Its × puts the panels of the set's groups back in the dock.
Close(Vec<usize>),
@@ -1015,9 +1016,9 @@ fn draw_groups(
ui.painter().rect_filled(gap, 0.0, t.tab_strip);
gap.top()
};
// Tab strip: a click shows a tab, a double-click collapses the group, a drag out of the
// strip floats a tab on its own; a lone tab or the strip right of the tabs moves the group
// (out of its set).
// Tab strip: a click shows a tab, a double-click on a tab or on the strip right of the tabs
// collapses the group, a drag out of the strip floats a tab on its own; a lone tab or the
// strip right of the tabs moves the group (out of its set).
let (strip, _) = ui.allocate_exact_size(vec2(width, STRIP), Sense::hover());
ui.painter().rect_filled(strip, 0.0, t.panel_darker);
let lone = g.ids.len() == 1;
@@ -1049,8 +1050,12 @@ fn draw_groups(
}
let rest = Rect::from_min_max(pos2(out.right, strip.top()), pos2(menu.left() - 4.0, strip.bottom()));
if rest.width() > 0.0 {
let resp = ui.interact(rest, ui.id().with(("strip", g.gi)), Sense::drag());
if resp.drag_started() {
// Click as well as drag, so a press there no longer starts the move at once and a
// double-click can collapse the group.
let resp = ui.interact(rest, ui.id().with(("strip", g.gi)), Sense::click_and_drag());
if resp.double_clicked() {
*action = Some(Action::Collapse(g.gi));
} else if resp.drag_started() {
*action = Some(match alone {
Some(pos) => Action::Move(g.active, pos),
None => Action::Detach(g.gi, strip.left()),
@@ -1777,6 +1782,35 @@ mod tests {
assert!(!h.app.ui.floating_panels[0].collapsed);
}
/// Illustrator also collapses the group from the blank part of the strip, see
/// <https://helpx.adobe.com/illustrator/using/customizing-workspace.html>.
#[test]
fn a_double_click_on_the_strip_right_of_the_tabs_collapses_a_floating_group_in_place() {
let mut h = Harness::new();
h.app.run("window.panel.float", json!({"panel": "layers", "x": 500, "y": 150})).unwrap();
h.app.run("window.panel.float", json!({"panel": "stroke", "below": "layers"})).unwrap();
h.settle();
let owned = |h: &Harness| format!("{:?}", sets(&h.app.ui.floating_panels));
let before = owned(&h);
let open = h.temp_rect(group_rect_id("layers"));
// Left of the panel menu, well right of the lone Layers tab.
let blank = pos2(open.right() - 40.0, open.top() + 1.0 + TITLE + STRIP / 2.0);
let double = |h: &mut Harness| {
h.time += 1.0;
h.frame(vec![Event::PointerMoved(blank), button(blank, true)]);
h.frame(vec![button(blank, false)]);
h.frame(vec![button(blank, true)]);
h.frame(vec![button(blank, false)]);
h.settle();
};
double(&mut h);
assert!(h.app.ui.floating_panels[0].collapsed, "collapsed");
assert_eq!(owned(&h), before, "still in its set");
assert_eq!(h.temp_rect(group_rect_id("layers")).min, open.min, "not moved");
double(&mut h);
assert!(!h.app.ui.floating_panels[0].collapsed, "expanded");
}
#[test]
fn sets_by_command_are_checked_and_kept_in_workspaces_and_preferences() {
let mut app = VectorcraftApp::new(Session::new(), Default::default());
+36 -1
View File
@@ -798,7 +798,9 @@ impl craft_ui::docking::DockContent<String> for PanelContent<'_> {
self.commands.push(("window.panel.dock", json!({"panel": active, "group": true})));
}
} else {
let response = ui.interact(grip, ui.id().with("dock-group-bar"), egui::Sense::drag());
// Click as well as drag, so a double-click here collapses the group
// (`tab_area_double_clicked`) rather than floating it on the first press.
let response = ui.interact(grip, ui.id().with("dock-group-bar"), egui::Sense::click_and_drag());
if response.drag_started()
&& let Some(pos) = response.interact_pointer_pos()
{
@@ -890,6 +892,36 @@ fn root_groups(ctx: &egui::Context, layout: &Layout<String>, content: egui::Rect
out
}
/// The panel shown by the dock group whose strip was double-clicked this frame right of its tabs
/// and left of its panel menu, from which Illustrator also collapses the group
/// (<https://helpx.adobe.com/illustrator/using/customizing-workspace.html>). Most of it has no
/// widget of its own, so the press is read from the input; `layer` is the dock's, so a press on a
/// floating group or a menu over the strip doesn't count. Accordions are left out.
fn tab_area_double_clicked(
ctx: &egui::Context,
layer: egui::LayerId,
layout: &Layout<String>,
groups: &[crate::floating::RootGroup],
) -> Option<&'static str> {
let at = ctx.input(|i| if i.pointer.button_double_clicked(egui::PointerButton::Primary) { i.pointer.interact_pos() } else { None })?;
// No layer: the background, where the dock panel is.
if ctx.layer_id_at(at).is_some_and(|l| l != layer) {
return None;
}
let group = groups.iter().find(|g| g.strip.contains(at))?;
tab_group_shortens(layout, group.shown)?;
let area = egui::Id::new("vectorcraft-panel-docking");
let right = |id: egui::Id| ctx.read_response(id).map(|r| r.rect.right());
let tabs = group
.panels
.iter()
.filter_map(|p| right(area.with(("tab", &p.to_string()))))
.chain(right(area.with(("overflow", &group.shown.to_string()))))
.fold(group.strip.left(), f32::max);
// The panel menu is 16 wide, 12 in from the right, with 4 to spare (`group_header`).
(at.x >= tabs && at.x < group.strip.right() - 24.0).then_some(group.shown)
}
pub(crate) fn show(app: &mut VectorcraftApp, ui: &mut egui::Ui) -> bool {
let root_key = egui::Id::new("vectorcraft-root-group-drag");
if let Some(drag) = ui.ctx().data(|data| data.get_temp::<RootGroupDrag>(root_key)) {
@@ -1078,6 +1110,9 @@ pub(crate) fn show(app: &mut VectorcraftApp, ui: &mut egui::Ui) -> bool {
}
}
let groups = if collapsed { Vec::new() } else { root_groups(ui.ctx(), &display, tree, style.tab_height) };
if let Some(panel) = tab_area_double_clicked(ui.ctx(), ui.layer_id(), &display, &groups) {
content.commands.push(("window.panel.collapse", json!({"panel": panel})));
}
ui.ctx().data_mut(|data| data.insert_temp(crate::floating::root_groups_id(), groups));
for (id, params) in content.commands {
if let Err(error) = app.run(id, params) {
+49
View File
@@ -1052,6 +1052,55 @@ fn a_double_click_on_a_legacy_dock_tab_collapses_the_tabbed_group() {
assert!(layers_drawn(&h), "Layers shows again");
}
/// Illustrator also collapses the group from the blank part of the strip, see
/// <https://helpx.adobe.com/illustrator/using/customizing-workspace.html>. The legacy dock's strip right of its tabs collapses the tabbed group, and floats nothing.
#[test]
fn a_double_click_on_the_legacy_dock_strip_right_of_the_tabs_collapses_the_tabbed_group() {
let mut app = app();
app.run("file.new", json!({"width": 400, "height": 300})).unwrap();
let mut h = harness(app, vec2(1280.0, 800.0));
let libraries = h.query_all_by_label("Libraries").find(|node| node.rect().height() <= 45.0).unwrap().rect();
let dock = h.ctx.data(|d| d.get_temp::<egui::Rect>(crate::floating::dock_rect_id())).unwrap();
let blank = pos2(libraries.right() + 12.0, libraries.center().y);
assert!(blank.x < dock.right() - 30.0, "left of the panel menu: {libraries:?} {dock:?}");
let tab = h.query_all_by_label("Layers").find(|node| node.rect().height() <= 45.0).unwrap().rect().center();
click(&mut h, tab);
h.run_steps(45);
assert!(layers_drawn(&h), "Layers shows");
double_click(&mut h, blank);
assert!(h.state().ui.docking.is_none() && h.state().ui.floating_panels.is_empty(), "nothing floats");
assert!(h.state().ui.docking_collapsed.contains("layers"));
assert!(!layers_drawn(&h), "no Layers body");
double_click(&mut h, blank);
assert!(h.state().ui.docking_collapsed.is_empty());
assert!(layers_drawn(&h), "Layers shows again");
}
/// The shared dock's strip right of a group's tabs, and the grip beside its panel menu, collapse
/// the group to its tabs and expand it, without floating it.
#[test]
fn a_double_click_on_the_shared_dock_strip_right_of_the_tabs_collapses_its_group() {
let mut app = app();
app.run("file.new", json!({"width": 400, "height": 300})).unwrap();
app.run("window.panel.move", json!({"panel":"swatches", "anchor":"layers", "zone":"top"})).unwrap();
let saved = app.ui.docking.clone();
let mut h = harness(app, vec2(1280.0, 800.0));
let strip = root_groups(&h)[0].strip;
let tab = h.ctx.read_response(egui::Id::new("vectorcraft-panel-docking").with(("tab", &"swatches".to_string()))).unwrap().rect;
// Between the tab and the group's header (80 wide), and on the header's grip.
let gap = pos2(tab.right() + 10.0, strip.center().y);
assert!(gap.x < strip.right() - 80.0, "{tab:?} {strip:?}");
let grip = pos2(strip.right() - 50.0, strip.center().y);
for at in [gap, grip] {
double_click(&mut h, at);
assert!(h.state().ui.docking_collapsed.contains("swatches"), "{at:?}");
assert!(root_groups(&h)[0].rect.height() < 40.0, "{at:?}");
double_click(&mut h, at);
assert!(h.state().ui.docking_collapsed.is_empty(), "{at:?}");
assert_eq!(h.state().ui.docking, saved, "nothing floats: {at:?}");
}
}
/// `window.panel.collapse` checks its parameters, and a collapsed group survives a reload.
#[test]
fn the_collapse_command_validates_and_survives_a_reload() {