Contextual Task Bar: direct-selected anchors offer Connect, Corner and Smooth

With anchor points selected with Direct Selection, the Contextual Task
Bar's anchor items were Remove Anchor Points and Cut Path. The
reference app's bar also offers to connect the selected end points and
to convert the selected anchors to corner or smooth.

The bar now offers Remove Anchor Points, Connect, Cut Path, Corner and
Smooth. Connect runs Join (path.join), and Corner and Smooth run
path.convertAnchors with `to` set to corner or smooth. These are the
commands of the Control bar's anchor buttons. "Connect" has a row in
every catalog; "Corner" and "Smooth" use the existing rows.

The reference app's Smooth item, which adjusts a path's curvature, is not
offered, because VectorCraft has no Object > Path > Smooth. Simplify,
which the reference app's bar also offers here, is still offered only
while no anchors are selected.

Docs: docs/target-app-parity.md.
This commit is contained in:
dotCooCoo
2026-10-17 19:38:48 +07:00
committed by bflatastic
parent 80b9753866
commit 5ad04faff3
24 changed files with 80 additions and 1 deletions
+57 -1
View File
@@ -2879,11 +2879,17 @@ fn task_bar(app: &mut VectorcraftApp, ui: &mut Ui, xf: &Xf) {
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.
// Direct-selected anchors take the place of a path's Offset Path and Simplify. For them the bar
// offers Remove, Connect, Cut and Convert to corner or smooth, as the reference app does
// (<https://helpx.adobe.com/illustrator/desktop/get-started/learn-the-basics/contextual-task-bar-overview.html>).
// These run the same commands as the Control bar's anchor buttons.
let anchors = !st.selection.anchors.is_empty();
if anchors {
items.push((tl!("Remove Anchor Points"), "pen-tool-delete", "path.removeAnchors"));
items.push((tl!("Connect"), "dc-join", "path.join"));
items.push((tl!("Cut Path"), "scissors", "path.cutAtAnchors"));
items.push((tl!("Corner"), "dc-anchor", TASK_BAR_TO_CORNER));
items.push((tl!("Smooth"), "dc-anchor-smooth", TASK_BAR_TO_SMOOTH));
}
if n > 1 {
items.push((tl!("Group"), "group", "object.group"));
@@ -3031,6 +3037,9 @@ fn task_bar(app: &mut VectorcraftApp, ui: &mut Ui, xf: &Xf) {
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),
// Corner and Smooth convert the direct-selected anchors, as the Control bar's buttons do.
Some((TASK_BAR_TO_CORNER, _)) => crate::menus::invoke(app, "path.convertAnchors", json!({"to": "corner"})),
Some((TASK_BAR_TO_SMOOTH, _)) => crate::menus::invoke(app, "path.convertAnchors", json!({"to": "smooth"})),
Some((c, p)) => crate::menus::invoke(app, c, p),
None => {}
}
@@ -3044,6 +3053,10 @@ const TASK_BAR_ARTBOARD_OPTIONS: &str = "taskBar.artboardOptions";
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";
/// The Contextual Task Bar's Corner button for direct-selected anchors.
const TASK_BAR_TO_CORNER: &str = "taskBar.toCorner";
/// The Contextual Task Bar's Smooth button for direct-selected anchors.
const TASK_BAR_TO_SMOOTH: &str = "taskBar.toSmooth";
#[cfg(test)]
mod tests {
@@ -6609,6 +6622,49 @@ mod tests {
assert_eq!(boards(&app), before, "one undo step");
}
/// With anchors direct-selected, the Contextual Task Bar offers Remove Anchor Points, Connect,
/// Cut Path, Corner and Smooth. Smooth and Corner convert the selected anchor, Cut Path opens
/// the path there and Connect closes it again, as the Control bar's anchor buttons do.
#[test]
fn the_task_bar_connects_cuts_and_converts_direct_selected_anchors() {
let mut app = VectorcraftApp::new(Session::new(), Default::default());
app.session.execute("file.new", &json!({"width": 400, "height": 300})).unwrap();
let id = app.session.execute("shape.rectangle", &json!({"x": 150, "y": 100, "width": 100, "height": 80})).unwrap()["id"].as_u64().unwrap();
app.select_tool("directSelection");
app.run("select.anchors", json!({"id": id, "anchors": [[0, 1]]})).unwrap();
let ctx = egui::Context::default();
let names = |app: &mut VectorcraftApp| task_bar_labels(app, &ctx).into_iter().map(|(t, _)| t).collect::<Vec<_>>();
assert_eq!(names(&mut app), ["Remove Anchor Points", "Connect", "Cut Path", "Corner", "Smooth", "Duplicate"]);
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)]);
};
let subpath = |app: &VectorcraftApp| {
app.session.active().unwrap().doc.node(vectorcraft_doc::NodeId(id)).unwrap().path_data().unwrap().subpaths[0].clone()
};
let last_undo = |app: &VectorcraftApp| app.session.active().unwrap().history.undo.last().unwrap().label.clone();
press(&mut app, "Smooth");
let a = subpath(&app).anchors[1];
assert!(a.has_in() && a.has_out(), "smooth: {a:?}");
assert_eq!(last_undo(&app), "Convert Anchor Points");
press(&mut app, "Corner");
let a = subpath(&app).anchors[1];
assert!(!a.has_in() && !a.has_out(), "corner: {a:?}");
press(&mut app, "Cut Path");
assert_eq!(last_undo(&app), "Cut Path");
let sp = subpath(&app);
assert!(!sp.closed && sp.anchors.len() == 5, "the rectangle opens at the anchor: {sp:?}");
press(&mut app, "Connect");
assert_eq!(last_undo(&app), "Join");
let sp = subpath(&app);
assert!(sp.closed && sp.anchors.len() == 4, "the ends join again: {sp:?}");
}
/// 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
@@ -532,6 +532,7 @@
Condition identifier: Идентификатор на условие:
Conflict {n} of {count} Конфликт {n} от {count}
Connect Selected End Points Свързване на избраните крайни точки
Connect Свързване
Consolidate All Windows Обединяване на всички прозорци
Constrain Angle Заключване на ъгъла
Constrain Width and Height Proportions Запазване на пропорциите на ширина и височина
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -1072,6 +1072,7 @@ lighting Light Světlo
Click or drag Kliknutí nebo tažení
Click the first point Kliknutí na první bod
Connect Selected End Points Spojit vybrané koncové body
Connect Spojit
Convert Selected Anchor Points to Corner Převést vybrané kotevní body na rohové
Convert Selected Anchor Points to Smooth Převést vybrané kotevní body na hladké
Convert: Převést:
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -997,6 +997,7 @@ lighting Light Lys
Click or drag Klik eller træk
Click the first point Klik på det første punkt
Connect Selected End Points Forbind markerede slutpunkter
Connect Forbind
Convert Selected Anchor Points to Corner Konvertér markerede ankerpunkter til hjørnepunkter
Convert Selected Anchor Points to Smooth Konvertér markerede ankerpunkter til glatte punkter
Convert: Konvertér:
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -532,6 +532,7 @@
Condition identifier: Kennung der Ausgabebedingung:
Conflict {n} of {count} Konflikt {n} von {count}
Connect Selected End Points Ausgewählte Endpunkte verbinden
Connect Verbinden
Consolidate All Windows Alle Fenster zusammenführen
Constrain Angle Winkelbeschränkung
Constrain Width and Height Proportions Proportionen von Breite und Höhe beibehalten
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -506,6 +506,7 @@
Condition identifier: Αναγνωριστικό συνθήκης:
Conflict {n} of {count} Διένεξη {n} από {count}
Connect Selected End Points Σύνδεση επιλεγμένων τελικών σημείων
Connect Σύνδεση
Consolidate All Windows Συγχώνευση όλων των παραθύρων
Constrain Angle Περιορισμός γωνίας
Constrain Width and Height Proportions Διατήρηση αναλογιών πλάτους και ύψους
1 # Ελληνικά (el) — VectorCraft
506
507
508
509
510
511
512
+1
View File
@@ -547,6 +547,7 @@
Condition identifier: Identificador de condición:
Conflict {n} of {count} Conflicto {n} de {count}
Connect Selected End Points Conectar los puntos finales seleccionados
Connect Conectar
Consolidate All Windows Consolidar todas las ventanas
Constrain Angle Restringir ángulo
Constrain Width and Height Proportions Restringir proporciones de anchura y altura
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -1050,6 +1050,7 @@ lighting Light Valo
Click or drag Napsauta tai vedä
Click the first point Napsauta ensimmäistä pistettä
Connect Selected End Points Yhdistä valitut päätepisteet
Connect Yhdistä
Convert Selected Anchor Points to Corner Muunna valitut ankkuripisteet kulmapisteiksi
Convert Selected Anchor Points to Smooth Muunna valitut ankkuripisteet pehmeiksi
Convert: Muunna:
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -548,6 +548,7 @@
Condition identifier: Identifiant de condition :
Conflict {n} of {count} Conflit {n} sur {count}
Connect Selected End Points Relier les extrémités sélectionnées
Connect Relier
Consolidate All Windows Regrouper toutes les fenêtres
Constrain Angle Contraindre l’angle
Constrain Width and Height Proportions Conserver les proportions de largeur et de hauteur
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -732,6 +732,7 @@
Condition identifier: Feltételazonosító:
Conflict {n} of {count} Ütközés: {n}/{count}
Connect Selected End Points Kijelölt végpontok összekötése
Connect Összekötés
Consolidate All Windows Összes ablak egyesítése
Constrain Korlátozás
Constrain Angle Korlátozási szög
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -997,6 +997,7 @@ lighting Light Cahaya
Click or drag Klik atau seret
Click the first point Klik titik pertama
Connect Selected End Points Hubungkan titik akhir terpilih
Connect Hubungkan
Convert Selected Anchor Points to Corner Konversi titik jangkar terpilih menjadi sudut
Convert Selected Anchor Points to Smooth Konversi titik jangkar terpilih menjadi halus
Convert: Konversi:
1 # Indonesian (id) catalog. Same format as zh-hant.tsv: context <TAB> source <TAB> translation
997
998
999
1000
1001
1002
1003
+1
View File
@@ -547,6 +547,7 @@
Condition identifier: Identificativo condizione:
Conflict {n} of {count} Conflitto {n} di {count}
Connect Selected End Points Collega i punti finali selezionati
Connect Collega
Consolidate All Windows Consolida tutte le finestre
Constrain Angle Vincola angolo
Constrain Width and Height Proportions Vincola proporzioni di larghezza e altezza
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -26,6 +26,7 @@
Center 中央
Character Alignment 文字揃え
Connect Selected End Points 選択した端点を連結
Connect 連結
Convert Selected Anchor Points to Corner 選択したアンカーポイントをコーナーポイントに変換
Convert Selected Anchor Points to Smooth 選択したアンカーポイントをスムーズポイントに変換
Convert: 変換:
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -22,6 +22,7 @@
Center 가운데
Character Alignment 문자 정렬
Connect Selected End Points 선택한 끝점 연결
Connect 연결
Convert Selected Anchor Points to Corner 선택한 앵커 포인트를 모퉁이로 변환
Convert Selected Anchor Points to Smooth 선택한 앵커 포인트를 부드러운 곡선으로 변환
Convert: 변환:
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -997,6 +997,7 @@ lighting Light Licht
Click or drag Klik of sleep
Click the first point Klik op het eerste punt
Connect Selected End Points Geselecteerde eindpunten verbinden
Connect Verbinden
Convert Selected Anchor Points to Corner Geselecteerde ankerpunten omzetten in hoekpunten
Convert Selected Anchor Points to Smooth Geselecteerde ankerpunten omzetten in vloeiende punten
Convert: Omzetten:
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -1005,6 +1005,7 @@ lighting Light Światło
Click or drag Kliknij lub przeciągnij
Click the first point Kliknij pierwszy punkt
Connect Selected End Points Połącz zaznaczone punkty końcowe
Connect Połącz
Convert Selected Anchor Points to Corner Konwertuj zaznaczone punkty kontrolne na narożne
Convert Selected Anchor Points to Smooth Konwertuj zaznaczone punkty kontrolne na gładkie
Convert: Konwertuj:
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -3058,6 +3058,7 @@ lighting Light Luz
Above Text Acima do texto
Position Panel Above Text Posicionar painel acima do texto
Connect Selected End Points Conectar extremidades selecionadas
Connect Conectar
Convert Selected Anchor Points to Corner Converter pontos de ancoragem selecionados em canto
Convert Selected Anchor Points to Smooth Converter pontos de ancoragem selecionados em suaves
Convert: Converter:
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1
View File
@@ -545,6 +545,7 @@
Condition identifier: Идентификатор условия:
Conflict {n} of {count} Конфликт {n} из {count}
Connect Selected End Points Соединить выбранные конечные точки
Connect Соединить
Consolidate All Windows Объединить все окна
Constrain Angle Ограничить угол
Constrain Width and Height Proportions Сохранять пропорции ширины и высоты
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -534,6 +534,7 @@
Condition identifier: Villkorsidentifierare:
Conflict {n} of {count} Konflikt {n} av {count}
Connect Selected End Points Sammanfoga markerade slutpunkter
Connect Sammanfoga
Consolidate All Windows Samla alla fönster
Constrain Angle Begränsa vinkel
Constrain Width and Height Proportions Behåll proportioner för bredd och höjd
Can't render this file because it contains an unexpected character in line 5 and column 25.
+1
View File
@@ -541,6 +541,7 @@
Condition identifier: Ідентифікатор умови:
Conflict {n} of {count} Конфлікт {n} з {count}
Connect Selected End Points З’єднати обрані кінцеві точки
Connect З’єднати
Consolidate All Windows Об’єднати всі вікна
Constrain Angle Обмежити кут
Constrain Width and Height Proportions Зберігати пропорції ширини та висоти
1 # Ukrainian (uk) catalog. Same format as zh-hant.tsv: context <TAB> source <TAB> translation.
541
542
543
544
545
546
547
+1
View File
@@ -637,6 +637,7 @@
Condition identifier: Mã định danh điều kiện:
Conflict {n} of {count} Xung đột {n} trong {count}
Connect Selected End Points Nối các điểm cuối đã chọn
Connect Nối
Consolidate All Windows Hợp nhất tất cả cửa sổ
Constrain Giữ tỷ lệ
Constrain Angle Ràng buộc góc
1 # Vietnamese (vi) catalog. Same format as zh-hant.tsv: context <TAB> source <TAB> translation
637
638
639
640
641
642
643
+1
View File
@@ -502,6 +502,7 @@
Condition identifier: 条件识别码:
Conflict {n} of {count} 冲突 {n}/{count}
Connect Selected End Points 连接所选端点
Connect 连接
Consolidate All Windows 合并所有窗口
Constrain Angle 约束角度
Constrain Width and Height Proportions 约束宽高比例
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -516,6 +516,7 @@
Condition identifier: 條件識別碼:
Conflict {n} of {count} 衝突 {n}/{count}
Connect Selected End Points 連接所選端點
Connect 連接
Consolidate All Windows 合併所有視窗
Constrain Angle 強制角度
Constrain Width and Height Proportions 強制寬度與高度比例
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1
View File
@@ -448,6 +448,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
- 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/)).
- With anchor points selected with Direct Selection, the contextual task bar offers Remove Anchor Points, Connect (`path.join`), Cut Path, Corner and Smooth (`path.convertAnchors` with `to` set to `corner` or `smooth`), the commands of the Control bar's anchor buttons. As in Illustrator ("Remove selected anchor points, Connect selected end points, Cut path at selected anchor points, Convert selected anchor points to corner, and Convert selected anchor points to smooth": [Contextual Task Bar overview](https://helpx.adobe.com/illustrator/desktop/get-started/learn-the-basics/contextual-task-bar-overview.html)). Not done: Simplify, which the bar offers only while no anchors are selected, and the bar's Smooth item that adjusts a path's curvature, a command the app doesn't have (Object › Path › Smooth).
- 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.