Merge pull request #4854 from dotCooCoo/feat/taskbar-shape-properties

Contextual Task Bar: a basic shape shows its properties in place of Lock
This commit is contained in:
BFlat
2026-11-01 01:29:17 +07:00
committed by GitHub
32 changed files with 265 additions and 38 deletions
+30 -5
View File
@@ -33,6 +33,7 @@ fn picked_artboards_bounds(app: &VectorcraftApp) -> Option<Rect> {
/// 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).
/// A basic shape selected with the Selection tool shows a Shape Properties button in place of Lock.
/// 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`]).
pub(super) fn task_bar(app: &mut VectorcraftApp, ui: &mut Ui, xf: &Xf) {
@@ -67,6 +68,17 @@ pub(super) fn task_bar(app: &mut VectorcraftApp, ui: &mut Ui, xf: &Xf) {
&& first.as_ref().is_some_and(|f| {
f.name.as_deref() == Some("Image Trace") && f.children().is_some_and(|c| c.first().is_some_and(|i| matches!(i.kind, NodeKind::Image(_))))
});
// A basic shape (a live rectangle, rounded rectangle, ellipse or polygon) selected whole with the
// Selection tool: its properties take the place of Lock, as in the reference app
// (<https://helpx.adobe.com/illustrator/desktop/get-started/learn-the-basics/contextual-task-bar-overview.html>).
use vectorcraft_doc::LiveShape;
let shape = first.clone().filter(|f| {
n == 1
&& st.selection.anchors.is_empty()
&& app.session.tool_id() == "selection"
&& matches!(&f.kind, NodeKind::Path { live: Some(l), .. }
if matches!(**l, LiveShape::Rectangle { .. } | LiveShape::Ellipse { .. } | LiveShape::Polygon { .. }))
});
let mut items: Vec<(&str, &str, &str, serde_json::Value)> = vec![]; // (label, icon, command, params)
let none = || json!({});
if on_artboards {
@@ -228,7 +240,8 @@ pub(super) fn task_bar(app: &mut VectorcraftApp, ui: &mut Ui, xf: &Xf) {
run = Some((*cmd, params.clone()));
}
}
// The selected objects' Fill, Stroke, Recolor Artwork and Lock, left out with the Artboard tool.
// The selected objects' Fill, Stroke, Recolor Artwork and Lock (a basic shape's
// properties instead), left out with the Artboard tool.
if !on_artboards {
let (r, resp) = ui.allocate_exact_size(vec2(26.0, 30.0), Sense::click());
let chip = egui::Rect::from_center_size(r.center(), vec2(16.0, 16.0));
@@ -251,10 +264,22 @@ pub(super) fn task_bar(app: &mut VectorcraftApp, ui: &mut Ui, xf: &Xf) {
if colored && widgets::icon_button(ui, "palette", tl!("Recolor Artwork"), false, 30.0).clicked() {
run = Some(("ui.recolorDialog", json!({})));
}
// The Lock button is off in isolation mode, as Object › Lock › Selection is.
let lockable = crate::menus::enabled(app, "object.lock");
if widgets::icon_button_enabled(ui, "lock", tl!("Lock (⌘2)"), false, lockable, 30.0).clicked() {
run = Some(("object.lock", json!({})));
if let Some(shape) = &shape
&& let NodeKind::Path { live: Some(live), .. } = &shape.kind
{
// The shape's properties in a popover that stays open while its fields and
// its Corner dropdown are used.
let props = widgets::icon_button(ui, "shapes", tl!("Shape Properties"), false, 30.0);
widgets::popover(&props, props.clicked(), |ui| {
ui.set_min_width(250.0);
crate::panels::transform::live_shape_rows(app, ui, shape, live, "task-bar-shape", true);
});
} else {
// The Lock button is off in isolation mode, as Object › Lock › Selection is.
let lockable = crate::menus::enabled(app, "object.lock");
if widgets::icon_button_enabled(ui, "lock", tl!("Lock (⌘2)"), false, lockable, 30.0).clicked() {
run = Some(("object.lock", json!({})));
}
}
}
let more = widgets::icon_button(ui, "ellipsis", tl!("More Options"), false, 30.0);
+10 -5
View File
@@ -6839,7 +6839,8 @@ fn the_task_bar_shows_a_stroke_chip_beside_fill() {
let mut out = ctx.run_ui(raw, |ui| show(&mut app, ui));
out.textures_delta.clear();
let bar = task_bar_rect(&ctx).expect("the task bar shows");
// Right to left: the frame's margin, More Options, Lock, Recolor, then Stroke and Fill.
// Right to left: the frame's margin, More Options, Shape Properties (Lock's place), Recolor,
// then Stroke and Fill.
let stroke_x = bar.right() - 8.0 - 3.0 * (30.0 + 6.0) - 13.0;
let fill_x = stroke_x - 26.0 - 6.0;
let blue = Color32::from_rgb(0x22, 0x44, 0xcc);
@@ -6896,7 +6897,8 @@ fn the_task_bar_chips_follow_the_proxies_and_show_swatches_anywhere() {
out.textures_delta.clear();
out.shapes
};
// Right to left: the frame's margin, More Options, Lock, Recolor, then Stroke and Fill.
// Right to left: the frame's margin, More Options, Shape Properties (Lock's place), Recolor,
// then Stroke and Fill.
let stroke_x = |bar: egui::Rect| bar.right() - 8.0 - 3.0 * (30.0 + 6.0) - 13.0;
let chip_with = |shapes: &[egui::epaint::ClippedShape], bar: egui::Rect, x: f32, color: Color32| {
shapes.iter().any(|c| match &c.shape {
@@ -6948,7 +6950,7 @@ fn the_task_bar_recolors_coloured_art() {
frame(app, &ctx, vec![]);
frame(app, &ctx, vec![]);
let bar = task_bar_rect(&ctx).expect("the task bar shows");
// Right to left: the frame's margin, More Options, Lock, then Recolor.
// Right to left: the frame's margin, More Options, Shape Properties (Lock's place), then Recolor.
let at = pos2(bar.right() - 8.0 - 30.0 - 6.0 - 30.0 - 6.0 - 15.0, bar.center().y);
let button = |pressed| egui::Event::PointerButton { pos: at, button: egui::PointerButton::Primary, pressed, modifiers: Default::default() };
frame(app, &ctx, vec![egui::Event::PointerMoved(at)]);
@@ -6975,8 +6977,11 @@ fn the_task_bar_recolors_coloured_art() {
fn the_task_bar_lock_button_is_off_in_isolation_mode() {
let mut app = VectorcraftApp::new(Session::new(), Default::default());
app.session.execute("file.new", &json!({"width": 400, "height": 300})).unwrap();
let a = app.session.execute("shape.rectangle", &json!({"x": 150, "y": 130, "width": 40, "height": 40})).unwrap()["id"].clone();
let b = app.session.execute("shape.rectangle", &json!({"x": 210, "y": 130, "width": 40, "height": 40})).unwrap()["id"].clone();
// Plain paths: a basic shape shows its properties in Lock's place.
let square =
|x: f64| json!({"anchors": [{"x": x, "y": 130}, {"x": x + 40.0, "y": 130}, {"x": x + 40.0, "y": 170}, {"x": x, "y": 170}], "closed": true});
let a = app.session.execute("path.create", &square(150.0)).unwrap()["id"].clone();
let b = app.session.execute("path.create", &square(210.0)).unwrap()["id"].clone();
app.session.execute("select.set", &json!({"ids": [a.clone(), b]})).unwrap();
let g = app.session.execute("object.group", &json!({})).unwrap()["id"].clone();
app.select_tool("selection");
+1
View File
@@ -2922,6 +2922,7 @@ font-technology Variable Променливи
Shape Mode Режим на фигура
Shape Modes: Режими на фигури:
Shape Properties: Свойства на фигурата:
Shape Properties Свойства на фигурата
Shaper Tool Инструмент „Shaper“
Shapes Фигури
Shear Срязване
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -2014,6 +2014,7 @@ lighting Light Svjetlo
Polygon Properties: Svojstva mnogougla:
Rectangle Properties: Svojstva pravougaonika:
Shape Properties: Svojstva oblika:
Shape Properties Svojstva oblika
Sides: Stranice:
Transform options Opcije transformacije
Transform Both Transformiši oboje
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -3753,6 +3753,7 @@
Shape Mode Mode de forma
Shape Modes: Modes de forma:
Shape Properties: Propietats de la forma:
Shape Properties Propietats de la forma
Shape: Forma:
Shaper Shaper
Shaper Tool Eina Shaper
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -2041,6 +2041,7 @@ lighting Light Světlo
Polygon Properties: Vlastnosti mnohoúhelníku:
Rectangle Properties: Vlastnosti obdélníku:
Shape Properties: Vlastnosti tvaru:
Shape Properties Vlastnosti tvaru
Sides: Strany:
Transform options Možnosti transformace
Transform Both Transformovat obojí
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -1852,6 +1852,7 @@ lighting Light Lys
Polygon Properties: Polygonegenskaber:
Rectangle Properties: Rektangelegenskaber:
Shape Properties: Figuregenskaber:
Shape Properties Figuregenskaber
Sides: Sider:
Transform options Transformeringsindstillinger
Transform Both Transformer begge
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -2957,6 +2957,7 @@ mark-style Japanese Japanisch
Shape Mode Formmodus
Shape Modes: Formmodi:
Shape Properties: Formeigenschaften:
Shape Properties Formeigenschaften
Shaper Tool Shaper-Werkzeug
Shapes Formen
Shear Verbiegen
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -3428,6 +3428,7 @@ theme Appearance Εμφάνιση
Shape Mode Λειτουργία σχήματος
Shape Modes: Λειτουργίες σχημάτων:
Shape Properties: Ιδιότητες σχήματος:
Shape Properties Ιδιότητες σχήματος
Shaper Tool Διαμορφωτής σχημάτων (Shaper Tool)
Shear Tool Διάτμηση (Shear Tool)
Shift to Color… Μετατόπιση προς χρώμα…
Can't render this file because it contains an unexpected character in line 164 and column 5.
+1
View File
@@ -2992,6 +2992,7 @@ font-technology Variable Variables
Shape Mode Modo de forma
Shape Modes: Modos de forma:
Shape Properties: Propiedades de forma:
Shape Properties Propiedades de forma
Shaper Tool Herramienta Shaper
Shapes Formas
Shear Inclinar
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -2003,6 +2003,7 @@ lighting Light Valo
Polygon Properties: Monikulmion ominaisuudet:
Rectangle Properties: Suorakulmion ominaisuudet:
Shape Properties: Muodon ominaisuudet:
Shape Properties Muodon ominaisuudet
Sides: Sivut:
Transform options Muunnosasetukset
Transform Both Muunna molemmat
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -2989,6 +2989,7 @@ font-technology Variable Variables
Shape Effects Effets de forme
Shape Mode Mode de forme
Shape Modes: Modes de forme :
Shape Properties Propriétés de la forme
Shape Properties: Propriétés de la forme :
Shaper Tool Outil Shaper
Shapes Formes
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -3602,6 +3602,7 @@ mark-style Japanese Japán
Shape Mode Alakzatmód
Shape Modes: Alakzatmódok:
Shape Properties: Alakzat tulajdonságai:
Shape Properties Alakzat tulajdonságai
Shape: Alak:
Shaper Alakformáló
Shaper Tool Alakformáló
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -1852,6 +1852,7 @@ lighting Light Cahaya
Polygon Properties: Properti poligon:
Rectangle Properties: Properti persegi panjang:
Shape Properties: Properti bentuk:
Shape Properties Properti bentuk
Sides: Sisi:
Transform options Opsi transformasi
Transform Both Transformasikan keduanya
1 # Indonesian (id) catalog. Same format as zh-hant.tsv: context <TAB> source <TAB> translation
1852
1853
1854
1855
1856
1857
1858
+1
View File
@@ -3026,6 +3026,7 @@ mark-style Japanese Giapponese
Shape Mode Metodo forma
Shape Modes: Metodi forma:
Shape Properties: Proprietà forma:
Shape Properties Proprietà forma
Shaper Tool Strumento Sagomatore
Shapes Forme
Shear Inclina
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -2868,6 +2868,7 @@ mark-style Japanese 日本式
Shape Mode シェイプモード
Shape Modes: シェイプモード:
Shape Properties: シェイプのプロパティ:
Shape Properties シェイプのプロパティ
Shaper Tool Shaper ツール
Shapes シェイプ
Shear シアー
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -1754,6 +1754,7 @@ lighting Light სინათლე
Polygon Properties: მრავალკუთხედის თვისებები:
Rectangle Properties: მართკუთხედის თვისებები:
Shape Properties: ფიგურის თვისებები:
Shape Properties ფიგურის თვისებები
Sides: გვერდები:
Transform options გარდაქმნის პარამეტრები
Transform Both ორივეს გარდაქმნა
Can't render this file because it is too large.
+1
View File
@@ -2858,6 +2858,7 @@ mark-style Japanese 일본식
Shape Mode 모양 모드
Shape Modes: 모양 모드:
Shape Properties: 모양 속성:
Shape Properties 모양 속성
Shaper Tool 셰이퍼 도구
Shapes 모양
Shear 기울이기
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -1866,6 +1866,7 @@ lighting Light Licht
Polygon Properties: Veelhoekeigenschappen:
Rectangle Properties: Rechthoekeigenschappen:
Shape Properties: Vormeigenschappen:
Shape Properties Vormeigenschappen
Sides: Zijden:
Transform options Transformatieopties
Transform Both Beide transformeren
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -1962,6 +1962,7 @@ lighting Light Światło
Polygon Properties: Właściwości wielokąta:
Rectangle Properties: Właściwości prostokąta:
Shape Properties: Właściwości kształtu:
Shape Properties Właściwości kształtu
Sides: Boki:
Transform options Opcje przekształcania
Transform Both Przekształć oba
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -2483,6 +2483,7 @@
Shape Mode Modo de Forma
Shape Modes: Modos de Forma:
Shape Properties: Propriedades da Forma:
Shape Properties Propriedades da Forma
Shaper Tool Ferramenta Formador
Shapes Formas
Shear Cisalhar
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1
View File
@@ -3022,6 +3022,7 @@ mark-style Japanese Японский
Shape Mode Режим фигуры
Shape Modes: Режимы фигур:
Shape Properties: Свойства фигуры:
Shape Properties Свойства фигуры
Shaper Tool Инструмент «Формообразование»
Shapes Фигуры
Shear Наклонить
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -2922,6 +2922,7 @@ font-technology Variable Variabla
Shape Mode Formläge
Shape Modes: Formlägen:
Shape Properties: Formegenskaper:
Shape Properties Formegenskaper
Shaper Tool Formningsverktyg
Shapes Former
Shear Skjuva
Can't render this file because it contains an unexpected character in line 5 and column 25.
+1
View File
@@ -2833,6 +2833,7 @@ shape Open Açık
Shape Mode Şekil Modu
Shape Modes: Şekil Modları:
Shape Properties: Şekil Özellikleri:
Shape Properties Şekil Özellikleri
Shaper Tool Şekillendirici Aracı
Shapes Şekiller
Shear Kesme
Can't render this file because it contains an unexpected character in line 5 and column 25.
+1
View File
@@ -3018,6 +3018,7 @@ mark-style Japanese Японський
Shape Mode Режим фігури
Shape Modes: Режими фігур:
Shape Properties: Властивості фігури:
Shape Properties Властивості фігури
Shaper Tool Інструмент «Формоутворення»
Shapes Фігури
Shear Нахилити
Can't render this file because it contains an unexpected character in line 178 and column 5.
+1
View File
@@ -3346,6 +3346,7 @@ mark-style Japanese Kiểu Nhật
Shape Mode Chế độ hình dạng
Shape Modes: Chế độ hình dạng:
Shape Properties: Thuộc tính hình dạng:
Shape Properties Thuộc tính hình dạng
Shape: Hình dạng:
Shaper Tạo hình
Shaper Tool Công cụ Tạo hình
Can't render this file because it contains an unexpected character in line 200 and column 5.
+1
View File
@@ -2859,6 +2859,7 @@ font-script Japanese 汉字
Shape Mode 形状模式
Shape Modes: 形状模式:
Shape Properties: 形状属性:
Shape Properties 形状属性
Shaper Tool Shaper 工具
Shapes 形状
Shear 倾斜
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -3833,6 +3833,7 @@ font-script Japanese 漢字
Shape Mode 形狀模式
Shape Modes: 形狀模式:
Shape Properties: 形狀內容:
Shape Properties 形狀內容
Shaper Tool Shaper 工具
Shapes 形狀
Shear 傾斜
Can't render this file because it contains an unexpected character in line 6 and column 47.
+2 -10
View File
@@ -6,7 +6,7 @@ use vectorcraft_color::{Color, Paint};
use vectorcraft_doc::{LiveCorners, LiveShape, NodeKind, Unit, WidthProfile};
use vectorcraft_engine::cmd::newdoc;
use super::{corner_radius_row, first_selected, pstate, set_pstate};
use super::{first_selected, pstate, set_pstate};
use crate::theme::Tokens;
use crate::widgets::{self, dim_label, divider, section_header};
use crate::{VectorcraftApp, icons};
@@ -798,15 +798,7 @@ pub fn transform_section(app: &mut VectorcraftApp, ui: &mut Ui) {
if let Some(n) = first_selected(app)
&& let NodeKind::Path { live: Some(live), .. } = &n.kind
{
match &**live {
vectorcraft_doc::LiveShape::Rectangle { .. } => corner_radius_row(app, ui, &n, "radius"),
vectorcraft_doc::LiveShape::Polygon { .. } => super::transform::polygon_rows(app, ui, &n, live, "props-polygon"),
vectorcraft_doc::LiveShape::Ellipse { pie, .. } => {
widgets::subheader(ui, tl!("Ellipse Properties:"));
super::transform::pie_rows(app, ui, *pie, "props-pie");
}
_ => {}
}
super::transform::live_shape_rows(app, ui, &n, live, "props", false);
}
}
+30 -17
View File
@@ -345,23 +345,9 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
&& !matches!(**live, vectorcraft_doc::LiveShape::Path { .. })
{
widgets::divider(ui);
match &**live {
vectorcraft_doc::LiveShape::Rectangle { .. } => {
widgets::subheader(ui, tl!("Rectangle Properties:"));
corner_radius_row(app, ui, &n, "xfp-radius");
}
vectorcraft_doc::LiveShape::Polygon { .. } => {
widgets::subheader(ui, tl!("Polygon Properties:"));
polygon_rows(app, ui, &n, live, "xfp-polygon");
}
vectorcraft_doc::LiveShape::Ellipse { pie, .. } => {
widgets::subheader(ui, tl!("Ellipse Properties:"));
pie_rows(app, ui, *pie, "xfp-pie");
}
_ => {
widgets::subheader(ui, tl!("Shape Properties:"));
widgets::dim_label(ui, tl!(n.kind_label()));
}
if !live_shape_rows(app, ui, &n, live, "xfp", true) {
widgets::subheader(ui, tl!("Shape Properties:"));
widgets::dim_label(ui, tl!(n.kind_label()));
}
}
if pstate::<bool>(ui.ctx(), "xf-hide-options") {
@@ -377,6 +363,33 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
}
}
/// A live rectangle's, polygon's or ellipse's properties (Transform and Properties panels, and the
/// Contextual Task Bar), their widget ids under `id`. `headed` adds the Rectangle and Polygon
/// Properties headings; an ellipse's heading always shows. Returns false, drawing nothing, for
/// other live shapes.
pub(crate) fn live_shape_rows(app: &mut VectorcraftApp, ui: &mut Ui, n: &Node, live: &LiveShape, id: &str, headed: bool) -> bool {
match live {
LiveShape::Rectangle { .. } => {
if headed {
widgets::subheader(ui, tl!("Rectangle Properties:"));
}
corner_radius_row(app, ui, n, &format!("{id}-radius"));
}
LiveShape::Polygon { .. } => {
if headed {
widgets::subheader(ui, tl!("Polygon Properties:"));
}
polygon_rows(app, ui, n, live, &format!("{id}-polygon"));
}
LiveShape::Ellipse { pie, .. } => {
widgets::subheader(ui, tl!("Ellipse Properties:"));
pie_rows(app, ui, *pie, &format!("{id}-pie"));
}
_ => return false,
}
true
}
/// An ellipse's Pie Start and End Angle (degrees, counterclockwise from 3 o'clock; 0 to 360 is the
/// whole ellipse) and Invert Pie, which shows the other part of it (Transform and Properties panels).
pub(crate) fn pie_rows(app: &mut VectorcraftApp, ui: &mut Ui, pie: (f64, f64), id: &str) {
+166 -1
View File
@@ -1,6 +1,7 @@
//! The Contextual Task Bar's actions for each kind of selection: open paths offer Join and Edit
//! Path, compound paths and clipping masks their Release, images selected with vector art Make
//! Clipping Mask, and type converts between point and area type.
//! Clipping Mask, type converts between point and area type, and a basic shape shows its
//! properties in place of Lock.
use egui::{Event, PointerButton, Pos2, Rect, vec2};
use serde_json::json;
@@ -219,6 +220,170 @@ fn direct_selected_anchors_still_offer_remove_and_cut() {
assert!(!has(&texts, "Edit Path"), "{texts:?}");
}
/// Click the task bar's button left of More Options (Lock, or a basic shape's Shape Properties),
/// and return the context and what the frame after the click draws.
fn click_lock_slot(app: &mut VectorcraftApp) -> (egui::Context, Vec<(String, Rect)>) {
let (ctx, _) = bar(app);
let r = canvas::task_bar_rect(&ctx).expect("the task bar shows");
// Right to left: the frame's margin, More Options, then the button.
let p = Pos2::new(r.right() - 8.0 - 30.0 - 6.0 - 15.0, r.center().y);
let press = |pressed| Event::PointerButton { pos: p, button: PointerButton::Primary, pressed, modifiers: Default::default() };
frame(app, &ctx, vec![Event::PointerMoved(p)]);
frame(app, &ctx, vec![press(true)]);
frame(app, &ctx, vec![press(false)]);
let texts = frame(app, &ctx, vec![]);
(ctx, texts)
}
/// Type `value` into the field showing `field` and press Enter.
fn type_into(app: &mut VectorcraftApp, ctx: &egui::Context, texts: &[(String, Rect)], field: &str, value: &str) {
let p = at(texts, field);
let press = |pressed| Event::PointerButton { pos: p, button: PointerButton::Primary, pressed, modifiers: Default::default() };
frame(app, ctx, vec![Event::PointerMoved(p), press(true), press(false)]);
// Focus selected the whole field: typing replaces it.
frame(app, ctx, vec![]);
frame(app, ctx, vec![Event::Text(value.into())]);
let enter = Event::Key { key: egui::Key::Enter, physical_key: None, pressed: true, repeat: false, modifiers: Default::default() };
frame(app, ctx, vec![enter]);
frame(app, ctx, vec![]);
}
fn live(app: &VectorcraftApp, id: u64) -> vectorcraft_doc::LiveShape {
match kind(app, id) {
NodeKind::Path { live: Some(l), .. } => *l,
k => panic!("not a live shape: {k:?}"),
}
}
fn locked(app: &VectorcraftApp, id: u64) -> bool {
app.session.active().unwrap().doc.node(NodeId(id)).unwrap().locked
}
/// A basic shape selected with the Selection tool shows Shape Properties in place of Lock: a
/// rectangle, rounded rectangle, ellipse or polygon, each under its own heading.
#[test]
fn a_basic_shape_shows_its_properties_in_place_of_lock() {
let mut app = app();
let shapes = [
(id(app.run("shape.rectangle", json!({"x": 40, "y": 40, "width": 80, "height": 60})).unwrap()), "Rectangle Properties:"),
(id(app.run("shape.rectangle", json!({"x": 40, "y": 40, "width": 80, "height": 60, "radius": 10})).unwrap()), "Rectangle Properties:"),
(id(app.run("shape.ellipse", json!({"x": 40, "y": 40, "width": 80, "height": 60})).unwrap()), "Ellipse Properties:"),
(id(app.run("shape.polygon", json!({"cx": 100, "cy": 100, "radius": 40, "sides": 6})).unwrap()), "Polygon Properties:"),
];
app.run("tool.select", json!({"tool": "selection"})).unwrap();
for (shape, heading) in shapes {
select(&mut app, &[shape]);
let (_, texts) = bar(&mut app);
assert!(!has(&texts, heading), "the popup starts closed: {texts:?}");
let (_, texts) = click_lock_slot(&mut app);
assert!(has(&texts, heading), "{heading}: {texts:?}");
assert!(!locked(&app, shape), "the button doesn't lock the shape");
}
}
/// The popup's fields edit the shape through `object.setLiveShape`, and Undo restores it.
#[test]
fn the_shape_properties_popup_edits_the_shape() {
let mut app = app();
app.run("tool.select", json!({"tool": "selection"})).unwrap();
let corners = |app: &VectorcraftApp, id: u64| match live(app, id) {
vectorcraft_doc::LiveShape::Rectangle { radii, .. } => radii,
l => panic!("{l:?}"),
};
let rect = id(app.run("shape.rectangle", json!({"x": 40, "y": 40, "width": 80, "height": 60, "radius": 10})).unwrap());
select(&mut app, &[rect]);
let (ctx, texts) = click_lock_slot(&mut app);
type_into(&mut app, &ctx, &texts, "10 pt", "20");
assert_eq!(corners(&app, rect), [20.0; 4]);
app.run("edit.undo", json!({})).unwrap();
assert_eq!(corners(&app, rect), [10.0; 4]);
let ellipse = id(app.run("shape.ellipse", json!({"x": 40, "y": 40, "width": 80, "height": 60})).unwrap());
select(&mut app, &[ellipse]);
let pie = |app: &VectorcraftApp| match live(app, ellipse) {
vectorcraft_doc::LiveShape::Ellipse { pie, .. } => pie,
l => panic!("{l:?}"),
};
let (ctx, texts) = click_lock_slot(&mut app);
type_into(&mut app, &ctx, &texts, "360°", "180");
assert_eq!(pie(&app), (0.0, 180.0));
app.run("edit.undo", json!({})).unwrap();
assert_eq!(pie(&app), (0.0, 360.0));
let polygon = id(app.run("shape.polygon", json!({"cx": 100, "cy": 100, "radius": 40, "sides": 6})).unwrap());
select(&mut app, &[polygon]);
let sides = |app: &VectorcraftApp| match live(app, polygon) {
vectorcraft_doc::LiveShape::Polygon { sides, .. } => sides,
l => panic!("{l:?}"),
};
let (ctx, texts) = click_lock_slot(&mut app);
type_into(&mut app, &ctx, &texts, "6", "5");
assert_eq!(sides(&app), 5);
app.run("edit.undo", json!({})).unwrap();
assert_eq!(sides(&app), 6);
}
/// The popup stays open while a polygon's Corner dropdown is open, so a corner type can be picked
/// from it.
#[test]
fn the_shape_properties_popup_keeps_its_corner_dropdown() {
let mut app = app();
app.run("tool.select", json!({"tool": "selection"})).unwrap();
let polygon = id(app.run("shape.polygon", json!({"cx": 100, "cy": 100, "radius": 40, "sides": 6})).unwrap());
select(&mut app, &[polygon]);
let (ctx, texts) = click_lock_slot(&mut app);
let click = |app: &mut VectorcraftApp, p: Pos2| {
let press = |pressed| Event::PointerButton { pos: p, button: PointerButton::Primary, pressed, modifiers: Default::default() };
frame(app, &ctx, vec![Event::PointerMoved(p), press(true), press(false)]);
frame(app, &ctx, vec![])
};
let texts = click(&mut app, at(&texts, "Round"));
assert!(has(&texts, "Polygon Properties:") && has(&texts, "Chamfer"), "the popup and its dropdown: {texts:?}");
click(&mut app, at(&texts, "Chamfer"));
let vectorcraft_doc::LiveShape::Polygon { kinds, .. } = live(&app, polygon) else { panic!("not a polygon") };
assert!(!kinds.is_empty() && kinds.iter().all(|k| *k == vectorcraft_geom::shapes::CornerKind::Chamfer), "{kinds:?}");
}
/// Every other selection keeps Lock: a plain closed path, a star (a plain path), a path with live
/// corners, a group and several objects. The Direct Selection tool keeps it for a basic shape.
#[test]
fn other_selections_keep_lock() {
let mut app = app();
app.run("tool.select", json!({"tool": "selection"})).unwrap();
let closed = json!({"anchors": [{"x": 50, "y": 200}, {"x": 150, "y": 80}, {"x": 250, "y": 200}], "closed": true});
let path = id(app.run("path.create", closed).unwrap());
let star = id(app.run("shape.star", json!({"cx": 200, "cy": 150, "radius1": 40})).unwrap());
let rounded = id(app.run("shape.star", json!({"cx": 300, "cy": 150, "radius1": 40})).unwrap());
select(&mut app, &[rounded]);
app.run("object.setLiveShape", json!({"radius": 4})).unwrap();
assert!(matches!(live(&app, rounded), vectorcraft_doc::LiveShape::Path { .. }));
let a = id(app.run("shape.rectangle", json!({"x": 20, "y": 20, "width": 30, "height": 30})).unwrap());
let b = id(app.run("shape.ellipse", json!({"x": 60, "y": 20, "width": 30, "height": 30})).unwrap());
select(&mut app, &[a, b]);
let several = [a, b];
let c = id(app.run("shape.rectangle", json!({"x": 20, "y": 250, "width": 30, "height": 30})).unwrap());
let d = id(app.run("shape.rectangle", json!({"x": 60, "y": 250, "width": 30, "height": 30})).unwrap());
select(&mut app, &[c, d]);
let group = id(app.run("object.group", json!({})).unwrap());
let headings = ["Rectangle Properties:", "Ellipse Properties:", "Polygon Properties:"];
for ids in [vec![path], vec![star], vec![rounded], vec![group], several.to_vec()] {
select(&mut app, &ids);
let (_, texts) = click_lock_slot(&mut app);
assert!(!headings.iter().any(|h| has(&texts, h)), "{ids:?}: {texts:?}");
assert!(ids.iter().all(|i| locked(&app, *i)), "{ids:?} locks");
app.run("object.unlockAll", json!({})).unwrap();
}
let shape = id(app.run("shape.rectangle", json!({"x": 300, "y": 40, "width": 30, "height": 30})).unwrap());
for tool in ["directSelection", "groupSelection"] {
app.run("tool.select", json!({ "tool": tool })).unwrap();
select(&mut app, &[shape]);
let (_, texts) = click_lock_slot(&mut app);
assert!(!has(&texts, "Rectangle Properties:"), "{tool}: {texts:?}");
assert!(locked(&app, shape), "{tool} keeps Lock");
app.run("object.unlockAll", json!({})).unwrap();
}
}
/// A compound path under the Direct Selection tool isn't offered Edit Path, which would only switch
/// to the tool already in use; the Selection tool still offers it.
#[test]
+1
View File
@@ -572,6 +572,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
- 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. This follows Illustrator's bar: for a selected image it includes Image Trace, Retype and Crop Image, and traced artwork can be expanded from it ([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). This follows Illustrator's bar: while the Artboard tool is in use, it has options for resizing, duplicating and exporting 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/)).
- The contextual task bar offers more next steps by selection (<https://helpx.adobe.com/illustrator/desktop/get-started/learn-the-basics/contextual-task-bar-overview.html>): an open path Join and Edit Path (the Direct Selection tool), a closed path Edit Path, a compound path Edit Path and Release Compound Path, a clipping group Edit Clipping Path and Release Clipping Mask instead of Ungroup and Isolate, an image selected with vector art Make Clipping Mask instead of Unite, and point or area type Convert To Area Type or Convert To Point Type beside Create Outlines. Evidence: `crates/ui-egui/src/tests/taskbar.rs`.
- With a live rectangle, rounded rectangle, ellipse or polygon selected with the Selection tool, the contextual task bar shows a Shape Properties button in place of Lock. It opens a popup with the shape's rows from the Transform panel (corner radius, pie angles, or sides, angle, corner and radius), which edit it through `object.setLiveShape`. The overview describes this: when a shape drawn with a shape tool (Rectangle, Star) is selected, the bar offers to change its properties where Lock Object would be ([Contextual Task Bar overview](https://helpx.adobe.com/illustrator/desktop/get-started/learn-the-basics/contextual-task-bar-overview.html)). Object › Lock and ⌘2 still lock the shape. Not done: stars, which the app draws as plain paths. Evidence: `a_basic_shape_shows_its_properties_in_place_of_lock`, `the_shape_properties_popup_edits_the_shape` and `other_selections_keep_lock` in `crates/ui-egui/src/tests/taskbar.rs`.
- The contextual task bar has a Stroke chip beside the Fill chip, drawn with the paint the Stroke proxy shows (a "?" when the selected objects' strokes differ, as on the Fill chip); a click brings the Stroke proxy forward and shows the Swatches panel wherever it is docked, floating or closed, as a click on the Fill chip does for the Fill proxy. The reference app's bar has Fill and Stroke controls for changing an object's colors ([Contextual Task Bar overview](https://helpx.adobe.com/illustrator/desktop/get-started/learn-the-basics/contextual-task-bar-overview.html)). Evidence: `the_task_bar_shows_a_stroke_chip_beside_fill` and `the_task_bar_chips_follow_the_proxies_and_show_swatches_anywhere` in `crates/ui-egui/src/canvas/tests.rs`.
- 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, whose bar has items that delete the selected anchors, join selected end points, split the path at the selected anchors, and make the selected anchors corner or smooth points ([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.