Settings: move the playhead onto layers the tools fit to the work area

With Fit New Layers to Work Area on and the playhead outside the work
area, the Type tool, shape tools and Pen now move the playhead to the
work area start so the new layer and its controls show. Layer timing is
unchanged; Layer ▸ New commands leave the playhead.
This commit is contained in:
nazar-gavrylyk committed 2026-11-11 08:34:05 +01:00
1 parent 34f62efbca
commit abdf4c369a
7 files changed
+97 -10

No files matched your search

+1 -1
View File
@@ -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};
+13
View File
@@ -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<LayerId> {
insert_layer_at(proj, st, cid, layer, None)
@@ -233,6 +243,9 @@ pub(super) fn new_text(s: &mut Session, p: &Value) -> Result<Value> {
}
Ok(id)
})?;
if edit {
show_span(s, span);
}
Ok(json!({"layer": id.0}))
}
+3
View File
@@ -363,6 +363,9 @@ fn new_shape(s: &mut Session, p: &Value) -> Result<Value> {
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}))
}
+3
View File
@@ -301,6 +301,9 @@ fn new_path(s: &mut Session, p: &Value) -> Result<Value> {
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}))
}
+15 -9
View File
@@ -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();
+30
View File
@@ -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
+32
View File
@@ -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();