//! 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"); }