G1: Properties: Edit Clipping Path and Edit Contents for one clip group

With one clip group selected, the Properties panel has Edit Clipping
Path and Edit Contents at its top, under the Clip Group heading, as the
reference app does: "With the clip group selected, click the Edit
Clipping Path button at the top of the Properties panel to select and
edit the mask, or click the Edit Contents button to select and edit the
masked content"
(https://www.adobe.com/learn/illustrator/web/crop-using-masks). They
run the existing object.clippingMask.editMask and editContents, which
select the clipping path or the clipped art for the selection tools, as
Object > Clipping Mask does. Edit Contents shows only when the group
clips something. With several objects selected they don't show (the
tutorial describes one selected clip group); the Contextual Task Bar
does the same. Both labels are already in every catalog.

Test: panels::properties::tests::one_clip_group_has_edit_clipping_path_and_edit_contents_at_the_top
(first under the heading, each selecting its part; Edit Clipping Path
alone once the clipped art is deleted; neither for a clip group
selected with another object in either order, or for a plain group).
Documented in docs/target-app-parity.md, docs/ui-parity.md and
docs/gaps.md.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Drew Dibble
2026-10-22 00:01:36 +07:00
committed by bflatastic
co-authored by Claude Opus 5.5
parent 7ceaf900d6
commit f19b33c219
4 changed files with 89 additions and 4 deletions
+80
View File
@@ -39,6 +39,12 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
document_sections(app, ui);
return;
}
if n_sel == 1
&& let Some(NodeKind::Group { clip: true, children }) = first.as_ref().map(|n| &n.kind)
{
clip_group_buttons(app, ui, children.len() > 1);
divider(ui);
}
transform_section(app, ui);
divider(ui);
// The Gradient tool: the gradient's type and, for a freeform one, how clicks add points (#921).
@@ -139,6 +145,24 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
}
}
/// The one selected clip group's Edit Clipping Path and Edit Contents, at the top of the panel as in
/// the reference app ("With the clip group selected, click the Edit Clipping Path button at the top
/// of the Properties panel to select and edit the mask, or click the Edit Contents button to select
/// and edit the masked content", <https://www.adobe.com/learn/illustrator/web/crop-using-masks>):
/// they select its clipping path or its clipped art for the selection tools, as Object › Clipping
/// Mask does. Edit Contents only when the group clips something.
fn clip_group_buttons(app: &mut VectorcraftApp, ui: &mut Ui, has_contents: bool) {
let w = (ui.available_width() - 6.0) / 2.0;
ui.horizontal(|ui| {
if widgets::flat_button(ui, tl!("Edit Clipping Path"), w).clicked() {
crate::menus::invoke(app, "object.clippingMask.editMask", json!({}));
}
if has_contents && widgets::flat_button(ui, tl!("Edit Contents"), w).clicked() {
crate::menus::invoke(app, "object.clippingMask.editContents", json!({}));
}
});
}
/// The one selected image: its file (or Embedded), colour mode and resolution, and the Links
/// panel's actions for it.
fn image_section(app: &mut VectorcraftApp, ui: &mut Ui) {
@@ -668,6 +692,62 @@ mod tests {
frame(app, ctx, vec![b(false)]);
}
/// One selected clip group has Edit Clipping Path and Edit Contents at the top of the panel,
/// under its Clip Group heading and before every section, and they select its clipping path or
/// its clipped art; one that clips nothing has Edit Clipping Path alone. A plain group, or a clip
/// group selected with another object, has neither.
#[test]
fn one_clip_group_has_edit_clipping_path_and_edit_contents_at_the_top() {
let mut app = VectorcraftApp::new(Session::new(), Default::default());
app.run("file.new", json!({"width": 400, "height": 300})).unwrap();
let art = app.run("shape.rectangle", json!({"x": 100, "y": 80, "width": 200, "height": 140})).unwrap()["id"].clone();
let mask = app.run("shape.ellipse", json!({"x": 150, "y": 100, "width": 100, "height": 100})).unwrap()["id"].clone();
app.run("select.set", json!({"ids": [art, mask]})).unwrap();
let group = app.run("object.clippingMask.make", json!({})).unwrap()["id"].clone();
app.run("select.set", json!({"ids": [group]})).unwrap();
let ctx = egui::Context::default();
let selected = |app: &VectorcraftApp| app.session.active().unwrap().selection.objects.iter().map(|n| json!(n.0)).collect::<Vec<_>>();
let labels = |texts: &[(String, Rect)]| texts.iter().map(|(t, _)| t.clone()).collect::<Vec<_>>();
let texts = frame(&mut app, &ctx, vec![]);
let shown = labels(&texts);
let at = |label: &str| shown.iter().position(|t| t == label).unwrap_or_else(|| panic!("{label} in {shown:?}"));
assert_eq!(at("Clip Group"), 0, "{shown:?}");
assert!(at("Edit Clipping Path") == 1 && at("Edit Contents") == 2, "{shown:?}");
click(&mut app, &ctx, &texts, "Edit Contents");
assert_eq!(selected(&app), std::slice::from_ref(&art), "Edit Contents selects the clipped art");
app.run("select.set", json!({"ids": [group]})).unwrap();
let texts = frame(&mut app, &ctx, vec![]);
click(&mut app, &ctx, &texts, "Edit Clipping Path");
assert_eq!(selected(&app), std::slice::from_ref(&mask), "Edit Clipping Path selects the clipping path");
let neither = |app: &mut VectorcraftApp| {
let shown = labels(&frame(app, &ctx, vec![]));
!shown.iter().any(|t| t == "Edit Clipping Path" || t == "Edit Contents")
};
// The clip group with another object, whichever is selected first.
let other = app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 40, "height": 40})).unwrap()["id"].clone();
app.run("select.set", json!({"ids": [group, other]})).unwrap();
assert!(neither(&mut app), "a clip group and another object");
app.run("select.set", json!({"ids": [other, group]})).unwrap();
assert!(neither(&mut app), "another object and a clip group");
// A plain group.
let b = app.run("shape.rectangle", json!({"x": 60, "y": 10, "width": 40, "height": 40})).unwrap()["id"].clone();
app.run("select.set", json!({"ids": [other, b]})).unwrap();
let plain = app.run("object.group", json!({})).unwrap()["id"].clone();
app.run("select.set", json!({"ids": [plain]})).unwrap();
assert!(labels(&frame(&mut app, &ctx, vec![])).iter().any(|t| t == "Ungroup"));
assert!(neither(&mut app), "a plain group");
// A clip group whose clipped art is deleted: Edit Clipping Path alone.
app.run("select.set", json!({"ids": [art]})).unwrap();
app.run("edit.clear", json!({})).unwrap();
app.run("select.set", json!({"ids": [group]})).unwrap();
let texts = frame(&mut app, &ctx, vec![]);
let shown = labels(&texts);
assert_eq!(shown.iter().position(|t| t == "Edit Clipping Path"), Some(1), "{shown:?}");
assert!(!shown.iter().any(|t| t == "Edit Contents"), "nothing to edit: {shown:?}");
click(&mut app, &ctx, &texts, "Edit Clipping Path");
assert_eq!(selected(&app), std::slice::from_ref(&mask), "Edit Clipping Path selects the clipping path");
}
/// The Snap Options and Scale Strokes & Effects checkboxes keep a visible box at rest in every
/// theme, as the Preferences ones do (#1080), and toggle when clicked.
#[test]
+3 -1
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+3 -2
View File
File diff suppressed because one or more lines are too long