diff --git a/crates/engine/src/cmd/layer.rs b/crates/engine/src/cmd/layer.rs index 89cf45158..de43454f4 100644 --- a/crates/engine/src/cmd/layer.rs +++ b/crates/engine/src/cmd/layer.rs @@ -24,7 +24,7 @@ mod transform; use contents::{add_mask, add_shape_item, mask_props, set_text}; use create::{add_item, new_adjustment, new_null, new_shape, new_solid, new_text}; -pub(crate) use create::{color_p, fit_span, index_p, insert_layer, insert_layer_at, new_layer_span, place, position_p}; +pub(crate) use create::{color_p, fit_span, index_p, insert_layer, insert_layer_at, new_layer_span, place, position_p, show_span}; pub(crate) use parent::parent_fixes; use parent::set_parent; use replace::{has_replace_source, replace_source}; diff --git a/crates/engine/src/cmd/layer/create.rs b/crates/engine/src/cmd/layer/create.rs index 14d9f1e0f..5147ccc71 100644 --- a/crates/engine/src/cmd/layer/create.rs +++ b/crates/engine/src/cmd/layer/create.rs @@ -30,6 +30,16 @@ pub(crate) fn fit_span(l: &mut Layer, span: Option<(Tick, Tick)>) { } } +/// Tools draw on the layer they create: when it was fitted to a span the playhead is outside, +/// move the playhead to the span's start so the new layer shows. The layer's timing is kept. +pub(crate) fn show_span(s: &mut Session, span: Option<(Tick, Tick)>) { + if let Some((start, end)) = span + && !(start..end).contains(&s.time()) + { + s.set_time(start); + } +} + /// Insert a new layer above the selection (or at the top) and select it. pub(crate) fn insert_layer(proj: &mut Project, st: &mut crate::EditorState, cid: ItemId, layer: Layer) -> Result { insert_layer_at(proj, st, cid, layer, None) @@ -233,6 +243,9 @@ pub(super) fn new_text(s: &mut Session, p: &Value) -> Result { } Ok(id) })?; + if edit { + show_span(s, span); + } Ok(json!({"layer": id.0})) } diff --git a/crates/engine/src/cmd/shape_tool.rs b/crates/engine/src/cmd/shape_tool.rs index 99f1cd859..d983ce8d0 100644 --- a/crates/engine/src/cmd/shape_tool.rs +++ b/crates/engine/src/cmd/shape_tool.rs @@ -363,6 +363,9 @@ fn new_shape(s: &mut Session, p: &Value) -> Result { st.selected_layers = vec![lid]; Ok((lid, group)) })?; + if target.is_none() { + super::layer::show_span(s, span); + } Ok(json!({"layer": lid.0, "group": group})) } diff --git a/crates/engine/src/cmd/viewer_cmds.rs b/crates/engine/src/cmd/viewer_cmds.rs index 39a5102c9..2840873cd 100644 --- a/crates/engine/src/cmd/viewer_cmds.rs +++ b/crates/engine/src/cmd/viewer_cmds.rs @@ -301,6 +301,9 @@ fn new_path(s: &mut Session, p: &Value) -> Result { st.selected_vertices = vec![VertexRef { layer: lid, mask: path_uid, index: n - 1 }]; Ok((lid, group, path_uid)) })?; + if target.is_none() { + super::layer::show_span(s, span); + } Ok(json!({"layer": lid.0, "group": group, "path": path_uid})) } diff --git a/crates/engine/src/tests/settings.rs b/crates/engine/src/tests/settings.rs index 34f7689f6..f8eb8ed82 100644 --- a/crates/engine/src/tests/settings.rs +++ b/crates/engine/src/tests/settings.rs @@ -398,20 +398,26 @@ fn new_layers_fit_the_work_area_when_asked() { s.execute("prefs.set", json!({"key": "general.fitNewLayersToWorkArea", "value": true})).unwrap(); s.execute("prefs.set", json!({"key": "general.createLayersAtCompStart", "value": false})).unwrap(); - s.set_time(effectcraft_time::Tick::from_seconds_f64(7.0)); - for (cmd, p) in [ - ("layer.newSolid", json!({})), - ("layer.newAdjustment", json!({})), - ("layer.newNull", json!({})), - ("layer.newText", json!({"text": "a"})), - ("layer.newShape", json!({"kind": "rect"})), - ("shape.newShape", json!({"kind": "ellipse"})), - ("shape.newPath", json!({"vertices": [[0, 0], [10, 0], [10, 10]]})), + // The tools that draw on their new layer (Type, shape tools, Pen) move the playhead into it; + // the menu commands leave it. + for (cmd, p, draws) in [ + ("layer.newSolid", json!({}), false), + ("layer.newAdjustment", json!({}), false), + ("layer.newNull", json!({}), false), + ("layer.newText", json!({"text": "a"}), false), + ("layer.newText", json!({"text": "a", "edit": true}), true), + ("layer.newShape", json!({"kind": "rect"}), false), + ("shape.newShape", json!({"kind": "ellipse"}), true), + ("shape.newPath", json!({"vertices": [[0, 0], [10, 0], [10, 10]]}), true), ] { + s.set_time(effectcraft_time::Tick::from_seconds_f64(7.0)); s.state.selected_layers.clear(); + s.state.text_edit = None; s.execute(cmd, p).unwrap(); assert_eq!(span(&s), (2.0, 2.0, 5.0), "{cmd}"); + assert_eq!(s.time().seconds().round(), if draws { 2.0 } else { 7.0 }, "{cmd} playhead"); } + s.set_time(effectcraft_time::Tick::from_seconds_f64(7.0)); let nested = s.execute("comp.new", json!({"name": "Nested", "duration": 3, "open": false})).unwrap()["comp"].as_u64().unwrap(); s.execute("layer.addItem", json!({"item": nested})).unwrap(); diff --git a/crates/ui-egui/tests/it/ui_text_edit.rs b/crates/ui-egui/tests/it/ui_text_edit.rs index 6b68028de..24a43522f 100644 --- a/crates/ui-egui/tests/it/ui_text_edit.rs +++ b/crates/ui-egui/tests/it/ui_text_edit.rs @@ -169,6 +169,36 @@ fn type_tool_click_type_edit_and_commit() { assert!(h.state().session.state.text_edit.is_none()); } +/// Fit New Layers to Work Area with the playhead past the work area: the Type tool's new layer +/// spans the work area and the playhead moves into it, so the text shows while it is typed and +/// after it is committed. +#[test] +fn type_tool_text_fitted_to_the_work_area_past_the_playhead_shows() { + let mut h = harness(app()); + h.state_mut().show_panel(PanelKind::Composition); + let s = &mut h.state_mut().session; + s.execute("prefs.set", json!({"key": "general.fitNewLayersToWorkArea", "value": true})).unwrap(); + s.execute("comp.workArea", json!({"start": 0.5, "end": 1})).unwrap(); + s.set_time(effectcraft_engine::time::Tick::from_seconds_f64(1.5)); + h.run_steps(3); + h.state_mut().ui.tool = effectcraft_ui_egui::state::Tool::Type; + h.run_steps(1); + let comp = rect(&h, "viewer.comp"); + click(&mut h, comp.center(), Modifiers::NONE); + type_text(&mut h, "Hi"); + assert!(h.state().auto.find("viewer.textEdit").is_some(), "the edit region shows"); + let (lid, text, _, _) = edited(&h); + assert_eq!(text, "Hi"); + key(&mut h, Key::Escape, Modifiers::NONE); + assert!(h.state().session.state.text_edit.is_none(), "committed"); + let s = &h.state().session; + let l = s.active_comp().unwrap().layer(LayerId(lid)).unwrap(); + let t = s.time(); + assert!(l.in_point <= t && t < l.out_point, "the committed text is on screen: {t:?} in {:?}..{:?}", l.in_point, l.out_point); + assert_eq!(((l.in_point.seconds() * 10.0).round(), (l.out_point.seconds() * 10.0).round()), (5.0, 10.0), "the layer keeps the work area"); + assert_eq!(layer_doc(s, LayerId(lid)).unwrap().text, "Hi"); +} + /// #831: clicking another panel (a solid's row in the Timeline) ends text editing and gives the /// keyboard back, whether the edit began with the Type tool (which stays selected) or by command: /// typed characters change no text and Space plays. The Character panel keeps the edit, and a diff --git a/crates/ui-egui/tests/it/ui_viewer.rs b/crates/ui-egui/tests/it/ui_viewer.rs index 7934168cd..264f19ad0 100644 --- a/crates/ui-egui/tests/it/ui_viewer.rs +++ b/crates/ui-egui/tests/it/ui_viewer.rs @@ -1145,6 +1145,38 @@ fn shape_pen_draws_a_closed_shape_layer() { assert_eq!(sp.vertices.len(), 4); } +/// Fit New Layers to Work Area with the playhead past the work area: the Pen's new shape layer +/// spans the work area and the playhead moves into it, so the path and its vertex controls show +/// and the closing click lands. +#[test] +fn pen_draws_on_a_layer_fitted_to_the_work_area_past_the_playhead() { + let mut h = harness(); + let s = &mut h.state_mut().session; + s.execute("prefs.set", json!({"key": "general.fitNewLayersToWorkArea", "value": true})).unwrap(); + s.execute("comp.workArea", json!({"start": 1, "end": 2})).unwrap(); + s.set_time(effectcraft_engine::time::Tick::from_seconds_f64(3.0)); + h.state_mut().ui.tool = Tool::Pen; + h.run_steps(2); + let n0 = h.state().session.active_comp().unwrap().layers.len(); + for p in [[200.0, 100.0], [400.0, 120.0], [300.0, 260.0]] { + let at = screen(&h, p); + click(&mut h, at); + } + let l = h.state().session.active_comp().unwrap().layers[0].clone(); + let t = h.state().session.time(); + assert!(l.in_point <= t && t < l.out_point, "the playhead is on the new layer: {t:?} in {:?}..{:?}", l.in_point, l.out_point); + assert_eq!((l.in_point.seconds().round(), l.out_point.seconds().round()), (1.0, 2.0), "the layer keeps the work area"); + let path = l.props.sub("contents").unwrap().groups().next().unwrap().sub("contents").unwrap().groups().next().unwrap().clone(); + assert!(h.state().auto.find(&format!("viewer.shapePath.{}.vertex.0", path.uid)).is_some(), "the Pen's vertex controls show"); + let at = screen(&h, [200.0, 100.0]); + click(&mut h, at); + let comp = h.state().session.active_comp().unwrap().clone(); + assert_eq!(comp.layers.len(), n0 + 1); + let sp = comp.layers[0].props.find_group(path.uid).unwrap().get("path").unwrap().value.as_path().unwrap().clone(); + assert_eq!(sp.vertices.len(), 3); + assert!(sp.closed, "the click on the first vertex closes the path"); +} + #[test] fn motion_path_key_drag_edits_that_key() { let mut h = harness();