diff --git a/crates/ui-egui/src/panels/viewer_text.rs b/crates/ui-egui/src/panels/viewer_text.rs index f4604f2e..57b34b29 100644 --- a/crates/ui-egui/src/panels/viewer_text.rs +++ b/crates/ui-egui/src/panels/viewer_text.rs @@ -118,12 +118,23 @@ fn edited(app: &EffectcraftApp, ectx: &EvalCtx, lid: LayerId) -> Option Some(Edited { layer, doc, lay, m, inv, carets }) } -/// The topmost text layer whose text is under screen point `s`. -fn text_layer_at(app: &EffectcraftApp, ectx: &EvalCtx, map: &ViewerMap, s: Pos2) -> Option { - super::viewer::selectable_layers(ectx.comp, ectx.time) +/// Type-tool clicks prefer a selected text layer, as viewer dragging does (#230). +/// Double-clicks retain topmost picking. Both use the same visibility/lock/time/solo filter. +fn text_layer_at(app: &EffectcraftApp, ectx: &EvalCtx, map: &ViewerMap, s: Pos2, prefer_selected: bool) -> Option { + let mut topmost = None; + for e in super::viewer::selectable_layers(ectx.comp, ectx.time) .filter(|l| matches!(l.source, LayerSource::Text)) .filter_map(|l| edited(app, ectx, l.id)) - .find(|e| e.contains(map, s)) + .filter(|e| e.contains(map, s)) + { + if !prefer_selected || app.session.state.selected_layers.contains(&e.layer.id) { + return Some(e); + } + if topmost.is_none() { + topmost = Some(e); + } + } + topmost } fn exec(app: &mut EffectcraftApp, id: &str, p: Value) -> Option { @@ -223,7 +234,7 @@ pub fn hook( } } if !handled && type_tool { - if let Some(e) = text_layer_at(app, ectx, map, pos) { + if let Some(e) = text_layer_at(app, ectx, map, pos, true) { let ci = e.hit(map, pos); exec(app, "text.edit", json!({"layer": e.layer.id.0, "caret": ci})); drag = Some(Drag::Select { layer: e.layer.id }); @@ -325,7 +336,7 @@ pub fn hook( owns = true; } _ if !matches!(tool, Tool::Hand | Tool::Zoom) && !tool.is_pen() && !type_tool => { - if let Some(e) = text_layer_at(app, ectx, map, p) { + if let Some(e) = text_layer_at(app, ectx, map, p, false) { exec(app, "text.edit", json!({"layer": e.layer.id.0})); ui.memory_mut(|m| m.request_focus(focus_id())); owns = true; diff --git a/crates/ui-egui/tests/ui_text_edit.rs b/crates/ui-egui/tests/ui_text_edit.rs index d4dc71ee..d147529b 100644 --- a/crates/ui-egui/tests/ui_text_edit.rs +++ b/crates/ui-egui/tests/ui_text_edit.rs @@ -529,3 +529,44 @@ fn overlapping_duplicate_text_remains_editable_by_double_click_and_selected_type key(&mut h, Key::Z, Modifiers::COMMAND); assert_eq!(edited(&h).1, "alpha beta"); } + +#[test] +fn type_tool_selected_text_preference_skips_ineligible_layers_and_falls_back_to_topmost() { + for exclusion in ["hidden", "locked", "inactive", "unsoloed", "outside", "unselected"] { + let mut a = app(); + let mut ids = vec![]; + for _ in 0..2 { + ids.push( + a.session.execute("layer.newText", json!({"text": "alpha beta", "size": 40, "box": [40, 60, 250, 100]})).unwrap()["layer"].as_u64().unwrap(), + ); + } + let original = ids[0]; + let topmost = ids[1]; + match exclusion { + "hidden" | "locked" => { + let switch = if exclusion == "hidden" { "video" } else { "locked" }; + a.session.execute("layer.setSwitch", json!({"layers": [original], "switch": switch, "value": exclusion == "locked"})).unwrap(); + } + "inactive" => { + let cid = a.session.active_comp_id().unwrap(); + std::sync::Arc::make_mut(&mut a.session.project).comp_mut(cid).unwrap().layer_mut(LayerId(original)).unwrap().out_point = + effectcraft_engine::time::Tick::ZERO; + } + "unsoloed" => { + a.session.execute("layer.setSwitch", json!({"layers": [topmost], "switch": "solo", "value": true})).unwrap(); + } + "outside" => { + a.session.execute("prop.set", json!({"layer": original, "path": "transform/position", "value": [500, 300]})).unwrap(); + } + _ => {} + } + a.session.state.selected_layers = if exclusion == "unselected" { vec![] } else { vec![LayerId(original)] }; + a.ui.tool = effectcraft_ui_egui::state::Tool::Type; + let mut h = Harness::builder().with_size(egui::vec2(1600.0, 1000.0)).with_step_dt(1.0 / 60.0).build_eframe(move |_| a); + h.run_steps(3); + let comp = rect(&h, "viewer.comp"); + let p = comp.min + egui::vec2(80.0, 85.0) * (comp.width() / 640.0); + click(&mut h, p, Modifiers::NONE); + assert_eq!(edited(&h).0, topmost, "{exclusion} selected text must not override the eligible hit"); + } +}