diff --git a/crates/engine/src/cmd/actions_cmds.rs b/crates/engine/src/cmd/actions_cmds.rs index 4dcb8bb8b9..46536e4024 100644 --- a/crates/engine/src/cmd/actions_cmds.rs +++ b/crates/engine/src/cmd/actions_cmds.rs @@ -367,9 +367,18 @@ fn stop(s: &mut Session, _p: &Value) -> Result { } /// A step Photoshop plays as a silent no-op although its menu item is greyed: Deselect with an -/// open document and nothing selected (actions often start with it to clear any selection, #6744). -fn noop_step(s: &Session, id: &str) -> bool { - id == "select.deselect" && s.active().is_some_and(|d| d.doc.selection.is_none()) +/// open document and nothing selected (actions often start with it to clear any selection, #6744), +/// and an Arrange that can't move the layer, such as Bring to Front on a new adjustment layer that +/// is already on top (#8453). +fn noop_step(s: &Session, id: &str, params: &Value) -> bool { + let Some(d) = s.active() else { return false }; + let dir = match id { + "select.deselect" => return d.doc.selection.is_none(), + "layer.arrange.bringForward" | "layer.arrange.bringToFront" => 1, + "layer.arrange.sendBackward" | "layer.arrange.sendToBack" => -1, + _ => return false, + }; + crate::commands::layer_param(s, params).is_ok_and(|layer| super::ops::arrange_at_end(&d.doc, layer, dir)) } fn run_recorded(s: &mut Session, action: &Action, from: usize) -> (u64, Option) { @@ -386,7 +395,7 @@ fn run_recorded(s: &mut Session, action: &Action, from: usize) -> (u64, Option = s.active().unwrap().doc.walk().into_iter().map(|(_, _, l)| l.id).collect(); + let bg = crate::background_cmds::background_id(&s.active().unwrap().doc); + for id in ids.into_iter().filter(|id| Some(*id) != bg) { + for (cmd, dir) in cmds { + let at_end = super::super::ops::arrange_at_end(&s.active().unwrap().doc, id, dir); + let ran = s.execute(cmd, json!({"layer": id.0})); + assert_eq!(at_end, ran.is_err(), "{cmd} on {id:?}: {ran:?}"); + if ran.is_ok() { + s.execute("edit.undo", json!({})).unwrap(); + } + } + } + + // Arranging the Background still stops playback: Photoshop refuses it too. + let bg = bg.unwrap(); + s.actions.list.push(Action::new("Bg", vec![("layer.arrange.bringToFront".to_string(), json!({"layer": bg.0}))])); + let played = s.execute("actions.play", json!({"action": 1})).unwrap(); + assert_eq!(played["ran"], 0, "{played}"); + assert!(played["failed"].is_object(), "{played}"); +} diff --git a/crates/engine/src/cmd/ops.rs b/crates/engine/src/cmd/ops.rs index c106af6aac..19a62d0a8b 100644 --- a/crates/engine/src/cmd/ops.rs +++ b/crates/engine/src/cmd/ops.rs @@ -2,6 +2,28 @@ use super::enabled::check_all_unlocked; use super::layer_params::{layer_param, mask_at_depth}; use super::{Document, EngineError, Layer, LayerContent, LayerId, LayerMask, Result, Session, Value, json, pixels}; +/// Is layer `id` already as far as Arrange can take it in direction `dir` (up when positive)? True +/// at the top or bottom of its stack, or just above the Background, which stays at the bottom. The +/// Background itself is not: arranging it is refused outright. +pub(crate) fn arrange_at_end(doc: &Document, id: LayerId, dir: i32) -> bool { + let Some(path) = doc.path_of(id) else { return false }; + let Some((&idx, parent)) = path.split_last() else { return false }; + let len = if parent.is_empty() { + doc.layers.len() + } else { + match doc.layer_at(parent).and_then(Layer::children) { + Some(children) => children.len(), + None => return false, + } + }; + let bg = if parent.is_empty() { crate::background_cmds::background_id(doc) } else { None }; + if bg == Some(id) { + return false; + } + let floor = usize::from(bg.is_some()); + if dir > 0 { idx.saturating_add(1) >= len } else { idx <= floor } +} + pub(super) fn arrange(s: &mut Session, p: &Value, delta: i32) -> Result { let id = layer_param(s, p)?; s.edit("Arrange", |doc, _| {