mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 09:46:59 -06:00
Merge pull request #3430 from addibble/feat/properties-clip-group-quick-actions
G1: Properties: Edit Clipping Path and Edit Contents for one clip group
This commit is contained in:
@@ -11,6 +11,8 @@ use crate::theme::Tokens;
|
||||
use crate::widgets::{self, dim_label, divider, section_header};
|
||||
use crate::{VectorcraftApp, icons};
|
||||
|
||||
/// The panel for what is selected: the selection's kind as its heading, then its sections (Edit
|
||||
/// Artboards' with the Artboard tool, the document's with nothing selected).
|
||||
pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let Some(st) = app.session.active() else {
|
||||
@@ -39,6 +41,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 +147,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) {
|
||||
@@ -645,6 +671,7 @@ mod tests {
|
||||
panel_frame(app, ctx, events, show)
|
||||
}
|
||||
|
||||
/// One frame of `draw` with `events` → the texts painted, with their rects.
|
||||
fn panel_frame(app: &mut VectorcraftApp, ctx: &egui::Context, events: Vec<Event>, draw: fn(&mut VectorcraftApp, &mut Ui)) -> Vec<(String, Rect)> {
|
||||
let raw = egui::RawInput { screen_rect: Some(Rect::from_min_size(Pos2::ZERO, vec2(300.0, 700.0))), events, ..Default::default() };
|
||||
let mut out = ctx.run_ui(raw, |ui| draw(app, ui));
|
||||
@@ -661,6 +688,7 @@ mod tests {
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Clicks the text `label` where the last frame painted it, over two frames (press, release).
|
||||
fn click(app: &mut VectorcraftApp, ctx: &egui::Context, texts: &[(String, Rect)], label: &str) {
|
||||
let at = texts.iter().find(|(t, _)| t == label).map(|(_, r)| r.center()).unwrap_or_else(|| panic!("no {label:?} in {texts:?}"));
|
||||
let b = |pressed| Event::PointerButton { pos: at, button: PointerButton::Primary, pressed, modifiers: Default::default() };
|
||||
@@ -668,6 +696,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
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+3
-2
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user