From 6a39da98c8845417dd5a6443ffeef94cc7da6b25 Mon Sep 17 00:00:00 2001 From: texjer Date: Sat, 10 Oct 2026 03:39:40 -0400 Subject: [PATCH] Move tool on a locked layer: Photoshop's message (#2249) * Move tool on a locked layer: Photoshop's message Dragging with the Move tool (or Cmd with another tool) on a layer it can't move showed nothing: the Background or a position-locked layer with no selection, or a locked layer whose selected pixels can't float. Photoshop puts up "Could not use the move tool because the layer is locked." when the drag starts (a plain click says nothing); move_lock.rs does the same with the Error dialog, translated in every catalogue. The float lock rule moves to float_cmds::locked_for_float so the UI can ask before the press goes anywhere. Also in Dutch and Ukrainian. The Cmd-with-a-painting-tool case lands with the Cmd-as-Move change, whichever merges second. * Fix merge: close the lock-message test --------- Co-authored-by: Brandon Thomas Co-authored-by: Claude Opus 5.5 --- crates/engine/src/float_cmds.rs | 11 +++- crates/ui-egui/src/canvas.rs | 41 +++++++++++--- crates/ui-egui/src/i18n/cs.tsv | 1 + crates/ui-egui/src/i18n/de.tsv | 1 + crates/ui-egui/src/i18n/el.tsv | 1 + crates/ui-egui/src/i18n/es.tsv | 1 + crates/ui-egui/src/i18n/fr.tsv | 1 + crates/ui-egui/src/i18n/id.tsv | 1 + crates/ui-egui/src/i18n/it.tsv | 1 + crates/ui-egui/src/i18n/ja.tsv | 1 + crates/ui-egui/src/i18n/ko.tsv | 1 + crates/ui-egui/src/i18n/nl.tsv | 1 + crates/ui-egui/src/i18n/pl.tsv | 1 + crates/ui-egui/src/i18n/pt-br.tsv | 1 + crates/ui-egui/src/i18n/ru.tsv | 1 + crates/ui-egui/src/i18n/uk.tsv | 1 + crates/ui-egui/src/i18n/zh-hans.tsv | 1 + crates/ui-egui/src/i18n/zh-hant.tsv | 1 + crates/ui-egui/src/lib.rs | 5 ++ crates/ui-egui/src/marquee_tests.rs | 87 +++++++++++++++++++++++++++++ crates/ui-egui/src/move_lock.rs | 48 ++++++++++++++++ 21 files changed, 197 insertions(+), 11 deletions(-) create mode 100644 crates/ui-egui/src/move_lock.rs diff --git a/crates/engine/src/float_cmds.rs b/crates/engine/src/float_cmds.rs index 44557a59..98ea5272 100644 --- a/crates/engine/src/float_cmds.rs +++ b/crates/engine/src/float_cmds.rs @@ -130,14 +130,19 @@ fn can_float(s: &Session) -> std::result::Result<(), String> { if !matches!(l.content, LayerContent::Raster(_)) { return Err("the active layer has no pixels to move".into()); } - let locks = st.doc.effective_locks(l.id); - // The Background is position-locked, but its selected pixels move (Photoshop). - if locks.all || (locks.position && !crate::extra_cmds::is_background(l)) { + if locked_for_float(&st.doc, l) { return Err(format!("layer \"{}\" is locked", l.name)); } Ok(()) } +/// Do `l`'s locks (its own, or a locked group's: `effective_locks`) stop its selected pixels +/// floating? The Background is position-locked, but its selected pixels move (Photoshop). +pub fn locked_for_float(doc: &Document, l: &photocraft_doc::Layer) -> bool { + let locks = doc.effective_locks(l.id); + locks.all || (locks.position && !crate::extra_cmds::is_background(l)) +} + fn float(s: &mut Session, p: &Value) -> Result { let (dx, dy) = (int_param(p, "dx"), int_param(p, "dy")); let copy = p.get("copy").and_then(Value::as_bool).unwrap_or(false); diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index ac29ba8c..267445d3 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -3690,6 +3690,25 @@ pub fn tool_event(app: &mut PhotocraftApp, ev: ToolEvent, mods: egui::Modifiers) ToolEvent::Up { .. } => return, } } + // A locked layer (the Background without a selection, say): no drag, and Photoshop's message + // once the pointer moves (a click says nothing). ⌘ with a selection tool here (#896); the Move + // tool below, once Auto-Select has picked the layer. + match ev { + ToolEvent::Down { x, y, .. } if tool != Tool::Move && app.ui.transform.is_none() && crate::move_lock::blocked(app, tool, [x, y], mods) => { + app.move_blocked = true; + return; + } + ToolEvent::Move { .. } if app.move_blocked => { + app.move_blocked = false; + crate::move_lock::prompt(app); + return; + } + ToolEvent::Up { .. } if app.move_blocked => { + app.move_blocked = false; + return; + } + _ => {} + } // Move tool over a guide drags the guide (off the canvas deletes it). match ev { ToolEvent::Down { x, y, pressure } if tool == Tool::Move => { @@ -3697,8 +3716,20 @@ pub fn tool_event(app: &mut PhotocraftApp, ev: ToolEvent, mods: egui::Modifiers) app.guide_drag = Some(crate::rulers::GuideDrag { vertical, index: Some(i), pos: if vertical { x } else { y } }); return; } + // Auto-Select (or ⌘-click while it is off) picks the layer under the pointer first + // (not when the selected pixels move: those are the active layer's). + if !crate::move_ui::moves_selected_pixels(app) && app.ui.tool_options.move_auto_select != mods.command { + let target = app.ui.tool_options.move_target.clone(); + let mode = if mods.shift { "add" } else { "replace" }; + let _ = app.run("layer.pickAt", json!({"x": x, "y": y, "target": target, "mode": mode})); + } + // A locked layer: no drag, and Photoshop's message once the pointer moves (`move_lock`). + if crate::move_lock::blocked(app, tool, [x, y], mods) { + app.move_blocked = true; + return; + } // With a selection: cut the selected pixels (⌥ copies them) and drag them as a floating - // piece, from anywhere, as a marquee ⌘-drag does (no Auto-Select pick). + // piece, from anywhere, as a marquee ⌘-drag does. if crate::move_ui::moves_selected_pixels(app) { if crate::move_ui::float_selected(app, mods.alt, 0.0, 0.0) { let mut d = Drag::new(tool, [x, y], vec![[x, y, pressure as f64]], mods, false); @@ -3707,12 +3738,6 @@ pub fn tool_event(app: &mut PhotocraftApp, ev: ToolEvent, mods: egui::Modifiers) } return; } - // Auto-Select (or ⌘-click while it is off) picks the layer under the pointer first. - if app.ui.tool_options.move_auto_select != mods.command { - let target = app.ui.tool_options.move_target.clone(); - let mode = if mods.shift { "add" } else { "replace" }; - let _ = app.run("layer.pickAt", json!({"x": x, "y": y, "target": target, "mode": mode})); - } } ToolEvent::Move { x, y, .. } => { if let Some(d) = app.guide_drag.as_mut().filter(|d| d.index.is_some()) { @@ -3953,7 +3978,7 @@ pub fn selection_drag_kind(app: &PhotocraftApp, tool: Tool, p: [f64; 2], mods: e /// Does a ⌘ (⌘⌥) press with selection tool `tool` at `p` move the whole layer (a duplicate with /// ⌥), as the Move tool would? Outside the selection or without one; inside it, ⌘ drags the /// selected pixels instead (`selection_drag_kind`). -fn command_moves_layer(app: &PhotocraftApp, tool: Tool, p: [f64; 2], mods: egui::Modifiers) -> bool { +pub(crate) fn command_moves_layer(app: &PhotocraftApp, tool: Tool, p: [f64; 2], mods: egui::Modifiers) -> bool { let selection_tool = matches!(tool, Tool::RectMarquee | Tool::EllipseMarquee | Tool::Lasso | Tool::PolygonLasso | Tool::MagicWand); let floating = app.session.active().is_some_and(|st| photocraft_engine::float_cmds::floating(st).is_some()); selection_tool && mods.command && !mods.shift && !floating && app.ui.polygon.is_empty() && !inside_selection(app, p) diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index 4e72a3e9..eb5d3cc9 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -1636,6 +1636,7 @@ Millimeters Milimetry Mobile Mobilní zařízení 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á. Multiply Násobit My Brushes Moje štětce Neutrals Neutrální diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index 1a99b2cb..f8d5f86c 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -1616,6 +1616,7 @@ Millimeters Millimeter Mobile Mobil 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. Multiply Multiplizieren My Brushes Meine Pinsel Neutrals Neutrale diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv index 8032a2c9..6f97b3c1 100644 --- a/crates/ui-egui/src/i18n/el.tsv +++ b/crates/ui-egui/src/i18n/el.tsv @@ -3215,6 +3215,7 @@ cameraRaw Yellow Κίτρινο Magic Eraser Tool Μαγική γόμα (Magic Eraser Tool) Magic Wand Tool Μαγικό ραβδί (Magic Wand Tool) Move Tool Μετακίνηση (Move Tool) + Could not use the move tool because the layer is locked. Δεν ήταν δυνατή η χρήση του εργαλείου Μετακίνηση, επειδή το επίπεδο είναι κλειδωμένο. Note Tool Σημείωση (Note Tool) Object Selection Tool Επιλογή αντικειμένου (Object Selection Tool) Paint Bucket Tool Κουβάς γεμίσματος (Paint Bucket Tool) diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index 391b7279..10088f31 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -1620,6 +1620,7 @@ Millimeters Milímetros Mobile Móvil 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. Multiply Multiplicar My Brushes Mis pinceles Neutrals Neutros diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index a103f74d..9d5ccd00 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -1599,6 +1599,7 @@ Millimeters Millimètres Mobile Mobile 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é. Multiply Produit My Brushes Mes pinceaux Neutrals Neutres diff --git a/crates/ui-egui/src/i18n/id.tsv b/crates/ui-egui/src/i18n/id.tsv index 99284ae5..8769f489 100644 --- a/crates/ui-egui/src/i18n/id.tsv +++ b/crates/ui-egui/src/i18n/id.tsv @@ -1593,6 +1593,7 @@ Millimeters Milimeter Mobile Seluler Move Tool Alat Pindah + Could not use the move tool because the layer is locked. Tidak dapat menggunakan alat Pindahkan karena layer terkunci. Multiply Multiply My Brushes Kuas Saya Neutrals Netral diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index a99a5ea0..bc767257 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -1618,6 +1618,7 @@ Millimeters Millimetri Mobile Mobile Move Tool Strumento Sposta + Could not use the move tool because the layer is locked. Impossibile usare lo strumento Sposta perché il livello è bloccato. Multiply Moltiplica My Brushes I miei pennelli Neutrals Neutri diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index aa60617f..3df42596 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -1625,6 +1625,7 @@ Millimeters ミリメートル Mobile モバイル Move Tool 移動ツール + Could not use the move tool because the layer is locked. レイヤーがロックされているため、移動ツールを使用できません。 Multiply 乗算 My Brushes マイブラシ Neutrals 中間色系 diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv index b26207b0..6fe6c169 100644 --- a/crates/ui-egui/src/i18n/ko.tsv +++ b/crates/ui-egui/src/i18n/ko.tsv @@ -1595,6 +1595,7 @@ Millimeters 밀리미터 Mobile 모바일 Move Tool 이동 도구 + Could not use the move tool because the layer is locked. 레이어가 잠겨 있어 이동 도구를 사용할 수 없습니다. Multiply 곱하기 My Brushes 내 브러시 Neutrals 중립색 diff --git a/crates/ui-egui/src/i18n/nl.tsv b/crates/ui-egui/src/i18n/nl.tsv index 8c4c5d4f..e72180e2 100644 --- a/crates/ui-egui/src/i18n/nl.tsv +++ b/crates/ui-egui/src/i18n/nl.tsv @@ -1608,6 +1608,7 @@ Millimeters Millimeter Mobile Mobiel Move Tool Verplaatsgereedschap + Could not use the move tool because the layer is locked. Kan het verplaatsgereedschap niet gebruiken omdat de laag is vergrendeld. Multiply Vermenigvuldigen My Brushes Mijn penselen Neutrals Neutrale tinten diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index 78f648f4..7602e691 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -1618,6 +1618,7 @@ Millimeters Milimetry Mobile Urządzenia mobilne 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. Multiply Mnożenie My Brushes Moje pędzle Neutrals Neutralne diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index a1bdc1a3..dd657227 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -1598,6 +1598,7 @@ Millimeters Milímetros Mobile Dispositivos móveis 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. Multiply Multiplicação My Brushes Meus pincéis Neutrals Neutros diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index 16748f15..ac09b817 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -1500,6 +1500,7 @@ Adjustment Layer Корректирующий слой {name} Properties Свойства: {name} Move Tool Перемещение + Could not use the move tool because the layer is locked. Не удалось использовать инструмент «Перемещение», потому что слой заблокирован. Rectangular Marquee Tool Прямоугольная область Elliptical Marquee Tool Овальная область Brush Tool Кисть diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index 66f0f9d7..d5adf0f7 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -1485,6 +1485,7 @@ Adjustment Layer Коригувальний шар {name} Properties Властивості: {name} Move Tool Інструмент «Переміщення» + Could not use the move tool because the layer is locked. Не вдалося використати інструмент «Переміщення», бо шар заблоковано. Rectangular Marquee Tool Інструмент «Прямокутне виділення» Elliptical Marquee Tool Інструмент «Еліптичне виділення» Brush Tool Інструмент «Пензель» diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index e677291f..2639a6ac 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -1633,6 +1633,7 @@ Millimeters 毫米 Mobile 移动设备 Move Tool 移动工具 + Could not use the move tool because the layer is locked. 无法使用移动工具,因为图层已锁定。 Multiply 正片叠底 My Brushes 我的画笔 Neutrals 中性色 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index 6e9ef866..11520b40 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -1626,6 +1626,7 @@ Millimeters 公釐 Mobile 行動裝置 Move Tool 移動工具 + Could not use the move tool because the layer is locked. 無法使用移動工具,因為圖層已鎖定。 Multiply 色彩增值 My Brushes 我的筆刷 Neutrals 中間色 diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 1c1e7881..af78328a 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -98,6 +98,7 @@ pub mod menu_catalog; pub mod menu_nav; pub mod menus; pub mod monitor_status; +pub mod move_lock; pub mod move_mods; pub mod move_ui; pub mod native_menu; @@ -371,6 +372,9 @@ pub struct PhotocraftApp { monitors: monitor_status::State, checker: Option, drag: Option, + /// A Move-tool press landed on a locked layer: the first pointer move shows Photoshop's + /// message (`move_lock`), a plain click shows nothing. + pub(crate) move_blocked: bool, /// The tool pointer events go to this frame when it isn't the selected one: the Move tool /// while ⌘ is held (`hold_keys::cmd_moves`). Set by the canvas for its gestures, never saved. pub(crate) tool_override: Option, @@ -571,6 +575,7 @@ impl PhotocraftApp { monitors: Default::default(), checker: None, drag: None, + move_blocked: false, tool_override: None, live_stroke: None, trail: None, diff --git a/crates/ui-egui/src/marquee_tests.rs b/crates/ui-egui/src/marquee_tests.rs index 4e8cc26d..bfc94bcd 100644 --- a/crates/ui-egui/src/marquee_tests.rs +++ b/crates/ui-egui/src/marquee_tests.rs @@ -409,6 +409,93 @@ fn alt_with_nothing_selected_draws_a_new_selection() { assert!(after.is_none_or(|r| r.is_empty() || r != before), "⌥ subtracted: {before:?} -> {after:?}"); } +fn error_dialogs(app: &PhotocraftApp) -> Vec { + 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() +} + +/// Photoshop's "Could not use the move tool because the layer is locked." when a Move-tool drag +/// (the Move tool, or ⌘ with a marquee) starts on a layer it can't move: the Background with no +/// selection, a position-locked layer; a plain click says nothing, and the drag never starts. +#[test] +fn move_on_the_background_without_a_selection_shows_photoshops_lock_message() { + let mut app = PhotocraftApp::new(photocraft_engine::Session::new(), crate::Services::default()); + app.run("file.new", json!({"width": 80, "height": 60})).unwrap(); + app.sync_views(); + app.ui.tool = Tool::Move; + app.ui.tool_options.move_auto_select = false; + let steps = app.session.active().unwrap().history.past_len(); + use crate::canvas::{ToolEvent, tool_event}; + // A click: no message. + tool_event(&mut app, ToolEvent::Down { x: 20.0, y: 20.0, pressure: 1.0 }, Modifiers::NONE); + tool_event(&mut app, ToolEvent::Up { x: 20.0, y: 20.0 }, Modifiers::NONE); + assert!(error_dialogs(&app).is_empty() && !app.move_blocked); + // A drag: the message once, nothing moved, no drag in progress. + tool_event(&mut app, ToolEvent::Down { x: 20.0, y: 20.0, pressure: 1.0 }, Modifiers::NONE); + tool_event(&mut app, ToolEvent::Move { x: 30.0, y: 20.0, pressure: 1.0 }, Modifiers::NONE); + tool_event(&mut app, ToolEvent::Move { x: 40.0, y: 20.0, pressure: 1.0 }, Modifiers::NONE); + tool_event(&mut app, ToolEvent::Up { x: 40.0, y: 20.0 }, Modifiers::NONE); + assert_eq!(error_dialogs(&app), [crate::move_lock::MESSAGE]); + assert!(app.drag.is_none(), "no drag started"); + assert_eq!(app.session.active().unwrap().history.past_len(), steps); + app.ui.dialogs.clear(); + // ⌘-drag with a marquee (the Move tool for the drag, #896): the same. + app.ui.tool = Tool::RectMarquee; + drag(&mut app, [20.0, 20.0], [40.0, 20.0], Modifiers::COMMAND); + assert_eq!(error_dialogs(&app), [crate::move_lock::MESSAGE]); + app.ui.dialogs.clear(); + // A position-locked layer: the same; unlocked, it moves. + app.run("layer.new.layer", json!({})).unwrap(); + app.session + .edit("lock", |doc, a| { + doc.layer_mut(a.unwrap()).unwrap().locks.position = true; + Ok(()) + }) + .unwrap(); + drag(&mut app, [20.0, 20.0], [40.0, 20.0], Modifiers::COMMAND); + assert_eq!(error_dialogs(&app).len(), 1); + app.ui.dialogs.clear(); + app.session + .edit("unlock", |doc, a| { + doc.layer_mut(a.unwrap()).unwrap().locks.position = false; + Ok(()) + }) + .unwrap(); + drag(&mut app, [20.0, 20.0], [40.0, 20.0], Modifiers::COMMAND); + assert!(error_dialogs(&app).is_empty()); +} + +/// With a selection the Background's pixels do move (its lock is partial: transparency and +/// position), so no message; a layer locked all over gets it, with the marquee's ⌘-drag inside the +/// selection as with the Move tool. +#[test] +fn selected_pixels_move_off_the_background_but_not_off_a_fully_locked_layer() { + let mut app = PhotocraftApp::new(photocraft_engine::Session::new(), crate::Services::default()); + app.run("file.new", json!({"width": 80, "height": 60})).unwrap(); + app.sync_views(); + app.ui.extras.snap = false; + app.ui.view.show.smart_guides = false; + app.ui.tool = Tool::RectMarquee; + app.run("select.rect", json!({"x": 10, "y": 10, "width": 20, "height": 20})).unwrap(); + drag(&mut app, [20.0, 20.0], [50.0, 20.0], Modifiers::COMMAND); + assert!(error_dialogs(&app).is_empty()); + assert_eq!(photocraft_engine::float_cmds::floating(app.session.active().unwrap()).map(|f| f.offset), Some((30, 0))); + let (mut app, layer) = painted(); + app.session + .edit("lock", |doc, _| { + doc.layer_mut(layer).unwrap().locks.all = true; + Ok(()) + }) + .unwrap(); + // ⌘ with the Brush is the Move tool too (#2248). + for (tool, m) in [(Tool::RectMarquee, Modifiers::COMMAND), (Tool::Move, Modifiers::NONE), (Tool::Brush, Modifiers::COMMAND)] { + app.ui.tool = tool; + drag(&mut app, [20.0, 20.0], [40.0, 20.0], m); + assert_eq!(error_dialogs(&app), [crate::move_lock::MESSAGE], "{tool:?}"); + assert!(photocraft_engine::float_cmds::floating(app.session.active().unwrap()).is_none(), "{tool:?}"); + app.ui.dialogs.clear(); + } +} + /// Photoshop's cursor over a selection: the move cursor where a marquee press moves the outline, /// scissors where ⌘ (or the Move tool) cuts the selected pixels, a double arrow where ⌥ copies /// them, a hollow arrowhead while they are dragged, and a plain arrow over the floating piece. diff --git a/crates/ui-egui/src/move_lock.rs b/crates/ui-egui/src/move_lock.rs new file mode 100644 index 00000000..2cef0f22 --- /dev/null +++ b/crates/ui-egui/src/move_lock.rs @@ -0,0 +1,48 @@ +//! Photoshop's "Could not use the move tool because the layer is locked." A Move-tool press (the +//! Move tool itself, or ⌘ with a selection tool, `canvas::command_moves_layer`) on a layer it +//! can't move opens that message in a dialog instead +//! of silently doing nothing: the Background and any position-locked layer when there is no +//! selection, and a layer whose selected pixels can't float +//! (`photocraft_engine::float_cmds::locked_for_float`) when there is one. As in Photoshop the +//! message comes when the pointer starts to drag; a plain click says nothing. OK closes it. + +use serde_json::json; + +use crate::PhotocraftApp; +use crate::state::{DialogKind, Tool}; + +pub const MESSAGE: &str = "Could not use the move tool because the layer is locked."; + +/// Would a press at document point `p` with `tool` in effect and `mods` held be a move that +/// fails because of a lock? +pub fn blocked(app: &PhotocraftApp, tool: Tool, p: [f64; 2], mods: egui::Modifiers) -> bool { + let Some(st) = app.session.active() else { return false }; + let floating = photocraft_engine::float_cmds::floating(st).is_some(); + // The selected pixels: only a fresh cut can be refused. + let pixels_locked = + || !floating && st.active_layer.and_then(|id| st.doc.layer(id)).is_some_and(|l| photocraft_engine::float_cmds::locked_for_float(&st.doc, l)); + // The layers: any of them locked in place refuses the whole move. + let layers_locked = || { + photocraft_engine::layer_multi_cmds::move_targets(&st.doc, &st.selected_layers()).iter().any(|id| { + let locks = st.doc.effective_locks(*id); + locks.position || locks.all + }) + }; + if tool == Tool::Move { + // `moves_selected_pixels` is false for a layer whose pixels can't float: then the Move tool + // moves the layer, which its position lock refuses too. + if crate::move_ui::moves_selected_pixels(app) { + return pixels_locked(); + } + return !floating && st.doc.selection.is_some() && pixels_locked() || layers_locked(); + } + if crate::canvas::command_moves_layer(app, tool, p, mods) { + return layers_locked(); + } + crate::canvas::selection_drag_kind(app, tool, p, mods) == Some(true) && mods.command && pixels_locked() +} + +/// Opens the message. The dialog is ordinary UI state (`ui.dialog.confirm` closes it). +pub fn prompt(app: &mut PhotocraftApp) { + app.ui.open_dialog(DialogKind::Error, json!({"message": tl!(MESSAGE)}).as_object().cloned().unwrap_or_default()); +}