Viewer: a press on a selected layer behind others acts on it (#230)

A press in the Composition panel always acted on the topmost layer
under the pointer, so a selected layer could not be dragged where
another layer covered it: the press selected the layer in front. As in
After Effects, a selected layer under the pointer now comes first: it
stays selected and a drag moves it. With no selected layer there the
topmost layer is picked as before, and Shift still toggles the topmost
one. The context menu's list of layers under the pointer shares the
same hit test.

Test: viewer_press_prefers_a_selected_layer_under_the_pointer
(ui_viewer).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
bflatastic
2026-10-08 12:47:30 +07:00
co-authored by Claude Opus 5.5
parent 5d1b818fa1
commit 2c7eca7ef2
2 changed files with 45 additions and 10 deletions
+19 -10
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)
+26
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();