mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-01 05:37:23 -06:00
Contextual Task Bar: a basic shape shows its properties in place of Lock
A live rectangle, rounded rectangle, ellipse or polygon selected with the Selection tool shows a Shape Properties button where Lock was. It opens a popup with the shape's rows from the Transform panel, which stays open while the fields are edited. Every other selection, and the Direct and Group Selection tools, keep Lock; Object > Lock and Cmd+2 still lock a shape. The Transform panel, the Properties panel and the popup draw the rows through one live_shape_rows function.
This commit is contained in:
@@ -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,21 @@ 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 popup that stays open while its fields are edited.
|
||||
let props = widgets::icon_button(ui, "shapes", tl!("Shape Properties"), false, 30.0);
|
||||
egui::Popup::menu(&props).close_behavior(egui::PopupCloseBehavior::CloseOnClickOutside).show(|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);
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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
|
||||
|
||||
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,149 @@ 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);
|
||||
}
|
||||
|
||||
/// 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]
|
||||
|
||||
@@ -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 says: "For a basic shape created with tools such as the Rectangle tool and the Star tool, you get the option to adjust its properties instead of Lock Object" ([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.
|
||||
|
||||
Reference in New Issue
Block a user