From 16dccbb8fa8bb0a9e37a99f17109e63d23c99da6 Mon Sep 17 00:00:00 2001 From: vangel76 Date: Sat, 7 Nov 2026 17:37:39 +0100 Subject: [PATCH] Effect Controls: a lock request with no layer shown is an error (#3193) With the lock kept for a layer of another comp and nothing selected in the active one, ui.set {effectControlsLocked: true} reported success. It now returns the documented error and leaves the kept lock alone. --- crates/ui-egui/src/control.rs | 14 +++++---- crates/ui-egui/tests/it/ui_effect_controls.rs | 30 +++++++++++++++++++ 2 files changed, 39 insertions(+), 5 deletions(-) diff --git a/crates/ui-egui/src/control.rs b/crates/ui-egui/src/control.rs index f6c255d69..b9ff46c4b 100644 --- a/crates/ui-egui/src/control.rs +++ b/crates/ui-egui/src/control.rs @@ -281,11 +281,15 @@ fn set_ui(app: &mut EffectcraftApp, ctx: &egui::Context, p: &Value) -> Outcome { { return err(e); } - // Effect Controls' tab lock: on locks it to the layer it shows. - if let Some(on) = p.get("effectControlsLocked").and_then(Value::as_bool) - && crate::panels::effect_controls::set_lock(app, on) != on - { - return err("effectControlsLocked: Effect Controls shows no layer to lock"); + // Effect Controls' tab lock: on locks it to the layer it shows. With no layer shown (the + // lock kept for a layer of another comp, nothing selected here) a lock request is an error + // and the kept lock stays. + if let Some(on) = p.get("effectControlsLocked").and_then(Value::as_bool) { + crate::panels::effect_controls::scope_lock(app); + let shown = crate::panels::effect_controls::shown_layer_id(app).is_some(); + if (on && !shown) || crate::panels::effect_controls::set_lock(app, on) != on { + return err("effectControlsLocked: Effect Controls shows no layer to lock"); + } } set_ui_layout(app, p) } diff --git a/crates/ui-egui/tests/it/ui_effect_controls.rs b/crates/ui-egui/tests/it/ui_effect_controls.rs index ce51a2b2f..ffafe867d 100644 --- a/crates/ui-egui/tests/it/ui_effect_controls.rs +++ b/crates/ui-egui/tests/it/ui_effect_controls.rs @@ -1013,6 +1013,36 @@ fn replacing_the_project_unlocks_effect_controls() { assert_eq!(h.state().ui.effect_controls_lock, None, "the lock belongs to the replaced project"); } +/// #3193: with the lock kept for a layer of another comp and no layer selected in the active one, +/// Effect Controls shows no layer: a lock request is an error and the kept lock stays. +#[test] +fn a_lock_request_with_no_layer_shown_fails_and_keeps_the_other_comps_lock() { + use effectcraft_ui_egui::control::{self, ControlRequest, Outcome}; + fn send(h: &mut Harness<'_, EffectcraftApp>) -> serde_json::Value { + let ctx = h.ctx.clone(); + let (req, _rx) = ControlRequest::new("ui.set", json!({"effectControlsLocked": true})); + let now = h.state().now(); + match control::handle(h.state_mut(), &ctx, &req, now) { + Outcome::Done(v) => v, + _ => panic!("ui.set: no immediate reply"), + } + } + let (app, a, _, _, _) = two_layers(); + let mut h = Harness::builder().with_size(egui::vec2(1600.0, 1000.0)).build_eframe(|_| app); + h.run_steps(4); + assert_eq!(send(&mut h)["ok"], true); + let kept = h.state().ui.effect_controls_lock; + assert_eq!(kept.map(|l| l.layer), Some(a)); + // Another comp becomes active, nothing selected in it. + h.state_mut().session.execute("comp.new", json!({"name": "Other", "width": 320, "height": 180, "frameRate": 30, "duration": 5})).unwrap(); + h.state_mut().session.state.selected_layers.clear(); + h.run_steps(3); + assert_ne!(h.state().session.active_comp_id().map(|c| c.0), kept.map(|l| l.comp), "another comp is active"); + let reply = send(&mut h); + assert_eq!(reply["ok"], false, "{reply}"); + assert_eq!(h.state().ui.effect_controls_lock, kept, "the other comp's lock stays"); +} + /// #3193: a lock request right after the project is replaced, before the next frame, is not /// answered with the old project's lock: the reply matches the lock the panel keeps. #[test]