diff --git a/crates/ui-egui/src/panel_docking.rs b/crates/ui-egui/src/panel_docking.rs index 2b12d673d..e3492f7a8 100644 --- a/crates/ui-egui/src/panel_docking.rs +++ b/crates/ui-egui/src/panel_docking.rs @@ -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> { - let front = panels.get(active).and_then(|p| normalize(p)); - let kept: Vec = panels.iter().filter_map(|p| self.take(p)).collect(); + fn group(&mut self, panels: &[impl AsRef], active: usize) -> Option> { + let front = panels.get(active).and_then(|p| normalize(p.as_ref())); + let kept: Vec = 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]>, placed: &mut Placed) -> Option> { let (groups, width): (Vec>, 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> = 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) }) } diff --git a/crates/ui-egui/src/panel_docking_tests.rs b/crates/ui-egui/src/panel_docking_tests.rs index 0b37fb33e..9d272eb3c 100644 --- a/crates/ui-egui/src/panel_docking_tests.rs +++ b/crates/ui-egui/src/panel_docking_tests.rs @@ -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> = icons.groups.iter().map(|g| g.panels.iter().map(String::as_str).collect()).collect(); + let expected: Vec> = 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> = 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(); diff --git a/crates/ui-egui/src/prefs.rs b/crates/ui-egui/src/prefs.rs index cdc822c4f..0e7273a00 100644 --- a/crates/ui-egui/src/prefs.rs +++ b/crates/ui-egui/src/prefs.rs @@ -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).