effectcraft/crates/ui-egui/tests/ui_effect_controls.rs

168 lines
7.5 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! Headless Effect Controls checks (egui_kittest): the AE-style widgets register their
//! automation ids, the on-viewer effect point control appears for the selected effect, and the
//! crosshair / eyedropper picks set parameters from a viewer click.
use effectcraft_engine::Session;
use effectcraft_engine::keyframe::Value as KV;
use effectcraft_engine::project::{Layer, LayerId};
use effectcraft_ui_egui::EffectcraftApp;
use effectcraft_ui_egui::dock::PanelKind;
use effectcraft_ui_egui::state::FxPick;
use egui_kittest::Harness;
use serde_json::json;
struct Ids {
small: u64,
point: u64,
color: u64,
angle: u64,
layer_ctl: u64,
curves: u64,
levels: u64,
point_fx: u64,
}
fn layer(s: &Session, id: u64) -> Layer {
s.active_comp().unwrap().layer(LayerId(id)).unwrap().clone()
}
/// A 640×360 comp: a full-frame green solid under a 200×100 solid scaled 200% at the centre,
/// carrying controls, Curves and Levels (Individual Controls).
fn app() -> (EffectcraftApp, Ids) {
let mut s = Session::default();
s.execute("comp.new", json!({"name": "Main", "width": 640, "height": 360, "frameRate": 30, "duration": 10})).unwrap();
s.execute("layer.newSolid", json!({"name": "Back", "color": "#20c040"})).unwrap();
let small = s.execute("layer.newSolid", json!({"name": "Small", "color": "#ff0000", "width": 200, "height": 100})).unwrap()["layer"].as_u64().unwrap();
s.execute("prop.set", json!({"layer": small, "path": "transform/scale", "value": [200.0, 200.0]})).unwrap();
let mut fx = |name: &str| s.execute("effect.apply", json!({"layers": [small], "effect": name})).unwrap()["effects"][0].as_u64().unwrap();
let point_fx = fx("Point Control");
fx("Color Control");
fx("Angle Control");
fx("Layer Control");
let curves = fx("Curves");
let levels = fx("Levels (Individual Controls)");
let l = layer(&s, small);
let fxg = l.effects().unwrap();
let param = |effect: u64, id: &str| fxg.groups().find(|g| g.uid == effect).and_then(|g| g.get(id)).map(|p| p.uid).unwrap();
let find = |m: &str, id: &str| fxg.groups().find(|g| g.match_id == m).and_then(|g| g.get(id)).map(|p| p.uid).unwrap();
let ids = Ids {
small,
point: param(point_fx, "point"),
color: find("ec.control.color", "color"),
angle: find("ec.control.angle", "angle"),
layer_ctl: find("ec.control.layer", "layer"),
curves,
levels,
point_fx,
};
s.state.selected_layers = vec![LayerId(small)];
s.state.selected_props.clear();
let mut app = EffectcraftApp::new(s);
app.show_panel(PanelKind::EffectControls);
(app, ids)
}
fn settle(h: &mut Harness<'_, EffectcraftApp>) {
for _ in 0..600 {
h.step();
if h.state().frames.inflight() == 0 && h.state().frames.last_ms.lock().map(|v| *v > 0.0).unwrap_or(false) {
break;
}
std::thread::sleep(std::time::Duration::from_millis(20));
}
for _ in 0..4 {
h.step();
}
}
fn ids(h: &Harness<'_, EffectcraftApp>) -> Vec<String> {
h.state().auto.previous.iter().chain(h.state().auto.elements.iter()).map(|e| e.id.clone()).collect()
}
fn click(h: &mut Harness<'_, EffectcraftApp>, pos: egui::Pos2) {
h.event(egui::Event::PointerMoved(pos));
h.step();
h.event(egui::Event::PointerButton { pos, button: egui::PointerButton::Primary, pressed: true, modifiers: Default::default() });
h.step();
h.event(egui::Event::PointerButton { pos, button: egui::PointerButton::Primary, pressed: false, modifiers: Default::default() });
h.step();
h.step();
}
#[test]
fn effect_controls_widgets_register_automation_ids() {
let (app, x) = app();
let mut h = Harness::builder().with_size(egui::vec2(1700.0, 1100.0)).build_eframe(|_| app);
settle(&mut h);
let have = ids(&h);
let want = [
format!("effectControls.prop.{}.crosshair", x.point),
format!("effectControls.prop.{}.eyedropper", x.color),
format!("effectControls.prop.{}.twirl", x.angle),
format!("effectControls.prop.{}.source", x.layer_ctl),
format!("effectControls.effect.{}.curves.graph", x.curves),
format!("effectControls.effect.{}.curves.channel", x.curves),
format!("effectControls.effect.{}.curves.reset", x.curves),
format!("effectControls.effect.{}.reset", x.point_fx),
];
let missing: Vec<&String> = want.iter().filter(|w| !have.contains(w)).collect();
assert!(missing.is_empty(), "missing {missing:?}");
// Levels sits below: collapse Curves to bring it into view.
h.state_mut().ui.fx_closed.insert(x.curves);
h.step();
h.step();
let have = ids(&h);
let want =
["histogram", "channel", "inBlack", "gamma", "inWhite", "outBlack", "outWhite"].map(|k| format!("effectControls.effect.{}.levels.{k}", x.levels));
let missing: Vec<&String> = want.iter().filter(|w| !have.contains(w)).collect();
assert!(missing.is_empty(), "missing {missing:?}");
// The angle reads AE-style.
let angle =
h.state().auto.previous.iter().chain(h.state().auto.elements.iter()).find(|e| e.id == format!("effectControls.prop.{}.value", x.angle)).cloned();
assert_eq!(angle.map(|e| e.label), Some("0x+0.0°".to_string()));
// Twirling the angle open shows its dial.
h.state_mut().ui.fx_slider_open.insert(x.angle);
h.step();
h.step();
assert!(ids(&h).contains(&format!("effectControls.prop.{}.dial", x.angle)));
// No effect selected: no on-viewer point control. Selecting the effect shows it.
let pt_id = format!("viewer.effectPoint.{}", x.point);
assert!(!ids(&h).contains(&pt_id));
h.state_mut().session.state.selected_props = vec![(LayerId(x.small), x.point_fx)];
h.step();
h.step();
assert!(ids(&h).contains(&pt_id), "effect point control shown for the selected effect");
}
#[test]
fn crosshair_and_eyedropper_pick_from_the_viewer() {
let (app, x) = app();
let mut h = Harness::builder().with_size(egui::vec2(1700.0, 1100.0)).build_eframe(|_| app);
settle(&mut h);
// Crosshair: a click at comp (330, 190) is layer (105, 55) on the 200%-scaled layer
// (anchor (100, 50) at position (320, 180)).
h.state_mut().ui.fx_pick = Some(FxPick { kind: "point".into(), layer: x.small, prop: x.point, name: "Point".into() });
h.step();
assert!(ids(&h).contains(&"viewer.fxPick".to_string()));
let pos = effectcraft_ui_egui::panels::viewer::comp_to_screen(&h.ctx, [330.0, 190.0]).unwrap();
click(&mut h, pos);
assert!(h.state().ui.fx_pick.is_none(), "the pick is used up");
let v = layer(&h.state().session, x.small).effects().unwrap().find(x.point).unwrap().value.clone();
let KV::Vec2(p) = v else { panic!("{v:?}") };
// One viewer point is up to 1/zoom comp pixels.
let tol = 1.0 / effectcraft_ui_egui::panels::viewer::last_fit(&h.ctx) as f64;
assert!((p[0] - 105.0).abs() <= tol && (p[1] - 55.0).abs() <= tol, "{p:?}");
// Eyedropper over the green background.
h.state_mut().ui.fx_pick = Some(FxPick { kind: "color".into(), layer: x.small, prop: x.color, name: "Color".into() });
h.step();
let pos = effectcraft_ui_egui::panels::viewer::comp_to_screen(&h.ctx, [24.0, 24.0]).unwrap();
click(&mut h, pos);
let v = layer(&h.state().session, x.small).effects().unwrap().find(x.color).unwrap().value.clone();
let KV::Color(c) = v else { panic!("{v:?}") };
let want = [0x20 as f64 / 255.0, 0xc0 as f64 / 255.0, 0x40 as f64 / 255.0];
for k in 0..3 {
assert!((c[k] - want[k]).abs() < 0.02, "{c:?} vs {want:?}");
}
assert_eq!(c[3], 1.0);
}