Fix #455: the toolbar's double arrow and Window › Toolbars switch one/two columns

The toolbar went to two columns whenever the window was shorter than the
single column (about 990 pt for Basic, more for Advanced), so on most
screens it was always double and the double arrow and the Window menu item
flipped a flag nothing read. The column count is now the user's choice
alone; a window too short for the tools scrolls them (#84).

- window.toolbarColumns takes {double?: bool} (set or toggle, returns the
  new state) and reports `checked`; the double arrow runs it.
- The Window › Toolbars item reads "Double Column" with a check mark;
  catalogs updated (cs, es, ja, pt-br, zh-hans, zh-hant) and an es @msg row
  for the new error.
- Headless tests: the arrow and the command toggle the column count in a
  700 pt window (both fail on main).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
bflatasticandClaude Opus 5.5 committed 2026-10-08 15:38:54 +07:00
1 parent d8e79078e5
commit 297f6e93d3
9 files changed
+79 -20

No files matched your search

+1 -1
View File
@@ -4,7 +4,7 @@ VectorCraft is a clean-room, open-source, pure-Rust reimplementation of the Adob
This file tracks **how far we are and what's left**. Time estimates are wall-clock hours of continuous Claude Opus 5.5 agent work (including builds and the CI gate), given both for **one agent** and for **4–6 parallel agents** on disjoint crates. They are counted from the remaining work (see [Parity estimate](#parity-estimate)), calibrated against measured throughput, and updated as work lands.
_Last updated: 2026-10-08 (after M4.14–M4.98, M8.1–M8.20, M14.4–M14.7, the MCP prompts, resource templates, completions and logging, the 2026-10-07 issue fixes, the Layers panel rework and the 2026-10-08 community contributions: Pen and shape-tool modifiers, path editing, Layers, Artboards, Swatches/Brushes and dialogs, Japanese composition and PDF text, Hebrew/Arabic type, interface languages (Spanish among them), the font menu, saved selections, MCP and EPS, the missing-font export warning (#319), open paths in the Shape Builder (#374), PDF/`.ai` import keeping strokes live and hidden and nested layers (#372), per-corner Live Corners (#381), SVG type opening at the size it draws at (#396), the Wayland clipboard with copied-file paste (#398), ruler guides that can be selected, moved and deleted (#414), two batches of Preferences options that were stored but unused (#394), skeleton reads with `document.find` for large documents over MCP (#416) and anchors dragged with Direct Selection snapping to points, segments and smart guides, clipped SVG layers opening as clipping layers (#420), PDF/`.ai` layers read on pages of tens of millions of operators (#419), Document Color Mode putting RGB greys on the black plate on request and converting pattern tiles (#421) and movable dialogs with Offset Path's Joins dropdown (#423) and numeric fields scrubbed by dragging their label (#400), stroke weight presets in the stroke unit (#426), the Appearance panel's Opacity popup (#430), type on a path brackets, flip and Type on a Path Options that keep the type in place (#429), Layers rows dragged with a ghost and dropped on the trash or New Layer (#435), tool flyouts opening on a right or corner press (#436), tool flyouts torn off into floating toolbars (#437), the Shape Builder's erase mode deleting edges too (#439), installed fonts registered outside the font folders and fonts installed while the app runs listed in the font menus (#443), alike circular corners on rectangles that aren't square (#442), tool cursors as OS cursors so the pointer no longer trails over the canvas (#444), artboards copied and pasted with their art (#446), guides dragged out of the rulers snapping to the art's side midpoints and the artboards, and artboard guides (#451); see [Honest assessment](#honest-assessment-2026-10-05))._
_Last updated: 2026-10-08 (after M4.14–M4.98, M8.1–M8.20, M14.4–M14.7, the MCP prompts, resource templates, completions and logging, the 2026-10-07 issue fixes, the Layers panel rework and the 2026-10-08 community contributions: Pen and shape-tool modifiers, path editing, Layers, Artboards, Swatches/Brushes and dialogs, Japanese composition and PDF text, Hebrew/Arabic type, interface languages (Spanish among them), the font menu, saved selections, MCP and EPS, the missing-font export warning (#319), open paths in the Shape Builder (#374), PDF/`.ai` import keeping strokes live and hidden and nested layers (#372), per-corner Live Corners (#381), SVG type opening at the size it draws at (#396), the Wayland clipboard with copied-file paste (#398), ruler guides that can be selected, moved and deleted (#414), two batches of Preferences options that were stored but unused (#394), skeleton reads with `document.find` for large documents over MCP (#416) and anchors dragged with Direct Selection snapping to points, segments and smart guides, clipped SVG layers opening as clipping layers (#420), PDF/`.ai` layers read on pages of tens of millions of operators (#419), Document Color Mode putting RGB greys on the black plate on request and converting pattern tiles (#421) and movable dialogs with Offset Path's Joins dropdown (#423) and numeric fields scrubbed by dragging their label (#400), stroke weight presets in the stroke unit (#426), the Appearance panel's Opacity popup (#430), type on a path brackets, flip and Type on a Path Options that keep the type in place (#429), Layers rows dragged with a ghost and dropped on the trash or New Layer (#435), tool flyouts opening on a right or corner press (#436), tool flyouts torn off into floating toolbars (#437), the Shape Builder's erase mode deleting edges too (#439), installed fonts registered outside the font folders and fonts installed while the app runs listed in the font menus (#443), alike circular corners on rectangles that aren't square (#442), tool cursors as OS cursors so the pointer no longer trails over the canvas (#444), artboards copied and pasted with their art (#446), guides dragged out of the rulers snapping to the art's side midpoints and the artboards, artboard guides (#451), and the toolbar's double arrow and Window › Toolbars › Double Column switching between one and two columns in any window height (#455); see [Honest assessment](#honest-assessment-2026-10-05))._
## Where we are
+1 -1
View File
@@ -491,7 +491,7 @@
Tools Nástroje
Toolbars Panely nástrojů
Advanced Rozšířený
Single / Double Column Jeden / dva sloupce
Double Column Dva sloupce
Actions Akce
Asset Export Export prvků
Attributes Atributy
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2 -1
View File
@@ -2430,7 +2430,7 @@
Simplify… Simplificar…
Simulate Colored Paper Simular papel de color
Simulate Paper Simular papel
Single / Double Column Columna simple / doble
Double Column Columna doble
Single Left Quotation Mark Comilla simple de apertura
Single Quotes: Comillas simples:
Single Right Quotation Mark Comilla simple de cierre
@@ -3539,3 +3539,4 @@ axis Both Ambos
@msg floating must be true or false floating debe ser true o false
@msg tool (a tool id) is required se requiere tool (un id de herramienta)
@msg {tool} isn't in a toolbar group of several tools {tool} no está en un grupo de la barra de herramientas con varias herramientas
@msg double must be true or false double debe ser true o false
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1 -1
View File
@@ -503,7 +503,7 @@
Tools ツール
Toolbars ツールバー
Advanced 詳細
Single / Double Column 1 列/2 列
Double Column 2 列
Actions アクション
Asset Export アセットの書き出し
Attributes 属性
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1 -1
View File
@@ -2153,7 +2153,7 @@
Simplify… Simplificar…
Simulate Colored Paper Simular Papel Colorido
Simulate Paper Simular Papel
Single / Double Column Coluna Única / Dupla
Double Column Coluna Dupla
Single Left Quotation Mark Aspa Esquerda Única
Single Quotes: Aspas Simples:
Single Right Quotation Mark Aspa Direita Única
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1 -1
View File
@@ -2366,7 +2366,7 @@
Simplify… 简化…
Simulate Colored Paper 模拟有色纸张
Simulate Paper 模拟纸张
Single / Double Column 单列/双列
Double Column 双列
Single Left Quotation Mark 左单引号
Single Quotes: 单引号:
Single Right Quotation Mark 右单引号
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1 -1
View File
@@ -2448,7 +2448,7 @@
Simplify… 簡化…
Simulate Colored Paper 模擬有色紙張
Simulate Paper 模擬紙張
Single / Double Column 單欄 / 雙欄
Double Column 雙欄
Single Left Quotation Mark 左單引號
Single Quotes: 單引號:
Single Right Quotation Mark 右單引號
Can't render this file because it contains an unexpected character in line 6 and column 47.
+16 -3
View File
@@ -197,7 +197,12 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[
("view.rotateReset", "Reset Rotate View", "Cmd+Shift+1", "{}"),
("window.control", "Control", "", "{}"),
("window.toolbar", "Tools", "", "{}"),
("window.toolbarColumns", "Toolbar: Single/Double Column", "", "{}"),
(
"window.toolbarColumns",
"Toolbar: Single/Double Column",
"",
"{double?: bool} show the toolbar's tools in two columns (true), one (false) or toggle (omitted), as the double arrow at the top of the toolbar does; returns the new state",
),
("window.toolbarAdvanced", "Toolbar: Advanced / Basic", "", "{}"),
(
"window.floatTools",
@@ -984,7 +989,14 @@ pub fn run_ui_command(app: &mut VectorcraftApp, id: &str, p: &Value) -> Option<R
}
"window.control" => flag(&mut app.ui.control_bar),
"window.toolbar" => flag(&mut app.ui.toolbar),
"window.toolbarColumns" => flag(&mut app.ui.toolbar_double),
"window.toolbarColumns" => {
app.ui.toolbar_double = match p.get("double") {
None | Some(Value::Null) => !app.ui.toolbar_double,
Some(Value::Bool(b)) => *b,
Some(_) => return Some(Err("double must be true or false".into())),
};
Ok(json!(app.ui.toolbar_double))
}
"window.toolbarAdvanced" => flag(&mut app.ui.toolbar_advanced),
"window.floatTools" => {
let floating = match p.get("floating") {
@@ -1317,6 +1329,7 @@ pub fn checked(app: &VectorcraftApp, id: &str, p: &Value) -> Option<bool> {
"window.control" => app.ui.control_bar,
"window.toolbar" => app.ui.toolbar,
"window.toolbarAdvanced" => app.ui.toolbar_advanced,
"window.toolbarColumns" => app.ui.toolbar_double,
"window.floatTools" => {
let tool = p.get("tool").and_then(Value::as_str).unwrap_or("");
app.ui.floating_flyouts.iter().any(|f| f.tools.iter().any(|id| id == tool))
@@ -2305,7 +2318,7 @@ pub fn menu_tree() -> Vec<(&'static str, Vec<Item>)> {
c("Control", "window.control"),
c("Contextual Task Bar", "window.taskBar"),
c("Tools", "window.toolbar"),
sub("Toolbars", vec![c("Advanced", "window.toolbarAdvanced"), c("Single / Double Column", "window.toolbarColumns")]),
sub("Toolbars", vec![c("Advanced", "window.toolbarAdvanced"), c("Double Column", "window.toolbarColumns")]),
Sep,
panel("Actions", "actions"),
panel("Align", "align"),
+55 -10
View File
@@ -1,5 +1,7 @@
//! The Tools panel. Default: Illustrator 2026's categorized single-column toolbar (Select, Shapes,
//! Draw, Modify, Type, Navigate, Color). Window → Toolbars → Advanced shows every tool group.
//! Draw, Modify, Type, Navigate, Color). Window → Toolbars → Advanced shows every tool group; the
//! double arrow at the top (or Window → Toolbars → Double Column, `window.toolbarColumns`) lays the
//! tools out in one or two columns.
//! Bottom: fill/stroke proxy, colour/gradient/none, drawing modes, screen mode, Edit Toolbar.
use egui::{Color32, CornerRadius, Sense, Stroke, Ui, pos2, vec2};
@@ -88,10 +90,8 @@ pub fn remember(app: &mut VectorcraftApp, id: &str) {
pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
let t = Tokens::get(ui.ctx());
let all = slots(app);
let avail = ui.available_height();
let labels = all.iter().filter(|s| s.0.is_some()).count() as f32 * 20.0;
let need = all.len() as f32 * PITCH + labels + 190.0;
let cols = if app.ui.toolbar_double || avail < need { 2 } else { 1 };
// The user's choice alone: a window too short for the tools scrolls them.
let cols = if app.ui.toolbar_double { 2 } else { 1 };
let w = if cols == 2 { 76.0 } else { WIDTH };
egui::Panel::left("toolbar")
.resizable(false)
@@ -108,8 +108,11 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
egui::Rect::from_min_size(hdr.left_top() + vec2(3.0, 2.0), vec2(10.0, 10.0)),
if hresp.hovered() { t.text_strong } else { t.text },
);
if hresp.on_hover_text(tl!("Toggle single/double column")).clicked() {
app.ui.toolbar_double = !app.ui.toolbar_double;
if hresp.on_hover_text(tl!("Toggle single/double column")).clicked()
// It only fails on bad params, which this never sends; show it all the same.
&& let Err(e) = app.run("window.toolbarColumns", json!({ "double": cols == 1 }))
{
app.ui.status = e;
}
let (grip, _) = ui.allocate_exact_size(vec2(ui.available_width(), 6.0), Sense::hover());
for k in 0..6 {
@@ -982,10 +985,52 @@ pub(crate) mod tests {
/// A click at `at`.
fn click(app: &mut VectorcraftApp, ctx: &egui::Context, time: f64, at: Pos2, button: PointerButton) {
click_in(app, ctx, time, at, button, 1200.0);
}
/// [`click`] in a window `height` points tall; returns the tool buttons' rects after it.
fn click_in(app: &mut VectorcraftApp, ctx: &egui::Context, time: f64, at: Pos2, button: PointerButton, height: f32) -> Vec<egui::Rect> {
let b = |pressed| Event::PointerButton { pos: at, button, pressed, modifiers: Default::default() };
frame(app, ctx, time, vec![Event::PointerMoved(at), b(true)]);
frame(app, ctx, time + 0.05, vec![b(false)]);
frame(app, ctx, time + 0.1, vec![]);
frame_in(app, ctx, time, vec![Event::PointerMoved(at), b(true)], height);
frame_in(app, ctx, time + 0.05, vec![b(false)], height);
frame_in(app, ctx, time + 0.1, vec![], height)
}
/// How many columns of tool buttons `rects` make.
fn columns(rects: &[egui::Rect]) -> usize {
let mut lefts: Vec<i32> = rects.iter().map(|r| r.left().round() as i32).collect();
lefts.sort_unstable();
lefts.dedup();
lefts.len()
}
#[test]
fn the_double_arrow_toggles_one_and_two_columns_in_a_short_window() {
let mut app = VectorcraftApp::new(Session::new(), Default::default());
let ctx = egui::Context::default();
// 700 pt: too short for the single column, which scrolls instead of turning into two.
assert_eq!(columns(&frame_in(&mut app, &ctx, 0.0, vec![], 700.0)), 1);
let arrow = pos2(8.0, 7.0);
assert_eq!(columns(&click_in(&mut app, &ctx, 1.0, arrow, PointerButton::Primary, 700.0)), 2);
assert!(app.ui.toolbar_double && crate::menus::checked(&app, "window.toolbarColumns", &json!({})) == Some(true));
assert_eq!(columns(&click_in(&mut app, &ctx, 2.0, arrow, PointerButton::Primary, 700.0)), 1);
assert!(!app.ui.toolbar_double);
}
#[test]
fn the_columns_command_toggles_or_sets_the_toolbar_columns() {
let mut app = VectorcraftApp::new(Session::new(), Default::default());
let ctx = egui::Context::default();
assert_eq!(app.run("window.toolbarColumns", json!({})).unwrap(), json!(true));
assert_eq!(columns(&frame_in(&mut app, &ctx, 0.0, vec![], 700.0)), 2);
assert_eq!(app.run("window.toolbarColumns", json!({"double": true})).unwrap(), json!(true));
assert_eq!(app.run("window.toolbarColumns", json!({})).unwrap(), json!(false));
assert_eq!(crate::menus::checked(&app, "window.toolbarColumns", &json!({})), Some(false));
assert_eq!(columns(&frame_in(&mut app, &ctx, 1.0, vec![], 700.0)), 1);
assert!(app.run("window.toolbarColumns", json!({"double": "yes"})).is_err());
// Advanced (every tool group) is taller still and keeps the column the user chose too.
app.run("window.toolbarAdvanced", json!({})).unwrap();
assert_eq!(columns(&frame_in(&mut app, &ctx, 2.0, vec![], 700.0)), 1);
}
#[test]