mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-30 04:57:54 -06:00
Merge pull request #1314 from tylerjenningsw/fix/expression-value-color
UI: colour expression-driven numeric values consistently (refs #1072)
This commit is contained in:
@@ -88,6 +88,11 @@ struct PropCx<'a> {
|
||||
}
|
||||
|
||||
impl PropCx<'_> {
|
||||
/// The tokens of the value fields: red while an expression drives the parameter (#1306).
|
||||
fn value_tokens(&self) -> Tokens {
|
||||
self.t.for_value(self.prop.has_expression())
|
||||
}
|
||||
|
||||
fn set(&self, actions: &mut Actions, v: serde_json::Value) {
|
||||
let (layer, uid, merge, scrub) = (self.layer, self.uid, format!("ec-{}", self.uid), self.scrub);
|
||||
actions.push(("prop.set".into(), json!({"layer": layer.id.0, "prop": uid, "value": v, "merge": merge, "selected": true, "offset": scrub})))
|
||||
@@ -243,8 +248,8 @@ fn value_angle(app: &mut EffectcraftApp, ui: &mut egui::Ui, cx: &PropCx, value:
|
||||
let Value::Scalar(v) = value else {
|
||||
return;
|
||||
};
|
||||
let PropCx { t, uid, cy, vx, prop, .. } = *cx;
|
||||
let (rr, dr, nv) = fw::angle_field(ui, pos2(vx, cy - 9.0), egui::Id::unique(("ec-v", uid)), *v, 1, &t);
|
||||
let PropCx { uid, cy, vx, prop, .. } = *cx;
|
||||
let (rr, dr, nv) = fw::angle_field(ui, pos2(vx, cy - 9.0), egui::Id::unique(("ec-v", uid)), *v, 1, &cx.value_tokens());
|
||||
app.auto.add(&format!("effectControls.prop.{uid}.value"), rr.union(dr), &fw::format_angle(*v, 1));
|
||||
app.auto.add(&format!("effectControls.prop.{uid}.revolutions"), rr, &prop.name);
|
||||
if let Some(nv) = nv {
|
||||
@@ -257,7 +262,7 @@ fn value_scalar(app: &mut EffectcraftApp, ui: &mut egui::Ui, cx: &PropCx, value:
|
||||
let Value::Scalar(v) = value else {
|
||||
return;
|
||||
};
|
||||
let PropCx { t, uid, cy, vx, prop, .. } = *cx;
|
||||
let PropCx { uid, cy, vx, prop, .. } = *cx;
|
||||
let (min, max, dec) = match &prop.ui {
|
||||
ParamUi::Slider { min, max, decimals, .. } => (*min, *max, *decimals as usize),
|
||||
_ => (-1e9, 1e9, 1),
|
||||
@@ -273,7 +278,7 @@ fn value_scalar(app: &mut EffectcraftApp, ui: &mut egui::Ui, cx: &PropCx, value:
|
||||
egui::Id::unique(("ec-v", uid)),
|
||||
*v,
|
||||
&widgets::HotSpec { speed, range: (min, max), decimals: dec.max(if dec == 0 { 0 } else { 1 }), suffix },
|
||||
&t,
|
||||
&cx.value_tokens(),
|
||||
);
|
||||
app.auto.add(&format!("effectControls.prop.{uid}.value"), vr, &prop.name);
|
||||
if let Some(nv) = nv {
|
||||
@@ -290,6 +295,7 @@ fn value_vector(app: &mut EffectcraftApp, ui: &mut egui::Ui, cx: &PropCx, value:
|
||||
let PropCx { t, uid, cy, vx, layer, prop, .. } = *cx;
|
||||
let c = value.components();
|
||||
let n = shown_dims(layer, prop, value);
|
||||
let vt = cx.value_tokens();
|
||||
let mut x = vx;
|
||||
if matches!(prop.ui, ParamUi::Point | ParamUi::Point3) {
|
||||
let cr = Rect::from_center_size(pos2(x + 8.0, cy), vec2(18.0, 18.0));
|
||||
@@ -310,7 +316,7 @@ fn value_vector(app: &mut EffectcraftApp, ui: &mut egui::Ui, cx: &PropCx, value:
|
||||
egui::Id::unique(("ec-v", uid, d)),
|
||||
c[d],
|
||||
&widgets::HotSpec { speed: 1.0, range: (-1e9, 1e9), decimals: 1, suffix: "" },
|
||||
&t,
|
||||
&vt,
|
||||
);
|
||||
app.auto.add(&format!("effectControls.prop.{uid}.value.{d}"), vr, &prop.name);
|
||||
dims.push(vr);
|
||||
|
||||
@@ -200,6 +200,8 @@ fn triangle(p: &egui::Painter, c: egui::Pos2, left: bool, col: Color32) {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn transform_row(app: &mut EffectcraftApp, ui: &mut egui::Ui, p: &egui::Painter, layer: &Layer, pr: &Property, ectx: &EvalCtx, r: Rect, actions: &mut Actions) {
|
||||
let t = app.tokens;
|
||||
// The values themselves: red while an expression drives the property (#1306).
|
||||
let vt = t.for_value(pr.has_expression());
|
||||
let cy = r.center().y;
|
||||
let uid = pr.uid;
|
||||
let lid = layer.id.0;
|
||||
@@ -276,7 +278,7 @@ fn transform_row(app: &mut EffectcraftApp, ui: &mut egui::Ui, p: &egui::Painter,
|
||||
match &value {
|
||||
Value::Scalar(v) if pr.name == "Rotation" || pr.name.ends_with(" Rotation") => {
|
||||
let (_, deg) = super::fx_widgets::split_angle(*v);
|
||||
let (rr, dr, nv) = super::fx_widgets::angle_field(ui, pos2(vx, cy - 9.0), egui::Id::unique(("props-v", uid)), *v, decimals(deg), &t);
|
||||
let (rr, dr, nv) = super::fx_widgets::angle_field(ui, pos2(vx, cy - 9.0), egui::Id::unique(("props-v", uid)), *v, decimals(deg), &vt);
|
||||
app.auto.add(&format!("{base}.value"), dr, &pr.name);
|
||||
app.auto.add(&format!("{base}.revolutions"), rr, &pr.name);
|
||||
if let Some(nv) = nv {
|
||||
@@ -291,7 +293,7 @@ fn transform_row(app: &mut EffectcraftApp, ui: &mut egui::Ui, p: &egui::Painter,
|
||||
egui::Id::unique(("props-v", uid)),
|
||||
*v,
|
||||
&widgets::HotSpec { speed: 0.5, range, decimals: decimals(*v), suffix },
|
||||
&t,
|
||||
&vt,
|
||||
);
|
||||
app.auto.add(&format!("{base}.value"), vr, &pr.name);
|
||||
if let Some(nv) = nv {
|
||||
@@ -323,7 +325,7 @@ fn transform_row(app: &mut EffectcraftApp, ui: &mut egui::Ui, p: &egui::Painter,
|
||||
egui::Id::unique(("props-v", uid, d)),
|
||||
c[d],
|
||||
&widgets::HotSpec { speed: 1.0, range: (-1e9, 1e9), decimals: decimals(c[d]), suffix },
|
||||
&t,
|
||||
&vt,
|
||||
);
|
||||
app.auto.add(&format!("{base}.value.{d}"), vr, &pr.name);
|
||||
if let Some(nv) = nv {
|
||||
|
||||
@@ -5530,7 +5530,8 @@ fn value_editor(app: &mut EffectcraftApp, ui: &mut egui::Ui, ve: &ValueEdit<'_>,
|
||||
fn scalar_editor(app: &mut EffectcraftApp, ui: &mut egui::Ui, ve: &ValueEdit<'_>, actions: &mut Vec<(String, serde_json::Value)>, value: &Value) {
|
||||
let Value::Scalar(v) = value else { return };
|
||||
let (layer, prop, at, uid) = (ve.layer, ve.prop, ve.at, ve.uid);
|
||||
let t = app.tokens;
|
||||
// The values themselves: red while an expression drives the property (#1306).
|
||||
let vt = app.tokens.for_value(prop.has_expression());
|
||||
let (x, y) = (at.x, at.y - 9.0);
|
||||
let (min, max, dec, speed) = match &prop.ui {
|
||||
ParamUi::Slider { min, max, decimals, .. } => (*min, *max, *decimals as usize, widgets::slider_scrub_speed(&prop.ui)),
|
||||
@@ -5538,7 +5539,7 @@ fn scalar_editor(app: &mut EffectcraftApp, ui: &mut egui::Ui, ve: &ValueEdit<'_>
|
||||
_ => (-1e9, 1e9, 1, 1.0),
|
||||
};
|
||||
if matches!(prop.ui, ParamUi::Angle) {
|
||||
let (rr, dr, nv) = super::fx_widgets::angle_field(ui, pos2(x, y), egui::Id::unique(("v", uid, 0)), *v, 1, &t);
|
||||
let (rr, dr, nv) = super::fx_widgets::angle_field(ui, pos2(x, y), egui::Id::unique(("v", uid, 0)), *v, 1, &vt);
|
||||
app.auto.add(&format!("timeline.prop.{uid}.value"), dr, &prop.name);
|
||||
app.auto.add(&format!("timeline.prop.{uid}.revolutions"), rr, &prop.name);
|
||||
if let Some(nv) = nv {
|
||||
@@ -5554,7 +5555,7 @@ fn scalar_editor(app: &mut EffectcraftApp, ui: &mut egui::Ui, ve: &ValueEdit<'_>
|
||||
egui::Id::unique(("v", uid, 0)),
|
||||
*v,
|
||||
&widgets::HotSpec { speed, range: (min, max), decimals: dec.max(1), suffix },
|
||||
&t,
|
||||
&vt,
|
||||
);
|
||||
app.auto.add(&format!("timeline.prop.{uid}.value"), r, &prop.name);
|
||||
if let Some(nv) = nv {
|
||||
@@ -5567,6 +5568,7 @@ fn scalar_editor(app: &mut EffectcraftApp, ui: &mut egui::Ui, ve: &ValueEdit<'_>
|
||||
fn vector_editor(app: &mut EffectcraftApp, ui: &mut egui::Ui, ve: &ValueEdit<'_>, actions: &mut Vec<(String, serde_json::Value)>, value: &Value) -> Vec<Rect> {
|
||||
let (p, layer, prop, at, uid) = (ve.p, ve.layer, ve.prop, ve.at, ve.uid);
|
||||
let t = app.tokens;
|
||||
let vt = t.for_value(prop.has_expression());
|
||||
let (mut x, y) = (at.x, at.y - 9.0);
|
||||
let is_3d = layer.is_3d();
|
||||
let mut dims = vec![];
|
||||
@@ -5599,7 +5601,7 @@ fn vector_editor(app: &mut EffectcraftApp, ui: &mut egui::Ui, ve: &ValueEdit<'_>
|
||||
egui::Id::unique(("v", uid, d)),
|
||||
c[d],
|
||||
&widgets::HotSpec { speed: if pct { 0.5 } else { 1.0 }, range, decimals: 1, suffix },
|
||||
&t,
|
||||
&vt,
|
||||
);
|
||||
app.auto.add(&format!("timeline.prop.{uid}.value.{d}"), r, &prop.name);
|
||||
dims.push(r);
|
||||
@@ -5622,7 +5624,7 @@ fn vector_editor(app: &mut EffectcraftApp, ui: &mut egui::Ui, ve: &ValueEdit<'_>
|
||||
}
|
||||
x = r.max.x + if d + 1 < n { 2.0 } else { 0.0 };
|
||||
if d + 1 < n {
|
||||
p.text(pos2(x, at.y), Align2::LEFT_CENTER, ",", Tokens::ui(12.0), t.hot_text);
|
||||
p.text(pos2(x, at.y), Align2::LEFT_CENTER, ",", Tokens::ui(12.0), vt.hot_text);
|
||||
x += 6.0;
|
||||
}
|
||||
}
|
||||
@@ -5697,7 +5699,7 @@ fn enum_editor(app: &mut EffectcraftApp, ui: &mut egui::Ui, ve: &ValueEdit<'_>,
|
||||
ve.set(actions, json!(ni));
|
||||
}
|
||||
} else {
|
||||
p.text(pos2(x, at.y), Align2::LEFT_CENTER, fmt_num(*i as f64, 0), Tokens::ui(12.0), t.hot_text);
|
||||
p.text(pos2(x, at.y), Align2::LEFT_CENTER, fmt_num(*i as f64, 0), Tokens::ui(12.0), t.for_value(prop.has_expression()).hot_text);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -129,6 +129,16 @@ impl Tokens {
|
||||
pub fn grad_top(&self, c: Color32) -> Color32 {
|
||||
if self.gradients { c.lerp_to_gamma(Color32::WHITE, 0.06) } else { c }
|
||||
}
|
||||
|
||||
/// The tokens a property's value fields use (#1306): with an expression driving the property
|
||||
/// (`expression`, [`Property::has_expression`]) its values show in the expression colour, red
|
||||
/// as in After Effects, hovered and dragged too; otherwise these tokens. For the values only:
|
||||
/// the name, stopwatch and swatches keep the ordinary tokens.
|
||||
///
|
||||
/// [`Property::has_expression`]: effectcraft_engine::project::Property::has_expression
|
||||
pub fn for_value(self, expression: bool) -> Tokens {
|
||||
if expression { Tokens { hot_text: self.expr_text, accent_hover: self.expr_text, ..self } } else { self }
|
||||
}
|
||||
}
|
||||
|
||||
impl Tokens {
|
||||
|
||||
@@ -21,6 +21,7 @@ mod ui_drag_drop;
|
||||
mod ui_effect_controls;
|
||||
mod ui_effect_drop_highlight;
|
||||
mod ui_effect_names;
|
||||
mod ui_expression_values;
|
||||
mod ui_font_search;
|
||||
mod ui_form_fit;
|
||||
mod ui_frame_step;
|
||||
|
||||
@@ -0,0 +1,296 @@
|
||||
//! #1306 (from #1072): a property driven by an expression shows its value in red, the expression
|
||||
//! colour, in the Timeline, Effect Controls and the Properties panel, as in After Effects: scalars,
|
||||
//! every part of a vector (with its separator) and both parts of an angle (with its sign), hovered
|
||||
//! and dragged too, in dark and light themes. A disabled, removed or empty expression shows the
|
||||
//! ordinary colour again. The tests read the colour the value text is painted with (the frame's
|
||||
//! text shapes: the numbers are painted with an override colour, the separator and sign as plain
|
||||
//! text), and require every expected part to be there.
|
||||
|
||||
use effectcraft_engine::Session;
|
||||
use effectcraft_engine::project::{LayerId, PropGroup};
|
||||
use effectcraft_ui_egui::EffectcraftApp;
|
||||
use effectcraft_ui_egui::dock::PanelKind;
|
||||
use effectcraft_ui_egui::theme::ThemeKind;
|
||||
use egui::epaint::Shape;
|
||||
use egui::{Color32, Event, Modifiers, PointerButton, Pos2, Rect, pos2, vec2};
|
||||
use egui_kittest::Harness;
|
||||
use serde_json::json;
|
||||
|
||||
const TIMELINE: &str = "timeline.prop";
|
||||
const EFFECT_CONTROLS: &str = "effectControls.prop";
|
||||
const PROPERTIES: &str = "properties.prop";
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
enum Kind {
|
||||
/// Two components (X, Y).
|
||||
Vector,
|
||||
/// Revolutions, sign and degrees.
|
||||
Angle,
|
||||
Scalar,
|
||||
}
|
||||
|
||||
/// A property whose value the tests check, and the panels that must show it.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
struct Checked {
|
||||
uid: u64,
|
||||
kind: Kind,
|
||||
panels: &'static [&'static str],
|
||||
}
|
||||
|
||||
/// A comp with one solid and the Transform effect on it.
|
||||
struct Ids {
|
||||
layer: u64,
|
||||
/// Transform Position, Rotation and Opacity (Timeline and Properties panel), and the Transform
|
||||
/// effect's Position, Rotation and Opacity (Effect Controls).
|
||||
driven: [Checked; 6],
|
||||
/// Transform Scale: no expression.
|
||||
scale: Checked,
|
||||
}
|
||||
|
||||
fn setup() -> (Harness<'static, EffectcraftApp>, Ids) {
|
||||
let mut s = Session::default();
|
||||
s.execute("comp.new", json!({"name": "E", "width": 320, "height": 180, "duration": 4})).unwrap();
|
||||
let l = s.execute("layer.newSolid", json!({"color": "#406080"})).unwrap()["layer"].as_u64().unwrap();
|
||||
let fx = s.execute("effect.apply", json!({"layers": [l], "effect": "Transform"})).unwrap()["effects"][0].as_u64().unwrap();
|
||||
let layer = s.active_comp().unwrap().layer(LayerId(l)).unwrap().clone();
|
||||
let uid = |path: &str| layer.props.prop(path).unwrap_or_else(|| panic!("no {path}")).uid;
|
||||
let g: &PropGroup = layer.props.find_group(fx).unwrap();
|
||||
let fuid = |m: &str| g.get(m).unwrap_or_else(|| panic!("no effect {m}")).uid;
|
||||
const LAYER: &[&str] = &[TIMELINE, PROPERTIES];
|
||||
const EFFECT: &[&str] = &[EFFECT_CONTROLS];
|
||||
let c = |uid, kind, panels| Checked { uid, kind, panels };
|
||||
let ids = Ids {
|
||||
layer: l,
|
||||
driven: [
|
||||
c(uid("transform/position"), Kind::Vector, LAYER),
|
||||
c(uid("transform/rotation"), Kind::Angle, LAYER),
|
||||
c(uid("transform/opacity"), Kind::Scalar, LAYER),
|
||||
c(fuid("position"), Kind::Vector, EFFECT),
|
||||
c(fuid("rotation"), Kind::Angle, EFFECT),
|
||||
c(fuid("opacity"), Kind::Scalar, EFFECT),
|
||||
],
|
||||
scale: c(uid("transform/scale"), Kind::Vector, LAYER),
|
||||
};
|
||||
let transform = layer.props.sub("transform").unwrap().uid;
|
||||
s.state.selected_layers = vec![LayerId(l)];
|
||||
let mut app = EffectcraftApp::new(s);
|
||||
app.show_panel(PanelKind::EffectControls);
|
||||
app.ui.timeline.open_layers.insert(l);
|
||||
app.ui.timeline.open_groups.insert(transform);
|
||||
let mut h = Harness::builder().with_size(vec2(1600.0, 1000.0)).build_eframe(|_| app);
|
||||
h.run_steps(4);
|
||||
(h, ids)
|
||||
}
|
||||
|
||||
fn set_expression(h: &mut Harness<'_, EffectcraftApp>, ids: &Ids, prop: u64, p: &serde_json::Value) {
|
||||
let mut p = p.clone();
|
||||
p["layer"] = json!(ids.layer);
|
||||
p["prop"] = json!(prop);
|
||||
h.state_mut().session.execute("prop.setExpression", p).unwrap();
|
||||
}
|
||||
|
||||
fn set_theme(h: &mut Harness<'_, EffectcraftApp>, theme: ThemeKind) {
|
||||
let ctx = h.ctx.clone();
|
||||
h.state_mut().set_theme(&ctx, theme);
|
||||
h.run_steps(3);
|
||||
}
|
||||
|
||||
fn rect_of(h: &Harness<'_, EffectcraftApp>, id: &str) -> Rect {
|
||||
let e = h.state().auto.find(id).unwrap_or_else(|| panic!("no {id}: the part isn't shown"));
|
||||
Rect::from_min_size(pos2(e.rect[0], e.rect[1]), vec2(e.rect[2], e.rect[3]))
|
||||
}
|
||||
|
||||
/// The area of `c`'s value in `panel`, every expected part required: both components of a vector,
|
||||
/// the revolutions and degrees of an angle (the separator and sign lie between them).
|
||||
fn value_area(h: &Harness<'_, EffectcraftApp>, panel: &str, c: Checked) -> Rect {
|
||||
let uid = c.uid;
|
||||
match c.kind {
|
||||
Kind::Vector => rect_of(h, &format!("{panel}.{uid}.value.0")).union(rect_of(h, &format!("{panel}.{uid}.value.1"))),
|
||||
Kind::Angle => rect_of(h, &format!("{panel}.{uid}.revolutions")).union(rect_of(h, &format!("{panel}.{uid}.value"))),
|
||||
Kind::Scalar => rect_of(h, &format!("{panel}.{uid}.value")),
|
||||
}
|
||||
}
|
||||
|
||||
/// The text painted inside `area` in the last frame, with its colour.
|
||||
fn texts(h: &Harness<'_, EffectcraftApp>, area: Rect) -> Vec<(String, Color32)> {
|
||||
fn walk(s: &Shape, area: Rect, out: &mut Vec<(String, Color32)>) {
|
||||
match s {
|
||||
Shape::Vec(v) => v.iter().for_each(|s| walk(s, area, out)),
|
||||
Shape::Text(t) if area.expand(1.0).contains_rect(t.visual_bounding_rect()) && !t.galley.text().trim().is_empty() => {
|
||||
out.push((t.galley.text().trim().to_string(), t.override_text_color.unwrap_or(t.fallback_color)));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
let mut out = vec![];
|
||||
for c in &h.output().shapes {
|
||||
walk(&c.shape, area, &mut out);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// `c`'s value in `panel` shows every part, all in `want`: two numbers and (in the Timeline) the
|
||||
/// `,` of a vector, the revolutions, sign and degrees of an angle, the number of a scalar.
|
||||
fn assert_value(h: &Harness<'_, EffectcraftApp>, panel: &str, c: Checked, want: Color32, what: &str) {
|
||||
let shown = texts(h, value_area(h, panel, c));
|
||||
let numbers = shown.iter().filter(|(s, _)| s.chars().any(|ch| ch.is_ascii_digit())).count();
|
||||
let has = |text: &str| shown.iter().any(|(s, _)| s == text);
|
||||
let ctx = format!("{what}: {:?} @{} in {panel}: {shown:?}", c.kind, c.uid);
|
||||
match c.kind {
|
||||
Kind::Vector => {
|
||||
assert!(numbers >= 2, "{ctx}: both components");
|
||||
if panel == TIMELINE {
|
||||
assert!(has(","), "{ctx}: the separator");
|
||||
}
|
||||
}
|
||||
Kind::Angle => {
|
||||
assert!(numbers >= 2, "{ctx}: revolutions and degrees");
|
||||
assert!(has("+") || has("-"), "{ctx}: the sign");
|
||||
}
|
||||
Kind::Scalar => assert!(numbers >= 1, "{ctx}: the number"),
|
||||
}
|
||||
assert!(shown.iter().all(|(_, col)| *col == want), "{ctx}: want {want:?}");
|
||||
}
|
||||
|
||||
/// Every checked property in every panel it belongs to.
|
||||
fn assert_all(h: &Harness<'_, EffectcraftApp>, checked: &[Checked], want: Color32, what: &str) {
|
||||
for c in checked {
|
||||
for panel in c.panels {
|
||||
assert_value(h, panel, *c, want, what);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Expression-driven values are red in every panel, in both themes; Scale (none) stays blue.
|
||||
#[test]
|
||||
fn expression_driven_values_are_red_in_every_panel() {
|
||||
let (mut h, ids) = setup();
|
||||
for c in ids.driven {
|
||||
set_expression(&mut h, &ids, c.uid, &json!({"expression": "value"}));
|
||||
}
|
||||
for theme in [ThemeKind::Dark, ThemeKind::Light] {
|
||||
set_theme(&mut h, theme);
|
||||
let t = h.state().tokens;
|
||||
assert_ne!(t.expr_text, t.hot_text);
|
||||
assert_all(&h, &ids.driven, t.expr_text, &format!("{theme:?} expression"));
|
||||
assert_all(&h, &[ids.scale], t.hot_text, &format!("{theme:?} no expression"));
|
||||
}
|
||||
}
|
||||
|
||||
/// Disabled, removed or whitespace-only: the ordinary colour; enabled again: red. Every part of
|
||||
/// every value is checked again after each change, in both themes.
|
||||
#[test]
|
||||
fn expression_state_changes_switch_the_colour() {
|
||||
let (mut h, ids) = setup();
|
||||
let states = [
|
||||
(json!({"expression": "value"}), true, "enabled"),
|
||||
(json!({"enabled": false}), false, "disabled"),
|
||||
(json!({"enabled": true}), true, "enabled again"),
|
||||
(json!({"expression": ""}), false, "removed"),
|
||||
(json!({"expression": "value"}), true, "set again"),
|
||||
(json!({"expression": " "}), false, "whitespace only"),
|
||||
];
|
||||
for theme in [ThemeKind::Dark, ThemeKind::Light] {
|
||||
set_theme(&mut h, theme);
|
||||
let t = h.state().tokens;
|
||||
for (p, red, what) in &states {
|
||||
for c in ids.driven {
|
||||
set_expression(&mut h, &ids, c.uid, p);
|
||||
}
|
||||
h.run_steps(3);
|
||||
let want = if *red { t.expr_text } else { t.hot_text };
|
||||
assert_all(&h, &ids.driven, want, &format!("{theme:?} {what}"));
|
||||
assert_all(&h, &[ids.scale], t.hot_text, &format!("{theme:?} {what}: Scale"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn hover(h: &mut Harness<'_, EffectcraftApp>, at: Pos2) {
|
||||
h.input_mut().events.push(Event::PointerMoved(at));
|
||||
h.run_steps(2);
|
||||
}
|
||||
|
||||
/// Hovering a value (an ordinary one takes the hover accent) keeps an expression-driven one red,
|
||||
/// in every panel and both themes.
|
||||
#[test]
|
||||
fn a_hovered_expression_value_stays_red() {
|
||||
let (mut h, ids) = setup();
|
||||
let opacity = ids.driven.iter().filter(|c| c.kind == Kind::Scalar).copied().collect::<Vec<_>>();
|
||||
for c in &opacity {
|
||||
set_expression(&mut h, &ids, c.uid, &json!({"expression": "value"}));
|
||||
}
|
||||
h.run_steps(3);
|
||||
for theme in [ThemeKind::Dark, ThemeKind::Light] {
|
||||
set_theme(&mut h, theme);
|
||||
let t = h.state().tokens;
|
||||
for c in &opacity {
|
||||
for panel in c.panels {
|
||||
let area = value_area(&h, panel, *c);
|
||||
hover(&mut h, area.center());
|
||||
let at = h.ctx.input(|i| i.pointer.hover_pos());
|
||||
assert!(at.is_some_and(|p| area.contains(p)), "{theme:?} {panel}: the pointer is over the value");
|
||||
assert_value(&h, panel, *c, t.expr_text, &format!("{theme:?} hovered"));
|
||||
}
|
||||
}
|
||||
// An ordinary value does take the hover accent.
|
||||
let area = value_area(&h, TIMELINE, ids.scale);
|
||||
let x = rect_of(&h, &format!("{TIMELINE}.{}.value.0", ids.scale.uid)).center();
|
||||
hover(&mut h, x);
|
||||
let shown = texts(&h, area);
|
||||
assert!(shown.iter().any(|(_, c)| *c == t.accent_hover), "{theme:?} an ordinary hovered value: {shown:?}");
|
||||
hover(&mut h, pos2(5.0, 5.0));
|
||||
}
|
||||
}
|
||||
|
||||
/// Dragging (scrubbing) an expression-driven value keeps it red while the drag is on, in the
|
||||
/// Timeline and Effect Controls.
|
||||
#[test]
|
||||
fn a_dragged_expression_value_stays_red() {
|
||||
let (mut h, ids) = setup();
|
||||
let opacity = ids.driven.iter().filter(|c| c.kind == Kind::Scalar).copied().collect::<Vec<_>>();
|
||||
for c in &opacity {
|
||||
set_expression(&mut h, &ids, c.uid, &json!({"expression": "value"}));
|
||||
}
|
||||
h.run_steps(3);
|
||||
let t = h.state().tokens;
|
||||
for (c, panel) in [(opacity[0], TIMELINE), (opacity[1], EFFECT_CONTROLS)] {
|
||||
let at = value_area(&h, panel, c).center();
|
||||
h.input_mut().events.push(Event::PointerMoved(at));
|
||||
h.step();
|
||||
h.input_mut().events.push(Event::PointerButton { pos: at, button: PointerButton::Primary, pressed: true, modifiers: Modifiers::NONE });
|
||||
h.step();
|
||||
for k in 1..=4 {
|
||||
h.input_mut().events.push(Event::PointerMoved(at + vec2(3.0 * k as f32, 0.0)));
|
||||
h.step();
|
||||
}
|
||||
assert!(h.ctx.dragged_id().is_some(), "{panel}: a drag is on");
|
||||
assert_value(&h, panel, c, t.expr_text, &format!("{panel} dragged"));
|
||||
let end = at + vec2(12.0, 0.0);
|
||||
h.input_mut().events.push(Event::PointerButton { pos: end, button: PointerButton::Primary, pressed: false, modifiers: Modifiers::NONE });
|
||||
h.run_steps(3);
|
||||
hover(&mut h, pos2(5.0, 5.0));
|
||||
}
|
||||
}
|
||||
|
||||
/// Separate Dimensions: X Position and Y Position are separate scalars, so an expression on X Position
|
||||
/// turns only X red, in the Timeline and Properties panel and both themes; Y Position stays blue.
|
||||
#[test]
|
||||
fn separated_position_colours_each_dimension_on_its_own() {
|
||||
let (mut h, ids) = setup();
|
||||
h.state_mut().session.execute("prop.separateDimensions", json!({"layer": ids.layer, "value": true})).unwrap();
|
||||
let uid = |h: &Harness<'_, EffectcraftApp>, path: &str| {
|
||||
let s = &h.state().session;
|
||||
s.active_comp().unwrap().layer(LayerId(ids.layer)).unwrap().props.prop(path).unwrap_or_else(|| panic!("no {path}")).uid
|
||||
};
|
||||
const LAYER: &[&str] = &[TIMELINE, PROPERTIES];
|
||||
let x = Checked { uid: uid(&h, "transform/positionX"), kind: Kind::Scalar, panels: LAYER };
|
||||
let y = Checked { uid: uid(&h, "transform/positionY"), kind: Kind::Scalar, panels: LAYER };
|
||||
set_expression(&mut h, &ids, x.uid, &json!({"expression": "value"}));
|
||||
h.run_steps(3);
|
||||
for theme in [ThemeKind::Dark, ThemeKind::Light] {
|
||||
set_theme(&mut h, theme);
|
||||
let t = h.state().tokens;
|
||||
assert_all(&h, &[x], t.expr_text, &format!("{theme:?} X Position (expression)"));
|
||||
assert_all(&h, &[y], t.hot_text, &format!("{theme:?} Y Position (none)"));
|
||||
}
|
||||
}
|
||||
@@ -169,6 +169,11 @@ The most important missing piece: it turns every other estimate here into a meas
|
||||
a Radial Stroke swatch on a selected shape layer and get the Gradient Editor on its Gradient Fill
|
||||
/ Gradient Stroke Colors (OK saves); without such a gradient the swatch still opens the options,
|
||||
and the words, Solid swatches and Alt-click are unchanged.
|
||||
- Regression evidence for [#1306](https://github.com/storytold/effectcraft/issues/1306) (from
|
||||
#1072): the `ui_expression_values` tests, checked headlessly on Windows, read the colour the
|
||||
values are painted in. Expression-driven vectors, angles and scalars are red in the Timeline,
|
||||
Effect Controls and Properties panel, in dark and light themes, hovered too; disabled, removed
|
||||
and empty expressions show the ordinary colour.
|
||||
- Regression evidence for the reports of 6 October, checked headlessly on Windows: real pointer
|
||||
drags in `ui_drag_drop` (Project items into the Timeline between layers and at a time, and onto
|
||||
the viewer; an effect onto a layer in the viewer: #85, #88, #89), `ui_render_queue` (a template
|
||||
|
||||
Reference in New Issue
Block a user