Merge pull request #930 from aniliashenko/text-edit-from-timeline

#637: double-clicking a layer's bar acts like its name; text layers edit their text

# Conflicts:
#	crates/ui-egui/tests/ui_text_edit.rs
This commit is contained in:
Brandon Thomas 2026-10-12 04:21:02 -04:00
commit 0d6adb07bf
3 changed files with 58 additions and 7 deletions

View File

@ -578,6 +578,18 @@ pub fn begin_rename(app: &mut EffectcraftApp, ctx: &egui::Context) {
}
}
/// What double-clicking a layer's name or bar does, as in After Effects: a precomp opens, footage
/// and solids open in the Layer panel (paint happens there), text is edited in the viewer with the
/// whole text selected. `None`: the name renames, the bar does nothing.
fn double_click_action(layer: &Layer) -> Option<(String, serde_json::Value)> {
match &layer.source {
LayerSource::Comp { item } => Some(("comp.open".into(), json!({"comp": item.0}))),
LayerSource::Footage { .. } | LayerSource::Solid { .. } => Some(("layer.openLayer".into(), json!({"layer": layer.id.0}))),
LayerSource::Text => Some(("text.edit".into(), json!({"layer": layer.id.0}))),
_ => None,
}
}
/// Open the rename field on layer `id`; it takes the keyboard focus on its first frame only.
fn start_rename(ctx: &egui::Context, id: u64, name: &str) {
ctx.data_mut(|d| {
@ -1888,13 +1900,9 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
}
}
if row_resp.double_clicked() {
match &layer.source {
LayerSource::Comp { item } => actions.push(("comp.open".into(), json!({"comp": item.0}))),
// Footage and solids open in the Layer panel (paint happens there).
LayerSource::Footage { .. } | LayerSource::Solid { .. } => {
actions.push(("layer.openLayer".into(), json!({"layer": layer.id.0})));
}
_ => start_rename(&ctx, layer.id.0, &layer.name),
match double_click_action(layer) {
Some(a) => actions.push(a),
None => start_rename(&ctx, layer.id.0, &layer.name),
}
}
layer_context_menu(&row_resp, &comp, layer, &selected, &mut actions);
@ -2098,6 +2106,11 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
if lin.hovered() || lout.hovered() || lin.dragged() || lout.dragged() {
ctx.set_cursor_icon(egui::CursorIcon::ResizeHorizontal);
}
if body.double_clicked()
&& let Some(a) = double_click_action(layer)
{
actions.push(a);
}
let shift = ui.input(|i| i.modifiers.shift);
if let Some(layers) = (body.clicked() && shift).then(|| shift_range(&ctx, cid, &comp, &rows, &selected, layer.id)).flatten() {
// Shift-click: the layers from the last one clicked to this one, as in

View File

@ -167,6 +167,18 @@ fn nested_comp_markers_show_on_the_precomp_bar() {
assert!((h.state().session.time().seconds() - 1.0).abs() < 0.034, "{}", h.state().session.time().seconds());
}
/// #637: double-clicking a precomp layer's bar opens its comp, as double-clicking its name does.
#[test]
fn double_clicking_a_precomp_bar_opens_its_comp() {
let (mut h, pre, main) = harness();
h.state_mut().session.execute("comp.open", json!({"comp": main})).unwrap();
h.run_steps(4);
let l = h.state().session.project.comp(ItemId(main)).unwrap().layers[0].id.0;
let bar = rect(&h, &format!("timeline.layer.{l}.bar"));
click_n(&mut h, bar.center(), 2);
assert_eq!(h.state().session.active_comp_id(), Some(ItemId(pre)));
}
/// Issue #46: a panel dragged by its panel-menu (hamburger) button onto the left edge of another
/// group docks there as its own group; dragging the gap between panels resizes them.
#[test]

View File

@ -438,3 +438,29 @@ fn layer_new_text_menu_and_shortcut_start_typing_into_source_text() {
assert_eq!(s.active_comp().unwrap().layer(LayerId(lid)).unwrap().name, "My First Project", "the name follows the text");
}
}
/// Double-clicking a text layer in the Timeline, on its name or on its bar, edits its text in the
/// Composition viewer with the whole text selected, as in After Effects (Enter renames).
#[test]
fn timeline_double_click_on_a_text_layer_edits_its_text() {
for target in ["row", "bar"] {
let mut s = Session::default();
s.execute("comp.new", json!({"name": "Type", "width": 640, "height": 360, "duration": 2})).unwrap();
let lid = s.execute("layer.newText", json!({"text": "alpha beta"})).unwrap()["layer"].as_u64().unwrap();
let mut h = Harness::builder().with_size(egui::vec2(1600.0, 1000.0)).with_step_dt(1.0 / 60.0).build_eframe(move |_| EffectcraftApp::new(s));
h.state_mut().show_panel(PanelKind::Composition);
h.run_steps(3);
let p = rect(&h, &format!("timeline.layer.{lid}.{target}")).center();
click(&mut h, p, Modifiers::NONE);
click(&mut h, p, Modifiers::NONE);
let (layer, text, anchor, caret) = edited(&h);
assert_eq!(layer, lid, "{target}");
assert_eq!((anchor.min(caret), anchor.max(caret)), (0, 10), "everything selected ({target})");
assert!(h.state().auto.find(&format!("timeline.layer.{lid}.nameEdit")).is_none(), "no rename field ({target})");
assert_eq!(text, "alpha beta");
type_text(&mut h, "gamma");
key(&mut h, Key::Escape, Modifiers::NONE);
assert!(h.state().session.state.text_edit.is_none(), "Escape commits ({target})");
assert_eq!(layer_doc(&h.state().session, LayerId(lid)).unwrap().text, "gamma", "typing replaced the text ({target})");
}
}