#2859: test the real marquee selection path

The unit test re-implemented Marquee::selection's box/outline split, so it
only covered `targets`. Move the anchor math into `Anchor::press` /
`Anchor::selection` over a plain `View`, call it from the test, and add a
scrolled-drag case that fails with a screen-fixed anchor.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
arnav-hiwarkar
2026-11-10 11:01:42 +05:30
co-authored by Claude Opus 5.5
parent 384184a12b
commit 58a1f3df17
2 changed files with 54 additions and 25 deletions
+38 -16
View File
@@ -16,10 +16,32 @@ struct Marquee {
rows: Arc<Vec<Row>>,
graph_on: bool,
layout: (Rect, f32, f32),
anchor: Anchor,
dragging: bool,
}
/// The pressed corner in content coordinates: a time (or an outline x) and a scroll-free y.
#[derive(Clone, Copy)]
pub(super) struct Anchor {
time: f64,
y: f32,
outline_x: Option<f32>,
dragging: bool,
}
/// The timeline layout a marquee is projected through.
#[derive(Clone, Copy)]
pub(super) struct View {
pub tm: TMap,
/// Screen y of the first row at scroll 0.
pub top: f32,
pub graph_x0: f32,
pub graph_on: bool,
}
impl View {
fn of(g: &Rw) -> Self {
Self { tm: g.tl.tm, top: g.tl.rows_rect.min.y, graph_x0: g.tl.graph_x0, graph_on: g.graph_on }
}
}
fn id() -> Id {
@@ -65,9 +87,7 @@ pub(super) fn prepare(app: &EffectcraftApp, ui: &egui::Ui, g: &Rw) {
rows: g.tl.rows.clone(),
graph_on: g.graph_on,
layout: (g.tl.rect, g.tl.rh, g.tl.graph_x0),
time: g.tl.tm.t(p.x.max(g.tl.graph_x0)),
y: p.y - g.tl.rows_rect.min.y + app.ui.timeline.scroll_y,
outline_x: (p.x < g.tl.graph_x0).then_some(p.x),
anchor: Anchor::press(&View::of(g), app.ui.timeline.scroll_y, p),
dragging: false,
},
);
@@ -98,19 +118,21 @@ pub(super) struct SelectionBox {
pub outline: bool,
}
impl Marquee {
impl Anchor {
/// Anchor a press at screen point `p` with the rows scrolled by `scroll`.
pub(super) fn press(v: &View, scroll: f32, p: Pos2) -> Self {
Self { time: v.tm.t(p.x.max(v.graph_x0)), y: p.y - v.top + scroll, outline_x: (p.x < v.graph_x0).then_some(p.x) }
}
/// Reproject the content anchor without clipping the logical selection bounds.
fn selection(&self, g: &Rw, scroll: f32, pointer: Pos2) -> (Rect, SelectionBox) {
let tm = g.tl.tm;
let anchor = pos2(self.outline_x.unwrap_or_else(|| tm.x(self.time)), g.tl.rows_rect.min.y + self.y - scroll);
pub(super) fn selection(&self, v: &View, scroll: f32, pointer: Pos2) -> (Rect, SelectionBox) {
let tm = v.tm;
let anchor = pos2(self.outline_x.unwrap_or_else(|| tm.x(self.time)), v.top + self.y - scroll);
let paint = Rect::from_two_pos(anchor, pointer);
let graph = self.outline_x.is_none() || pointer.x >= g.tl.graph_x0;
let keys = if graph && !g.graph_on {
Rect::from_two_pos(pos2(tm.x(self.time), anchor.y), pos2(pointer.x.max(g.tl.graph_x0), pointer.y))
} else {
Rect::NOTHING
};
(paint, SelectionBox { keys, outline: self.outline_x.is_some() || pointer.x < g.tl.graph_x0 })
let graph = self.outline_x.is_none() || pointer.x >= v.graph_x0;
let keys =
if graph && !v.graph_on { Rect::from_two_pos(pos2(tm.x(self.time), anchor.y), pos2(pointer.x.max(v.graph_x0), pointer.y)) } else { Rect::NOTHING };
(paint, SelectionBox { keys, outline: self.outline_x.is_some() || pointer.x < v.graph_x0 })
}
}
@@ -151,7 +173,7 @@ pub(super) fn finish(app: &EffectcraftApp, ui: &egui::Ui, g: &mut Rw) {
return;
}
let Some(pointer) = ctx.input(|i| i.pointer.latest_pos()) else { return };
let (paint, selection) = drag.selection(g, app.ui.timeline.scroll_y, pointer);
let (paint, selection) = drag.anchor.selection(&View::of(g), app.ui.timeline.scroll_y, pointer);
if dropped {
ctx.data_mut(|d| d.remove::<Marquee>(id()));
let (layers, keys) = targets(&g.tl.comp, &g.tl.rows, &g.tl.tm, g.tl.rows_rect.min.y - app.ui.timeline.scroll_y, g.tl.rh, paint, &selection);
+16 -9
View File
@@ -960,25 +960,32 @@ fn a_box_over_both_columns_selects_the_rows_it_crosses_and_the_keys_inside() {
}
// Rows are 20 pt and 400 pt wide: the outline is left of x 200, a key at 0 s sits at x 205.
let tm = TMap { x0: 205.0, start: 0.0, pps: 100.0 };
let go = |br: Rect| {
let keys = if br.max.x < 200.0 { Rect::NOTHING } else { Rect::from_min_max(pos2(br.min.x.max(200.0), br.min.y), br.max) };
marquee::targets(comp, &rows, &tm, 0.0, 20.0, br, &marquee::SelectionBox { keys, outline: br.min.x < 200.0 })
let view = marquee::View { tm, top: 0.0, graph_x0: 200.0, graph_on: false };
// Press at `from` with the rows scrolled by `s0`, release at `to` scrolled by `s1`.
let drag = |from: Pos2, s0: f32, to: Pos2, s1: f32| {
let (paint, sel) = marquee::Anchor::press(&view, s0, from).selection(&view, s1, to);
marquee::targets(comp, &rows, &tm, -s1, 20.0, paint, &sel)
};
let go = |from: Pos2, to: Pos2| drag(from, 0.0, to, 0.0);
let keyed = |ks: &[effectcraft_engine::KeyRef]| ks.iter().map(|k| k.layer.0).collect::<Vec<_>>();
// From the outline across the boundary: layer A's row (y 0..20) and B's row, and their keys.
let (ls, ks) = go(Rect::from_min_max(pos2(10.0, 5.0), pos2(260.0, 65.0)));
let (ls, ks) = go(pos2(10.0, 5.0), pos2(260.0, 65.0));
assert_eq!(ls, [layers[0], layers[1]]);
assert_eq!(keyed(&ks), [layers[0], layers[1]]);
// The same box dragged the other way is the same rectangle: the same rows, the same keys.
let (ls, ks) = go(Rect::from_two_pos(pos2(260.0, 65.0), pos2(10.0, 5.0)));
// The same box dragged the other way: the same rows, the same keys.
let (ls, ks) = go(pos2(260.0, 65.0), pos2(10.0, 5.0));
assert_eq!((ls, keyed(&ks)), (vec![layers[0], layers[1]], vec![layers[0], layers[1]]));
// Only over the outline: rows, no keys; only over the graph: keys, and the rows crossed.
let (ls, ks) = go(Rect::from_min_max(pos2(10.0, 5.0), pos2(150.0, 65.0)));
let (ls, ks) = go(pos2(10.0, 5.0), pos2(150.0, 65.0));
assert_eq!((ls.len(), ks.len()), (2, 0), "a key under the outline isn't touched");
// Kept inside the time graph: keys only, no rows (a marquee over empty track area).
let (ls, ks) = go(Rect::from_min_max(pos2(204.0, 5.0), pos2(260.0, 65.0)));
let (ls, ks) = go(pos2(204.0, 5.0), pos2(260.0, 65.0));
assert_eq!((ls.len(), keyed(&ks)), (0, vec![layers[0], layers[1]]));
// A locked layer is not selected as a row (the engine drops its keys in keys.select).
let (ls, _) = go(Rect::from_min_max(pos2(10.0, 5.0), pos2(260.0, 119.0)));
let (ls, _) = go(pos2(10.0, 5.0), pos2(260.0, 119.0));
assert_eq!(ls, [layers[0], layers[1]]);
// Scrolled 20 pt mid-drag: the pressed corner stays on layer A's row (#2859), so A is still
// selected; a screen-fixed anchor would start the box below A and select only B.
let (ls, _) = drag(pos2(10.0, 5.0), 0.0, pos2(260.0, 45.0), 20.0);
assert_eq!(ls, [layers[0], layers[1]]);
}