UI: let Type tool edit selected overlapping text while respecting visibility

(cherry picked from commit 9fd5b1e5618a6ad19638494b480460e9527debde)

(cherry picked from commit 812e34fd7834e828bebd3f79440f831d188a1a99)
(cherry picked from commit 283adb1a30e6b51ab6603488e57b9997a0ca83f0)
This commit is contained in:
Elias
2026-10-11 12:29:57 +00:00
committed by Claude
parent 88e40f444a
commit 4bf9bb273e
2 changed files with 58 additions and 6 deletions
+17 -6
View File
@@ -117,12 +117,23 @@ fn edited(app: &EffectcraftApp, ectx: &EvalCtx, lid: LayerId) -> Option<Edited>
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<Edited> {
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<Edited> {
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<Value> {
@@ -207,7 +218,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 });
@@ -309,7 +320,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;
+41
View File
@@ -342,3 +342,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");
}
}