M1: Crop chosen in the colour picker enters crop mode (#2150)

Choosing Crop from the image fill's mode dropdown only set scale_mode: crop
mode never started, the first press on a handle closed the picker and went
to the tool, and the picture lost its place (no placement crop). For one
layer's image fill it now runs image.crop, as Object > Crop image and the
Fill row's button do; other targets only set the mode, as before.

Fixes #2150
This commit is contained in:
ljkx committed 2026-11-09 12:44:21 +01:00
1 parent 3839fe4a0c
commit 8330ffb32a
2 files changed
+81 -3

No files matched your search

+53
View File
@@ -128,3 +128,56 @@ fn in_crop_mode_the_layers_edges_crop_without_moving_the_picture() {
key(&mut u, Key::Escape);
assert!(ghost(&mut u).is_none(), "gone with crop mode");
}
/// Choose `item` from the image mode dropdown of the open colour picker, which reads `shown`.
fn choose_mode(u: &mut Ui, shown: &str, item: &str) {
for text in [shown, item] {
// The picker is drawn over the Design panel, so its text comes last.
let shapes = flat(&u.frame_output(vec![]).shapes);
let (_, p, size) = crate::live_tests::texts(&shapes).into_iter().rfind(|(t, _, _)| t == text).unwrap_or_else(|| panic!("no {text:?}"));
u.button(p + size / 2.0, true);
u.button(p + size / 2.0, false);
}
u.frame(vec![]);
}
/// Crop chosen as the image fill's mode in the colour picker enters crop mode, as Crop image does
/// (#2150): the picker closes, the picture keeps its place, and a corner handle then crops it
/// with the layer still selected. The other modes only set the mode.
#[test]
fn crop_chosen_in_the_colour_picker_enters_crop_mode() {
let (mut u, layer, _) = setup();
u.app.ui.picker = Some(crate::colorpicker::PickerTarget::Paint { fills: true, index: 0, ids: vec![layer] });
for _ in 0..4 {
u.frame(vec![]);
}
choose_mode(&mut u, "Fill", "Fit");
assert_eq!(mode(&u, layer), (ImageScaleMode::Fit, None));
assert!(u.app.ui.picker.is_some() && u.app.ui.crop.is_none(), "Fit is only a mode");
choose_mode(&mut u, "Fit", "Crop");
assert_eq!(u.app.ui.crop, Some((layer, 0)), "crop mode on the image fill");
assert!(u.app.ui.picker.is_none(), "crop mode takes the canvas from the picker");
// The picture keeps its place: the crop is the one Crop image makes of the fitted image.
let (m, Some(before)) = mode(&u, layer) else { panic!("a crop where the picture was") };
assert_eq!(m, ImageScaleMode::Crop);
let (mut menu, other, _) = setup();
let mut fit = menu.app.session.doc().unwrap().node(other).unwrap().fills[0].clone();
if let PaintKind::Image(img) = &mut fit.kind {
img.scale_mode = ImageScaleMode::Fit;
}
menu.app.run("fill.set", json!({"ids": [other], "index": 0, "paint": fit}));
assert_eq!(mode(&menu, other), (ImageScaleMode::Fit, None));
menu.app.run("image.crop", json!({}));
assert_eq!(mode(&menu, other), (ImageScaleMode::Crop, Some(before)));
// The bottom right corner, dragged in, crops the picture to the top left 3/4 of the layer.
let (from, to) = (u.app.to_screen(200.0, 100.0), u.app.to_screen(150.0, 75.0));
u.drag_to(from, to);
u.button(to, false);
assert_eq!(u.app.session.selection(), vec![layer], "the layer stays selected");
assert_eq!(u.app.ui.crop, Some((layer, 0)), "still cropping");
let n = u.app.session.doc().unwrap().node(layer).unwrap().clone();
assert!((n.width - 150.0).abs() < 1e-6 && (n.height - 75.0).abs() < 1e-6, "{} x {}", n.width, n.height);
let (_, Some(after)) = mode(&u, layer) else { panic!("a crop") };
assert!((after[0] - before[0] * 0.75).abs() < 1e-9 && (after[3] - before[3] * 0.75).abs() < 1e-9, "{before:?} -> {after:?}");
assert!((after[4] - before[4]).abs() < 1e-9 && (after[5] - before[5]).abs() < 1e-9, "the top left stays: {before:?} -> {after:?}");
}
+28 -3
View File
@@ -760,6 +760,21 @@ fn blend_row(ui: &mut Ui, app: &mut FigApp, target: &PickerTarget, paint: &mut P
});
}
/// The layer and fill index to crop on the canvas (`image.crop`) when Crop is chosen for `target`:
/// one layer's image fill as the Fill section shows it (a text's characters' shared fills
/// included). Strokes, several layers at once and the regions selected in vector edit mode have
/// no crop mode, so for them Crop only sets the paint's mode.
fn crop_on_canvas(app: &FigApp, target: &PickerTarget) -> Option<(NodeId, usize)> {
let PickerTarget::Paint { fills: true, index, ids } = target else { return None };
let [id] = ids[..] else { return None };
if figcraft_engine::vectoredit::selected_region_fills(&app.session, id).is_some() {
return None;
}
let doc = app.session.doc().ok()?;
let shown = figcraft_engine::cmd::lists::character_fills(doc.node(id)?)?;
matches!(shown.get(*index)?.kind, PaintKind::Image(_)).then_some((id, *index))
}
/// The image paint's scale mode, rotation, colour profile, presets and filter sliders.
fn image_controls(app: &mut FigApp, ui: &mut Ui, target: &PickerTarget, paint: &mut Paint, mut img: ImagePaint, t: &Tokens) {
ui.horizontal(|ui| {
@@ -773,9 +788,19 @@ fn image_controls(app: &mut FigApp, ui: &mut Ui, target: &PickerTarget, paint: &
(tr("Tile"), ImageScaleMode::Tile),
] {
if crate::right::menu_item(ui, l, "") {
img.scale_mode = m;
paint.kind = PaintKind::Image(img.clone());
write(app, target, paint, false);
match crop_on_canvas(app, target).filter(|_| m == ImageScaleMode::Crop) {
// Crop for a layer's own image fill is Crop image (#2150): the picture keeps
// its place and crop mode takes the canvas, closing the picker, so its
// handles crop it as they do when crop mode comes from the menu.
Some((id, index)) => {
app.run("image.crop", json!({"ids": [id], "index": index}));
}
None => {
img.scale_mode = m;
paint.kind = PaintKind::Image(img.clone());
write(app, target, paint, false);
}
}
ui.close();
}
}