diff --git a/crates/ui-egui/src/panels/viewer.rs b/crates/ui-egui/src/panels/viewer.rs index 95a4dd0b..f3ba1562 100644 --- a/crates/ui-egui/src/panels/viewer.rs +++ b/crates/ui-egui/src/panels/viewer.rs @@ -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 = @@ -1754,17 +1751,29 @@ pub(crate) fn selectable_layers(comp: &Comp, time: Tick) -> impl Iterator(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 + '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 { - 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 = 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) diff --git a/crates/ui-egui/tests/ui_viewer.rs b/crates/ui-egui/tests/ui_viewer.rs index 52033fe3..b2014509 100644 --- a/crates/ui-egui/tests/ui_viewer.rs +++ b/crates/ui-egui/tests/ui_viewer.rs @@ -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();