Effect Controls: a lock request while locked in another comp is an error (#3193)

Locked to a layer of another comp, the panel shows the active comp's
selection and its tab lock reads locked (a click unlocks). ui.set
{effectControlsLocked: true} reported success there without locking the
shown layer; it now returns an error and keeps the other comp's lock.
This commit is contained in:
vangel76 committed 2026-11-07 18:22:03 +01:00
1 parent 16dccbb8fa
commit d1bdb701b7
3 files changed
+37 -1

No files matched your search

+5
View File
@@ -290,6 +290,11 @@ fn set_ui(app: &mut EffectcraftApp, ctx: &egui::Context, p: &Value) -> Outcome {
if (on && !shown) || crate::panels::effect_controls::set_lock(app, on) != on {
return err("effectControlsLocked: Effect Controls shows no layer to lock");
}
// Locked to a layer of another comp, the panel shows this comp's selection: the tab's
// lock reads locked and a click unlocks, so a lock request does not lock what is shown.
if on && app.ui.effect_controls_lock.is_some_and(|k| app.session.active_comp_id().is_none_or(|c| c.0 != k.comp)) {
return err("effectControlsLocked: Effect Controls is locked to a layer of another composition; unlock it first");
}
}
set_ui_layout(app, p)
}
@@ -1043,6 +1043,37 @@ fn a_lock_request_with_no_layer_shown_fails_and_keeps_the_other_comps_lock() {
assert_eq!(h.state().ui.effect_controls_lock, kept, "the other comp's lock stays");
}
/// #3193: locked to a layer of another comp, the panel shows this comp's selection; a lock request
/// does not claim to lock that layer: it is an error and the other comp's lock stays.
#[test]
fn a_lock_request_while_locked_in_another_comp_fails() {
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 with a selected layer becomes active: the panel shows that layer.
h.state_mut().session.execute("comp.new", json!({"name": "Other", "width": 320, "height": 180, "frameRate": 30, "duration": 5})).unwrap();
let c = h.state_mut().session.execute("layer.newSolid", json!({"name": "C", "color": "#00ff00"})).unwrap()["layer"].as_u64().unwrap();
h.state_mut().session.state.selected_layers = vec![LayerId(c)];
h.run_steps(3);
assert_ne!(h.state().session.active_comp_id().map(|x| x.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]
+1 -1
View File
@@ -66,7 +66,7 @@ panel registers an element id; both are enforced by `crates/ui-egui/tests/it/ui_
|---|---|---|
| `ui.inspect` | `{}` | Window size, pixels per point, fps, serialized `UiState`, playback, time, active comp, selection, open dialog, last render ms, `keyboardFocus`, `windowFocused`, `workspaceStorageError`. |
| `ui.elements` | `{prefix?}` | `[{id, label, rect: [x, y, w, h]}]`: the widgets drawn in the last frame, in points. |
| `ui.set` | `{tool?, workspace?, theme?, focused?, viewer?, effectControlsLocked?, timeline?, menuBar?, home?}` | `viewer: {zoom (null = fit), res: Full\|Half\|Third\|Quarter\|Auto, pan: [x,y], grid, rulers, safeMargins, transparencyGrid}`; `effectControlsLocked: true` locks Effect Controls on the layer it shows (an error when it shows none), `false` unlocks it (the tab's lock icon, `panel.tab.EffectControls.lock`; `ui.inspect` reports it as `effect_controls_lock: {comp, layer}`); `timeline: {pps (null = fit), start, graphEditor, showModes, openLayers: [ids], openGroups: [uids]}`. |
| `ui.set` | `{tool?, workspace?, theme?, focused?, viewer?, effectControlsLocked?, timeline?, menuBar?, home?}` | `viewer: {zoom (null = fit), res: Full\|Half\|Third\|Quarter\|Auto, pan: [x,y], grid, rulers, safeMargins, transparencyGrid}`; `effectControlsLocked: true` locks Effect Controls on the layer it shows (an error when it shows none, or while it is locked to a layer of another composition), `false` unlocks it (the tab's lock icon, `panel.tab.EffectControls.lock`; `ui.inspect` reports it as `effect_controls_lock: {comp, layer}`); `timeline: {pps (null = fit), start, graphEditor, showModes, openLayers: [ids], openGroups: [uids]}`. |
| `ui.panel.show` / `ui.panel.close` | `{panel}` | Panels: `Project`, `Composition`, `Timeline`, `EffectControls`, `EffectsPresets`, `Info`, `Preview`, `Character`, `Paragraph`, `Align` and others. |
| `ui.playback` | `{action: play\|stop\|toggle\|status}` | `{playing, time, audio, levelsDb, peaksDb}` (audio: an audio preview is running; L/R meter levels and held peaks in dBFS) |
| `ui.resize` | `{width, height}` | Resizes the window (points). |