518 lines
27 KiB
Rust
518 lines
27 KiB
Rust
//! Inline text fields (a name, the current time) open with their whole text selected so typing
|
|
//! replaces it, commit on Enter and cancel on Escape (#170).
|
|
|
|
use effectcraft_engine::Session;
|
|
use effectcraft_ui_egui::EffectcraftApp;
|
|
use egui::{Event, Key, Modifiers, PointerButton, Pos2, pos2};
|
|
use egui_kittest::Harness;
|
|
use egui_kittest::kittest::{NodeT, Queryable};
|
|
use serde_json::json;
|
|
|
|
fn harness() -> (Harness<'static, EffectcraftApp>, u64) {
|
|
let mut s = Session::default();
|
|
s.execute("comp.new", json!({"name": "Main", "width": 320, "height": 180, "duration": 4, "fps": 24})).unwrap();
|
|
let comp = s.active_comp_id().unwrap().0;
|
|
let mut h = Harness::builder().with_size(egui::vec2(1600.0, 1000.0)).build_eframe(|_| EffectcraftApp::new(s));
|
|
h.run_steps(4);
|
|
(h, comp)
|
|
}
|
|
|
|
fn center(h: &Harness<'_, EffectcraftApp>, id: &str) -> Pos2 {
|
|
let e = h.state().auto.find(id).unwrap_or_else(|| panic!("no {id}"));
|
|
pos2(e.rect[0] + e.rect[2] / 2.0, e.rect[1] + e.rect[3] / 2.0)
|
|
}
|
|
|
|
fn click(h: &mut Harness<'_, EffectcraftApp>, p: Pos2, times: usize) {
|
|
h.input_mut().events.push(Event::PointerMoved(p));
|
|
h.step();
|
|
// All in one frame (the harness' frames are longer apart than a double-click).
|
|
for _ in 0..times {
|
|
h.input_mut().events.push(Event::PointerButton { pos: p, button: PointerButton::Primary, pressed: true, modifiers: Modifiers::NONE });
|
|
h.input_mut().events.push(Event::PointerButton { pos: p, button: PointerButton::Primary, pressed: false, modifiers: Modifiers::NONE });
|
|
}
|
|
h.run_steps(3);
|
|
}
|
|
|
|
/// A primary-button drag from `from` to `to`, in steps.
|
|
fn drag(h: &mut Harness<'_, EffectcraftApp>, from: Pos2, to: Pos2) {
|
|
h.input_mut().events.push(Event::PointerMoved(from));
|
|
h.step();
|
|
h.input_mut().events.push(Event::PointerButton { pos: from, button: PointerButton::Primary, pressed: true, modifiers: Modifiers::NONE });
|
|
h.step();
|
|
for k in 1..=8 {
|
|
h.input_mut().events.push(Event::PointerMoved(from + (to - from) * (k as f32 / 8.0)));
|
|
h.step();
|
|
}
|
|
h.input_mut().events.push(Event::PointerButton { pos: to, button: PointerButton::Primary, pressed: false, modifiers: Modifiers::NONE });
|
|
h.run_steps(3);
|
|
}
|
|
|
|
fn key(h: &mut Harness<'_, EffectcraftApp>, key: Key) {
|
|
h.input_mut().events.push(Event::Key { key, physical_key: None, pressed: true, repeat: false, modifiers: Modifiers::NONE });
|
|
h.input_mut().events.push(Event::Key { key, physical_key: None, pressed: false, repeat: false, modifiers: Modifiers::NONE });
|
|
h.run_steps(2);
|
|
}
|
|
|
|
fn type_text(h: &mut Harness<'_, EffectcraftApp>, text: &str) {
|
|
assert!(h.ctx.egui_wants_keyboard_input(), "the field has the keyboard");
|
|
h.input_mut().events.push(Event::Text(text.into()));
|
|
h.step();
|
|
}
|
|
|
|
#[test]
|
|
fn project_rename_replaces_the_name() {
|
|
let (mut h, comp) = harness();
|
|
let name = |h: &Harness<'_, EffectcraftApp>| h.state().session.project.items.values().find(|i| i.id.0 == comp).unwrap().name.clone();
|
|
let p = center(&h, &format!("project.item.{comp}.name"));
|
|
click(&mut h, p, 1);
|
|
key(&mut h, Key::Enter);
|
|
type_text(&mut h, "Hero");
|
|
key(&mut h, Key::Enter);
|
|
assert_eq!(name(&h), "Hero");
|
|
assert!(!h.ctx.egui_wants_keyboard_input(), "Enter releases the keyboard");
|
|
}
|
|
|
|
#[test]
|
|
fn timecode_field_replaces_commits_and_cancels() {
|
|
let (mut h, _) = harness();
|
|
let frame = |h: &Harness<'_, EffectcraftApp>| h.state().session.active_comp().unwrap().frame_rate.frame_at(h.state().session.time());
|
|
let p = center(&h, "timeline.timecode");
|
|
click(&mut h, p, 2);
|
|
type_text(&mut h, "0:00:01:00");
|
|
key(&mut h, Key::Enter);
|
|
assert_eq!(frame(&h), 24, "Enter commits the typed time");
|
|
assert!(!h.ctx.egui_wants_keyboard_input(), "Enter releases the keyboard");
|
|
click(&mut h, p, 2);
|
|
type_text(&mut h, "0:00:02:00");
|
|
key(&mut h, Key::Escape);
|
|
assert_eq!(frame(&h), 24, "Escape cancels");
|
|
assert!(!h.ctx.egui_wants_keyboard_input(), "Escape releases the keyboard");
|
|
}
|
|
|
|
/// #284: dragging the expression pick whip while the expression is being edited puts the
|
|
/// reference at the cursor (After Effects), and the editing goes on after it; without an edit
|
|
/// in progress it replaces the expression.
|
|
#[test]
|
|
fn the_expression_pick_whip_inserts_at_the_cursor_while_editing() {
|
|
let (mut h, _) = harness();
|
|
let (layer, opacity, rotation, transform) = {
|
|
let s = &mut h.state_mut().session;
|
|
let l = s.execute("layer.newSolid", json!({"name": "Plate", "color": "#406080"})).unwrap()["layer"].as_u64().unwrap();
|
|
s.execute("prop.setExpression", json!({"layer": l, "path": "transform/opacity", "expression": "50 + 0"})).unwrap();
|
|
let tr = s.active_comp().unwrap().layer(effectcraft_engine::project::LayerId(l)).unwrap().props.sub("transform").unwrap().clone();
|
|
(l, tr.get("opacity").unwrap().uid, tr.get("rotation").unwrap().uid, tr.uid)
|
|
};
|
|
h.state_mut().ui.timeline.open_layers.insert(layer);
|
|
h.state_mut().ui.timeline.open_groups.insert(transform);
|
|
h.run_steps(3);
|
|
let expr = |h: &Harness<'_, EffectcraftApp>| {
|
|
let comp = h.state().session.active_comp().unwrap();
|
|
comp.layer(effectcraft_engine::project::LayerId(layer)).unwrap().props.find(opacity).unwrap().expr.as_ref().unwrap().text.clone()
|
|
};
|
|
let whip = |h: &mut Harness<'_, EffectcraftApp>| {
|
|
let (from, to) = (center(h, &format!("timeline.prop.{opacity}.pickWhip")), center(h, &format!("timeline.prop.{rotation}.name")));
|
|
drag(h, from, to);
|
|
};
|
|
// Editing: the cursor at the end after typing " * ".
|
|
let p = center(&h, &format!("timeline.prop.{opacity}.expression"));
|
|
click(&mut h, p, 1);
|
|
key(&mut h, Key::End);
|
|
type_text(&mut h, " * ");
|
|
whip(&mut h);
|
|
assert_eq!(expr(&h), "50 + 0 * transform.rotation");
|
|
// The editor has the keyboard again, its cursor after the reference.
|
|
type_text(&mut h, " + 1");
|
|
// Ctrl/Cmd+Enter commits.
|
|
h.input_mut().events.push(Event::ModifiersChanged(Modifiers::COMMAND));
|
|
h.input_mut().events.push(Event::Key { key: Key::Enter, physical_key: None, pressed: true, repeat: false, modifiers: Modifiers::COMMAND });
|
|
h.step();
|
|
h.input_mut().events.push(Event::ModifiersChanged(Modifiers::NONE));
|
|
h.run_steps(3);
|
|
assert_eq!(expr(&h), "50 + 0 * transform.rotation + 1");
|
|
// Not editing: the reference replaces the expression.
|
|
whip(&mut h);
|
|
assert_eq!(expr(&h), "transform.rotation");
|
|
}
|
|
|
|
/// #363: dropped on one of a property's values the expression pick whip picks that dimension
|
|
/// (`position[1]`), as in After Effects; on its name, the whole property.
|
|
#[test]
|
|
fn the_expression_pick_whip_picks_the_dimension_it_is_dropped_on() {
|
|
let (mut h, _) = harness();
|
|
let (layer, opacity, position, transform) = {
|
|
let s = &mut h.state_mut().session;
|
|
let l = s.execute("layer.newSolid", json!({"name": "Plate", "color": "#406080"})).unwrap()["layer"].as_u64().unwrap();
|
|
s.execute("prop.setExpression", json!({"layer": l, "path": "transform/opacity", "expression": "50"})).unwrap();
|
|
let tr = s.active_comp().unwrap().layer(effectcraft_engine::project::LayerId(l)).unwrap().props.sub("transform").unwrap().clone();
|
|
(l, tr.get("opacity").unwrap().uid, tr.get("position").unwrap().uid, tr.uid)
|
|
};
|
|
h.state_mut().ui.timeline.open_layers.insert(layer);
|
|
h.state_mut().ui.timeline.open_groups.insert(transform);
|
|
h.run_steps(3);
|
|
let expr = |h: &Harness<'_, EffectcraftApp>| {
|
|
let comp = h.state().session.active_comp().unwrap();
|
|
comp.layer(effectcraft_engine::project::LayerId(layer)).unwrap().props.find(opacity).unwrap().expr.as_ref().unwrap().text.clone()
|
|
};
|
|
let whip_to = |h: &mut Harness<'_, EffectcraftApp>, target: &str| {
|
|
let (from, to) = (center(h, &format!("timeline.prop.{opacity}.pickWhip")), center(h, target));
|
|
drag(h, from, to);
|
|
};
|
|
whip_to(&mut h, &format!("timeline.prop.{position}.value.1"));
|
|
assert_eq!(expr(&h), "transform.position[1]");
|
|
whip_to(&mut h, &format!("timeline.prop.{position}.name"));
|
|
assert_eq!(expr(&h), "transform.position[0]", "the whole Position into one value: its first, as before");
|
|
}
|
|
|
|
/// #363: every property has a pick whip in the Parent & Link column (After Effects CC 2018 and
|
|
/// later). Dropped on another property's name or one of its values, in the Timeline or in Effect
|
|
/// Controls, it gives the property an expression referencing it; one it already has is replaced
|
|
/// by the reference, and a disabled one is enabled.
|
|
#[test]
|
|
fn the_property_pick_whip_links_a_property_to_another() {
|
|
let (mut h, _) = harness();
|
|
let (layer, opacity, rotation, position, transform) = {
|
|
let s = &mut h.state_mut().session;
|
|
let l = s.execute("layer.newSolid", json!({"name": "Plate", "color": "#406080"})).unwrap()["layer"].as_u64().unwrap();
|
|
s.execute("layer.select", json!({"layers": [l]})).unwrap();
|
|
for fx in ["Slider Control", "Point Control"] {
|
|
s.execute("effect.apply", json!({"layers": [l], "effect": fx})).unwrap();
|
|
}
|
|
let tr = s.active_comp().unwrap().layer(effectcraft_engine::project::LayerId(l)).unwrap().props.sub("transform").unwrap().clone();
|
|
(l, tr.get("opacity").unwrap().uid, tr.get("rotation").unwrap().uid, tr.get("position").unwrap().uid, tr.uid)
|
|
};
|
|
h.state_mut().ui.timeline.open_layers.insert(layer);
|
|
h.state_mut().ui.timeline.open_groups.insert(transform);
|
|
h.state_mut().show_panel(effectcraft_ui_egui::dock::PanelKind::EffectControls);
|
|
h.run_steps(3);
|
|
let props =
|
|
|h: &Harness<'_, EffectcraftApp>| h.state().session.active_comp().unwrap().layer(effectcraft_engine::project::LayerId(layer)).unwrap().props.clone();
|
|
let expr = |h: &Harness<'_, EffectcraftApp>| props(h).find(opacity).unwrap().expr.clone().map(|e| (e.text, e.enabled));
|
|
let whip_to = |h: &mut Harness<'_, EffectcraftApp>, target: &str| {
|
|
let (from, to) = (center(h, &format!("timeline.prop.{opacity}.propertyPickWhip")), center(h, target));
|
|
drag(h, from, to);
|
|
};
|
|
assert_eq!(expr(&h), None);
|
|
// Onto another property's name: no expression yet, one is added.
|
|
whip_to(&mut h, &format!("timeline.prop.{rotation}.name"));
|
|
assert_eq!(expr(&h), Some(("transform.rotation".into(), true)));
|
|
// Onto one of a property's values: that dimension.
|
|
whip_to(&mut h, &format!("timeline.prop.{position}.value.1"));
|
|
assert_eq!(expr(&h), Some(("transform.position[1]".into(), true)));
|
|
// A disabled expression is replaced by the reference, and enabled.
|
|
h.state_mut().session.execute("prop.setExpression", json!({"layer": layer, "prop": opacity, "expression": "50 + 1", "enabled": false})).unwrap();
|
|
h.run_steps(3);
|
|
whip_to(&mut h, &format!("timeline.prop.{rotation}.name"));
|
|
assert_eq!(expr(&h), Some(("transform.rotation".into(), true)));
|
|
// Onto an effect's property in Effect Controls: its value, then one of a point's values.
|
|
let (slider, point) = (props(&h).prop("effects/#1/slider").unwrap().uid, props(&h).prop("effects/#2/point").unwrap().uid);
|
|
whip_to(&mut h, &format!("effectControls.prop.{slider}.value"));
|
|
assert_eq!(expr(&h), Some(("effect(\"Slider Control\")(\"Slider\")".into(), true)));
|
|
whip_to(&mut h, &format!("effectControls.prop.{point}.value.1"));
|
|
assert_eq!(expr(&h), Some(("effect(\"Point Control\")(\"Point\")[1]".into(), true)));
|
|
// Onto itself: nothing changes.
|
|
whip_to(&mut h, &format!("timeline.prop.{opacity}.name"));
|
|
assert_eq!(expr(&h), Some(("effect(\"Point Control\")(\"Point\")[1]".into(), true)));
|
|
}
|
|
|
|
/// #362: an expression pick whip held near the bottom or top of the Timeline's layers scrolls
|
|
/// them (After Effects), so a property out of view can be picked.
|
|
#[test]
|
|
fn the_pick_whip_scrolls_the_timeline_at_its_edges() {
|
|
let (mut h, _) = harness();
|
|
let (top, opacity, transform) = {
|
|
let s = &mut h.state_mut().session;
|
|
for i in 0..40 {
|
|
s.execute("layer.newSolid", json!({"name": format!("Layer {i}"), "color": "#406080"})).unwrap();
|
|
}
|
|
let l = s.execute("layer.newSolid", json!({"name": "Top", "color": "#406080"})).unwrap()["layer"].as_u64().unwrap();
|
|
s.execute("prop.setExpression", json!({"layer": l, "path": "transform/opacity", "expression": "50"})).unwrap();
|
|
let tr = s.active_comp().unwrap().layer(effectcraft_engine::project::LayerId(l)).unwrap().props.sub("transform").unwrap().clone();
|
|
(l, tr.get("opacity").unwrap().uid, tr.uid)
|
|
};
|
|
h.state_mut().ui.timeline.open_layers.insert(top);
|
|
h.state_mut().ui.timeline.open_groups.insert(transform);
|
|
h.run_steps(3);
|
|
let scroll = |h: &Harness<'_, EffectcraftApp>| h.state().ui.timeline.scroll_y;
|
|
let rows = h.state().auto.find("timeline.scroll").unwrap().rect;
|
|
let from = center(&h, &format!("timeline.prop.{opacity}.pickWhip"));
|
|
let (bottom, upper) = (pos2(from.x, rows[1] + rows[3] - 2.0), pos2(from.x, rows[1] + 2.0));
|
|
h.input_mut().events.push(Event::PointerMoved(from));
|
|
h.step();
|
|
h.input_mut().events.push(Event::PointerButton { pos: from, button: PointerButton::Primary, pressed: true, modifiers: Modifiers::NONE });
|
|
h.step();
|
|
for k in 1..=8 {
|
|
h.input_mut().events.push(Event::PointerMoved(from + (bottom - from) * (k as f32 / 8.0)));
|
|
h.step();
|
|
}
|
|
h.run_steps(20);
|
|
let down = scroll(&h);
|
|
assert!(down > 50.0, "held at the bottom edge the layers scroll up: {down}");
|
|
h.input_mut().events.push(Event::PointerMoved(upper));
|
|
h.run_steps(10);
|
|
assert!(scroll(&h) < down, "and at the top edge back down: {} after {down}", scroll(&h));
|
|
h.input_mut().events.push(Event::PointerMoved(pos2(from.x, rows[1] + rows[3] / 2.0)));
|
|
h.run_steps(2);
|
|
let still = scroll(&h);
|
|
h.run_steps(10);
|
|
assert_eq!(scroll(&h), still, "away from the edges it stays");
|
|
h.input_mut().events.push(Event::PointerButton { pos: upper, button: PointerButton::Primary, pressed: false, modifiers: Modifiers::NONE });
|
|
h.run_steps(3);
|
|
}
|
|
|
|
/// #371: the inline expression field grows with the text being typed (it used to keep the
|
|
/// saved text's height until committed), and its bottom edge drags to any height in lines.
|
|
#[test]
|
|
fn the_expression_field_grows_while_editing_and_drags_taller() {
|
|
let (mut h, _) = harness();
|
|
let (layer, opacity, transform) = {
|
|
let s = &mut h.state_mut().session;
|
|
let l = s.execute("layer.newSolid", json!({"name": "Plate", "color": "#406080"})).unwrap()["layer"].as_u64().unwrap();
|
|
s.execute("prop.setExpression", json!({"layer": l, "path": "transform/opacity", "expression": "50"})).unwrap();
|
|
let tr = s.active_comp().unwrap().layer(effectcraft_engine::project::LayerId(l)).unwrap().props.sub("transform").unwrap().clone();
|
|
(l, tr.get("opacity").unwrap().uid, tr.uid)
|
|
};
|
|
h.state_mut().ui.timeline.open_layers.insert(layer);
|
|
h.state_mut().ui.timeline.open_groups.insert(transform);
|
|
h.run_steps(3);
|
|
let id = format!("timeline.prop.{opacity}.expression");
|
|
let height = |h: &Harness<'_, EffectcraftApp>| h.state().auto.find(&id).unwrap().rect[3];
|
|
let line = h.state().tokens.row_h;
|
|
let one = height(&h);
|
|
let p = center(&h, &id);
|
|
click(&mut h, p, 1);
|
|
key(&mut h, Key::End);
|
|
for more in ["+ 1", "+ 2"] {
|
|
key(&mut h, Key::Enter);
|
|
type_text(&mut h, more);
|
|
}
|
|
h.run_steps(2);
|
|
assert!((height(&h) - one - 2.0 * line).abs() < 0.5, "three lines while editing: {} from {one}", height(&h));
|
|
// Its bottom edge: dragged down 4 lines, then up past the top (one line).
|
|
let edge_id = format!("timeline.prop.{opacity}.expressionHeight");
|
|
let drag_edge = |h: &mut Harness<'_, EffectcraftApp>, dy: f32| {
|
|
let edge = center(h, &edge_id);
|
|
let to = edge + egui::vec2(0.0, dy);
|
|
h.input_mut().events.push(Event::PointerMoved(edge));
|
|
h.step();
|
|
h.input_mut().events.push(Event::PointerButton { pos: edge, button: PointerButton::Primary, pressed: true, modifiers: Modifiers::NONE });
|
|
h.step();
|
|
for k in 1..=6 {
|
|
h.input_mut().events.push(Event::PointerMoved(edge + (to - edge) * (k as f32 / 6.0)));
|
|
h.step();
|
|
}
|
|
h.input_mut().events.push(Event::PointerButton { pos: to, button: PointerButton::Primary, pressed: false, modifiers: Modifiers::NONE });
|
|
h.run_steps(3);
|
|
};
|
|
drag_edge(&mut h, 4.0 * line);
|
|
assert_eq!(h.state().ui.timeline.expr_lines.get(&opacity), Some(&7));
|
|
assert!((height(&h) - one - 6.0 * line).abs() < 0.5, "{}", height(&h));
|
|
drag_edge(&mut h, -20.0 * line);
|
|
assert_eq!(h.state().ui.timeline.expr_lines.get(&opacity), Some(&1));
|
|
}
|
|
|
|
fn key_with(h: &mut Harness<'_, EffectcraftApp>, key: Key, modifiers: Modifiers) {
|
|
h.input_mut().events.push(Event::ModifiersChanged(modifiers));
|
|
h.input_mut().events.push(Event::Key { key, physical_key: None, pressed: true, repeat: false, modifiers });
|
|
h.input_mut().events.push(Event::Key { key, physical_key: None, pressed: false, repeat: false, modifiers });
|
|
h.step();
|
|
h.input_mut().events.push(Event::ModifiersChanged(Modifiers::NONE));
|
|
h.run_steps(2);
|
|
}
|
|
|
|
/// #361: Tab in a value being typed commits it and types in the next value (the next dimension,
|
|
/// then the next property's), Shift+Tab the previous, in the Timeline and in Effect Controls,
|
|
/// as in After Effects. It used to drop the keyboard instead.
|
|
#[test]
|
|
fn tab_commits_a_value_and_moves_to_the_next() {
|
|
let (mut h, _) = harness();
|
|
let (layer, transform) = {
|
|
let s = &mut h.state_mut().session;
|
|
let l = s.execute("layer.newSolid", json!({"name": "Plate", "color": "#406080"})).unwrap()["layer"].as_u64().unwrap();
|
|
s.execute("layer.select", json!({"layers": [l]})).unwrap();
|
|
for fx in ["Point Control", "Slider Control"] {
|
|
s.execute("effect.apply", json!({"layers": [l], "effect": fx})).unwrap();
|
|
}
|
|
let tr = s.active_comp().unwrap().layer(effectcraft_engine::project::LayerId(l)).unwrap().props.sub("transform").unwrap().uid;
|
|
(l, tr)
|
|
};
|
|
h.state_mut().ui.timeline.open_layers.insert(layer);
|
|
h.state_mut().ui.timeline.open_groups.insert(transform);
|
|
h.state_mut().show_panel(effectcraft_ui_egui::dock::PanelKind::EffectControls);
|
|
h.run_steps(3);
|
|
let props =
|
|
|h: &Harness<'_, EffectcraftApp>| h.state().session.active_comp().unwrap().layer(effectcraft_engine::project::LayerId(layer)).unwrap().props.clone();
|
|
let value = |h: &Harness<'_, EffectcraftApp>, path: &str| props(h).prop(path).unwrap().value.components();
|
|
let uid = |h: &Harness<'_, EffectcraftApp>, path: &str| props(h).prop(path).unwrap().uid;
|
|
let (position, scale) = (uid(&h, "transform/position"), uid(&h, "transform/scale"));
|
|
let none = Modifiers::NONE;
|
|
// Timeline: Position X, Tab, Position Y, Tab, Scale (linked).
|
|
let p = center(&h, &format!("timeline.prop.{position}.value.0"));
|
|
click(&mut h, p, 1);
|
|
type_text(&mut h, "100");
|
|
key_with(&mut h, Key::Tab, none);
|
|
type_text(&mut h, "50");
|
|
key_with(&mut h, Key::Tab, none);
|
|
type_text(&mut h, "200");
|
|
key(&mut h, Key::Enter);
|
|
assert_eq!(value(&h, "transform/position")[..2], [100.0, 50.0]);
|
|
assert_eq!(value(&h, "transform/scale")[..2], [200.0, 200.0]);
|
|
assert!(!h.ctx.egui_wants_keyboard_input(), "Enter ends the typing");
|
|
// Shift+Tab: from Scale X back to Position Y.
|
|
let p = center(&h, &format!("timeline.prop.{scale}.value.0"));
|
|
click(&mut h, p, 1);
|
|
type_text(&mut h, "120");
|
|
key_with(&mut h, Key::Tab, Modifiers::SHIFT);
|
|
type_text(&mut h, "60");
|
|
key(&mut h, Key::Enter);
|
|
assert_eq!(value(&h, "transform/position")[..2], [100.0, 60.0]);
|
|
assert_eq!(value(&h, "transform/scale")[..2], [120.0, 120.0]);
|
|
// Effect Controls: Point Control's Y, Tab, Slider Control's Slider (the next effect).
|
|
let point = uid(&h, "effects/#1/point");
|
|
let p = center(&h, &format!("effectControls.prop.{point}.value.1"));
|
|
click(&mut h, p, 1);
|
|
type_text(&mut h, "7");
|
|
key_with(&mut h, Key::Tab, none);
|
|
type_text(&mut h, "42");
|
|
key(&mut h, Key::Enter);
|
|
assert_eq!(value(&h, "effects/#1/point")[1], 7.0);
|
|
assert_eq!(value(&h, "effects/#2/slider"), [42.0]);
|
|
}
|
|
|
|
/// A click at `p` with `modifiers` held.
|
|
fn click_with(h: &mut Harness<'_, EffectcraftApp>, p: Pos2, modifiers: Modifiers) {
|
|
h.input_mut().events.push(Event::PointerMoved(p));
|
|
h.input_mut().events.push(Event::ModifiersChanged(modifiers));
|
|
h.step();
|
|
h.input_mut().events.push(Event::PointerButton { pos: p, button: PointerButton::Primary, pressed: true, modifiers });
|
|
h.input_mut().events.push(Event::PointerButton { pos: p, button: PointerButton::Primary, pressed: false, modifiers });
|
|
h.step();
|
|
h.input_mut().events.push(Event::ModifiersChanged(Modifiers::NONE));
|
|
h.run_steps(3);
|
|
}
|
|
|
|
/// #370: Ctrl/Cmd-click on the current-time display (the Timeline's or the Composition
|
|
/// panel's) toggles the Time Display Style between Timecode and Frames, one undo step each, as
|
|
/// in After Effects; under the Timeline's display the other style shows.
|
|
#[test]
|
|
fn ctrl_click_on_the_time_display_toggles_timecode_and_frames() {
|
|
use effectcraft_engine::project::TimeDisplayStyle;
|
|
let (mut h, _) = harness();
|
|
h.state_mut().session.execute("time.set", json!({"frame": 30})).unwrap();
|
|
h.run_steps(2);
|
|
let shown = |h: &Harness<'_, EffectcraftApp>, id: &str| h.state().auto.find(id).unwrap().label.clone();
|
|
let style = |h: &Harness<'_, EffectcraftApp>| h.state().session.project.settings.time_display;
|
|
let undo = h.state().session.history.undo.len();
|
|
let at = center(&h, "timeline.timecode");
|
|
click_with(&mut h, at, Modifiers::COMMAND);
|
|
assert_eq!(style(&h), TimeDisplayStyle::Frames);
|
|
assert_eq!(shown(&h, "timeline.timecode"), "00030");
|
|
assert_eq!(h.state().session.history.undo.len(), undo + 1, "one undo step");
|
|
assert!(!h.ctx.egui_wants_keyboard_input(), "no time entry opened");
|
|
let at = center(&h, "viewer.timecode");
|
|
click_with(&mut h, at, Modifiers::COMMAND);
|
|
assert_eq!(style(&h), TimeDisplayStyle::Timecode);
|
|
assert_eq!(shown(&h, "timeline.timecode"), "0:00:01:06");
|
|
// A plain click changes nothing.
|
|
click_with(&mut h, at, Modifiers::NONE);
|
|
let at = center(&h, "timeline.timecode");
|
|
click_with(&mut h, at, Modifiers::NONE);
|
|
assert_eq!(style(&h), TimeDisplayStyle::Timecode);
|
|
// Feet + Frames goes to Timecode.
|
|
h.state_mut().session.execute("file.projectSettings", json!({"timeDisplay": "feet35"})).unwrap();
|
|
h.run_steps(2);
|
|
click_with(&mut h, at, Modifiers::COMMAND);
|
|
assert_eq!(style(&h), TimeDisplayStyle::Timecode);
|
|
}
|
|
|
|
/// Hold Alt and Shift and let go of them, as Windows' keyboard-layout hotkey does; `shift_first`
|
|
/// releases Shift before Alt.
|
|
fn switch_layout(h: &mut Harness<'_, EffectcraftApp>, shift_first: bool) {
|
|
let held = if shift_first { Modifiers::ALT } else { Modifiers::SHIFT };
|
|
for m in [Modifiers::ALT, Modifiers::ALT | Modifiers::SHIFT, held, Modifiers::NONE] {
|
|
h.input_mut().events.push(Event::ModifiersChanged(m));
|
|
h.step();
|
|
}
|
|
h.run_steps(2);
|
|
}
|
|
|
|
/// #394: switching the keyboard layout to Arabic with Alt+Shift while a name is being typed
|
|
/// leaves the field with the keyboard: releasing Shift before Alt left Alt held alone, which
|
|
/// counted as an Alt tap, so the menu bar took the keyboard, the rename ended and the Arabic
|
|
/// letters went nowhere. Layer and Project names keep what is typed in any script.
|
|
#[test]
|
|
fn a_layout_switch_keeps_the_name_field_and_arabic_names_are_kept() {
|
|
let (mut h, comp) = harness();
|
|
let layer = h.state_mut().session.execute("layer.newNull", json!({})).unwrap()["layer"].as_u64().unwrap();
|
|
h.run_steps(3);
|
|
let layer_name =
|
|
|h: &Harness<'_, EffectcraftApp>| h.state().session.active_comp().unwrap().layer(effectcraft_engine::project::LayerId(layer)).unwrap().name.clone();
|
|
let p = center(&h, &format!("timeline.layer.{layer}.row"));
|
|
click(&mut h, p, 2);
|
|
assert!(h.state().auto.find(&format!("timeline.layer.{layer}.nameEdit")).is_some(), "double-click opened the rename field");
|
|
for shift_first in [true, false] {
|
|
switch_layout(&mut h, shift_first);
|
|
assert!(h.state().auto.find(&format!("timeline.layer.{layer}.nameEdit")).is_some(), "the rename field stays open");
|
|
type_text(&mut h, if shift_first { "طبقة " } else { "مرحبا" });
|
|
}
|
|
key(&mut h, Key::Enter);
|
|
assert_eq!(layer_name(&h), "طبقة مرحبا");
|
|
// The Project panel's rename field, with Hebrew, Devanagari and Thai.
|
|
let p = center(&h, &format!("project.item.{comp}.name"));
|
|
click(&mut h, p, 1);
|
|
key(&mut h, Key::Enter);
|
|
switch_layout(&mut h, true);
|
|
type_text(&mut h, "שכבה नाम ชื่อ");
|
|
key(&mut h, Key::Enter);
|
|
assert_eq!(h.state().session.project.items.values().find(|i| i.id.0 == comp).unwrap().name, "שכבה नाम ชื่อ");
|
|
// A plain Alt tap still takes the keyboard to the menu bar (#279).
|
|
if !cfg!(target_os = "macos") {
|
|
h.input_mut().events.push(Event::ModifiersChanged(Modifiers::ALT));
|
|
h.step();
|
|
h.input_mut().events.push(Event::ModifiersChanged(Modifiers::NONE));
|
|
h.run_steps(2);
|
|
let focused = h.query_by(|n| n.is_focused()).and_then(|n| n.accesskit_node().label()).unwrap_or_default();
|
|
assert_eq!(focused.trim(), "File", "Alt focused the menu bar");
|
|
}
|
|
}
|
|
|
|
/// #488: once a rename field is open, the arrow keys move its caret (the whole name is selected
|
|
/// only as it opens), so part of a name can be edited.
|
|
#[test]
|
|
fn arrow_keys_move_the_caret_in_rename_fields() {
|
|
let (mut h, comp) = harness();
|
|
let name = |h: &Harness<'_, EffectcraftApp>| h.state().session.project.items.values().find(|i| i.id.0 == comp).unwrap().name.clone();
|
|
let p = center(&h, &format!("project.item.{comp}.name"));
|
|
click(&mut h, p, 1);
|
|
key(&mut h, Key::Enter);
|
|
// The arrows keep the field (Up / Down too); in "Main", two to the left of its end.
|
|
key(&mut h, Key::ArrowRight);
|
|
key(&mut h, Key::ArrowLeft);
|
|
key(&mut h, Key::ArrowUp);
|
|
key(&mut h, Key::ArrowDown);
|
|
key(&mut h, Key::End);
|
|
key(&mut h, Key::ArrowLeft);
|
|
key(&mut h, Key::ArrowLeft);
|
|
type_text(&mut h, "X");
|
|
key(&mut h, Key::Backspace);
|
|
key(&mut h, Key::Backspace);
|
|
type_text(&mut h, "Y");
|
|
key(&mut h, Key::Enter);
|
|
assert_eq!(name(&h), "MYin");
|
|
// The Timeline's layer rename.
|
|
let layer = h.state_mut().session.execute("layer.newSolid", json!({"name": "Plate", "color": "#406080"})).unwrap()["layer"].as_u64().unwrap();
|
|
h.state_mut().session.execute("layer.select", json!({"layers": [layer]})).unwrap();
|
|
h.run_steps(3);
|
|
let p = center(&h, &format!("timeline.layer.{layer}.row"));
|
|
click(&mut h, p, 1);
|
|
key(&mut h, Key::Enter);
|
|
key(&mut h, Key::ArrowRight);
|
|
key(&mut h, Key::ArrowUp);
|
|
key(&mut h, Key::End);
|
|
key(&mut h, Key::ArrowLeft);
|
|
key(&mut h, Key::ArrowLeft);
|
|
type_text(&mut h, "-");
|
|
key(&mut h, Key::Enter);
|
|
let layer_name = h.state().session.active_comp().unwrap().layer(effectcraft_engine::project::LayerId(layer)).unwrap().name.clone();
|
|
assert_eq!(layer_name, "Pla-te");
|
|
}
|