diff --git a/crates/ui-egui/src/panels/timeline.rs b/crates/ui-egui/src/panels/timeline.rs index 50a341ee..77916d09 100644 --- a/crates/ui-egui/src/panels/timeline.rs +++ b/crates/ui-egui/src/panels/timeline.rs @@ -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 diff --git a/crates/ui-egui/tests/ui_tabs.rs b/crates/ui-egui/tests/ui_tabs.rs index 4d6777b6..7f4689bf 100644 --- a/crates/ui-egui/tests/ui_tabs.rs +++ b/crates/ui-egui/tests/ui_tabs.rs @@ -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] diff --git a/crates/ui-egui/tests/ui_text_edit.rs b/crates/ui-egui/tests/ui_text_edit.rs index 6e16b676..d3be6a44 100644 --- a/crates/ui-egui/tests/ui_text_edit.rs +++ b/crates/ui-egui/tests/ui_text_edit.rs @@ -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})"); + } +}