Merge pull request #11252 from NaufalDzz/fix-transform-several-selection-rules

fix(transform): refuse the Background among several layers and apply the box on a selection change
This commit is contained in:
David Mann authored and GitHub committed 2026-11-07 01:12:41 +01:00
commit 0f286b57a9
40 files changed
+513 -69

No files matched your search

+24 -8
View File
@@ -272,9 +272,9 @@ pub fn locked_for_transform(doc: &Document, id: LayerId) -> bool {
}
/// The layers Free Transform moves with the Layers panel's selection, bottom-to-top: with several
/// layers selected, every one that no selected group already carries, as in Photoshop (#6501), but
/// the Background, which Show Transform Controls doesn't frame either. With one layer selected (or
/// only the Background besides it), that layer; the active layer when nothing else is.
/// layers selected, every one that no selected group already carries, as in Photoshop (#6501). The
/// Background stays among them, so such a box refuses as locked (#11101, [`background_among`]).
/// With one layer selected, that layer; the active layer when nothing else is.
pub fn selection_targets(st: &crate::DocState) -> Vec<LayerId> {
targets_of(&st.doc, &st.selected_layers(), st.active_layer)
}
@@ -285,14 +285,23 @@ pub(crate) fn targets_of(doc: &Document, selected: &[LayerId], active: Option<La
if selected.len() < 2 {
return active.into_iter().collect();
}
let ids: Vec<LayerId> = crate::layer_multi_cmds::top_level(doc, selected).into_iter().filter(|id| !doc.is_background(*id)).collect();
let ids = crate::layer_multi_cmds::top_level(doc, selected);
if ids.is_empty() { active.into_iter().collect() } else { ids }
}
/// The Background when it is among several layers one box would move: Photoshop refuses that Free
/// Transform with its locked-layer alert rather than leaving the Background out (#11101).
pub fn background_among(doc: &Document, ids: &[LayerId]) -> Option<LayerId> {
if ids.len() < 2 {
return None;
}
ids.iter().copied().find(|id| doc.is_background(*id))
}
/// The layers one box over `targets` moves: `targets` with the layers linked to them, as the Move
/// tool takes them along. In Photoshop transforming one linked layer transforms the whole link
/// group (#11217). A linked Background joins only when it is a target itself: `targets_of`
/// leaves it out of a box over other layers, and a link doesn't bring it back in.
/// group (#11217). A linked Background joins only when it is a target itself: a link doesn't
/// bring it into a box over other layers.
pub fn box_targets(doc: &Document, targets: &[LayerId]) -> Vec<LayerId> {
let mut ids = crate::layer_multi_cmds::move_targets(doc, targets);
ids.retain(|id| targets.contains(id) || !doc.is_background(*id));
@@ -874,7 +883,14 @@ fn transform(s: &mut Session, p: &Value) -> Result<Value> {
(Some(ids), _) => ids,
_ if lone.is_some() => Vec::new(),
(None, Some(id)) => vec![id],
(None, None) => selection_targets(st),
(None, None) => {
let ids = selection_targets(st);
if let Some(bg) = background_among(&st.doc, &ids) {
let name = st.doc.layer(bg).map_or("Background", |l| l.name.as_str());
return Err(EngineError::Other(format!("layer \"{name}\" is locked")));
}
ids
}
};
if let Some(missing) = ids.iter().find(|id| st.doc.layer(**id).is_none()) {
return Err(EngineError::NoLayer(*missing));
@@ -1016,7 +1032,7 @@ pub fn specs() -> Vec<CommandSpec> {
label: "Free Transform",
menu: &[],
shortcut: None,
params: r##"{"layer":id?,"layers":[id,…]? (several layers moved by one box, one step; default with neither = the selected layers but the Background, as Photoshop's Free Transform with several selected),"rect":[x0,y0,x1,y1]? (source frame; default = the layers' content ∩ selection),"quad":[[x,y]×4]? (where the frame's corners go, clockwise from top-left),"matrix":[a,b,c,d,e,f]? (affine alternative),"scaleX":%?,"scaleY":%?,"rotate":deg?,"dx":px?,"dy":px?,"reference":"center|topLeft|top|topRight|left|right|bottomLeft|bottom|bottomRight"|[x,y]="center" (relative alternative: scale and rotate about this point, then move),"interpolation":"bicubic|bilinear|nearest|bicubicSharper|bicubicSmoother|bicubicAutomatic"="bicubic" (Photoshop's bicubic variants render as Bicubic),"stepName":"Transform Path"? (History step name; the shape tools' bounding box sends it; default "Free Transform"; any other value is refused),"target":"pixels"|"mask"|"quickMask"|{"channel":i}=Channels panel target (an unlinked mask, an alpha channel or the Quick Mask transforms alone; a linked mask moves with its layer)}"##,
params: r##"{"layer":id?,"layers":[id,…]? (several layers moved by one box, one step; default with neither = the selected layers, as Photoshop's Free Transform with several selected; refused as locked when the Background is among several),"rect":[x0,y0,x1,y1]? (source frame; default = the layers' content ∩ selection),"quad":[[x,y]×4]? (where the frame's corners go, clockwise from top-left),"matrix":[a,b,c,d,e,f]? (affine alternative),"scaleX":%?,"scaleY":%?,"rotate":deg?,"dx":px?,"dy":px?,"reference":"center|topLeft|top|topRight|left|right|bottomLeft|bottom|bottomRight"|[x,y]="center" (relative alternative: scale and rotate about this point, then move),"interpolation":"bicubic|bilinear|nearest|bicubicSharper|bicubicSmoother|bicubicAutomatic"="bicubic" (Photoshop's bicubic variants render as Bicubic),"stepName":"Transform Path"? (History step name; the shape tools' bounding box sends it; default "Free Transform"; any other value is refused),"target":"pixels"|"mask"|"quickMask"|{"channel":i}=Channels panel target (an unlinked mask, an alpha channel or the Quick Mask transforms alone; a linked mask moves with its layer)}"##,
enabled: has_layer,
journal: true,
run: transform,
@@ -91,13 +91,17 @@ fn the_layers_param_names_the_layers_and_a_listed_group_carries_its_children() {
}
#[test]
fn the_background_stays_out_of_a_selection_of_several() {
fn the_background_among_a_selection_of_several_refuses_as_locked() {
// Photoshop shows its locked-layer alert rather than leaving the Background out (#11101).
let (mut s, paint, text) = two_layers();
let bg = s.active().unwrap().doc.layers[0].id;
s.execute("layer.select", json!({"layer": bg.0, "mode": "add"})).unwrap();
let r = s.execute("edit.transform", json!({"matrix": [1, 0, 0, 1, 3, 0]})).unwrap();
assert_eq!(r["layers"], json!([paint.0, text.0]));
let st = s.active().unwrap();
let (before, revision) = (st.doc.clone(), st.revision);
let err = s.execute("edit.transform", json!({"matrix": [1, 0, 0, 1, 3, 0]})).unwrap_err().to_string();
assert!(err.contains("\"Background\" is locked"), "{err}");
let st = s.active().unwrap();
assert!(st.doc == before && st.revision == revision, "nothing moved, not even {paint:?} or {text:?}");
assert!(st.doc.is_background(bg), "still the Background");
assert_eq!(st.doc.layer(bg).unwrap().surface().unwrap().content_bounds(), Rect::new(0, 0, 200, 200));
}
+10 -8
View File
@@ -118,6 +118,7 @@ use textures::{clamp_texture_side, preview_bounds, texture_factor};
use textures::{ensure_gpu, redraw_focus, visible_doc_rect};
pub use tool_input::{ToolEvent, selection_drag_kind, tool_event};
pub(crate) use tool_input::{command_guide_at, command_moves_layer, event_tool};
pub(crate) use transform_controls::ControlsPress;
pub(crate) use transform_controls::{LAYER_EDGES, layer_edges_rect};
use transform_controls::{begin_transform_controls_at, draw_transform_controls, transform_controls_cursor, transform_controls_rect, transform_controls_turn};
#[cfg(test)]
@@ -1036,12 +1037,13 @@ fn handle_canvas_tool_stroke(
if buttons.started
&& let Some(p) = ui.input(|i| i.pointer.press_origin()).filter(|p| rect.contains(*p)).or(response.interact_pointer_pos())
{
if tool == Tool::Move && app.ui.transform.is_none() {
begin_transform_controls_at(app, ctx, xf, p);
// A press the controls refused (the Background among the layers) goes nowhere else (#11101).
let refused = tool == Tool::Move && app.ui.transform.is_none() && begin_transform_controls_at(app, ctx, xf, p) == ControlsPress::Refused;
if !refused {
let d = xf.to_doc(p);
let press_mods = if capture_press { ctx.data(|d| d.get_temp(tool_press)).unwrap_or(mods) } else { mods };
tool_event(app, ToolEvent::Down { x: d[0], y: d[1], pressure: app.stylus.pressure() }, press_mods);
}
let d = xf.to_doc(p);
let press_mods = if capture_press { ctx.data(|d| d.get_temp(tool_press)).unwrap_or(mods) } else { mods };
tool_event(app, ToolEvent::Down { x: d[0], y: d[1], pressure: app.stylus.pressure() }, press_mods);
}
// A stroke started on the press takes its moves from the press on, not only once egui
// recognises a drag (past its click distance): otherwise the first few points are lost
@@ -1200,10 +1202,10 @@ fn handle_canvas_tool_clicks(
app.ui.status = e;
}
}
// A click the transform controls refused (the Background among the layers) goes
// nowhere else (#11101).
_ if tool == Tool::Move && app.ui.transform.is_none() && begin_transform_controls_at(app, ctx, xf, p) == ControlsPress::Refused => {}
_ => {
if tool == Tool::Move && app.ui.transform.is_none() {
begin_transform_controls_at(app, ctx, xf, p);
}
// If this tool was not handled on pointer-down, fall back to a
// click-sized gesture while preserving pen pressure (mouse stays at 1).
tool_event(app, ToolEvent::Down { x: d[0], y: d[1], pressure: app.stylus.pressure() }, click_mods);
+38 -14
View File
@@ -22,7 +22,9 @@ pub(crate) fn layer_edges_rect(app: &PhotocraftApp, idx: usize) -> Option<photoc
Some(b.translate(dx, dy))
}
/// The layers Move › Show Transform Controls frames: every selected layer but the Background.
/// The layers Move › Show Transform Controls frames: every selected layer, the Background too when
/// it is among several (a handle drag then refuses, [`begin_transform_controls_at`]), but not the
/// Background alone (#11101).
/// A corner radius drag hides the box, as in Photoshop. So does View › Extras (⌘H, #6699): the
/// controls are an Extra, hidden and inert until Extras comes back, while Show Transform Controls
/// keeps its setting.
@@ -37,21 +39,21 @@ pub(super) fn transform_controls_layers(app: &PhotocraftApp) -> Vec<&photocraft_
return Vec::new();
}
let Some(st) = app.session.active() else { return Vec::new() };
st.selected_layers().into_iter().filter_map(|id| st.doc.layer(id)).filter(|l| !crate::doc_props_ui::is_background(&st.doc, l)).collect()
let selected = st.selected_layers();
let several = selected.len() > 1;
selected.into_iter().filter_map(|id| st.doc.layer(id)).filter(|l| several || !crate::doc_props_ui::is_background(&st.doc, l)).collect()
}
impl PhotocraftApp {
/// Control-channel presses use document coordinates, but the canvas owns the screen-sized
/// handle and rotation hit areas. Start the same session before forwarding the press.
pub(crate) fn begin_move_transform_controls(&mut self, ctx: &egui::Context, x: f64, y: f64) {
pub(crate) fn begin_move_transform_controls(&mut self, ctx: &egui::Context, x: f64, y: f64) -> ControlsPress {
if !crate::menus::modal_allows(self, "edit.freeTransform") {
return;
return ControlsPress::Missed;
}
let Some(xf) = ViewXform::active(self) else { return };
let Some(xf) = ViewXform::active(self) else { return ControlsPress::Missed };
let p = xf.to_screen(x as f32, y as f32);
if p.is_finite() && xf.rect.contains(p) {
begin_transform_controls_at(self, ctx, &xf, p);
}
if p.is_finite() && xf.rect.contains(p) { begin_transform_controls_at(self, ctx, &xf, p) } else { ControlsPress::Missed }
}
}
@@ -119,18 +121,40 @@ pub(super) fn transform_controls_hit(r: Rect, p: Pos2) -> bool {
transform_controls_cursor(r, p).is_some()
}
/// The reference application's alert when a Move-tool transform control is dragged on a frame
/// that holds the Background among other layers (measured, #11101).
pub(crate) const CONTROLS_LOCKED: &str = "Could not transform because the layer is locked.";
/// What a press on the Move tool's transform controls did.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ControlsPress {
/// Not on a control, or no session could start: the press is the Move tool's.
Missed,
/// A Free Transform session started; the press goes on to drive it.
Started,
/// Refused with the locked-layer alert: the press goes nowhere else (#11101).
Refused,
}
/// Enter the existing Free Transform session when a Move-tool transform control is pressed: on
/// every selected layer, as the frame shows them (#6501).
pub(super) fn begin_transform_controls_at(app: &mut PhotocraftApp, ctx: &egui::Context, xf: &ViewXform, p: Pos2) -> bool {
let Some(r) = transform_controls_rect(app, xf) else { return false };
/// every selected layer, as the frame shows them (#6501). With the Background among them the press
/// shows the locked-layer alert and changes nothing (#11101).
pub(super) fn begin_transform_controls_at(app: &mut PhotocraftApp, ctx: &egui::Context, xf: &ViewXform, p: Pos2) -> ControlsPress {
let Some(r) = transform_controls_rect(app, xf) else { return ControlsPress::Missed };
if !transform_controls_hit(r, p) {
return false;
return ControlsPress::Missed;
}
let background = app.session.active().is_some_and(|st| photocraft_engine::transform_cmds::background_among(&st.doc, &st.selected_layers()).is_some());
if background {
let message = serde_json::json!({ "message": tl!(CONTROLS_LOCKED) });
app.ui.open_dialog(crate::state::DialogKind::Error, message.as_object().cloned().unwrap_or_default());
return ControlsPress::Refused;
}
match crate::transform_tool::begin(app, ctx) {
Ok(()) => true,
Ok(()) => ControlsPress::Started,
Err(e) => {
app.ui.status = e;
false
ControlsPress::Missed
}
}
}
@@ -146,13 +146,16 @@ fn several_selected_layers_share_one_frame_that_transforms_them_all() {
assert!(both.contains_rect(one) && both.width() > one.width() && both.height() > one.height(), "{both:?} frames both");
let (lt, rb) = (xf.to_doc(both.left_top()), xf.to_doc(both.right_bottom()));
assert!((lt[0] - 20.0).abs() < 1e-3 && (lt[1] - 30.0).abs() < 1e-3 && (rb[0] - 150.0).abs() < 1e-3 && (rb[1] - 150.0).abs() < 1e-3, "{lt:?} {rb:?}");
// The Background never joins the frame.
// The Background among them joins the frame, as in Photoshop; its handles refuse (#11101).
let bg = app.session.active().unwrap().doc.layers[0].id;
app.session.execute("layer.select", json!({"layer": bg.0, "mode": "add"})).unwrap();
assert_eq!(transform_controls_layers(&app).len(), 3);
assert_eq!(transform_controls_rect(&app, &xf), Some(Rect::from_min_size(Pos2::ZERO, vec2(200.0, 200.0))));
app.session.execute("layer.select", json!({"layer": bg.0, "mode": "toggle"})).unwrap();
assert_eq!(transform_controls_layers(&app).len(), 2);
assert_eq!(transform_controls_rect(&app, &xf), Some(both));
let ctx = egui::Context::default();
assert!(begin_transform_controls_at(&mut app, &ctx, &xf, both.right_bottom()));
assert_eq!(begin_transform_controls_at(&mut app, &ctx, &xf, both.right_bottom()), ControlsPress::Started);
let t = app.ui.transform.as_ref().expect("the shared frame's handle starts Free Transform");
assert_eq!(t.rect, [20.0, 30.0, 150.0, 150.0]);
assert_eq!(t.layers, [a.0, b.0]);
@@ -173,7 +176,7 @@ fn move_transform_controls_start_free_transform_for_vector_shapes() {
assert!(transform_controls_hit(r, r.right_bottom()));
let ctx = egui::Context::default();
assert!(begin_transform_controls_at(&mut app, &ctx, &xf, r.right_bottom()));
assert_eq!(begin_transform_controls_at(&mut app, &ctx, &xf, r.right_bottom()), ControlsPress::Started);
let t = app.ui.transform.as_ref().expect("control press starts Free Transform");
assert_eq!(t.rect, [20.0, 30.0, 100.0, 70.0]);
}
@@ -210,7 +213,7 @@ fn hiding_extras_hides_the_move_transform_controls() {
assert!(transform_controls_rect(&app, &xf).is_none(), "no box with Extras hidden");
assert_eq!(painted(&mut app), 0, "nothing is drawn");
// Hidden handles don't act: a press on a corner doesn't start Free Transform.
assert!(!begin_transform_controls_at(&mut app, &ctx, &xf, shown.right_bottom()));
assert_eq!(begin_transform_controls_at(&mut app, &ctx, &xf, shown.right_bottom()), ControlsPress::Missed);
assert!(app.ui.transform.is_none());
assert!(app.ui.tool_options.move_show_transform, "Show Transform Controls is unchanged");
assert_eq!(app.session.active().unwrap().doc.layers, layer, "the layer is unchanged");
@@ -235,7 +238,7 @@ fn move_transform_controls_follow_floating_selected_pixels() {
let r = transform_controls_rect(&app, &xf).unwrap();
assert_eq!(r, Rect::from_min_max(pos2(200.0, 150.0), pos2(300.0, 210.0)));
let ctx = egui::Context::default();
assert!(begin_transform_controls_at(&mut app, &ctx, &xf, r.right_bottom()));
assert_eq!(begin_transform_controls_at(&mut app, &ctx, &xf, r.right_bottom()), ControlsPress::Started);
assert_eq!(app.ui.transform.as_ref().unwrap().rect, [200.0, 150.0, 300.0, 210.0]);
}
@@ -256,7 +259,7 @@ fn move_transform_controls_frame_the_selection() {
let r = transform_controls_rect(&app, &xf).unwrap();
assert_eq!(r, Rect::from_min_max(pos2(60.0, 80.0), pos2(160.0, 200.0)));
let ctx = egui::Context::default();
assert!(begin_transform_controls_at(&mut app, &ctx, &xf, r.right_bottom()));
assert_eq!(begin_transform_controls_at(&mut app, &ctx, &xf, r.right_bottom()), ControlsPress::Started);
assert_eq!(app.ui.transform.as_ref().unwrap().rect, [60.0, 80.0, 160.0, 200.0]);
}
@@ -296,7 +299,7 @@ fn free_transform_on_a_locked_layer_is_refused_up_front() {
let xf =
ViewXform { rect: Rect::from_min_size(Pos2::ZERO, vec2(200.0, 200.0)), zoom: 1.0, center: [100.0, 100.0], flip: false, rotation: 0.0, aspect: 1.0 };
let r = transform_controls_rect(&app, &xf).unwrap();
assert!(!begin_transform_controls_at(&mut app, &ctx, &xf, r.right_bottom()));
assert_eq!(begin_transform_controls_at(&mut app, &ctx, &xf, r.right_bottom()), ControlsPress::Missed);
assert!(app.ui.transform.is_none());
assert_eq!(errors(&app), [crate::transform_tool::LOCKED]);
}
@@ -357,3 +360,42 @@ fn transform_control_hit_keeps_move_interior_free_and_has_rotation_band() {
assert!(!transform_controls_hit(r, r.center()));
assert!(!transform_controls_hit(r, pos2(60.0, 4.0)));
}
#[test]
fn the_background_among_several_joins_the_frame_and_its_handles_refuse() {
// Measured in the reference application (#11101): the box frames them all, the Background
// included, and a handle drag shows its locked alert and changes nothing.
let mut app = PhotocraftApp::new(photocraft_engine::Session::new(), crate::Services::default());
app.session.execute("file.new", json!({"width": 200, "height": 200})).unwrap();
app.session.execute("shape.create", json!({"kind": "rect", "rect": [20, 30, 40, 40], "fill": "#ff0000"})).unwrap();
let a = app.session.active().unwrap().active_layer.unwrap();
app.session.execute("shape.create", json!({"kind": "rect", "rect": [100, 120, 50, 30], "fill": "#ff0000"})).unwrap();
let b = app.session.active().unwrap().active_layer.unwrap();
let bg = app.session.active().unwrap().doc.layers[0].id;
app.session.execute("layer.select", json!({"layer": bg.0, "mode": "add"})).unwrap();
app.sync_views();
app.ui.tool = Tool::Move;
app.ui.tool_options.move_show_transform = true;
let xf =
ViewXform { rect: Rect::from_min_size(Pos2::ZERO, vec2(200.0, 200.0)), zoom: 1.0, center: [100.0, 100.0], flip: false, rotation: 0.0, aspect: 1.0 };
let r = transform_controls_rect(&app, &xf).expect("the box frames them all");
assert_eq!(r, Rect::from_min_size(Pos2::ZERO, vec2(200.0, 200.0)), "the Background included");
let before = app.session.active().unwrap().doc.clone();
let ctx = egui::Context::default();
assert_eq!(begin_transform_controls_at(&mut app, &ctx, &xf, r.right_bottom()), ControlsPress::Refused, "the press is taken, not a move");
assert!(app.ui.transform.is_none(), "no box opens");
let alerts: Vec<_> =
app.ui.dialogs.iter().filter(|d| d.kind == crate::state::DialogKind::Error).filter_map(|d| d.fields.get("message")?.as_str()).collect();
assert_eq!(alerts, [super::transform_controls::CONTROLS_LOCKED]);
assert!(app.session.active().unwrap().doc == before, "nothing changed");
// The Background alone: no box at all.
app.session.execute("layer.select", json!({"layer": bg.0})).unwrap();
assert_eq!(transform_controls_rect(&app, &xf), None);
// Without the Background the handles start Free Transform, as before.
app.ui.dialogs.clear();
app.session.execute("layer.select", json!({"layer": a.0})).unwrap();
app.session.execute("layer.select", json!({"layer": b.0, "mode": "add"})).unwrap();
let r = transform_controls_rect(&app, &xf).unwrap();
assert_eq!(begin_transform_controls_at(&mut app, &ctx, &xf, r.right_bottom()), ControlsPress::Started);
assert_eq!(app.ui.transform.as_ref().unwrap().layers, [a.0, b.0]);
}
+4 -1
View File
@@ -1272,7 +1272,10 @@ fn pointer(app: &mut PhotocraftApp, ctx: &egui::Context, p: &Value) -> Outcome {
continue;
}
if app.ui.tool == Tool::Move && !gesture.secondary && !gesture.middle && !gesture.space && matches!(sample.ev, ToolEvent::Down { .. }) {
app.begin_move_transform_controls(ctx, sample.x, sample.y);
// A press the controls refused (the Background among the layers) goes nowhere else (#11101).
if app.begin_move_transform_controls(ctx, sample.x, sample.y) == crate::canvas::ControlsPress::Refused {
continue;
}
}
forward_pointer_event(app, &gesture, sample);
}
+1
View File
@@ -1787,6 +1787,7 @@
Move Tool أداة التحريك
Could not use the move tool because the layer is locked. تعذر استخدام أداة التحريك لأن الطبقة مقفلة.
Could not complete the Free Transform command because the layer is locked. تعذر إكمال أمر التحويل الحر لأن الطبقة مقفلة.
Could not transform because the layer is locked. تعذر التحويل لأن الطبقة مقفلة.
Multiply ضرب
My Brushes الفُرش الخاصة بي
Neutrals الدرجات المحايدة
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1
View File
@@ -1711,6 +1711,7 @@
Move Tool Alat za pomjeranje
Could not use the move tool because the layer is locked. Nije moguće koristiti alat za pomjeranje jer je sloj zaključan.
Could not complete the Free Transform command because the layer is locked. Naredbu Slobodna transformacija nije moguće dovršiti jer je sloj zaključan.
Could not transform because the layer is locked. Transformacija nije moguća jer je sloj zaključan.
Multiply Množenje
My Brushes Moje četkice
Neutrals Neutralni tonovi
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1
View File
@@ -1901,6 +1901,7 @@
Move Tool Nástroj přesun
Could not use the move tool because the layer is locked. Nástroj Přesun nelze použít, protože vrstva je zamčená.
Could not complete the Free Transform command because the layer is locked. Příkaz Volná transformace nelze dokončit, protože vrstva je zamčená.
Could not transform because the layer is locked. Transformaci nelze provést, protože vrstva je zamčená.
Multiply Násobit
My Brushes Moje štětce
Neutrals Neutrální
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1
View File
@@ -1638,6 +1638,7 @@
Move Tool Flytteværktøj
Could not use the move tool because the layer is locked. Kunne ikke bruge flytteværktøjet, fordi laget er låst.
Could not complete the Free Transform command because the layer is locked. Kunne ikke fuldføre kommandoen Fri transformering, fordi laget er låst.
Could not transform because the layer is locked. Kunne ikke transformere, fordi laget er låst.
Rectangular Marquee Tool Rektangulært markeringsværktøj
Elliptical Marquee Tool Elliptisk markeringsværktøj
Single Row Marquee Tool Markeringsværktøj til enkelt række
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1
View File
@@ -1862,6 +1862,7 @@
Move Tool Verschieben-Werkzeug
Could not use the move tool because the layer is locked. Das Verschieben-Werkzeug konnte nicht verwendet werden, weil die Ebene gesperrt ist.
Could not complete the Free Transform command because the layer is locked. Der Befehl „Frei transformieren“ konnte nicht ausgeführt werden, weil die Ebene gesperrt ist.
Could not transform because the layer is locked. Transformieren nicht möglich, weil die Ebene gesperrt ist.
Multiply Multiplizieren
My Brushes Meine Pinsel
Neutrals Neutrale
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1
View File
@@ -3481,6 +3481,7 @@ cameraRaw Yellow Κίτρινο
Move Tool Μετακίνηση (Move Tool)
Could not use the move tool because the layer is locked. Δεν ήταν δυνατή η χρήση του εργαλείου Μετακίνηση, επειδή το επίπεδο είναι κλειδωμένο.
Could not complete the Free Transform command because the layer is locked. Δεν ήταν δυνατή η ολοκλήρωση της εντολής Ελεύθερος μετασχηματισμός, επειδή το επίπεδο είναι κλειδωμένο.
Could not transform because the layer is locked. Δεν ήταν δυνατός ο μετασχηματισμός, επειδή το επίπεδο είναι κλειδωμένο.
Note Tool Σημείωση (Note Tool)
Object Selection Tool Επιλογή αντικειμένου (Object Selection Tool)
Paint Bucket Tool Κουβάς γεμίσματος (Paint Bucket Tool)
Can't render this file because it contains an unexpected character in line 47 and column 53.
+1
View File
@@ -1832,6 +1832,7 @@
Move Tool Herramienta Mover
Could not use the move tool because the layer is locked. No se pudo usar la herramienta Mover porque la capa está bloqueada.
Could not complete the Free Transform command because the layer is locked. No se pudo completar el comando Transformación libre porque la capa está bloqueada.
Could not transform because the layer is locked. No se pudo transformar porque la capa está bloqueada.
Multiply Multiplicar
My Brushes Mis pinceles
Neutrals Neutros
Can't render this file because it contains an unexpected character in line 4 and column 47.
+1
View File
@@ -1639,6 +1639,7 @@
Move Tool Siirtotyökalu
Could not use the move tool because the layer is locked. Siirtotyökalua ei voitu käyttää, koska taso on lukittu.
Could not complete the Free Transform command because the layer is locked. Vapaa muunnos -komentoa ei voitu suorittaa, koska taso on lukittu.
Could not transform because the layer is locked. Muuntaminen ei onnistunut, koska taso on lukittu.
Rectangular Marquee Tool Suorakulmainen valintakehys
Elliptical Marquee Tool Elliptinen valintakehys
Single Row Marquee Tool Yhden rivin valintakehys
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1
View File
@@ -1840,6 +1840,7 @@
Move Tool Outil Déplacement
Could not use the move tool because the layer is locked. Impossible d'utiliser l'outil Déplacement, car le calque est verrouillé.
Could not complete the Free Transform command because the layer is locked. Impossible d'exécuter la commande Transformation libre, car le calque est verrouillé.
Could not transform because the layer is locked. Impossible de transformer, car le calque est verrouillé.
Multiply Produit
My Brushes Mes pinceaux
Neutrals Neutres
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1
View File
@@ -1073,6 +1073,7 @@
Control: Vezérlés:
Corners Sarkok
Could not complete the Free Transform command because the layer is locked. A szabad alakítás nem hajtható végre, mert a réteg zárolva van.
Could not transform because the layer is locked. Az alakítás nem hajtható végre, mert a réteg zárolva van.
Could not transform: the layer is empty Az alakítás nem hajtható végre: a réteg üres
Could not use Puppet Warp: no opaque pixels A bábtorzítás nem használható: nincsenek átlátszatlan pixelek
Could not use Puppet Warp: the layer is empty A bábtorzítás nem használható: a réteg üres
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1
View File
@@ -1851,6 +1851,7 @@
Move Tool Alat Pindah
Could not use the move tool because the layer is locked. Tidak dapat menggunakan alat Pindahkan karena layer terkunci.
Could not complete the Free Transform command because the layer is locked. Tidak dapat menyelesaikan perintah Transformasi Bebas karena layer terkunci.
Could not transform because the layer is locked. Tidak dapat mentransformasi karena layer terkunci.
Multiply Multiply
My Brushes Kuas Saya
Neutrals Netral
Can't render this file because it contains an unexpected character in line 4 and column 47.
+1
View File
@@ -1877,6 +1877,7 @@
Move Tool Strumento Sposta
Could not use the move tool because the layer is locked. Impossibile usare lo strumento Sposta perché il livello è bloccato.
Could not complete the Free Transform command because the layer is locked. Impossibile completare il comando Trasformazione libera perché il livello è bloccato.
Could not transform because the layer is locked. Impossibile trasformare perché il livello è bloccato.
Multiply Moltiplica
My Brushes I miei pennelli
Neutrals Neutri
Can't render this file because it contains an unexpected character in line 79 and column 53.
+1
View File
@@ -1888,6 +1888,7 @@
Move Tool 移動ツール
Could not use the move tool because the layer is locked. レイヤーがロックされているため、移動ツールを使用できません。
Could not complete the Free Transform command because the layer is locked. レイヤーがロックされているため、自由変形コマンドを完了できません。
Could not transform because the layer is locked. レイヤーがロックされているため、変形できません。
Multiply 乗算
My Brushes マイブラシ
Neutrals 中間色系
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1
View File
@@ -1863,6 +1863,7 @@
Move Tool 이동 도구
Could not use the move tool because the layer is locked. 레이어가 잠겨 있어 이동 도구를 사용할 수 없습니다.
Could not complete the Free Transform command because the layer is locked. 레이어가 잠겨 있어 자유 변형 명령을 완료할 수 없습니다.
Could not transform because the layer is locked. 레이어가 잠겨 있어 변형할 수 없습니다.
Multiply 곱하기
My Brushes 내 브러시
Neutrals 중립색
Can't render this file because it contains an unexpected character in line 63 and column 53.
+1
View File
@@ -1638,6 +1638,7 @@
Move Tool Flytteverktøy
Could not use the move tool because the layer is locked. Kunne ikke bruke flytteverktøyet fordi laget er låst.
Could not complete the Free Transform command because the layer is locked. Kunne ikke fullføre Fri transformering fordi laget er låst.
Could not transform because the layer is locked. Kunne ikke transformere fordi laget er låst.
Rectangular Marquee Tool Rektangulær markeringsramme
Elliptical Marquee Tool Elliptisk markeringsramme
Single Row Marquee Tool Markeringsramme for enkeltrad
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1
View File
@@ -1809,6 +1809,7 @@
Move Tool Verplaatsgereedschap
Could not use the move tool because the layer is locked. Kan het verplaatsgereedschap niet gebruiken omdat de laag is vergrendeld.
Could not complete the Free Transform command because the layer is locked. Kan de opdracht Vrije transformatie niet voltooien omdat de laag is vergrendeld.
Could not transform because the layer is locked. Kan niet transformeren omdat de laag is vergrendeld.
Multiply Vermenigvuldigen
My Brushes Mijn penselen
Neutrals Neutrale tinten
Can't render this file because it contains an unexpected character in line 8 and column 34.
+1
View File
@@ -1874,6 +1874,7 @@
Move Tool Narzędzie Przesunięcie
Could not use the move tool because the layer is locked. Nie można użyć narzędzia Przesunięcie, ponieważ warstwa jest zablokowana.
Could not complete the Free Transform command because the layer is locked. Nie można ukończyć polecenia Dowolne przekształcanie, ponieważ warstwa jest zablokowana.
Could not transform because the layer is locked. Nie można przekształcić, ponieważ warstwa jest zablokowana.
Multiply Mnożenie
My Brushes Moje pędzle
Neutrals Neutralne
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1
View File
@@ -1834,6 +1834,7 @@
Move Tool Ferramenta Mover
Could not use the move tool because the layer is locked. Não foi possível usar a ferramenta Mover porque a camada está bloqueada.
Could not complete the Free Transform command because the layer is locked. Não foi possível concluir o comando Transformação livre porque a camada está bloqueada.
Could not transform because the layer is locked. Não foi possível transformar porque a camada está bloqueada.
Multiply Multiplicação
My Brushes Meus pincéis
Neutrals Neutros
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1
View File
@@ -1749,6 +1749,7 @@
Move Tool Перемещение
Could not use the move tool because the layer is locked. Не удалось использовать инструмент «Перемещение», потому что слой заблокирован.
Could not complete the Free Transform command because the layer is locked. Не удалось выполнить команду «Свободное трансформирование», потому что слой заблокирован.
Could not transform because the layer is locked. Не удалось выполнить трансформирование, потому что слой заблокирован.
Rectangular Marquee Tool Прямоугольная область
Elliptical Marquee Tool Овальная область
Single Row Marquee Tool Область (горизонтальная строка)
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1
View File
@@ -1847,6 +1847,7 @@
Move Tool Alatka za pomeranje
Could not use the move tool because the layer is locked. Alatka za pomeranje ne može da se koristi jer je sloj zaključan.
Could not complete the Free Transform command because the layer is locked. Komanda Slobodna transformacija ne može da se izvrši jer je sloj zaključan.
Could not transform because the layer is locked. Transformacija nije moguća jer je sloj zaključan.
Multiply Množenje
My Brushes Moje četkice
Neutrals Neutralne
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1
View File
@@ -1636,6 +1636,7 @@
Move Tool Flyttverktyg
Could not use the move tool because the layer is locked. Det gick inte att använda flyttverktyget eftersom lagret är låst.
Could not complete the Free Transform command because the layer is locked. Det gick inte att slutföra kommandot Ombilda fritt eftersom lagret är låst.
Could not transform because the layer is locked. Det gick inte att omforma eftersom lagret är låst.
Rectangular Marquee Tool Verktyget Rektangulär markeringsram
Elliptical Marquee Tool Verktyget Elliptisk markeringsram
Single Row Marquee Tool Verktyget Enradsmarkeringsram
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1
View File
@@ -1762,6 +1762,7 @@
Move Tool Taşıma Aracı
Could not use the move tool because the layer is locked. Katman kilitli olduğundan taşıma aracı kullanılamadı.
Could not complete the Free Transform command because the layer is locked. Katman kilitli olduğundan Serbest Dönüştürme komutu tamamlanamadı.
Could not transform because the layer is locked. Katman kilitli olduğundan dönüştürme yapılamadı.
Multiply Çoğalt
My Brushes Fırçalarım
Neutrals Nötrler
Can't render this file because it contains an unexpected character in line 8 and column 34.
+1
View File
@@ -1719,6 +1719,7 @@
Move Tool Інструмент «Переміщення»
Could not use the move tool because the layer is locked. Не вдалося використати інструмент «Переміщення», бо шар заблоковано.
Could not complete the Free Transform command because the layer is locked. Не вдалося виконати команду «Вільна трансформація», бо шар заблоковано.
Could not transform because the layer is locked. Не вдалося трансформувати, бо шар заблоковано.
Rectangular Marquee Tool Інструмент «Прямокутне виділення»
Elliptical Marquee Tool Інструмент «Еліптичне виділення»
Single Row Marquee Tool Інструмент «Виділення одного рядка»
Can't render this file because it contains an unexpected character in line 152 and column 53.
+1
View File
@@ -2718,6 +2718,7 @@ shape Appearance Diện mạo
Measurement Scale Marker Dấu tỉ lệ đo
Could not use the move tool because the layer is locked. Không thể dùng công cụ di chuyển vì lớp đã bị khoá.
Could not complete the Free Transform command because the layer is locked. Không thể hoàn tất lệnh Biến đổi tự do vì lớp đã bị khoá.
Could not transform because the layer is locked. Không thể biến đổi vì lớp đã bị khoá.
My Brushes Cọ của tôi
Neutrals Vùng trung tính
New Workspace Không gian làm việc mới
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1
View File
@@ -1912,6 +1912,7 @@
Move Tool 移动工具
Could not use the move tool because the layer is locked. 无法使用移动工具,因为图层已锁定。
Could not complete the Free Transform command because the layer is locked. 无法完成“自由变换”命令,因为图层已锁定。
Could not transform because the layer is locked. 无法变换,因为图层已锁定。
Multiply 正片叠底
My Brushes 我的画笔
Neutrals 中性色
Can't render this file because it contains an unexpected character in line 63 and column 53.
+1
View File
@@ -1882,6 +1882,7 @@
Move Tool 移動工具
Could not use the move tool because the layer is locked. 無法使用移動工具,因為圖層已鎖定。
Could not complete the Free Transform command because the layer is locked. 無法完成「任意變形」指令,因為圖層已鎖定。
Could not transform because the layer is locked. 無法變形,因為圖層已鎖定。
Multiply 色彩增值
My Brushes 我的筆刷
Neutrals 中間色
Can't render this file because it contains an unexpected character in line 6 and column 47.
+3
View File
@@ -848,6 +848,9 @@ mod transform_reference_point_tests;
#[cfg(test)]
mod transform_several_tests;
#[cfg(test)]
mod transform_selection_rules_tests;
#[cfg(test)]
mod psd_open_tests;
#[cfg(test)]
+3 -2
View File
@@ -127,8 +127,9 @@ pub fn disabled(app: &crate::PhotocraftApp, id: &str) -> bool {
if transform && (target.is_object() || target == "quickMask") {
return false;
}
// Several layers selected: one box moves them all, the Background left out (#6501). It doesn't
// warp, and doesn't distort while editable type is among them.
// Several layers selected: one box moves them all (#6501); with the Background among them it
// refuses as locked when it opens (#11101). It doesn't warp, and doesn't distort while
// editable type is among them.
if transform {
let targets = photocraft_engine::transform_cmds::selection_targets(st);
// Judged on the layers the box moves, linked ones included (#11217): one linked layer
+2 -2
View File
@@ -73,8 +73,8 @@ pub struct TransformSession {
pub skew_v: f64,
/// Free Transform of several selected layers (#6501): every layer the box moves, in
/// `edit.transform`'s `layers` order. Empty when it moves `layer` alone. `layer` stays the
/// active layer, which need not be one of them (the Background is left out, a selected group
/// carries its selected children).
/// active layer, which need not be one of them (a selected group carries its selected
/// children).
#[serde(default)]
pub layers: Vec<u64>,
}
@@ -228,6 +228,9 @@ struct Follow {
revision: u64,
/// The moving layers hidden in `doc` (nothing was lifted out of them).
hidden: Vec<LayerId>,
/// The Layers panel's selection when the box opened, sorted; `None` when the box doesn't
/// follow it. Any change to it applies the box (`end_if_left`, #11101).
selected: Option<Vec<LayerId>>,
}
/// Photoshop's alert when Free Transform (⌘T, Edit › Transform, the Move tool's transform controls)
@@ -85,7 +85,7 @@ pub fn begin(app: &mut PhotocraftApp, ctx: &egui::Context) -> Result<(), String>
opacity: layer.opacity * layer.fill_opacity,
above: (!frame_only).then_some(above),
styled: (styled && !frame_only).then(|| crate::transform_fx::StyledPreview::new(format!("transform-{session}-styled"))),
follow: Some(Follow { revision, hidden: if lifted.is_none() && !frame_only { vec![id] } else { Vec::new() } }),
follow: Some(Follow { revision, hidden: if lifted.is_none() && !frame_only { vec![id] } else { Vec::new() }, selected: selected_now(app) }),
committing: None,
gesture: None,
warp_drag: None,
@@ -136,7 +136,8 @@ pub(super) fn interpolation_preference(interpolation: &str) -> Option<photocraft
/// layer move when there is a selection; type, shapes, smart objects and groups move whole.
fn begin_several(app: &mut PhotocraftApp, ctx: &egui::Context, doc: Arc<Document>, revision: u64, active: LayerId, ids: Vec<LayerId>) -> Result<(), String> {
use photocraft_engine::transform_cmds as tc;
if ids.iter().any(|id| tc::locked_for_transform(&doc, *id)) {
// The Background among them refuses the same way, rather than being left out (#11101).
if tc::background_among(&doc, &ids).is_some() || ids.iter().any(|id| tc::locked_for_transform(&doc, *id)) {
app.ui.open_dialog(crate::state::DialogKind::Error, json!({"message": tl!(LOCKED)}).as_object().cloned().unwrap_or_default());
return Err(LOCKED.into());
}
@@ -192,7 +193,7 @@ fn begin_several(app: &mut PhotocraftApp, ctx: &egui::Context, doc: Arc<Document
opacity: 1.0,
above: Some(above),
styled: None,
follow: Some(Follow { revision, hidden }),
follow: Some(Follow { revision, hidden, selected: selected_now(app) }),
committing: None,
gesture: None,
warp_drag: None,
@@ -240,6 +241,13 @@ pub(crate) fn moving_texel(app: &PhotocraftApp, p: [i32; 2]) -> (Option<Color32>
(texel, texture_quad(t, pv))
}
/// The Layers panel's selection, sorted, as a layer's box records it when it opens (#11101).
pub(super) fn selected_now(app: &PhotocraftApp) -> Option<Vec<LayerId>> {
let mut ids = app.session.active()?.selected_layers();
ids.sort_unstable();
Some(ids)
}
/// The layers the box moves: every layer of a several-layer session (#6501), else its one layer.
pub(crate) fn moving_layers(t: &TransformSession) -> Vec<LayerId> {
if t.layers.is_empty() { vec![LayerId(t.layer)] } else { t.layers.iter().copied().map(LayerId).collect() }
@@ -343,7 +351,7 @@ pub(crate) fn begin_content_aware(
texture,
opacity: layer.opacity * layer.fill_opacity,
above: Some(above),
follow: Some(Follow { revision, hidden: Vec::new() }),
follow: Some(Follow { revision, hidden: Vec::new(), selected: None }),
committing: None,
gesture: None,
warp_drag: None,
@@ -205,11 +205,30 @@ pub fn end_if_left(app: &mut PhotocraftApp) {
}
}
/// Another tool picked, or (outside Transform Selection) another layer selected, since the box
/// opened: the session ends ([`end_if_left`], #1400).
/// Another tool picked, or (outside Transform Selection) another layer selected or the selected
/// layers changed, since the box opened: the session ends ([`end_if_left`], #1400, #11101).
fn switched_away(app: &PhotocraftApp) -> bool {
let (Some(t), Some(st)) = (&app.ui.transform, app.session.active()) else { return false };
app.transform_preview.as_ref().is_some_and(|pv| pv.tool != app.ui.tool) || (!t.selection && st.active_layer.is_some_and(|active| active.0 != t.layer))
let opened = app.transform_preview.as_ref().and_then(|pv| pv.follow.as_ref()).and_then(|f| f.selected.as_deref());
app.transform_preview.as_ref().is_some_and(|pv| pv.tool != app.ui.tool)
|| (!t.selection && st.active_layer.is_some_and(|active| active.0 != t.layer))
|| selected_layers_changed(st, t, opened)
}
/// A layer's box no longer matches the Layers panel's selection it opened with (`opened`): a layer
/// was added to the selection or taken out of it, a group's child included. Photoshop applies the
/// box first, to the layers it moves, and doesn't open it again (#11101). The selection itself is
/// compared, not the layers the box moves: those also hold the layers linked to it (#11217) and
/// leave out the children of a selected group. Boxes over a selection, a mask or channel, a path,
/// a placed file or Content-Aware Scale don't follow the selection.
fn selected_layers_changed(st: &photocraft_engine::DocState, t: &TransformSession, opened: Option<&[LayerId]>) -> bool {
if t.selection || t.target.is_some() || t.path.is_some() || t.made == Some(MadeLayer::Place) || t.content_aware.is_some() {
return false;
}
let Some(opened) = opened else { return false };
let mut now = st.selected_layers();
now.sort_unstable();
now != opened
}
/// Edits made while the box is open show in the preview at once, not only after the commit
@@ -0,0 +1,303 @@
//! Free Transform's rules for the Layers panel's selection (#11101): the Background among several
//! selected layers refuses with the locked-layer alert, and any change to the selected layers
//! while the box is open applies the box first, as in Photoshop.
use photocraft_doc::LayerId;
use photocraft_geom::Rect;
use serde_json::json;
use crate::PhotocraftApp;
use crate::transform_tool::LOCKED;
/// A 200 × 200 document on a white Background: "A" with a red block at x 10..50, y 10..30, and
/// "B" with a blue one at x 100..140, y 100..120. Only B is selected.
fn app() -> (PhotocraftApp, LayerId, LayerId, LayerId) {
let mut app = PhotocraftApp::new(photocraft_engine::Session::new(), crate::Services::default());
app.run("file.new", json!({"width": 200, "height": 200, "background": "white"})).unwrap();
app.sync_views();
let bg = app.session.active().unwrap().doc.layers[0].id;
let mut block = |name: &str, rect: [i32; 4], color: &str| {
let id = LayerId(app.run("layer.new.layer", json!({"name": name})).unwrap()["layer"].as_u64().unwrap());
app.run("select.rect", json!({"x": rect[0], "y": rect[1], "width": rect[2], "height": rect[3]})).unwrap();
app.run("edit.fill", json!({"contents": "color", "color": color})).unwrap();
app.run("select.deselect", json!({})).unwrap();
id
};
let a = block("A", [10, 10, 40, 20], "#ff0000");
let b = block("B", [100, 100, 40, 20], "#0000ff");
(app, bg, a, b)
}
fn bounds(app: &PhotocraftApp, id: LayerId) -> Rect {
let st = app.session.active().unwrap();
photocraft_engine::transform_cmds::transform_bounds(&st.doc, st.doc.layer(id).unwrap())
}
fn select(app: &mut PhotocraftApp, id: LayerId, mode: &str) {
app.run("layer.select", json!({"layer": id.0, "mode": mode})).unwrap();
}
fn invoke(app: &mut PhotocraftApp, id: &str) -> Result<(), String> {
crate::menus::invoke(app, &egui::Context::default(), id, json!({})).map(|_| ())
}
fn alerts(app: &PhotocraftApp) -> Vec<String> {
app.ui.dialogs.iter().filter(|d| d.kind == crate::state::DialogKind::Error).filter_map(|d| d.fields.get("message")?.as_str().map(String::from)).collect()
}
/// Drag the open box `dx` px right, with nearest neighbour so the blocks keep their edges.
fn nudge(app: &mut PhotocraftApp, dx: f64) {
let t = app.ui.transform.as_mut().expect("a box is open");
t.interpolation = "nearest".into();
t.quad = t.quad.map(|[x, y]| [x + dx, y]);
}
#[test]
fn the_background_among_several_layers_shows_the_locked_alert_and_opens_no_box() {
for id in ["edit.freeTransform", "edit.transform.scale", "edit.transform.rotate"] {
let (mut app, bg, a, _) = app();
select(&mut app, a, "replace");
select(&mut app, bg, "add");
let before = app.session.active().unwrap().doc.clone();
assert_eq!(invoke(&mut app, id), Err(LOCKED.into()), "{id}");
assert!(app.ui.transform.is_none(), "{id}: no box opens");
assert_eq!(alerts(&app), [LOCKED], "{id}: the same alert as a position-locked layer");
assert!(app.session.active().unwrap().doc == before, "{id}: nothing moved");
}
}
#[test]
fn the_background_is_refused_whichever_of_them_is_active() {
let (mut app, bg, a, _) = app();
select(&mut app, a, "replace");
select(&mut app, bg, "add");
select(&mut app, a, "add");
assert_eq!(app.session.active().unwrap().active_layer, Some(a));
assert_eq!(invoke(&mut app, "edit.freeTransform"), Err(LOCKED.into()));
assert!(app.ui.transform.is_none());
}
#[test]
fn the_engines_default_targets_refuse_the_background_but_an_explicit_list_is_kept() {
let (mut app, bg, a, b) = app();
select(&mut app, a, "replace");
select(&mut app, bg, "add");
let err = app.run("edit.transform", json!({"matrix": [1, 0, 0, 1, 5, 0]})).unwrap_err();
assert!(err.contains("locked"), "{err}");
// A recorded or scripted step naming its layers moves those, as before.
app.run("edit.transform", json!({"layers": [a.0, b.0], "matrix": [1, 0, 0, 1, 5, 0]})).unwrap();
assert_eq!(bounds(&app, a), Rect::new(15, 10, 55, 30));
assert_eq!(bounds(&app, b), Rect::new(105, 100, 145, 120));
}
#[test]
fn several_layers_without_the_background_still_open_one_box() {
let (mut app, _, a, b) = app();
select(&mut app, a, "add");
invoke(&mut app, "edit.freeTransform").unwrap();
let t = app.ui.transform.as_ref().unwrap();
assert_eq!(t.layers, [a.0, b.0]);
assert!(alerts(&app).is_empty());
}
#[test]
fn taking_a_layer_out_of_the_selection_applies_the_box_to_both_first() {
// The reporter's case: the box over A and B (B active), then ⌘-click A in the Layers panel.
let (mut app, _, a, b) = app();
select(&mut app, a, "replace");
select(&mut app, b, "add");
assert_eq!(app.session.active().unwrap().active_layer, Some(b));
invoke(&mut app, "edit.freeTransform").unwrap();
let steps = app.session.active().unwrap().history.past_len();
nudge(&mut app, 10.0);
select(&mut app, a, "toggle");
assert_eq!(app.session.active().unwrap().active_layer, Some(b), "the active layer didn't change");
crate::transform_tool::end_if_left(&mut app);
assert!(app.ui.transform.is_none(), "the box is applied and not opened again");
assert_eq!(bounds(&app, a), Rect::new(20, 10, 60, 30), "A moved with the box");
assert_eq!(bounds(&app, b), Rect::new(110, 100, 150, 120));
let st = app.session.active().unwrap();
assert_eq!(st.history.past_len(), steps + 1, "one Free Transform step");
assert_eq!(st.selected_layers(), [b], "then A leaves the selection");
// Nudging B now moves B alone: the old box is gone.
invoke(&mut app, "edit.freeTransform").unwrap();
assert!(app.ui.transform.as_ref().unwrap().layers.is_empty(), "a box over B alone");
}
#[test]
fn adding_a_layer_to_the_selection_applies_the_box_first() {
let (mut app, _, a, b) = app();
invoke(&mut app, "edit.freeTransform").unwrap();
nudge(&mut app, 10.0);
// ⌘-click A, then make B active again: the set changed though the active layer is B.
select(&mut app, a, "add");
select(&mut app, b, "add");
assert_eq!(app.session.active().unwrap().active_layer, Some(b));
crate::transform_tool::end_if_left(&mut app);
assert!(app.ui.transform.is_none());
assert_eq!(bounds(&app, b), Rect::new(110, 100, 150, 120), "B moved");
assert_eq!(bounds(&app, a), Rect::new(10, 10, 50, 30), "A wasn't in the box");
assert_eq!(app.session.active().unwrap().selected_layers(), [a, b]);
}
#[test]
fn an_unchanged_selection_keeps_the_box_open() {
let (mut app, _, a, b) = app();
select(&mut app, a, "add");
invoke(&mut app, "edit.freeTransform").unwrap();
nudge(&mut app, 10.0);
// Selecting what is selected already, or editing a moving layer, isn't a change.
select(&mut app, a, "add");
app.run("layer.setProps", json!({"layer": b.0, "opacity": 0.5})).unwrap();
crate::transform_tool::follow_edits(&mut app);
crate::transform_tool::end_if_left(&mut app);
assert!(app.ui.transform.is_some(), "the box stays open");
}
#[test]
fn the_background_alone_keeps_todays_box() {
let (mut app, bg, _, _) = app();
select(&mut app, bg, "replace");
let r = invoke(&mut app, "edit.freeTransform");
assert_ne!(r, Err(LOCKED.into()), "not the several-layers alert");
assert!(alerts(&app).is_empty());
}
#[test]
fn a_box_over_a_linked_layer_stays_open_while_the_selection_stays() {
// The box over A also moves B, linked to it (#11217); the selection is still A alone, so
// nothing changed and the box stays open.
let (mut app, _, a, b) = app();
select(&mut app, a, "add");
app.run("layer.linkLayers", json!({})).unwrap();
select(&mut app, a, "replace");
invoke(&mut app, "edit.freeTransform").unwrap();
assert_eq!(app.ui.transform.as_ref().unwrap().layers, [a.0, b.0], "the box moves the link group");
nudge(&mut app, 10.0);
crate::transform_tool::end_if_left(&mut app);
assert!(app.ui.transform.is_some(), "the box stays open");
// Adding B to the selection is a change, though the box already moves B.
select(&mut app, b, "add");
select(&mut app, a, "add");
crate::transform_tool::end_if_left(&mut app);
assert!(app.ui.transform.is_none(), "the box is applied");
assert_eq!(bounds(&app, a), Rect::new(20, 10, 60, 30));
assert_eq!(bounds(&app, b), Rect::new(110, 100, 150, 120));
}
#[test]
fn taking_a_child_of_a_selected_group_out_of_the_selection_applies_the_box() {
// The box over group G moves its child A whether A is selected or not; ⌘-clicking A out of
// the selection still changes the selection, so the box is applied first.
let (mut app, _, a, b) = app();
select(&mut app, a, "replace");
let g = LayerId(app.run("layer.new.groupFromLayers", json!({"name": "G"})).unwrap()["layer"].as_u64().unwrap());
select(&mut app, g, "replace");
select(&mut app, a, "add");
select(&mut app, b, "add");
select(&mut app, g, "add");
invoke(&mut app, "edit.freeTransform").unwrap();
nudge(&mut app, 10.0);
select(&mut app, a, "toggle");
assert_eq!(app.session.active().unwrap().active_layer, Some(g));
crate::transform_tool::end_if_left(&mut app);
assert!(app.ui.transform.is_none(), "the box is applied and not opened again");
assert_eq!(bounds(&app, a), Rect::new(20, 10, 60, 30), "A moved with its group");
assert_eq!(bounds(&app, b), Rect::new(110, 100, 150, 120));
}
/// An unselected layer "C" under the frame's bottom-right handle (x, y 180..200), so a press there
/// that reached the Move tool's Auto-Select would select it.
fn corner_layer(app: &mut PhotocraftApp) -> LayerId {
let c = LayerId(app.run("layer.new.layer", json!({"name": "C"})).unwrap()["layer"].as_u64().unwrap());
app.run("select.rect", json!({"x": 180, "y": 180, "width": 20, "height": 20})).unwrap();
app.run("edit.fill", json!({"contents": "color", "color": "#00ff00"})).unwrap();
app.run("select.deselect", json!({})).unwrap();
c
}
#[test]
fn a_refused_handle_press_on_the_canvas_reaches_no_move_tool() {
// The Background among the selected layers: a press on the Move tool's transform handle shows
// the locked-layer alert and goes nowhere else. Auto-Select (on) used to take it and select
// C under the handle, a click and a drag alike.
use egui::{Event, Modifiers, PointerButton, Pos2, vec2};
for drag in [false, true] {
let (mut app, bg, a, _) = app();
corner_layer(&mut app);
select(&mut app, a, "replace");
select(&mut app, bg, "add");
app.ui.tool = crate::state::Tool::Move;
app.ui.tool_options.move_show_transform = true;
app.ui.tool_options.move_auto_select = true;
app.sync_views();
let mut view = app.ui.views[0].clone();
view.zoom = 1.0;
view.center = [100.0, 100.0];
view.fit_pending = false;
view.fill_pending = false;
let rect = egui::Rect::from_min_size(Pos2::ZERO, vec2(300.0, 300.0));
let ctx = egui::Context::default();
let mut frame = |app: &mut PhotocraftApp, events: Vec<Event>| {
let input = egui::RawInput { screen_rect: Some(rect), events, ..Default::default() };
let mut out = ctx.run_ui(input, |ui| {
view = crate::canvas::canvas_view(app, ui, 0, rect, view.clone(), true);
});
out.textures_delta.clear();
};
let press = |pos: Pos2, pressed: bool| Event::PointerButton { pos, button: PointerButton::Primary, pressed, modifiers: Modifiers::NONE };
// The frame holds the Background, the whole canvas: its bottom-right handle is at doc
// (200, 200), screen (250, 250); the press is inside its grab area, over C.
let at = Pos2::new(246.0, 246.0);
let before = app.session.active().unwrap().doc.clone();
let selected = app.session.active().unwrap().selected_layers();
frame(&mut app, vec![Event::PointerMoved(at)]);
frame(&mut app, vec![press(at, true)]);
let mut end = at;
if drag {
for i in 1..=6 {
end = at + vec2(-8.0 * i as f32, -8.0 * i as f32);
frame(&mut app, vec![Event::PointerMoved(end)]);
}
}
frame(&mut app, vec![press(end, false)]);
frame(&mut app, vec![]);
assert!(app.ui.transform.is_none(), "drag {drag}: no box opens");
assert!(alerts(&app).iter().any(|m| m == "Could not transform because the layer is locked."), "drag {drag}: {:?}", alerts(&app));
let st = app.session.active().unwrap();
assert_eq!(st.selected_layers(), selected, "drag {drag}: the selection is unchanged");
assert!(st.doc == before, "drag {drag}: the document is unchanged");
}
}
#[test]
fn a_refused_handle_press_over_the_control_channel_reaches_no_move_tool() {
// `ui.pointer` takes the same path: the alert, and the press is not forwarded to the Move tool.
use crate::control::{ControlRequest, Outcome};
let (mut app, bg, a, _) = app();
corner_layer(&mut app);
select(&mut app, a, "replace");
select(&mut app, bg, "add");
app.sync_views();
app.last_canvas_rect = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(300.0, 300.0));
app.ui.tool_options.move_auto_select = true;
let ctx = egui::Context::default();
let call = |app: &mut PhotocraftApp, method: &str, params: serde_json::Value| {
let (request, _receiver) = ControlRequest::new(method, params);
let Outcome::Done(reply) = crate::control::handle(app, &ctx, &request) else { panic!("expected an immediate {method} reply") };
assert_eq!(reply["ok"], true, "{method}: {reply}");
};
call(&mut app, "ui.set", json!({"tool": "Move", "moveShowTransform": true, "zoom": 1.0, "center": [100, 100]}));
let before = app.session.active().unwrap().doc.clone();
let selected = app.session.active().unwrap().selected_layers();
call(
&mut app,
"ui.pointer",
json!({"events": [{"kind": "down", "x": 196, "y": 196}, {"kind": "move", "x": 180, "y": 180}, {"kind": "up", "x": 180, "y": 180}]}),
);
assert!(app.ui.transform.is_none(), "no box opens");
assert!(alerts(&app).iter().any(|m| m == "Could not transform because the layer is locked."), "{:?}", alerts(&app));
let st = app.session.active().unwrap();
assert_eq!(st.selected_layers(), selected, "the selection is unchanged");
assert!(st.doc == before, "the document is unchanged");
}
+7 -17
View File
@@ -153,26 +153,16 @@ fn a_locked_layer_among_them_refuses_before_the_box_opens() {
}
#[test]
fn with_the_background_active_the_box_moves_the_other_selected_layers() {
let (mut app, paint, text) = app();
fn with_the_background_active_among_them_the_box_refuses_as_locked() {
// Photoshop's locked-layer alert, not a box that leaves the Background out (#11101).
let (mut app, _, _) = app();
let bg = app.session.active().unwrap().doc.layers[0].id;
app.run("layer.select", json!({"layer": bg.0, "mode": "add"})).unwrap();
assert_eq!(app.session.active().unwrap().active_layer, Some(bg));
assert!(crate::menus::is_enabled(&app, "edit.freeTransform"), "not greyed for the Background");
free_transform(&mut app);
let t = app.ui.transform.as_ref().unwrap();
assert_eq!((t.layer, t.layers.clone()), (bg.0, vec![paint.0, text.0]));
let p0 = bounds(&app, paint);
if let Some(t) = app.ui.transform.as_mut() {
// Nearest neighbour: no resampling fringe at the block's edge.
t.interpolation = "nearest".into();
t.quad = t.quad.map(|[x, y]| [x + 5.0, y]);
}
crate::transform_tool::commit(&mut app);
assert_eq!(app.ui.status, "");
let st = app.session.active().unwrap();
assert!(st.doc.is_background(bg), "the Background is left alone");
assert_eq!(bounds(&app, paint), p0.translate(5, 0));
assert!(crate::menus::is_enabled(&app, "edit.freeTransform"), "not greyed: choosing it shows the alert");
let r = crate::menus::invoke(&mut app, &egui::Context::default(), "edit.freeTransform", json!({}));
assert_eq!(r, Err(crate::transform_tool::LOCKED.into()));
assert!(app.ui.transform.is_none());
}
#[test]