Merge pull request #2556 from dotCooCoo/feat/contextual-task-bar-artboard

Contextual Task Bar: the Artboard tool offers Resize, Duplicate, Rearrange All and Export
This commit is contained in:
BFlat
2026-10-17 15:48:57 +07:00
committed by GitHub
21 changed files with 205 additions and 50 deletions
+184 -49
View File
@@ -2722,17 +2722,28 @@ pub(crate) fn has_colors(doc: &vectorcraft_doc::Document, ids: &[vectorcraft_doc
})
}
/// The bounds of the artboards the Artboard tool has selected ([`crate::panels::artboards::picked`]).
fn picked_artboards_bounds(app: &VectorcraftApp) -> Option<Rect> {
let st = app.session.active()?;
let picked = crate::panels::artboards::picked(app, st.doc.artboards.len());
picked.iter().filter_map(|i| st.doc.artboards.get(*i)).map(|a| a.rect).reduce(|a, b| a.union(b))
}
/// The Contextual Task Bar: a floating pill under the selection with the most likely next actions.
/// With the Artboard tool it sits under the selected artboards and offers Resize (Artboard
/// Options), Duplicate, Rearrange All (with two or more artboards) and Export (Export for Screens).
/// Its handle drags it anywhere on the canvas; unpinned it keeps that offset as it follows the
/// selection, pinned it stays put ([`crate::state::TaskBarPlace`]).
fn task_bar(app: &mut VectorcraftApp, ui: &mut Ui, xf: &Xf) {
let t = Tokens::get(ui.ctx());
if app.session.active().is_none_or(|st| st.selection.is_empty())
|| !matches!(app.session.tool_id(), "selection" | "directSelection" | "groupSelection")
let on_artboards = app.session.tool_id() == "artboard";
if !on_artboards
&& (app.session.active().is_none_or(|st| st.selection.is_empty())
|| !matches!(app.session.tool_id(), "selection" | "directSelection" | "groupSelection"))
{
return;
}
let Some(b) = app.selection_bounds() else { return };
let Some(b) = (if on_artboards { picked_artboards_bounds(app) } else { app.selection_bounds() }) else { return };
let Some(st) = app.session.active() else { return };
let n = st.selection.len();
let first = st.selection.objects.first().and_then(|id| st.doc.node(*id)).cloned();
@@ -2745,35 +2756,47 @@ fn task_bar(app: &mut VectorcraftApp, ui: &mut Ui, xf: &Xf) {
f.name.as_deref() == Some("Image Trace") && f.children().is_some_and(|c| c.first().is_some_and(|i| matches!(i.kind, NodeKind::Image(_))))
});
let mut items: Vec<(&str, &str, &str)> = vec![]; // (label, icon, command)
// As in the reference app, an image offers Image Trace and Crop Image, and traced art offers
// Expand (<https://community.adobe.com/announcements-651/contextual-task-bar-in-illustrator-2025-quick-actions-when-you-need-them-1546580>).
if n == 1 && is_image {
items.push((tl!("Image Trace"), "image", TASK_BAR_IMAGE_TRACE));
items.push((tl!("Crop Image"), "dc-pf-crop", "ui.cropImage"));
if on_artboards {
// As in the reference app, the Artboard tool offers to resize, duplicate and export the
// artboards (<https://community.adobe.com/questions-652/contextual-task-bar-in-illustrator-2025-quick-actions-when-you-need-them-1546580>),
// and to rearrange them (<https://creativepro.com/managing-artboards-in-illustrator/>).
items.push((tl!("Resize"), "move-diagonal-2", TASK_BAR_ARTBOARD_OPTIONS));
items.push((tl!("Duplicate"), "copy", TASK_BAR_DUPLICATE_ARTBOARDS));
if st.doc.artboards.len() > 1 {
items.push((tl!("Rearrange All"), "dc-rearrange", TASK_BAR_REARRANGE_ARTBOARDS));
}
items.push((tl!("Export"), "share-2", "file.exportForScreens"));
} else {
// As in the reference app, an image offers Image Trace and Crop Image, and traced art offers
// Expand (<https://community.adobe.com/announcements-651/contextual-task-bar-in-illustrator-2025-quick-actions-when-you-need-them-1546580>).
if n == 1 && is_image {
items.push((tl!("Image Trace"), "image", TASK_BAR_IMAGE_TRACE));
items.push((tl!("Crop Image"), "dc-pf-crop", "ui.cropImage"));
}
// Direct-selected anchors take the place of a path's Offset Path and Simplify.
let anchors = !st.selection.anchors.is_empty();
if anchors {
items.push((tl!("Remove Anchor Points"), "pen-tool-delete", "path.removeAnchors"));
items.push((tl!("Cut Path"), "scissors", "path.cutAtAnchors"));
}
if n > 1 {
items.push((tl!("Group"), "group", "object.group"));
items.push((tl!("Unite"), "squares-unite", "object.pathfinder.unite"));
} else if is_trace {
items.push((tl!("Expand"), "maximize-2", "imageTrace.expand"));
} else if is_group {
items.push((tl!("Ungroup"), "ungroup", "object.ungroup"));
items.push((tl!("Isolate"), "square-dashed", "object.isolate"));
} else if is_text {
items.push((tl!("Create Outlines"), "type", "type.createOutlines"));
} else if is_image {
// Offset Path and Simplify don't apply to an image.
} else if !anchors {
items.push((tl!("Offset Path"), "square-dashed", "object.path.offsetPath"));
items.push((tl!("Simplify"), "spline", "object.path.simplify"));
}
items.push((tl!("Duplicate"), "copy", "edit.duplicate"));
}
// Direct-selected anchors take the place of a path's Offset Path and Simplify.
let anchors = !st.selection.anchors.is_empty();
if anchors {
items.push((tl!("Remove Anchor Points"), "pen-tool-delete", "path.removeAnchors"));
items.push((tl!("Cut Path"), "scissors", "path.cutAtAnchors"));
}
if n > 1 {
items.push((tl!("Group"), "group", "object.group"));
items.push((tl!("Unite"), "squares-unite", "object.pathfinder.unite"));
} else if is_trace {
items.push((tl!("Expand"), "maximize-2", "imageTrace.expand"));
} else if is_group {
items.push((tl!("Ungroup"), "ungroup", "object.ungroup"));
items.push((tl!("Isolate"), "square-dashed", "object.isolate"));
} else if is_text {
items.push((tl!("Create Outlines"), "type", "type.createOutlines"));
} else if is_image {
// Offset Path and Simplify don't apply to an image.
} else if !anchors {
items.push((tl!("Offset Path"), "square-dashed", "object.path.offsetPath"));
items.push((tl!("Simplify"), "spline", "object.path.simplify"));
}
items.push((tl!("Duplicate"), "copy", "edit.duplicate"));
let fill = first.as_ref().map(|f| f.appearance.fill_paint()).unwrap_or_default();
let colored = has_colors(&st.doc, &st.selection.objects);
let doc = st.uid;
@@ -2840,24 +2863,27 @@ fn task_bar(app: &mut VectorcraftApp, ui: &mut Ui, xf: &Xf) {
run = Some((*cmd, json!({})));
}
}
let (r, resp) = ui.allocate_exact_size(vec2(26.0, 30.0), Sense::click());
widgets::paint_chip(ui, egui::Rect::from_center_size(r.center(), vec2(16.0, 16.0)), &fill);
ui.painter().rect_stroke(
egui::Rect::from_center_size(r.center(), vec2(16.0, 16.0)),
0.0,
Stroke::new(1.0, t.button_border),
StrokeKind::Outside,
);
if resp.on_hover_text(tl!("Fill")).clicked() {
app.session.fill_active = true;
app.ui.open_panel = Some("swatches".into());
}
// Recolor Artwork, when the selection has colours to recolour.
if colored && widgets::icon_button(ui, "palette", tl!("Recolor Artwork"), false, 30.0).clicked() {
run = Some(("ui.recolorDialog", json!({})));
}
if widgets::icon_button(ui, "lock", tl!("Lock (⌘2)"), false, 30.0).clicked() {
run = Some(("object.lock", json!({})));
// The selected objects' Fill, Recolor Artwork and Lock, left out with the Artboard tool.
if !on_artboards {
let (r, resp) = ui.allocate_exact_size(vec2(26.0, 30.0), Sense::click());
widgets::paint_chip(ui, egui::Rect::from_center_size(r.center(), vec2(16.0, 16.0)), &fill);
ui.painter().rect_stroke(
egui::Rect::from_center_size(r.center(), vec2(16.0, 16.0)),
0.0,
Stroke::new(1.0, t.button_border),
StrokeKind::Outside,
);
if resp.on_hover_text(tl!("Fill")).clicked() {
app.session.fill_active = true;
app.ui.open_panel = Some("swatches".into());
}
// Recolor Artwork, when the selection has colours to recolour.
if colored && widgets::icon_button(ui, "palette", tl!("Recolor Artwork"), false, 30.0).clicked() {
run = Some(("ui.recolorDialog", json!({})));
}
if widgets::icon_button(ui, "lock", tl!("Lock (⌘2)"), false, 30.0).clicked() {
run = Some(("object.lock", json!({})));
}
}
let more = widgets::icon_button(ui, "ellipsis", tl!("More Options"), false, 30.0);
egui::Popup::menu(&more).show(|ui| {
@@ -2894,6 +2920,11 @@ fn task_bar(app: &mut VectorcraftApp, ui: &mut Ui, xf: &Xf) {
// Image Trace traces the image at once with the Default preset. As a quick trace from
// Properties or the Control bar, it leaves the Image Trace panel as it was.
Some((TASK_BAR_IMAGE_TRACE, _)) => crate::menus::invoke(app, "imageTrace.make", json!({"preset": "Default"})),
// The Artboard tool's buttons: Artboard Options for the active artboard, as Properties'
// icon opens it; Duplicate for every selected artboard; Rearrange All Artboards.
Some((TASK_BAR_ARTBOARD_OPTIONS, _)) => crate::menus::invoke(app, "tool.options", json!({"tool": "artboard"})),
Some((TASK_BAR_DUPLICATE_ARTBOARDS, _)) => crate::panels::artboards::duplicate_picked(app),
Some((TASK_BAR_REARRANGE_ARTBOARDS, _)) => crate::panels::artboards::open_rearrange(app),
Some((c, p)) => crate::menus::invoke(app, c, p),
None => {}
}
@@ -2901,6 +2932,12 @@ fn task_bar(app: &mut VectorcraftApp, ui: &mut Ui, xf: &Xf) {
/// The Contextual Task Bar's Image Trace button: a trace with the Default preset.
const TASK_BAR_IMAGE_TRACE: &str = "taskBar.imageTrace";
/// The Contextual Task Bar's Resize button with the Artboard tool: Artboard Options.
const TASK_BAR_ARTBOARD_OPTIONS: &str = "taskBar.artboardOptions";
/// The Contextual Task Bar's Duplicate button with the Artboard tool.
const TASK_BAR_DUPLICATE_ARTBOARDS: &str = "taskBar.duplicateArtboards";
/// The Contextual Task Bar's Rearrange All button with the Artboard tool.
const TASK_BAR_REARRANGE_ARTBOARDS: &str = "taskBar.rearrangeArtboards";
#[cfg(test)]
mod tests {
@@ -6033,6 +6070,104 @@ mod tests {
assert_eq!(images, 0, "only the traced paths are kept");
}
/// The labels the Contextual Task Bar draws in a headless frame (after one to lay it out), with
/// their rects; none while it isn't shown. Icons, which draw as ⚠ without an image loader, are
/// left out.
fn task_bar_labels(app: &mut VectorcraftApp, ctx: &egui::Context) -> Vec<(String, egui::Rect)> {
frame(app, ctx, vec![]);
let raw = egui::RawInput { screen_rect: Some(egui::Rect::from_min_size(Pos2::ZERO, vec2(800.0, 600.0))), ..Default::default() };
let mut out = ctx.run_ui(raw, |ui| show(app, ui));
out.textures_delta.clear();
let Some(bar) = task_bar_rect(ctx) else { return vec![] };
out.shapes
.iter()
.filter_map(|c| match &c.shape {
egui::Shape::Text(t) if bar.contains(t.visual_bounding_rect().center()) && t.galley.text() != "⚠" => {
Some((t.galley.text().to_string(), t.visual_bounding_rect()))
}
_ => None,
})
.collect()
}
/// With the Artboard tool, the Contextual Task Bar sits under the selected artboards and offers
/// Resize, Duplicate, Rearrange All (with two or more artboards) and Export instead of the
/// selected objects' actions. With the Selection tool it offers the objects' actions again, and
/// with nothing selected it doesn't show.
#[test]
fn the_task_bar_offers_the_artboard_tools_actions() {
let mut app = VectorcraftApp::new(Session::new(), Default::default());
app.session.execute("file.new", &json!({"width": 200, "height": 100, "artboards": 3})).unwrap();
app.select_tool("selection");
let ctx = egui::Context::default();
let names = |app: &mut VectorcraftApp| task_bar_labels(app, &ctx).into_iter().map(|(t, _)| t).collect::<Vec<_>>();
assert!(names(&mut app).is_empty() && task_bar_rect(&ctx).is_none(), "nothing selected: no bar");
app.select_tool("artboard");
crate::panels::artboards::select(&mut app, 1);
// Artboard 2 in the middle of the window, at 100%.
let b = app.session.active().unwrap().doc.artboards[1].rect;
let view = app.view_mut().unwrap();
(view.center, view.zoom) = (b.center(), 1.0);
assert_eq!(names(&mut app), ["Resize", "Duplicate", "Rearrange All", "Export"]);
// Under artboard 2.
let xf = Xf::new(app.canvas_rect.unwrap(), app.view().unwrap());
let bar = task_bar_rect(&ctx).unwrap();
let center = xf.to_screen(b.center());
assert!(bar.top() > xf.to_screen(Point::new(b.x0, b.y1)).y && bar.x_range().contains(center.x), "{bar:?} under {b:?}");
// A selected object with the Selection tool: its own actions, none of the artboards'.
app.select_tool("selection");
app.session.execute("shape.rectangle", &json!({"x": 20, "y": 20, "width": 40, "height": 40})).unwrap();
assert_eq!(names(&mut app), ["Offset Path", "Simplify", "Duplicate"]);
// One artboard: nothing to rearrange.
app.session.execute("file.new", &json!({"width": 200, "height": 100})).unwrap();
app.select_tool("artboard");
assert_eq!(names(&mut app), ["Resize", "Duplicate", "Export"]);
}
/// The Artboard tool's Contextual Task Bar buttons act on its selected artboards: Resize opens
/// Artboard Options for the active one, Rearrange All opens Rearrange All Artboards, Export opens
/// Export for Screens with the selected artboards checked, and Duplicate copies each selected
/// artboard in one undo step and selects the copies.
#[test]
fn the_artboard_tools_task_bar_buttons_act_on_the_selected_artboards() {
let mut app = VectorcraftApp::new(Session::new(), Default::default());
app.session.execute("file.new", &json!({"width": 200, "height": 100, "artboards": 3})).unwrap();
app.select_tool("artboard");
crate::panels::artboards::select(&mut app, 2);
app.session.set_tool_option("selected", &json!([2, 0]));
let ctx = egui::Context::default();
let press = |app: &mut VectorcraftApp, label: &str| {
let labels = task_bar_labels(app, &ctx);
let at = labels.iter().find(|(t, _)| t == label).unwrap_or_else(|| panic!("{label} in {labels:?}")).1.center();
let button =
|pressed| egui::Event::PointerButton { pos: at, button: egui::PointerButton::Primary, pressed, modifiers: Default::default() };
frame(app, &ctx, vec![egui::Event::PointerMoved(at)]);
frame(app, &ctx, vec![button(true)]);
frame(app, &ctx, vec![button(false)]);
app.ui.dialog.take()
};
let d = press(&mut app, "Resize").expect("Artboard Options");
assert_eq!((d.kind.as_str(), d.f64("index", 9.0)), ("artboardOptions", 2.0));
let d = press(&mut app, "Rearrange All").expect("Rearrange All Artboards");
assert_eq!(d.kind, crate::dialogs::rearrange_artboards::KIND);
let d = press(&mut app, "Export").expect("Export for Screens");
assert_eq!((d.kind.as_str(), &d.fields["boards"]), (crate::dialogs::EXPORT_FOR_SCREENS, &json!([true, false, true])));
let boards =
|app: &VectorcraftApp| app.session.active().unwrap().doc.artboards.iter().map(|a| (a.name.clone(), a.rect.size())).collect::<Vec<_>>();
let before = boards(&app);
assert!(press(&mut app, "Duplicate").is_none());
let after = boards(&app);
assert_eq!(after[..3], before[..]);
assert_eq!(after[3..], [("Artboard 1 copy".to_string(), before[0].1), ("Artboard 3 copy".to_string(), before[2].1)]);
assert_eq!(app.session.tool_options()["selected"], json!([4, 3]), "the copies are selected, artboard 3's copy active");
app.run("edit.undo", json!({})).unwrap();
assert_eq!(boards(&app), before, "one undo step");
}
/// The Contextual Task Bar's handle drags it: unpinned it keeps that offset as it follows the
/// selection, pinned it stays put when the selection changes, unpinned again it follows from
/// there, and Reset Bar Position puts it back under the selection. It never leaves the canvas,
+1
View File
@@ -539,6 +539,7 @@
Contextual Task Bar Контекстна лента със задачи
Pin Bar Position Закачане на позицията на лентата
Reset Bar Position Нулиране на позицията на лентата
Resize Преоразмеряване
Contrast Контраст
Control Контрол
Convert Конвертиране
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -578,6 +578,7 @@
Contextual Task Bar Kontextový panel úloh
Pin Bar Position Připnout polohu panelu
Reset Bar Position Obnovit polohu panelu
Resize Změnit velikost
Tools Nástroje
Toolbars Panely nástrojů
Advanced Rozšířené
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -539,6 +539,7 @@
Contextual Task Bar Kontextbezogene Taskleiste
Pin Bar Position Position der Leiste fixieren
Reset Bar Position Position der Leiste zurücksetzen
Resize Größe ändern
Contrast Kontrast
Control Steuerung
Convert Konvertieren
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -513,6 +513,7 @@
Contextual Task Bar Γραμμή εργασιών βάσει περιβάλλοντος
Pin Bar Position Σταθεροποίηση θέσης γραμμής
Reset Bar Position Επαναφορά θέσης γραμμής
Resize Αλλαγή μεγέθους
Contrast Αντίθεση
Control Έλεγχος
Convert Μετατροπή
1 # Ελληνικά (el) — VectorCraft
513
514
515
516
517
518
519
+1
View File
@@ -554,6 +554,7 @@
Contextual Task Bar Barra de tareas contextual
Pin Bar Position Fijar posición de la barra
Reset Bar Position Restablecer posición de la barra
Resize Cambiar tamaño
Contrast Contraste
Control Control
Convert Convertir
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -569,6 +569,7 @@
Contextual Task Bar Kontekstuaalinen tehtäväpalkki
Pin Bar Position Kiinnitä palkin sijainti
Reset Bar Position Palauta palkin sijainti
Resize Muuta kokoa
Tools Työkalut
Toolbars Työkalurivit
Advanced Lisäasetukset
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -555,6 +555,7 @@
Contextual Task Bar Barre des tâches contextuelle
Pin Bar Position Épingler la position de la barre
Reset Bar Position Réinitialiser la position de la barre
Resize Redimensionner
Contrast Contraste
Control Contrôle
Convert Convertir
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -3008,6 +3008,7 @@ mark-style Japanese Japán
Reshape Spine Segment Gerincszakasz átformálása
Reshape Tool Átformálás
Reshape Type Area Szövegterület átformálása
Resize Átméretezés
Resize panel window Panelablak átméretezése
Resize panels Panelek átméretezése
Resolution: Felbontás:
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -554,6 +554,7 @@
Contextual Task Bar Barra attività contestuale
Pin Bar Position Fissa posizione barra
Reset Bar Position Ripristina posizione barra
Resize Ridimensiona
Contrast Contrasto
Control Controllo
Convert Converti
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -556,6 +556,7 @@
Contextual Task Bar コンテキストタスクバー
Pin Bar Position バーの位置を固定
Reset Bar Position バーの位置をリセット
Resize サイズを変更
Tools ツール
Toolbars ツールバー
Advanced 詳細
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -554,6 +554,7 @@
Contextual Task Bar 상황별 작업 표시줄
Pin Bar Position 표시줄 위치 고정
Reset Bar Position 표시줄 위치 재설정
Resize 크기 조절
Tools 도구
Toolbars 도구 모음
Advanced 고급
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -528,6 +528,7 @@
Contextual Task Bar Kontekstowy pasek zadań
Pin Bar Position Przypnij położenie paska
Reset Bar Position Resetuj położenie paska
Resize Zmień rozmiar
Tools Narzędzia
Toolbars Paski narzędzi
Advanced Zaawansowany
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -509,6 +509,7 @@
Contextual Task Bar Barra de Tarefas Contextual
Pin Bar Position Fixar Posição da Barra
Reset Bar Position Redefinir Posição da Barra
Resize Redimensionar
Control Controle
Convert Converter
Convert All Spot Colors to Process Converter Todas as Cores Spot em Processo
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1
View File
@@ -552,6 +552,7 @@
Contextual Task Bar Контекстная панель задач
Pin Bar Position Закрепить положение панели
Reset Bar Position Сбросить положение панели
Resize Изменить размер
Contrast Контраст
Control Control
Convert Преобразовать
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -541,6 +541,7 @@
Contextual Task Bar Kontextuellt aktivitetsfält
Pin Bar Position Fäst fältets position
Reset Bar Position Återställ fältets position
Resize Ändra storlek
Contrast Kontrast
Control Kontroll
Convert Konvertera
Can't render this file because it contains an unexpected character in line 5 and column 25.
+1
View File
@@ -548,6 +548,7 @@
Contextual Task Bar Контекстна панель завдань
Pin Bar Position Закріпити положення панелі
Reset Bar Position Скинути положення панелі
Resize Змінити розмір
Contrast Контраст
Control Панель керування
Convert Перетворити
1 # Ukrainian (uk) catalog. Same format as zh-hant.tsv: context <TAB> source <TAB> translation.
548
549
550
551
552
553
554
+1
View File
@@ -2753,6 +2753,7 @@ mark-style Japanese Kiểu Nhật
Reshape Spine Segment Định hình lại đoạn trục
Reshape Tool Công cụ Định hình lại
Reshape Type Area Định hình lại vùng chữ
Resize Thay đổi kích thước
Resize panel window Thay đổi kích thước cửa sổ bảng
Resize panels Thay đổi kích thước bảng
Resolution: Độ phân giải:
1 # Vietnamese (vi) catalog. Same format as zh-hant.tsv: context <TAB> source <TAB> translation
2753
2754
2755
2756
2757
2758
2759
+1
View File
@@ -509,6 +509,7 @@
Contextual Task Bar 上下文任务栏
Pin Bar Position 固定任务栏位置
Reset Bar Position 重置任务栏位置
Resize 调整大小
Contrast 对比
Control 控制栏
Convert 转换
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -523,6 +523,7 @@
Contextual Task Bar 快捷工作列
Pin Bar Position 釘選工作列位置
Reset Bar Position 重設工作列位置
Resize 調整大小
Contrast 對比
Control 控制
Convert 轉換
Can't render this file because it contains an unexpected character in line 6 and column 47.
+2 -1
View File
@@ -432,8 +432,9 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
- Hint bar, contextual task bar, 19 dock panels with ≡ menus.
- Numeric fields keep the pointer's pressed character as the text-drag start when movement exceeds the click threshold (#1326, in part); a completed focus click selects the unit too. Pixel/point, plain and optional fields are regression-tested in both directions, before/after focus, with replacement text; keyboard focus, double-click, arrows and wheel keep their prior behavior. Native egui input confirms partial replacement gives 17 px instead of 700 px, one Undo/Redo, window captures and an exact saved-document read in a separate CLI process. Short movements still counted as clicks can select all; that case remains open.
- Unit fields and length strings take `%` in arithmetic: `3cm*50%` reads as 1.5 cm, and `50pt+25%` reads as 62.5 pt.
- The contextual task bar's handle drags it anywhere on the canvas (kept whole on it as the window resizes); unpinned it keeps that offset as it follows the selection, and its More Options menu hides it, pins its position across selection changes (`window.taskBar.pin`), resets it under the selection (`window.taskBar.reset`) and shows the Properties panel. As in Illustrator the position isn't kept across launches. Not done: the bar's tool-specific variants (only the selection tools show it).
- The contextual task bar's handle drags it anywhere on the canvas (kept whole on it as the window resizes); unpinned it keeps that offset as it follows the selection, and its More Options menu hides it, pins its position across selection changes (`window.taskBar.pin`), resets it under the selection (`window.taskBar.reset`) and shows the Properties panel. As in Illustrator the position isn't kept across launches. Not done: the bar's variants for other tools (only the selection tools and the Artboard tool show it).
- With one image selected, the contextual task bar offers Image Trace, which traces the image at once with the Default preset (the Image Trace panel is left as it was), and Crop Image; with an Image Trace object it offers Expand, which keeps only the traced paths. As in Illustrator ("Selecting images displays options like Image Trace, Retype, and Crop Image"; "You can also quickly expand traced artwork from the bar": [Contextual Task Bar announcement](https://community.adobe.com/announcements-651/contextual-task-bar-in-illustrator-2025-quick-actions-when-you-need-them-1546580)). Offset Path and Simplify, which don't apply to an image, aren't offered for one. Not done: Retype, which the app doesn't have.
- With the Artboard tool, the contextual task bar sits under the selected artboards and offers Resize (Artboard Options for the active artboard), Duplicate (each selected artboard, in one undo step, the copies then selected), Rearrange All (with two or more artboards) and Export (Export for Screens with the selected artboards checked). As in Illustrator ("the Contextual Task Bar reveals options to resize, duplicate, and export artboards": the [Contextual Task Bar announcement](https://community.adobe.com/questions-652/contextual-task-bar-in-illustrator-2025-quick-actions-when-you-need-them-1546580); Rearrange and Export on the bar: [Managing Artboards in Illustrator](https://creativepro.com/managing-artboards-in-illustrator/)).
- Tool flyouts tear off: dragging or clicking the tear-off bar on a flyout's right side (or releasing the long press over it) floats its group as a strip of tool buttons, moved by the same bar and put back in the toolbar by the × at its top (`window.floatTools`). While a group floats, the presses that would open its flyout raise the strip instead. Floating strips are kept with the preferences and in user workspaces. Not done: turning a strip vertical, docking it.
- Screen readers get a name for the Tools panel's tool buttons, flyout rows, floating strips and tear-off bars, and for the icon buttons, text buttons, toggles, checkboxes, radio buttons, tabs, chips, sliders, angle dials and combo boxes that panels and dialogs draw with `crates/ui-egui/src/widgets.rs`: the text, label or tool tip each one shows, or a name of its own where it shows none (the angle dial, the spinner, the preset arrow, the color wheel and the Reference Point's squares). The tool buttons, toggles, checkboxes, radio buttons, tabs and the Reference Point's nine squares also report whether they are on, an icon button that is lit, such as the chosen Align To button, reports that it is pressed, and the angle dial reports its angle. The status bar's First, Previous, Next and Last Artboard buttons have tool tips.
- Panels undock (#495): a dock tab, a panel icon or a popped-out panel's title dragged out of the dock floats that panel inside the window, and the strip right of the dock's tabs floats the whole tabbed group. A floating group moves by its title bar, a tab dragged out of it floats on its own, and dropped on another group's title bar or tabs it stacks with that group; dropped on the dock, which lights up under the pointer, its panels go back where they live (the tabbed group or the icon column), as its × and the panel menus' Float Panel / Dock Panel do (`window.panel.float`, `window.panel.dock`). The Tools panel floats by its title bar and docks again on the window's left edge. Floating groups stack into sets: dropped on the upper or lower half of another group (a line shows where), a group joins it right above or below it, and the set's groups stack top to bottom, each showing one panel, moving together by the top group's title bar, whose × docks them all; a group dragged by its tab strip leaves the set (`window.panel.float {below, above}`). A double-click on a tab collapses a floating group to its tabs (`collapsed`). Sets dropped on the left edge of the icon column or of another dock column dock as dock columns (a multi-column dock, `window.panel.dock {column}`), whose groups stack, tab and tear out as a floating set's do, and whose top strip floats the column again. Floating panels, their sets and the Tools panel's position are kept with the preferences and in user workspaces, and clamped into a smaller window. Not done: panels in separate OS windows, docking a panel into the default tabbed group it doesn't belong to, reordering tabs by dragging, collapsing a set or dock column to icons, dock columns right of the default dock, resizing floating panels and the groups of a set.