mirror of
https://github.com/storytold/effectcraft.git
synced 2026-11-15 06:42:31 +00:00
Effect Controls: lock the panel to the layer it shows (#3193)
The Effect Controls tab gets the lock icon the Composition viewers and the
Timeline have (panel.tab.EffectControls.lock). Locked, the panel and its
tab name keep the layer they showed while that layer's comp is active,
whatever is selected next, so a pick whip from another layer's property
can reach its parameters. An effect dropped on the locked panel goes to
its layer and leaves the selection alone; one applied to another layer
says the panel is locked. Deleting the layer or its comp, or replacing
the project, unlocks the panel, which follows the selection again. The
lock is UI state (UiState::effect_controls_lock) and agents set it with
ui.set {effectControlsLocked}.
Fixes #3193
This commit is contained in:
1 parent
bb2d2005ce
commit
f3a9afda14
10 files changed
+296
-17
No files matched your search
@@ -421,6 +421,7 @@ impl EffectcraftApp {
|
||||
crate::color_picker::note_dialog(&ctx, self.dialog);
|
||||
// After the shortcuts, menus and header, which can replace the project (New, Open).
|
||||
self.ui.timeline.scope_links(self.session.script_ui.project_epoch);
|
||||
panels::effect_controls::scope_lock(self);
|
||||
self.dock_area(ui, body);
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
prefs_live::dropped_files(self, ui, full);
|
||||
|
||||
@@ -281,6 +281,12 @@ 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");
|
||||
}
|
||||
set_ui_layout(app, p)
|
||||
}
|
||||
|
||||
|
||||
@@ -676,8 +676,8 @@ pub fn doc_tabs(
|
||||
out
|
||||
}
|
||||
|
||||
/// A viewer lock button (Composition viewers and the Timeline keep their comp while locked): a
|
||||
/// closed padlock while locked, an open one while unlocked.
|
||||
/// A panel lock button (Composition viewers and the Timeline keep their comp while locked,
|
||||
/// Effect Controls its layer): a closed padlock while locked, an open one while unlocked.
|
||||
/// Returns true when clicked. Automation: `panel.tab.<panel>.lock`.
|
||||
pub fn lock_button(ui: &mut egui::Ui, rect: Rect, panel: PanelKind, locked: bool, t: &Tokens, reg: &mut crate::automation::Registry) -> bool {
|
||||
let resp = ui.interact(rect, egui::Id::unique(("tab-lock", panel.id())), Sense::click());
|
||||
|
||||
@@ -333,8 +333,11 @@ impl EffectcraftApp {
|
||||
}
|
||||
}
|
||||
let Some(comp) = self.session.active_comp() else { return out };
|
||||
if let Some(l) = self.session.state.selected_layers.first().and_then(|id| comp.layer(*id)) {
|
||||
// Effect Controls names the layer it shows (the one it is locked to, if any).
|
||||
if let Some(l) = panels::effect_controls::shown_layer_id(self).and_then(|id| comp.layer(id)) {
|
||||
out.push((PanelKind::EffectControls, format!("{} {}", crate::i18n::command_label("window.panel", "Effect Controls"), l.name)));
|
||||
}
|
||||
if let Some(l) = self.session.state.selected_layers.first().and_then(|id| comp.layer(*id)) {
|
||||
out.push((PanelKind::Properties, format!("{}: {}", crate::i18n::command_label("window.panel", "Properties"), l.name)));
|
||||
}
|
||||
out
|
||||
@@ -492,7 +495,13 @@ impl EffectcraftApp {
|
||||
}
|
||||
}
|
||||
for p in locks {
|
||||
if !self.ui.locked_tabs.remove(&p.id()) {
|
||||
if p == PanelKind::EffectControls {
|
||||
// Locks on the layer the panel shows (nothing to lock without one).
|
||||
let on = self.ui.effect_controls_lock.is_none();
|
||||
if panels::effect_controls::set_lock(self, on) != on {
|
||||
self.ui.status = crate::i18n::tr("Select a layer to lock Effect Controls").to_string();
|
||||
}
|
||||
} else if !self.ui.locked_tabs.remove(&p.id()) {
|
||||
self.ui.locked_tabs.insert(p.id().to_string());
|
||||
}
|
||||
}
|
||||
@@ -638,10 +647,19 @@ fn dock_style(ui: &egui::Ui, t: &Tokens) -> DockStyle {
|
||||
style
|
||||
}
|
||||
|
||||
/// Panels with a viewer lock: the Composition viewers and the Timeline keep their comp while
|
||||
/// locked.
|
||||
/// Panels with a lock: the Composition viewers and the Timeline keep their comp while locked,
|
||||
/// Effect Controls its layer.
|
||||
fn has_lock(panel: PanelKind) -> bool {
|
||||
matches!(panel, PanelKind::Composition | PanelKind::Viewer(_) | PanelKind::Timeline)
|
||||
matches!(panel, PanelKind::Composition | PanelKind::Viewer(_) | PanelKind::Timeline | PanelKind::EffectControls)
|
||||
}
|
||||
|
||||
/// Whether `panel`'s tab lock is on. Effect Controls keeps the layer it is locked to
|
||||
/// (`UiState::effect_controls_lock`), the others are in `UiState::locked_tabs`.
|
||||
fn is_locked(app: &EffectcraftApp, panel: PanelKind) -> bool {
|
||||
match panel {
|
||||
PanelKind::EffectControls => app.ui.effect_controls_lock.is_some(),
|
||||
_ => app.ui.locked_tabs.contains(&panel.id()),
|
||||
}
|
||||
}
|
||||
|
||||
/// EffectCraft's side of the dock area for one frame.
|
||||
@@ -865,7 +883,7 @@ impl DockContent<PanelKind> for EffectContent<'_> {
|
||||
let menu = Rect::from_min_max(pos2((header.max.x - MENU_W).max(header.min.x), header.min.y), header.max);
|
||||
if has_lock(*active) {
|
||||
let lock = Rect::from_center_size(pos2(menu.min.x - LOCK_W / 2.0, header.center().y), vec2(14.0, 14.0));
|
||||
let locked = self.app.ui.locked_tabs.contains(&active.id());
|
||||
let locked = is_locked(self.app, *active);
|
||||
if dock::lock_button(ui, lock, *active, locked, &t, &mut self.app.auto) {
|
||||
self.locks.push(*active);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
//! Effect Controls: the selected layer's effects with all parameters, in After Effects'
|
||||
//! conventions:
|
||||
//!
|
||||
//! - the tab's lock icon keeps the panel on the layer it shows whatever is selected next (a
|
||||
//! pick whip from another layer's property can then reach its parameters); deleting that
|
||||
//! layer or replacing the project unlocks it (see [`set_lock`]). The lock holds while the
|
||||
//! locked layer's comp is the active comp: with another comp active the panel shows that
|
||||
//! comp's selection (its edits address layers of the active comp), and the locked layer
|
||||
//! again when its comp is back. The Effect menu of a right-click on empty space applies to
|
||||
//! the selected layers, as in After Effects;
|
||||
//! - effect headers with the fx switch, twirl, Reset and About…; click selects the effect
|
||||
//! (Edit ▸ Copy / Paste / Duplicate and Delete then act on it), drag reorders the stack, the
|
||||
//! context menu copies, pastes, duplicates, resets and removes;
|
||||
@@ -34,8 +41,8 @@ mod panel;
|
||||
mod rows;
|
||||
mod viewer;
|
||||
|
||||
pub(crate) use common::gesture_key;
|
||||
pub use common::{choose_files_after_apply, reveal_applied};
|
||||
pub use common::{choose_files_after_apply, reveal_applied, set_lock};
|
||||
pub(crate) use common::{gesture_key, scope_lock, shown_layer_id};
|
||||
pub use panel::{show, start_rename};
|
||||
pub(crate) use rows::toggle_pick;
|
||||
pub use viewer::viewer_hook;
|
||||
@@ -27,10 +27,66 @@ pub(super) fn effect_active(g: &PropGroup, layer: effectcraft_engine::project::L
|
||||
selected.iter().any(|(l, uid)| *l == layer && contains(g, *uid, 0))
|
||||
}
|
||||
|
||||
pub(super) fn selected_layer(app: &EffectcraftApp) -> Option<Layer> {
|
||||
/// The layer Effect Controls shows: the layer it is locked to (its tab's lock) while that layer's
|
||||
/// comp is the active comp, else the first selected layer of the active comp.
|
||||
pub(crate) fn shown_layer_id(app: &EffectcraftApp) -> Option<effectcraft_engine::project::LayerId> {
|
||||
let comp = app.session.active_comp()?;
|
||||
let id = app.session.state.selected_layers.first()?;
|
||||
comp.layer(*id).cloned()
|
||||
if let Some(lock) = app.ui.effect_controls_lock
|
||||
&& app.session.active_comp_id().is_some_and(|c| c.0 == lock.comp)
|
||||
&& comp.layer(effectcraft_engine::project::LayerId(lock.layer)).is_some()
|
||||
{
|
||||
return Some(effectcraft_engine::project::LayerId(lock.layer));
|
||||
}
|
||||
app.session.state.selected_layers.first().copied().filter(|id| comp.layer(*id).is_some())
|
||||
}
|
||||
|
||||
pub(super) fn shown_layer(app: &EffectcraftApp) -> Option<Layer> {
|
||||
let id = shown_layer_id(app)?;
|
||||
app.session.active_comp()?.layer(id).cloned()
|
||||
}
|
||||
|
||||
/// Lock Effect Controls on the layer it shows (`on`), or unlock it. Locking with no layer shown
|
||||
/// does nothing; locking while already locked keeps the layer it is locked to. Returns whether
|
||||
/// the panel is locked afterwards. The tab's lock icon and `ui.set {effectControlsLocked}`.
|
||||
pub fn set_lock(app: &mut EffectcraftApp, on: bool) -> bool {
|
||||
if !on {
|
||||
app.ui.effect_controls_lock = None;
|
||||
return false;
|
||||
}
|
||||
if app.ui.effect_controls_lock.is_some() {
|
||||
return true;
|
||||
}
|
||||
let Some(comp) = app.session.active_comp_id() else { return false };
|
||||
let Some(layer) = shown_layer_id(app) else { return false };
|
||||
app.ui.effect_controls_lock = Some(crate::state::EffectControlsLock { comp: comp.0, layer: layer.0 });
|
||||
app.ui.effect_controls_lock_epoch = app.session.script_ui.project_epoch;
|
||||
true
|
||||
}
|
||||
|
||||
/// Once a frame: a lock made in another project (New, Open: the ids start over) or on a layer or
|
||||
/// comp since deleted is dropped, whether or not Effect Controls is on screen.
|
||||
pub(crate) fn scope_lock(app: &mut EffectcraftApp) {
|
||||
let epoch = app.session.script_ui.project_epoch;
|
||||
if app.ui.effect_controls_lock_epoch != epoch {
|
||||
app.ui.effect_controls_lock_epoch = epoch;
|
||||
app.ui.effect_controls_lock = None;
|
||||
}
|
||||
prune_lock(app);
|
||||
}
|
||||
|
||||
/// A lock whose layer or comp no longer exists (deleted, or its comp deleted) is dropped, so the
|
||||
/// panel follows the selection again and its tab shows it unlocked. Another comp being active
|
||||
/// keeps the lock: the panel shows the locked layer again when its comp comes back.
|
||||
pub(super) fn prune_lock(app: &mut EffectcraftApp) {
|
||||
let Some(lock) = app.ui.effect_controls_lock else { return };
|
||||
let alive = app
|
||||
.session
|
||||
.project
|
||||
.comp(effectcraft_engine::project::ItemId(lock.comp))
|
||||
.is_some_and(|c| c.layer(effectcraft_engine::project::LayerId(lock.layer)).is_some());
|
||||
if !alive {
|
||||
app.ui.effect_controls_lock = None;
|
||||
}
|
||||
}
|
||||
|
||||
/// An effect was just applied (`effect.apply` / `effect.applyLast` with `params`): Effect
|
||||
@@ -39,10 +95,14 @@ pub(super) fn selected_layer(app: &EffectcraftApp) -> Option<Layer> {
|
||||
pub fn reveal_applied(app: &mut EffectcraftApp, params: &serde_json::Value) {
|
||||
// `effect.apply` selects the new effect on (the last of) the layers it went to.
|
||||
let target = app.session.state.selected_props.last().map(|(l, _)| *l);
|
||||
// Effect Controls locked to a layer of the active comp keeps showing that layer.
|
||||
let locked = app.ui.effect_controls_lock.filter(|k| app.session.active_comp_id().is_some_and(|c| c.0 == k.comp));
|
||||
if let Some(l) = target
|
||||
&& !app.session.state.selected_layers.contains(&l)
|
||||
&& params.get("comp").is_none()
|
||||
&& app.session.active_comp().is_some_and(|c| c.layer(l).is_some())
|
||||
// Dropped on the locked panel: it shows that layer already, the selection stays.
|
||||
&& locked.is_none_or(|k| k.layer != l.0)
|
||||
{
|
||||
// Show that layer (dropped on an unselected one), keeping the new effect selected.
|
||||
let props = app.session.state.selected_props.clone();
|
||||
@@ -51,6 +111,14 @@ pub fn reveal_applied(app: &mut EffectcraftApp, params: &serde_json::Value) {
|
||||
Err(e) => app.ui.status = e.to_string(),
|
||||
}
|
||||
}
|
||||
// The effect went to another layer than the one the locked panel shows: say why it isn't
|
||||
// there (After Effects opens another Effect Controls tab).
|
||||
if let (Some(k), Some(l)) = (locked, target)
|
||||
&& k.layer != l.0
|
||||
&& let Some(name) = app.session.active_comp().and_then(|c| c.layer(effectcraft_engine::project::LayerId(k.layer))).map(|x| x.name.clone())
|
||||
{
|
||||
app.ui.status = crate::i18n::tr_args("Effect Controls is locked to {}", &[&name]);
|
||||
}
|
||||
app.raise_panel(crate::dock::PanelKind::EffectControls);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use super::common::{Actions, PARAM_NAME_X, ROW, effect_active, selected_layer};
|
||||
use super::common::{Actions, PARAM_NAME_X, ROW, effect_active, prune_lock, shown_layer};
|
||||
use super::editors::{curves_editor, editor_height, histogram_only, levels_editor, object_matte_editor, roto_editor, warp_editor};
|
||||
use super::rows::{FxScope, group_rows, toggle_closed};
|
||||
use super::{Align2, Color32, EffectcraftApp, EvalCtx, GroupKind, Icon, Layer, PropGroup, Rect, Sense, Stroke, Tokens, json, pos2, vec2, widgets};
|
||||
@@ -158,7 +158,8 @@ fn about_button(app: &mut EffectcraftApp, ui: &mut egui::Ui, bp: &egui::Painter,
|
||||
}
|
||||
}
|
||||
|
||||
/// The Effect Controls panel for the selected layer: a header with the comp and layer names, then
|
||||
/// The Effect Controls panel for the selected layer (or the layer the panel is locked to, see
|
||||
/// `common::shown_layer_id`): a header with the comp and layer names, then
|
||||
/// one header per effect (Fx toggle, twirl, rename, Reset, About, label bar, reorder drag and its
|
||||
/// right-click menu) followed by that effect's custom editor and its property rows. Dragging
|
||||
/// effects or presets onto the panel applies them; a right-click on empty space opens the Effect
|
||||
@@ -168,7 +169,9 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
||||
let t = app.tokens;
|
||||
let p = ui.painter().with_clip_rect(rect);
|
||||
let ctx = ui.ctx().clone();
|
||||
let Some(layer) = selected_layer(app) else {
|
||||
// A locked layer that was deleted unlocks the panel, which then follows the selection.
|
||||
prune_lock(app);
|
||||
let Some(layer) = shown_layer(app) else {
|
||||
p.text(rect.center(), Align2::CENTER_CENTER, crate::i18n::tr("Select a layer to see its effects"), Tokens::ui(12.0), t.text_faint);
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -594,6 +594,14 @@ impl Default for AnimToolsState {
|
||||
}
|
||||
}
|
||||
|
||||
/// The layer a locked Effect Controls panel keeps showing (the tab's lock icon, as After Effects'
|
||||
/// panel lock): comp item id and layer id.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct EffectControlsLock {
|
||||
pub comp: u64,
|
||||
pub layer: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum ProjectStatus {
|
||||
@@ -651,6 +659,17 @@ pub struct UiState {
|
||||
/// bring a locked panel forward (the tab's lock icon, as After Effects' viewer lock).
|
||||
#[serde(default)]
|
||||
pub locked_tabs: BTreeSet<String>,
|
||||
/// Effect Controls locked to a layer (its tab's lock icon): the panel keeps showing that
|
||||
/// layer whatever is selected while its comp is the active comp. Cleared when the layer or
|
||||
/// its comp is deleted, and when the project is replaced (see
|
||||
/// `panels::effect_controls::set_lock`).
|
||||
#[serde(default)]
|
||||
pub effect_controls_lock: Option<EffectControlsLock>,
|
||||
/// The project the lock was made in (`ScriptUi::project_epoch`): a new or opened project
|
||||
/// reuses comp and layer ids, so its lock is dropped (see
|
||||
/// `panels::effect_controls::scope_lock`).
|
||||
#[serde(skip)]
|
||||
pub effect_controls_lock_epoch: u64,
|
||||
/// The comp each Composition viewer shows, by viewer id (0 = the Composition panel; see
|
||||
/// `panels::viewers`).
|
||||
#[serde(default)]
|
||||
@@ -773,6 +792,8 @@ impl Default for UiState {
|
||||
native_outer: Vec::new(),
|
||||
maximized: None,
|
||||
locked_tabs: BTreeSet::new(),
|
||||
effect_controls_lock: None,
|
||||
effect_controls_lock_epoch: 0,
|
||||
viewers: Default::default(),
|
||||
active_viewer: 0,
|
||||
focused: PanelKind::Composition,
|
||||
|
||||
@@ -912,3 +912,158 @@ fn double_click(h: &mut Harness<'_, EffectcraftApp>, pos: egui::Pos2) {
|
||||
}
|
||||
h.run_steps(2);
|
||||
}
|
||||
|
||||
/// Two solids in one comp, `A` with a Point Control and `B` with an Angle Control, `A` selected
|
||||
/// and Effect Controls open: the app, A, B and their effects.
|
||||
fn two_layers() -> (EffectcraftApp, u64, u64, u64, u64) {
|
||||
let mut s = Session::default();
|
||||
s.execute("comp.new", json!({"name": "Main", "width": 320, "height": 180, "frameRate": 30, "duration": 5})).unwrap();
|
||||
let a = s.execute("layer.newSolid", json!({"name": "A", "color": "#ff0000"})).unwrap()["layer"].as_u64().unwrap();
|
||||
let b = s.execute("layer.newSolid", json!({"name": "B", "color": "#0000ff"})).unwrap()["layer"].as_u64().unwrap();
|
||||
let fa = s.execute("effect.apply", json!({"layers": [a], "effect": "Point Control"})).unwrap()["effects"][0].as_u64().unwrap();
|
||||
let fb = s.execute("effect.apply", json!({"layers": [b], "effect": "Angle Control"})).unwrap()["effects"][0].as_u64().unwrap();
|
||||
s.state.selected_layers = vec![LayerId(a)];
|
||||
s.state.selected_props.clear();
|
||||
let mut app = EffectcraftApp::new(s);
|
||||
app.show_panel(PanelKind::EffectControls);
|
||||
(app, a, b, fa, fb)
|
||||
}
|
||||
|
||||
/// Whether Effect Controls drew effect `fx`'s header in the last frame (not the frame before:
|
||||
/// a stale id must not pass).
|
||||
fn shows_effect(h: &Harness<'_, EffectcraftApp>, fx: u64) -> bool {
|
||||
let id = format!("effectControls.effect.{fx}");
|
||||
h.state().auto.elements.iter().any(|e| e.id == id)
|
||||
}
|
||||
|
||||
fn select_layer(h: &mut Harness<'_, EffectcraftApp>, layer: u64) {
|
||||
h.state_mut().session.execute("layer.select", json!({"layers": [layer]})).unwrap();
|
||||
h.run_steps(3);
|
||||
}
|
||||
|
||||
/// #3193: the Effect Controls tab's lock keeps the panel (and its tab name) on the layer it showed
|
||||
/// when locked whatever is selected next, through undo; deleting that layer unlocks it without a
|
||||
/// crash and the panel follows the selection again.
|
||||
#[test]
|
||||
fn locked_effect_controls_keep_their_layer_until_it_is_deleted() {
|
||||
let (app, a, b, fa, fb) = two_layers();
|
||||
let mut h = Harness::builder().with_size(egui::vec2(1600.0, 1000.0)).build_eframe(|_| app);
|
||||
h.run_steps(4);
|
||||
assert!(shows_effect(&h, fa) && !shows_effect(&h, fb), "A is shown before locking");
|
||||
let comp = h.state().session.active_comp_id().unwrap().0;
|
||||
let lock = rect_of(&h, "panel.tab.EffectControls.lock").center();
|
||||
click(&mut h, lock);
|
||||
assert_eq!(h.state().ui.effect_controls_lock, Some(effectcraft_ui_egui::state::EffectControlsLock { comp, layer: a }));
|
||||
// Selecting B keeps A in the panel and its tab.
|
||||
select_layer(&mut h, b);
|
||||
assert!(shows_effect(&h, fa) && !shows_effect(&h, fb), "the locked panel keeps A");
|
||||
let tab = h.state().auto.elements.iter().find(|e| e.id == "panel.tab.EffectControls").map(|e| e.label.clone()).unwrap_or_default();
|
||||
assert!(tab.ends_with(" A"), "the tab names the locked layer: {tab:?}");
|
||||
// An edit and its undo leave the lock alone.
|
||||
h.state_mut().session.execute("prop.set", json!({"layer": b, "path": "transform/opacity", "value": 50.0})).unwrap();
|
||||
h.state_mut().session.execute("edit.undo", json!({})).unwrap();
|
||||
h.run_steps(3);
|
||||
assert!(shows_effect(&h, fa) && !shows_effect(&h, fb), "undo keeps the lock");
|
||||
// The lock state is UI state: it survives a serde round trip.
|
||||
let ui = serde_json::to_value(&h.state().ui).unwrap();
|
||||
let back: effectcraft_ui_egui::state::UiState = serde_json::from_value(ui).unwrap();
|
||||
assert_eq!(back.effect_controls_lock, h.state().ui.effect_controls_lock);
|
||||
// Unlocking follows the selection again.
|
||||
let lock = rect_of(&h, "panel.tab.EffectControls.lock").center();
|
||||
click(&mut h, lock);
|
||||
assert_eq!(h.state().ui.effect_controls_lock, None);
|
||||
assert!(shows_effect(&h, fb) && !shows_effect(&h, fa), "unlocked, the panel shows the selection");
|
||||
// Lock on A again, select B, delete A: no crash, the lock is gone, B is shown.
|
||||
select_layer(&mut h, a);
|
||||
let lock = rect_of(&h, "panel.tab.EffectControls.lock").center();
|
||||
click(&mut h, lock);
|
||||
assert_eq!(h.state().ui.effect_controls_lock.map(|l| l.layer), Some(a));
|
||||
select_layer(&mut h, b);
|
||||
h.state_mut().session.execute("edit.clear", json!({"layers": [a]})).unwrap();
|
||||
h.run_steps(3);
|
||||
assert_eq!(h.state().ui.effect_controls_lock, None, "deleting the locked layer unlocks the panel");
|
||||
assert!(!shows_effect(&h, fa), "A is gone from the panel");
|
||||
// Deleting clears the selection; the panel follows the next one.
|
||||
select_layer(&mut h, b);
|
||||
assert!(shows_effect(&h, fb), "the panel follows the selection");
|
||||
// Undoing the delete brings A back without bringing back (or breaking) the lock.
|
||||
h.state_mut().session.execute("edit.undo", json!({})).unwrap();
|
||||
h.run_steps(3);
|
||||
assert_eq!(h.state().ui.effect_controls_lock, None);
|
||||
let selected = h.state().session.state.selected_layers.first().map(|l| l.0);
|
||||
let (on, off) = if selected == Some(a) { (fa, fb) } else { (fb, fa) };
|
||||
assert!(shows_effect(&h, on) && !shows_effect(&h, off), "the panel shows the selection ({selected:?})");
|
||||
}
|
||||
|
||||
/// #3193: a new or opened project reuses comp and layer ids, so a lock does not carry into it,
|
||||
/// even onto a layer with the locked one's ids.
|
||||
#[test]
|
||||
fn replacing_the_project_unlocks_effect_controls() {
|
||||
let (app, a, b, _, _) = two_layers();
|
||||
let mut h = Harness::builder().with_size(egui::vec2(1600.0, 1000.0)).build_eframe(|_| app);
|
||||
h.run_steps(4);
|
||||
let lock = rect_of(&h, "panel.tab.EffectControls.lock").center();
|
||||
click(&mut h, lock);
|
||||
assert_eq!(h.state().ui.effect_controls_lock.map(|l| l.layer), Some(a));
|
||||
select_layer(&mut h, b);
|
||||
// The same project opened again: every id is alive.
|
||||
let project = (*h.state().session.project).clone();
|
||||
h.state_mut().session.replace_project(project, None);
|
||||
h.run_steps(3);
|
||||
assert_eq!(h.state().ui.effect_controls_lock, None, "the lock belongs to the replaced project");
|
||||
}
|
||||
|
||||
/// #3193: an effect dropped on the locked panel goes to the locked layer and leaves the selection
|
||||
/// alone; one applied to another layer says why the panel doesn't show it.
|
||||
#[test]
|
||||
fn applying_an_effect_while_locked_keeps_the_selection() {
|
||||
let (app, a, b, fa, _) = two_layers();
|
||||
let mut h = Harness::builder().with_size(egui::vec2(1600.0, 1000.0)).build_eframe(|_| app);
|
||||
h.run_steps(4);
|
||||
let lock = rect_of(&h, "panel.tab.EffectControls.lock").center();
|
||||
click(&mut h, lock);
|
||||
select_layer(&mut h, b);
|
||||
let ctx = h.ctx.clone();
|
||||
effectcraft_ui_egui::menus::invoke(h.state_mut(), &ctx, "effect.apply", json!({"effect": "Color Control", "layers": [a]})).unwrap();
|
||||
h.run_steps(3);
|
||||
assert_eq!(h.state().session.state.selected_layers, vec![LayerId(b)], "the selection stays on B");
|
||||
assert!(shows_effect(&h, fa), "the panel still shows A");
|
||||
assert_eq!(h.state().ui.effect_controls_lock.map(|l| l.layer), Some(a));
|
||||
// Applied to the selected B: the panel stays on A and says so.
|
||||
h.state_mut().ui.status.clear();
|
||||
effectcraft_ui_egui::menus::invoke(h.state_mut(), &ctx, "effect.apply", json!({"effect": "Color Control", "layers": [b]})).unwrap();
|
||||
h.run_steps(3);
|
||||
assert!(shows_effect(&h, fa));
|
||||
assert_eq!(h.state().ui.status, "Effect Controls is locked to A");
|
||||
}
|
||||
|
||||
/// #3193: agents lock Effect Controls with `ui.set {effectControlsLocked}`; with no layer shown
|
||||
/// there is nothing to lock.
|
||||
#[test]
|
||||
fn effect_controls_lock_over_the_control_channel() {
|
||||
use effectcraft_ui_egui::control::{self, ControlRequest, Outcome};
|
||||
fn send(h: &mut Harness<'_, EffectcraftApp>, params: serde_json::Value) -> serde_json::Value {
|
||||
let ctx = h.ctx.clone();
|
||||
let (req, _rx) = ControlRequest::new("ui.set", params);
|
||||
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, b, fa, fb) = 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, json!({"effectControlsLocked": true}))["ok"], true);
|
||||
assert_eq!(h.state().ui.effect_controls_lock.map(|l| l.layer), Some(a));
|
||||
select_layer(&mut h, b);
|
||||
assert!(shows_effect(&h, fa) && !shows_effect(&h, fb));
|
||||
assert_eq!(send(&mut h, json!({"effectControlsLocked": false}))["ok"], true);
|
||||
h.run_steps(3);
|
||||
assert!(shows_effect(&h, fb) && !shows_effect(&h, fa));
|
||||
// Nothing selected: nothing to lock.
|
||||
h.state_mut().session.state.selected_layers.clear();
|
||||
h.run_steps(2);
|
||||
assert_eq!(send(&mut h, json!({"effectControlsLocked": true}))["ok"], false);
|
||||
assert_eq!(h.state().ui.effect_controls_lock, None);
|
||||
}
|
||||
@@ -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?, timeline?, menuBar?, home?}` | `viewer: {zoom (null = fit), res: Full\|Half\|Third\|Quarter\|Auto, pan: [x,y], grid, rulers, safeMargins, transparencyGrid}`; `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), `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). |
|
||||
|
||||
Reference in new issue
Block a user