From ef8dc9ea30157d3d5c0499bd7a551bded9e899df Mon Sep 17 00:00:00 2001 From: F4LCON <107556180+itsF4LCON@users.noreply.github.com> Date: Tue, 13 Oct 2026 18:47:10 +0200 Subject: [PATCH] Layer locks: locking the pixels clears the transparency lock, as in Photoshop (#4892) (#4898) --- crates/doc/src/lib.rs | 21 +++++++++++++++++ crates/engine/src/commands.rs | 9 ++------ crates/engine/src/layer_multi_cmds.rs | 33 +++++++++++++++++++++------ crates/ui-egui/src/panels.rs | 4 +++- 4 files changed, 52 insertions(+), 15 deletions(-) diff --git a/crates/doc/src/lib.rs b/crates/doc/src/lib.rs index a5c5381ee..f2fd637b9 100644 --- a/crates/doc/src/lib.rs +++ b/crates/doc/src/lib.rs @@ -99,6 +99,27 @@ pub struct Locks { } impl Locks { + /// Set the lock a command names (`transparency`, `pixels`, `position`, `artboard`, `all`); + /// false for an unknown name. Lock Image Pixels includes Lock Transparent Pixels, so, as in + /// Photoshop, locking the pixels clears the transparency lock, and that one stays off while the + /// pixels are locked (#4892). + pub fn set(&mut self, key: &str, on: bool) -> bool { + match key { + "transparency" => self.transparency = on && !self.pixels, + "pixels" => { + self.pixels = on; + if on { + self.transparency = false; + } + } + "position" => self.position = on, + "artboard" => self.artboard = on, + "all" => self.all = on, + _ => return false, + } + true + } + /// Whether any lock is set: Photoshop then shows the lock icon on the layer's row. pub fn any(self) -> bool { self.transparency || self.pixels || self.position || self.artboard || self.all diff --git a/crates/engine/src/commands.rs b/crates/engine/src/commands.rs index 89278165e..2a769f068 100644 --- a/crates/engine/src/commands.rs +++ b/crates/engine/src/commands.rs @@ -701,13 +701,8 @@ fn build() -> Vec { if let Some(Value::Object(m)) = p.get("locks") { for (k, v) in m { let v = v.as_bool().unwrap_or(false); - match k.as_str() { - "transparency" => l.locks.transparency = v, - "pixels" => l.locks.pixels = v, - "position" => l.locks.position = v, - "artboard" => l.locks.artboard = v, - "all" => l.locks.all = v, - other => return Err(bad("layer.setProps", format!("unknown lock `{other}`"))), + if !l.locks.set(k, v) { + return Err(bad("layer.setProps", format!("unknown lock `{k}`"))); } } } diff --git a/crates/engine/src/layer_multi_cmds.rs b/crates/engine/src/layer_multi_cmds.rs index 85cef3665..8d71810dc 100644 --- a/crates/engine/src/layer_multi_cmds.rs +++ b/crates/engine/src/layer_multi_cmds.rs @@ -1168,13 +1168,7 @@ fn lock_layers(s: &mut Session, p: &Value) -> Result { for id in &sel { let l = doc.layer_mut(*id).ok_or(EngineError::NoLayer(*id))?; for (k, v) in &given { - match *k { - "transparency" => l.locks.transparency = *v, - "pixels" => l.locks.pixels = *v, - "position" => l.locks.position = *v, - "artboard" => l.locks.artboard = *v, - _ => l.locks.all = *v, - } + l.locks.set(k, *v); } } Ok(()) @@ -2606,6 +2600,31 @@ mod tests { assert_eq!(s.active().unwrap().history.entries().last().map(|e| e.as_str()), Some("Reverse")); } + /// #4892: Lock Image Pixels includes Lock Transparent Pixels, so the two are never both on: + /// locking the pixels clears the transparency lock, which stays off while they are locked, + /// through both `layer.setProps` and `layer.lockLayers`. + #[test] + fn locking_pixels_clears_and_holds_off_the_transparency_lock() { + let mut s = Session::new(); + s.execute("file.new", json!({"width": 8, "height": 8})).unwrap(); + let id = LayerId(s.execute("layer.new.layer", json!({})).unwrap()["layer"].as_u64().unwrap()); + let locks = |s: &Session| doc(s).layer(id).unwrap().locks; + s.execute("layer.setProps", json!({"locks": {"transparency": true}})).unwrap(); + assert!(locks(&s).transparency); + s.execute("layer.setProps", json!({"locks": {"pixels": true}})).unwrap(); + assert!(locks(&s).pixels && !locks(&s).transparency, "locking the pixels clears the transparency lock"); + s.execute("layer.setProps", json!({"locks": {"transparency": true}})).unwrap(); + assert!(!locks(&s).transparency, "and it stays off while the pixels are locked"); + s.execute("layer.setProps", json!({"locks": {"transparency": true, "pixels": true}})).unwrap(); + assert!(locks(&s).pixels && !locks(&s).transparency, "both at once: the pixel lock wins"); + s.execute("layer.lockLayers", json!({"pixels": false})).unwrap(); + s.execute("layer.lockLayers", json!({"transparency": true})).unwrap(); + assert!(locks(&s).transparency && !locks(&s).pixels, "with the pixels unlocked it can be set again"); + s.execute("layer.lockLayers", json!({"pixels": true})).unwrap(); + assert!(locks(&s).pixels && !locks(&s).transparency); + assert!(s.execute("layer.setProps", json!({"locks": {"nope": true}})).is_err()); + } + #[test] fn inspect_reports_selection() { let mut s = session(8); diff --git a/crates/ui-egui/src/panels.rs b/crates/ui-egui/src/panels.rs index a69b40c41..9de772632 100644 --- a/crates/ui-egui/src/panels.rs +++ b/crates/ui-egui/src/panels.rs @@ -2285,7 +2285,9 @@ fn layers(app: &mut PhotocraftApp, ui: &mut egui::Ui) { ("artboard", "scan", tl!("Prevent auto-nesting in and out of Artboards and Frames"), lk.artboard), ("all", "lock", tl!("Lock all"), lk.all), ] { - if icons::button(ui, icon, 20.0, on, tip).clicked() { + // Lock Image Pixels includes Lock Transparent Pixels, which waits for it (#4892). + let enabled = key != "transparency" || !lk.pixels; + if ui.add_enabled_ui(enabled, |ui| icons::button(ui, icon, 20.0, on, tip)).inner.clicked() { actions.push(("layer.setProps".into(), json!({"layer": l.id.0, "locks": { key: !on }}))); } }