#1311: group the dock's panel icons as InDesign does

This commit is contained in:
aiongg
2026-10-21 04:49:27 +00:00
parent 801dc17988
commit 8097e2cf4d
3 changed files with 72 additions and 19 deletions
+41 -16
View File
@@ -92,30 +92,55 @@ impl Placed {
/// A group of the unplaced registered panels of `panels`, with the one at `active` in front
/// when it is among them.
fn group(&mut self, panels: &[String], active: usize) -> Option<Group<String>> {
let front = panels.get(active).and_then(|p| normalize(p));
let kept: Vec<String> = panels.iter().filter_map(|p| self.take(p)).collect();
fn group(&mut self, panels: &[impl AsRef<str>], active: usize) -> Option<Group<String>> {
let front = panels.get(active).and_then(|p| normalize(p.as_ref()));
let kept: Vec<String> = panels.iter().filter_map(|p| self.take(p.as_ref())).collect();
let active = front.and_then(|f| kept.iter().position(|p| p == f)).unwrap_or(0);
(!kept.is_empty()).then(|| Group::new(kept, active))
}
}
/// A workspace's iconic column: its named `icon_groups` (Essentials Classic, Typography), or
/// one group per panel not placed yet. Window › Styles opens the style panels as a floating
/// group, so they start outside it.
/// The iconic column's groups, top to bottom, in a workspace without its own `icon_groups`:
/// InDesign's panel families, with a separator only between families.
const ICON_GROUPS: &[&[&str]] = &[
&["stroke", "color", "swatches", "gradient"],
&["effects"],
&["character", "paragraph", "typography"],
&["glyphs"],
&["table"],
&["textWrap"],
&["align", "pathfinder"],
&["flexLayout", "liquid"],
&["links", "library"],
&["info"],
&["attributes", "preflight"],
&["notes", "trackChanges", "conditions"],
&["hyperlinks", "bookmarks"],
&["buttons", "states"],
&["media", "transitions"],
&["articles", "tags"],
&["book", "dataMerge", "scripts"],
];
/// A workspace's iconic column of the panels not placed yet: its named `icon_groups`
/// (Essentials Classic, Typography), or [`ICON_GROUPS`] followed by any other dock panel as a
/// group of its own. Window › Styles opens the style panels as a floating group, so they start
/// outside it.
fn icon_column(icon_groups: Option<&[Vec<String>]>, placed: &mut Placed) -> Option<Column<String>> {
let (groups, width): (Vec<Group<String>>, f32) = match icon_groups {
Some(groups) => (groups.iter().filter_map(|g| placed.group(g, 0)).collect(), NAMED_ICON_WIDTH),
None => (
crate::dock::DOCK_TABS
.iter()
.chain(crate::dock::ICON_PANELS)
.filter(|p| !FLOATING_GROUPS.iter().any(|g| g.contains(&p.0)))
.filter_map(|p| placed.take(p.0))
.map(|id| Group::new(vec![id], 0))
.collect(),
ICON_WIDTH,
),
None => {
let mut groups: Vec<Group<String>> = ICON_GROUPS.iter().filter_map(|g| placed.group(g, 0)).collect();
groups.extend(
crate::dock::DOCK_TABS
.iter()
.chain(crate::dock::ICON_PANELS)
.filter(|p| !FLOATING_GROUPS.iter().any(|g| g.contains(&p.0)))
.filter_map(|p| placed.take(p.0))
.map(|id| Group::new(vec![id], 0)),
);
(groups, ICON_WIDTH)
}
};
(!groups.is_empty()).then_some(Column { groups, width: COLUMN_WIDTH, iconic: true, iconic_width: Some(width) })
}
+29 -1
View File
@@ -811,7 +811,9 @@ fn default_workspaces_are_the_canvas_beside_an_end_dock() {
assert_eq!(outer.groups.len(), 1);
assert_eq!(outer.groups[0].panels, ["properties", "pages", "layers"]);
assert!(icons.iconic);
assert!(icons.groups.iter().all(|g| g.panels.len() == 1));
let groups: Vec<Vec<&str>> = icons.groups.iter().map(|g| g.panels.iter().map(String::as_str).collect()).collect();
let expected: Vec<Vec<&str>> = ICON_GROUPS.iter().map(|g| g.to_vec()).collect();
assert_eq!(groups, expected, "the iconic column groups InDesign's panel families");
assert!(!layout.contains(&"paragraphStyles".to_string()), "Window › Styles opens the styles floating");
// Essentials Classic: only its named icon groups, in one iconic column.
app.run("window.workspace", json!({"name": "Essentials Classic"})).unwrap();
@@ -826,6 +828,32 @@ fn default_workspaces_are_the_canvas_beside_an_end_dock() {
assert!(groups.iter().flatten().all(|p| expected.iter().flatten().any(|e| e == p)));
}
/// Every dock panel that starts in the iconic column has one place in [`ICON_GROUPS`], so a new
/// panel is not left as a group of its own.
#[test]
fn every_icon_panel_is_in_one_default_icon_group() {
for (id, _, _) in crate::dock::ICON_PANELS.iter().filter(|p| !FLOATING_GROUPS.iter().any(|g| g.contains(&p.0))) {
assert_eq!(ICON_GROUPS.iter().flat_map(|g| g.iter()).filter(|p| *p == id).count(), 1, "{id}");
}
for id in ICON_GROUPS.iter().flat_map(|g| g.iter()) {
assert!(crate::dock::ICON_PANELS.iter().any(|p| p.0 == *id), "{id} is not a dock panel");
}
}
/// A panel the workspace places elsewhere leaves its icon group; a group it empties goes.
#[test]
fn placed_panels_leave_their_icon_groups() {
let mut placed = Placed::default();
for id in ["properties", "pages", "layers", "swatches", "effects"] {
placed.take(id);
}
let column = icon_column(None, &mut placed).unwrap();
let groups: Vec<Vec<&str>> = column.groups.iter().map(|g| g.panels.iter().map(String::as_str).collect()).collect();
assert_eq!(groups.first().unwrap(), &["stroke", "color", "gradient"]);
assert_eq!(groups.get(1).unwrap(), &["character", "paragraph", "typography"]);
assert_eq!(groups.len(), ICON_GROUPS.len() - 1);
}
#[test]
fn the_dock_is_at_the_right_in_english_and_the_left_in_arabic() {
let tab = |h: &Harness<'static, DesignApp>| h.ctx.read_response(tab_id("properties")).map(|r| r.rect).unwrap();
+2 -2
View File
@@ -228,8 +228,8 @@ mod tests {
assert_eq!(columns[0].groups[0].active, 3);
assert!(!columns[0].iconic);
let icons = &columns[1];
assert!(icons.iconic && icons.groups.iter().any(|g| g.panels == ["stroke"]));
assert!(icons.groups.iter().all(|g| g.panels != ["swatches"]), "each panel appears once");
assert!(icons.iconic && icons.groups.iter().any(|g| g.panels == ["stroke", "color", "gradient"]));
assert!(icons.groups.iter().all(|g| !g.panels.iter().any(|p| p == "swatches")), "each panel appears once");
// Splits: a vertical split stacks groups in one column, a horizontal one adds a column
// (the dock lists columns from the window edge inwards).