Merge pull request #239 from storytold/fix/230-viewer-selected-layer-first

Viewer: a press on a selected layer behind others acts on it (#230)
This commit is contained in:
BFlat 2026-10-08 12:50:11 +07:00 committed by GitHub
commit bb3e63e8f3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 45 additions and 10 deletions

View File

@ -909,10 +909,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
// becomes the target, using the same visible/unlocked hit test as left-click.
pick(app, &ectx, map.to_comp(pos), false);
let point = map.to_comp(pos);
let hits: Vec<(u64, String)> = selectable_layers(&comp, time)
.filter(|l| layer_quad(&ectx, l).is_some_and(|(_, q, _)| point_in_quad(point, &q)))
.map(|l| (l.id.0, l.name.clone()))
.collect();
let hits: Vec<(u64, String)> = layers_at(&ectx, point).map(|l| (l.id.0, l.name.clone())).collect();
ui.data_mut(|d| d.insert_temp(menu_hits_id, hits));
}
let context_layers: Vec<u64> =
@ -1754,17 +1751,29 @@ pub(crate) fn selectable_layers(comp: &Comp, time: Tick) -> impl Iterator<Item =
.filter(move |l| l.is_active_at(time) && l.switches.video && !l.switches.locked && !l.is_camera() && !l.is_light() && (!any_solo || l.switches.solo))
}
/// Topmost selectable layer under a comp point.
pub(crate) fn layer_at<'a>(ectx: &EvalCtx<'a>, cpt: [f64; 2]) -> Option<&'a Layer> {
selectable_layers(ectx.comp, ectx.time).find(|l| layer_quad(ectx, l).is_some_and(|(_, q, _)| point_in_quad(cpt, &q)))
/// Selectable layers under a comp point, front to back.
fn layers_at<'a, 'b>(ectx: &'b EvalCtx<'a>, cpt: [f64; 2]) -> impl Iterator<Item = &'a Layer> + 'b {
selectable_layers(ectx.comp, ectx.time).filter(move |l| layer_quad(ectx, l).is_some_and(|(_, q, _)| point_in_quad(cpt, &q)))
}
/// Topmost layer under a comp point (selects it; shift toggles). Returns the hit layer.
/// Topmost selectable layer under a comp point.
pub(crate) fn layer_at<'a>(ectx: &EvalCtx<'a>, cpt: [f64; 2]) -> Option<&'a Layer> {
layers_at(ectx, cpt).next()
}
/// The layer a press at a comp point acts on, selected (Shift toggles the topmost layer). As in
/// After Effects, a selected layer under the point comes before the layers in front of it, so it
/// can be dragged where they cover it; otherwise the topmost layer (#230).
pub(crate) fn pick(app: &mut EffectcraftApp, ectx: &EvalCtx, cpt: [f64; 2], toggle: bool) -> Option<LayerId> {
let hit = layer_at(ectx, cpt).map(|l| l.id)?;
if toggle {
let hit = layer_at(ectx, cpt)?.id;
let _ = app.session.execute("layer.select", json!({"layers": [hit.0], "toggle": true}));
} else if !app.session.state.selected_layers.contains(&hit) {
return Some(hit);
}
let selected = &app.session.state.selected_layers;
let hits: Vec<LayerId> = layers_at(ectx, cpt).map(|l| l.id).collect();
let hit = *hits.iter().find(|id| selected.contains(id)).or(hits.first())?;
if !selected.contains(&hit) {
let _ = app.session.execute("layer.select", json!({"layers": [hit.0]}));
}
Some(hit)

View File

@ -134,6 +134,32 @@ fn viewer_click_ignores_inactive_solo_layers() {
assert_eq!(h.state().session.state.selected_layers, vec![back]);
}
/// A selected layer under the pointer takes the press before the layers in front of it, as in
/// After Effects: it stays selected and a drag moves it. With none selected there, the topmost
/// layer does (#230).
#[test]
fn viewer_press_prefers_a_selected_layer_under_the_pointer() {
let mut h = selection_harness();
let comp = h.state().session.active_comp().unwrap();
let (front, back) = (comp.layers[0].id, comp.layers[1].id);
h.state_mut().session.execute("layer.select", json!({"layers": [back.0]})).unwrap();
h.run_steps(3);
let at = screen(&h, [320.0, 180.0]);
click(&mut h, at);
assert_eq!(h.state().session.state.selected_layers, vec![back], "a click keeps the selected layer behind");
h.run_steps(40);
let (front0, back0) = (position_and_anchor(&h, front).0, position_and_anchor(&h, back).0);
drag(&mut h, at, at + vec2(60.0, 0.0));
assert_eq!(h.state().session.state.selected_layers, vec![back]);
assert_eq!(position_and_anchor(&h, front).0, front0, "the layer in front stays");
assert!(position_and_anchor(&h, back).0[0] > back0[0] + 10.0, "the selected layer behind moves");
h.state_mut().session.execute("edit.deselectAll", json!({})).unwrap();
h.run_steps(40);
let at = screen(&h, [300.0, 180.0]);
click(&mut h, at);
assert_eq!(h.state().session.state.selected_layers, vec![front], "nothing selected: the topmost layer");
}
#[test]
fn bottom_bar_in_after_effects_order() {
let h = harness();