Merge pull request #2258 from dotCooCoo/feat/control-bar-isolation

Control bar: buttons isolate the selected object and exit isolation mode
This commit is contained in:
BFlat
2026-10-16 03:13:08 +07:00
committed by GitHub
18 changed files with 127 additions and 8 deletions
+10 -8
View File
@@ -279,14 +279,8 @@ impl Tool for SelectionTool {
if cx.doc.node(top).is_some_and(|n| matches!(n.kind, vectorcraft_doc::NodeKind::Text(_))) {
return vec![Action::SwitchTool("type".into())];
}
// A group, a compound path or a single path, image, mesh or placed file, not
// the one already isolated. Blends, envelopes, repeats and symbol instances
// aren't: they have editing modes of their own.
use vectorcraft_doc::NodeKind as K;
let isolates = cx.doc.node(top).is_some_and(|n| {
matches!(n.kind, K::Group { .. } | K::Compound { .. } | K::Path { .. } | K::Image(_) | K::Mesh(_) | K::PlacedDocument(_))
});
if cx.double_click_isolate && isolates && cx.isolation != Some(top) {
// An object that [`isolates`], not the one already isolated.
if cx.double_click_isolate && cx.doc.node(top).is_some_and(|n| isolates(&n.kind)) && cx.isolation != Some(top) {
return vec![Action::Exec("object.isolate".into(), json!({ "id": top.0 }))];
}
} else if cx.isolation.is_some() {
@@ -568,6 +562,14 @@ pub(crate) fn is_area_type(n: &vectorcraft_doc::Node) -> bool {
&& matches!(&n.kind, vectorcraft_doc::NodeKind::Text(t) if matches!(t.kind, vectorcraft_doc::TextKind::Area { .. }))
}
/// Does a double-click or the Control bar's Isolate Selected Object button isolate an object of
/// this kind? A group, a compound path or a single path, image, mesh or placed file does. Blends,
/// envelopes, repeats and symbol instances do not; they have editing modes of their own.
pub fn isolates(kind: &vectorcraft_doc::NodeKind) -> bool {
use vectorcraft_doc::NodeKind as K;
matches!(kind, K::Group { .. } | K::Compound { .. } | K::Path { .. } | K::Image(_) | K::Mesh(_) | K::PlacedDocument(_))
}
/// The resize cursor for handle `h` of a box turned by `angle` (counter-clockwise degrees): the
/// handle that sits where `h` appears on screen, in 45° steps.
fn handle_cursor(h: Handle, angle: f64) -> Cursor {
+101
View File
@@ -208,6 +208,36 @@ pub fn anchor_buttons(app: &mut VectorcraftApp, ui: &mut Ui, controls: AnchorCon
}
}
/// The id of the Control bar's isolation button.
const ISOLATION_BUTTON: &str = "cb-isolation";
/// The Control bar's isolation button. In isolation mode it is Exit Isolation Mode, except while a
/// pattern is edited (the isolation bar's Done and Cancel end that). Otherwise, with one object
/// selected that [`vectorcraft_tools::select::isolates`], it is Isolate Selected Object, which
/// also isolates a path inside a group that Direct Selection selected. An envelope's contents
/// get no button: the envelope's Edit Envelope and Edit Contents buttons show instead.
fn isolation_button(app: &mut VectorcraftApp, ui: &mut Ui) {
let Some(st) = app.session.active() else { return };
let (icon, tip, id) = if st.isolation.is_some() {
if st.doc.pattern_edit.is_some() {
return;
}
("chevron-left", tl!("Exit Isolation Mode"), "object.exitIsolation")
} else if let [one] = st.selection.objects[..]
&& st.doc.node(one).is_some_and(|n| vectorcraft_tools::select::isolates(&n.kind))
&& vectorcraft_doc::live::envelope_of(&st.doc, one).is_none()
{
("square-dashed", tl!("Isolate Selected Object"), "object.isolate")
} else {
return;
};
let (rect, _) = ui.allocate_exact_size(vec2(24.0, 24.0), Sense::hover());
let resp = ui.interact(rect, egui::Id::new(ISOLATION_BUTTON), Sense::click());
if widgets::paint_icon_button(ui, resp, icon, tip, false).clicked() {
crate::menus::invoke(app, id, json!({}));
}
}
/// The room the Control bar's inline X/Y/W/H fields need; with less, only the Transform link shows.
const INLINE_TRANSFORM_WIDTH: f32 = 440.0;
@@ -252,6 +282,7 @@ pub fn control_bar(app: &mut VectorcraftApp, ui: &mut Ui) {
if anchors != AnchorControls::None {
anchor_buttons(app, ui, anchors);
}
isolation_button(app, ui);
ui.add_space(6.0);
// An image or an Image Trace object shows its own controls in place of Fill and Stroke.
let image = crate::place::control_bar_details(app, ui) || crate::panels::image_trace::control_bar(app, ui);
@@ -1158,6 +1189,76 @@ mod tests {
assert!(title(&s).ends_with("(RGB/Preview)"));
}
/// One headless frame of the Control bar; returns the isolation button's rectangle, if it shows.
fn isolation_button(app: &mut crate::VectorcraftApp, ctx: &egui::Context, events: Vec<egui::Event>) -> Option<egui::Rect> {
let screen = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(1600.0, 300.0));
let mut out = ctx.run_ui(egui::RawInput { screen_rect: Some(screen), events, ..Default::default() }, |ui| super::control_bar(app, ui));
out.textures_delta.clear();
ctx.viewport(|vp| vp.prev_pass.widgets.get(egui::Id::new(super::ISOLATION_BUTTON)).map(|w| w.rect))
}
/// The Control bar's Isolate Selected Object isolates a path inside a group that Direct
/// Selection selected, and Exit Isolation Mode leaves with the path selected. Type, symbol
/// instances, blends, two objects and an envelope's contents get no isolate button, and pattern
/// editing gets no exit button.
#[test]
fn the_control_bar_isolates_the_selected_object_and_exits_isolation_mode() {
use vectorcraft_doc::NodeId;
let mut app = crate::VectorcraftApp::new(Session::new(), Default::default());
app.run("file.new", json!({"width": 400, "height": 200})).unwrap();
let rect = |app: &mut crate::VectorcraftApp, x: f64| {
app.run("shape.rectangle", json!({"x": x, "y": 10, "width": 30, "height": 20})).unwrap()["id"].as_u64().unwrap()
};
let (a, b) = (rect(&mut app, 10.0), rect(&mut app, 50.0));
app.run("select.set", json!({"ids": [a, b]})).unwrap();
app.run("object.group", json!({})).unwrap();
app.select_tool("directSelection");
app.run("select.anchors", json!({"id": a, "anchors": [[0, 0]]})).unwrap();
let ctx = egui::Context::default();
crate::theme::install_fonts(&ctx);
let click = |app: &mut crate::VectorcraftApp| {
let r = isolation_button(app, &ctx, vec![]).expect("the bar shows the isolation button");
let press = |pressed| egui::Event::PointerButton {
pos: r.center(),
button: egui::PointerButton::Primary,
pressed,
modifiers: Default::default(),
};
for e in [egui::Event::PointerMoved(r.center()), press(true), press(false)] {
isolation_button(app, &ctx, vec![e]);
}
};
click(&mut app);
assert_eq!(app.session.active().unwrap().isolation, Some(NodeId(a)), "Isolate Selected Object isolates the path");
click(&mut app);
let st = app.session.active().unwrap();
assert_eq!((st.isolation, st.selection.objects.clone()), (None, vec![NodeId(a)]), "Exit Isolation Mode selects the path");
app.select_tool("selection");
// Type, a symbol instance, a blend and two objects.
let text = app.run("text.create", json!({"x": 10, "y": 100, "text": "Hi"})).unwrap()["id"].as_u64().unwrap();
let c = rect(&mut app, 100.0);
app.run("select.set", json!({"ids": [c]})).unwrap();
let symbol = app.run("symbol.new", json!({"name": "Box"})).unwrap()["id"].as_u64().unwrap();
let (d, e) = (rect(&mut app, 150.0), rect(&mut app, 250.0));
let blend = app.run("object.blend.make", json!({"ids": [d, e], "steps": 1})).unwrap()["id"].as_u64().unwrap();
for ids in [json!([text]), json!([symbol]), json!([blend]), json!([text, symbol])] {
app.run("select.set", json!({ "ids": ids })).unwrap();
assert_eq!(isolation_button(&mut app, &ctx, vec![]), None, "{ids}");
}
// An envelope's contents, while Edit Contents is on.
let g = rect(&mut app, 350.0);
app.run("select.set", json!({"ids": [g]})).unwrap();
app.run("object.envelope.makeWithWarp", json!({})).unwrap();
app.run("object.envelope.editContents", json!({"editing": true})).unwrap();
assert_eq!(app.session.active().unwrap().selection.objects, vec![NodeId(g)]);
assert_eq!(isolation_button(&mut app, &ctx, vec![]), None, "the envelope's controls show instead");
let f = rect(&mut app, 300.0);
app.run("select.set", json!({"ids": [f]})).unwrap();
app.run("object.pattern.make", json!({"name": "Dots", "width": 20, "height": 20})).unwrap();
assert!(app.session.active().unwrap().isolation.is_some());
assert_eq!(isolation_button(&mut app, &ctx, vec![]), None, "the isolation bar ends pattern editing");
}
/// The Zoom tool's hint follows Performance › Animated Zoom (#394): a drag zooms as it goes,
/// or (off) zooms into the area dragged across.
#[test]
+1
View File
@@ -665,6 +665,7 @@
Auto Color Automatická barva
Automatic Automaticky
Isolate Selected Group Izolovat vybranou skupinu
Isolate Selected Object Izolovat vybraný objekt
Exit Isolation Mode Ukončit izolovaný režim
Make Clipping Mask Vytvořit ořezovou masku
Release Clipping Mask Uvolnit ořezovou masku
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -3320,6 +3320,7 @@ imageTrace Outline Kontur
to set spacing and orientation legt Abstand und Ausrichtung fest
: :
Isolate Selected Group Ausgewählte Gruppe isolieren
Isolate Selected Object Ausgewähltes Objekt isolieren
Release Clipping Mask Schnittmaske zurückwandeln
Make Compound Path Zusammengesetzten Pfad erstellen
Release Compound Path Zusammengesetzten Pfad zurückwandeln
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -3419,6 +3419,7 @@ imageTrace Outline Contorno
to set spacing and orientation para definir espaciado y orientación
: :
Isolate Selected Group Aislar grupo seleccionado
Isolate Selected Object Aislar objeto seleccionado
Release Clipping Mask Soltar máscara de recorte
Make Compound Path Crear trazado compuesto
Release Compound Path Soltar trazado compuesto
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -3416,6 +3416,7 @@ imageTrace Outline Contour
to set spacing and orientation pour définir l’espacement et l’orientation
:  :
Isolate Selected Group Isoler le groupe sélectionné
Isolate Selected Object Isoler l’objet sélectionné
Release Clipping Mask Annuler le masque d’écrêtage
Make Compound Path Créer un tracé transparent
Release Compound Path Annuler le tracé transparent
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -1746,6 +1746,7 @@
Isolate Blending Keverés elkülönítése
Isolate Group Csoport elkülönítése
Isolate Selected Group Kijelölt csoport elkülönítése
Isolate Selected Object Kijelölt objektum elkülönítése
Italian Olasz
Italic Dőlt
Its graphics, colors and text styles are deleted from this machine. A grafikái, színei és szövegstílusai törlődnek erről a gépről.
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -3421,6 +3421,7 @@ imageTrace Outline Contorno
to set spacing and orientation per impostare spaziatura e orientamento
: :
Isolate Selected Group Isola gruppo selezionato
Isolate Selected Object Isola oggetto selezionato
Release Clipping Mask Rilascia maschera di ritaglio
Make Compound Path Crea tracciato composto
Release Compound Path Rilascia tracciato composto
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -670,6 +670,7 @@
Auto Color 自動カラー
Automatic 自動
Isolate Selected Group 選択したグループを分離
Isolate Selected Object 選択したオブジェクトを分離
Exit Isolation Mode 分離モードを終了
Make Clipping Mask クリッピングマスクを作成
Release Clipping Mask クリッピングマスクを解除
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -668,6 +668,7 @@
Auto Color 자동 색상
Automatic 자동
Isolate Selected Group 선택한 그룹 격리
Isolate Selected Object 선택한 개체 격리
Exit Isolation Mode 격리 모드 종료
Make Clipping Mask 클리핑 마스크 만들기
Release Clipping Mask 클리핑 마스크 해제
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -612,6 +612,7 @@
Auto Color Kolor automatyczny
Automatic Automatycznie
Isolate Selected Group Izoluj zaznaczoną grupę
Isolate Selected Object Izoluj zaznaczony obiekt
Exit Isolation Mode Wyjdź z trybu izolacji
Make Clipping Mask Utwórz maskę przycinającą
Release Clipping Mask Uwolnij maskę przycinającą
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -1179,6 +1179,7 @@
Isolate Blending Isolar Mesclagem
Isolate Group Isolar Grupo
Isolate Selected Group Isolar Grupo Selecionado
Isolate Selected Object Isolar Objeto Selecionado
Italian Italiano
Join Unir
Join Our Discord Junte-se ao nosso Discord
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1
View File
@@ -3417,6 +3417,7 @@ imageTrace Outline Контур
to set spacing and orientation для задания интервала и ориентации
: :
Isolate Selected Group Изолировать выбранную группу
Isolate Selected Object Изолировать выбранный объект
Release Clipping Mask Освободить маску обрезки
Make Compound Path Создать составной контур
Release Compound Path Освободить составной контур
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -3414,6 +3414,7 @@ imageTrace Outline Контур
to set spacing and orientation для завдання інтервалу та орієнтації
: :
Isolate Selected Group Ізолювати обрану групу
Isolate Selected Object Ізолювати обраний об’єкт
Release Clipping Mask Звільнити відсічну маску
Make Compound Path Створити складений контур
Release Compound Path Звільнити складений контур
Can't render this file because it contains an unexpected character in line 713 and column 40.
+1
View File
@@ -1608,6 +1608,7 @@
Isolate Blending Tách biệt hòa trộn
Isolate Group Tách biệt nhóm
Isolate Selected Group Tách biệt nhóm đã chọn
Isolate Selected Object Tách biệt đối tượng đã chọn
Isometric Bottom Đẳng góc dưới
Isometric Left Đẳng góc trái
Isometric Right Đẳng góc phải
1 # Vietnamese (vi) catalog. Same format as zh-hant.tsv: context <TAB> source <TAB> translation
1608
1609
1610
1611
1612
1613
1614
+1
View File
@@ -3276,6 +3276,7 @@ imageTrace Outline 轮廓
to set spacing and orientation 可设置间距与方向
: :
Isolate Selected Group 隔离选择的编组
Isolate Selected Object 隔离选择的对象
Release Clipping Mask 释放剪切蒙版
Make Compound Path 制作复合路径
Release Compound Path 释放复合路径
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -3407,6 +3407,7 @@ imageTrace Outline 輪廓
to set spacing and orientation 可設定間距與方向
: :
Isolate Selected Group 隔離選取的群組
Isolate Selected Object 隔離選取的物件
Release Clipping Mask 釋放剪裁遮色片
Make Compound Path 製作複合路徑
Release Compound Path 釋放複合路徑
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1
View File
@@ -429,6 +429,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
- Dialogs open with their first field focused and its value selected: type a value and press Enter (Move, Rotate, Scale…). A dialog opened from a menu takes the keyboard focus every time it opens, not only the first (#1367), and one whose first field starts greyed out (Blend Options on Smooth Color, Layers Panel Options) gives the focus to that field once it is enabled (#1395). Number fields in panels, the Control bar and dialogs (preferences too) step with Up/Down, or with the mouse wheel over the focused field (#485: a notch a step; the panel under it then stays put, and an unfocused field leaves the wheel to scrolling) (Shift: ten, Ctrl/Cmd: a tenth of their unit), applied at once.
- Scrubby labels (#400, beyond Illustrator, as in Photoshop): dragging a numeric field's label left or right (the ↔ cursor shows over it) steps the value one unit every 2 points (Shift: ten, Ctrl/Cmd: a tenth; counts stay whole), in the field's unit and within its limits, applied live as the art follows; the whole drag is one undo step and Escape puts the value back. A click on the label focuses the field; the text box itself never scrubs, and a field with no label doesn't. Text labels before a field (Transform, Properties, Control bar, Stroke, Transparency, form rows in dialogs) and the Character and Paragraph panels' icons act as labels. Preferences › General › Scrub Numeric Fields by Dragging (on by default) turns it off.
- The Control bar's underlined Transform link opens the Transform panel in a popover under it (reference point, X/Y/W/H, Rotate and Shear with their icons and angle presets, flips, shape properties, Scale Corners and Scale Strokes & Effects), as its Stroke link does the Stroke panel; X/Y/W/H also show inline while the bar has room. The reference point locator is nine squares, the outer eight joined around the box.
- In isolation mode the Control bar has an Exit Isolation Mode button (`object.exitIsolation`), except while a pattern is edited. Outside isolation mode, with one group, compound path, path, image, mesh or placed file selected, it has an Isolate Selected Object button (`object.isolate`), which also isolates a path inside a group that Direct Selection selected.
- Tab shows and hides the panels (in the Type tool it types a tab) without moving the keyboard focus onto a field, so the next shortcut or letter still works; in dialogs and panel fields it moves between fields.
- Cmd/Ctrl+A follows the keyboard: in a panel, dialog or search field it selects the field's text, while the Type tool edits it selects all of that text (also `select.all` over MCP), and only otherwise every object. On macOS the menu bar's Select All, Cut, Copy and Paste (keys or clicks) act on a focused field too, and elsewhere the in-window menus' do: the field that had the keyboard when the menu opened gets them, with its selection, and the keyboard back (#516). Not yet: Select All across threaded frames (one frame's text is selected).
- Shortcut sets other apps save as text (`.kys`) open in Import Shortcut Set from File › Open, from Edit › Keyboard Shortcuts › Import… in the desktop app, or from a drop on the window. It matches each entry to a tool, command or panel by name and decodes the file's key and modifier codes where VectorCraft's default shortcuts agree on them; the user answers the rest, and Apply fills the Keyboard Shortcuts dialog's working set. The answers are kept with the preferences for the next import (`shortcutSet.read`, `importMatches.*`).