Symbols: test the Symbols panel, panel menu and Control bar entries to symbol editing mode

Headless tests cover a double-click on a symbol in Thumbnail View and List View, the panel
menu's Edit Symbol with and without a selected instance, the Control bar's Edit Symbol button,
and the Control bar's Exit Symbol Editing Mode and Exit Isolation Mode buttons inside the
symbol's art. The panel's module comment now says a double-click opens the symbol in symbol
editing mode, and docs/mcp.md lists symbol.edit's ids parameter.
This commit is contained in:
dotCooCoo
2026-11-02 12:04:18 +07:00
committed by bflatastic
parent 10235563ae
commit 7c1d73d818
3 changed files with 159 additions and 2 deletions
+57
View File
@@ -1074,6 +1074,63 @@ fn the_control_bar_isolates_the_selected_object_and_exits_isolation_mode() {
assert_eq!(isolation_button(&mut app, &ctx, vec![]), None, "the isolation bar ends pattern editing");
}
/// In symbol editing mode the Control bar's isolation button is Exit Symbol Editing Mode and
/// leaves the mode. With a group isolated inside the symbol's art it is Exit Isolation Mode and
/// goes back to the symbol's art.
#[test]
fn the_control_bar_exits_symbol_editing_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();
app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 30, "height": 20})).unwrap();
app.run("symbol.new", json!({"name": "Box"})).unwrap();
app.run("symbol.edit", json!({"name": "Box"})).unwrap();
let layer = app.session.active().unwrap().doc.symbol_edit.as_ref().expect("symbol editing mode").layer;
let ctx = egui::Context::default();
crate::theme::install_fonts(&ctx);
ctx.enable_accesskit();
// The isolation button's label (the one exit label the bar shows), and a click on it.
let label = |app: &mut crate::VectorcraftApp| {
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), ..Default::default() }, |ui| super::control_bar(app, ui));
out.textures_delta.clear();
let nodes = &out.platform_output.accesskit_update.as_ref().expect("an accessibility tree").nodes;
let exits: Vec<String> = nodes
.iter()
.filter_map(|(_, n)| n.label().map(|l| l.trim().to_string()))
.filter(|l| l == "Exit Symbol Editing Mode" || l == "Exit Isolation Mode")
.collect();
assert!(exits.len() <= 1, "{exits:?}");
exits.into_iter().next()
};
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]);
}
};
assert_eq!(label(&mut app).as_deref(), Some("Exit Symbol Editing Mode"));
// Two rectangles drawn into the symbol's art, grouped and isolated.
let a = app.run("shape.rectangle", json!({"x": 100, "y": 100, "width": 20, "height": 20})).unwrap()["id"].as_u64().unwrap();
let b = app.run("shape.rectangle", json!({"x": 130, "y": 100, "width": 20, "height": 20})).unwrap()["id"].as_u64().unwrap();
app.run("select.set", json!({"ids": [a, b]})).unwrap();
let g = app.run("object.group", json!({})).unwrap()["id"].as_u64().unwrap();
app.run("object.isolate", json!({"id": g})).unwrap();
assert_eq!(label(&mut app).as_deref(), Some("Exit Isolation Mode"));
click(&mut app);
let st = app.session.active().unwrap();
assert_eq!((st.isolation, st.doc.symbol_edit.is_some()), (Some(layer), true), "back to the symbol's art");
assert_eq!(label(&mut app).as_deref(), Some("Exit Symbol Editing Mode"));
click(&mut app);
let st = app.session.active().unwrap();
assert_eq!((st.isolation, st.doc.symbol_edit.is_some()), (None::<NodeId>, false), "symbol editing mode ends");
// The symbol keeps the art drawn while editing.
let symbol = st.doc.symbols.iter().find(|s| s.name == "Box").expect("the symbol");
assert!(symbol.art.children().is_some_and(|c| c.len() >= 2), "the edit reached the symbol");
}
/// #4819: with Direct Selection, a selected blend shows the Control bar's and the Properties
/// panel's Convert buttons, as a path selected whole does; with the Selection tool it doesn't.
#[test]
+101 -1
View File
@@ -1,5 +1,5 @@
//! Symbols panel: the document's symbols as rendered thumbnails or a list. Click selects a symbol
//! (it becomes the Symbol Sprayer's current symbol); double-click places an instance. Symbol
//! (it becomes the Symbol Sprayer's current symbol); double-click opens it in symbol editing mode. Symbol
//! libraries open in library panels ([`SymbolLibraries`]).
use std::cell::RefCell;
@@ -979,6 +979,106 @@ mod tests {
assert_eq!(editing_art(&app), Some([585.0, 35.0, 615.0, 65.0]));
}
/// The symbol in symbol editing mode, if any.
fn editing(app: &VectorcraftApp) -> Option<String> {
app.session.active()?.doc.symbol_edit.as_ref().map(|e| e.symbol.clone())
}
/// The first instance of symbol `name` in the active document.
fn instance_of(app: &VectorcraftApp, name: &str) -> Option<NodeId> {
let mut found = None;
for l in &app.session.active()?.doc.layers {
l.walk(&mut |n| {
if let NodeKind::SymbolInstance { symbol, .. } = &n.kind
&& symbol == name
&& found.is_none()
{
found = Some(n.id);
}
});
}
found
}
fn double_click(at: Pos2) -> Vec<egui::Event> {
vec![egui::Event::PointerMoved(at), button(at, true), button(at, false), button(at, true), button(at, false)]
}
/// A double-click on a symbol in the panel, in Thumbnail View and in List View, opens it in
/// symbol editing mode and places no instance; a single click only makes it current.
#[test]
fn double_clicking_a_symbol_in_the_panel_edits_it_in_either_view() {
let mut app = leaf_and_dot();
let ctx = egui::Context::default();
crate::theme::install_fonts(&ctx);
// Thumbnail View: the first tile is Leaf's.
let tile = pos2(26.0, 26.0);
frame(&ctx, &mut app, vec![]);
frame(&ctx, &mut app, vec![egui::Event::PointerMoved(tile), button(tile, true), button(tile, false)]);
assert_eq!((editing(&app), current(&mut app)), (None, Some("Leaf".into())), "a click makes it current");
let ctx = egui::Context::default();
crate::theme::install_fonts(&ctx);
frame(&ctx, &mut app, vec![]);
frame(&ctx, &mut app, double_click(tile));
assert_eq!(editing(&app).as_deref(), Some("Leaf"));
assert_eq!(art(&app, "Leaf").0, 1, "no instance is placed");
app.run("symbol.exitEditing", json!({})).unwrap();
// List View: the row named Dot.
let ctx = egui::Context::default();
crate::theme::install_fonts(&ctx);
set_pstate(&ctx, "sym-list", true);
let raw = egui::RawInput { screen_rect: Some(Rect::from_min_size(Pos2::ZERO, vec2(236.0, 400.0))), ..Default::default() };
let mut out = ctx.run_ui(raw, |ui| show(&mut app, ui));
out.textures_delta.clear();
let labels = crate::tests_removeanchors::shapes_text(&out.shapes.iter().map(|c| c.shape.clone()).collect::<Vec<_>>());
let row = labels.iter().find(|(t, _)| t.trim() == "Dot").map(|(_, r)| r.center()).expect("a row named Dot");
frame(&ctx, &mut app, double_click(row));
assert_eq!(editing(&app).as_deref(), Some("Dot"));
assert_eq!(art(&app, "Dot").0, 0, "no instance is placed");
}
/// The panel menu's Edit Symbol edits the selected instance's symbol, and with no instance
/// selected the symbol current in the panel.
#[test]
fn the_panel_menus_edit_symbol_edits_the_instances_symbol_or_the_panels() {
let mut app = leaf_and_dot();
let ctx = egui::Context::default();
crate::theme::install_fonts(&ctx);
click_menu(&ctx, &mut app, "Edit Symbol");
assert_eq!(editing(&app).as_deref(), Some("Dot"), "no instance selected: the panel's symbol");
app.run("symbol.exitEditing", json!({})).unwrap();
let leaf = instance_of(&app, "Leaf").expect("a Leaf instance");
app.run("select.set", json!({"ids": [leaf.0]})).unwrap();
click_menu(&ctx, &mut app, "Edit Symbol");
assert_eq!(editing(&app).as_deref(), Some("Leaf"), "the selected instance's symbol");
assert_eq!(instance_of(&app, "Leaf"), Some(leaf), "the instance keeps its link");
}
/// With instances of one symbol selected, the Control bar's Edit Symbol opens that symbol in
/// symbol editing mode and keeps the instance linked.
#[test]
fn the_control_bars_edit_symbol_edits_the_selected_instances_symbol() {
let mut app = leaf_and_dot();
let leaf = instance_of(&app, "Leaf").expect("a Leaf instance");
app.run("select.set", json!({"ids": [leaf.0]})).unwrap();
let ctx = egui::Context::default();
crate::theme::install_fonts(&ctx);
ctx.enable_accesskit();
let bar = |app: &mut VectorcraftApp, events: Vec<egui::Event>| {
let raw = egui::RawInput { screen_rect: Some(Rect::from_min_size(Pos2::ZERO, vec2(800.0, 400.0))), events, ..Default::default() };
let mut out = ctx.run_ui(raw, |ui| {
ui.horizontal(|ui| control_bar(app, ui));
});
out.textures_delta.clear();
out
};
let out = bar(&mut app, vec![]);
let at = clickable(&out).into_iter().find(|(l, _)| l == "Edit Symbol").expect("an Edit Symbol button").1;
bar(&mut app, vec![egui::Event::PointerMoved(at), button(at, true), button(at, false)]);
assert_eq!(editing(&app).as_deref(), Some("Leaf"));
assert_eq!(instance_of(&app, "Leaf"), Some(leaf), "the instance keeps its link");
}
fn dialog_kind(app: &VectorcraftApp) -> Option<&str> {
app.ui.dialog.as_ref().map(|d| d.kind.as_str())
}
+1 -1
View File
@@ -1572,7 +1572,7 @@ already walks Image Trace. Short agent entry points:
of `symbol.list` carry `nineSlice`: the guides or `null`; `type` is Symbol Type, `static` by default for the command
and `dynamic` by default in the Symbol Options dialog),
`symbol.place {name?, x?, y?}`, `symbol.breakLink {ids?}`, `symbol.replace`,
`symbol.edit {id?, name?, x?, y?, artboard?, viewCenter?}` (symbol editing mode: the symbol's art on a temporary
`symbol.edit {id?, ids?, name?, x?, y?, artboard?, viewCenter?}` (symbol editing mode: the symbol's art on a temporary
isolated layer, where the instance `id` shows it or centered on (x, y), by default on the center of artboard
`artboard`, the active one the app passes, or in a document without artboards on `viewCenter`, the view's center the
app passes; every instance follows each edit and stays linked), `symbol.exitEditing`