Canvas · the wheel scrolls as scroll boxes do; pans hold at the press

A wheel without a modifier now slides the content by its delta, as
gpui's scroll boxes do; it moved the other way. The wheel reads the
live viewport and skips an end with no delta, and a drag pans from the
press by the pointer's whole way with the viewport held at the press,
so events that land in one frame no longer overwrite each other. Tests:
a plain wheel moves the content up with it; three wheels and an empty
end add up, and four moves pan their sum; a small frame fits at a zoom
of one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ZacharyZhang-NYandClaude Opus 5.5 committed 2026-09-27 07:15:01 -04:00
1 parent 2c099ceffb
commit ea99d30e3b
4 files changed
+73 -15

No files matched your search

+1
View File
@@ -373,3 +373,4 @@ Per-component lines live in `tasks/`. Tags there:
| T31c | 2 | PASS | |
| T31d | 1 | FAIL | a focused segment past the strip's fold stayed hidden; the tracker lost focus as Start and Stop swapped; Stop and Skip went untested; percent did not refuse content |
| T31d | 2 | PASS | |
| T32a | 1 | FAIL | the plain wheel panned against every scroll box; the handlers panned from the render's viewport, so events within a frame overwrote each other; the fit's cap at one went untested |
+19 -15
View File
@@ -22,11 +22,11 @@ struct Pan {
owner: EntityId,
}
/// The canvas's box and the pointer of a pan under way.
/// The canvas's box, and the pointer and viewport at the press of a pan under way.
#[derive(Default)]
struct Plane {
bounds: Bounds<Pixels>,
grip: Option<Point<Pixels>>,
grip: Option<(Point<Pixels>, Viewport)>,
}
/// The view pixels of a canvas frame.
@@ -400,6 +400,7 @@ impl RenderOnce for InfiniteCanvas {
let owner = plane.entity_id();
let (wheel, pan, drop) = (set.clone(), set, plane.clone());
let (scrolled, dragged, pressed) = (plane.clone(), plane.clone(), plane);
let (live, held) = (local.clone(), local.clone());
div()
.id(id)
.relative()
@@ -416,34 +417,37 @@ impl RenderOnce for InfiniteCanvas {
(lines.x * line, lines.y * line)
}
};
if delta == (0.0, 0.0) {
return;
}
let pointer = event.position - bounds.origin;
let about = (f32::from(pointer.x), f32::from(pointer.y));
let now = view;
let now = live.read(cx).value;
let next = match event.modifiers.platform || event.modifiers.control {
true => now.zoomed((-delta.1 / 240.0).exp(), about),
false => now.panned((-delta.0, -delta.1)),
false => now.panned(delta),
};
cx.stop_propagation();
wheel(next, window, cx);
})
.on_mouse_down(gpui::MouseButton::Left, move |event, _, cx| {
pressed.update(cx, |plane, _| plane.grip = Some(event.position))
let now = held.read(cx).value;
pressed.update(cx, |plane, _| plane.grip = Some((event.position, now)))
})
.on_drag(Pan { owner }, |_, _, _, cx| cx.new(|_| EmptyView))
.on_drag_move::<Pan>(move |event, window, cx| {
if event.drag(cx).owner != owner {
return;
}
let pointer = event.event.position;
let last = dragged.update(cx, |plane, _| plane.grip.replace(pointer));
if let Some(last) = last {
let delta = pointer - last;
pan(
view.panned((f32::from(delta.x), f32::from(delta.y))),
window,
cx,
);
}
let Some((press, start)) = dragged.read(cx).grip else {
return;
};
let way = event.event.position - press;
pan(
start.panned((f32::from(way.x), f32::from(way.y))),
window,
cx,
);
})
.on_drop(move |_: &Pan, _, cx| drop.update(cx, |plane, _| plane.grip = None))
.child(painted)
+50
View File
@@ -155,3 +155,53 @@ fn a_press_on_the_map_centers_the_view_there(cx: &mut TestAppContext) {
"the view centers on the point pressed: {middle:?}"
);
}
fn wheel(at: (f32, f32), dy: f32, modifiers: Modifiers) -> ScrollWheelEvent {
ScrollWheelEvent {
position: point(px(at.0), px(at.1)),
delta: ScrollDelta::Pixels(point(px(0.0), px(dy))),
modifiers,
touch_phase: TouchPhase::Moved,
}
}
#[gpui::test]
fn a_plain_wheel_moves_the_content_as_a_scroll_box_does(cx: &mut TestAppContext) {
let (host, cx) = stage(plane, cx);
cx.simulate_event(wheel((100.0, 50.0), -50.0, Modifiers::none()));
settle(cx);
let view = *heard(&host, cx).last().expect("a scroll");
assert_eq!(
view.to_view((0.0, 0.0)),
(0.0, -50.0),
"the content went up with the wheel"
);
}
#[gpui::test]
fn wheels_and_moves_add_up(cx: &mut TestAppContext) {
let (host, cx) = stage(plane, cx);
for dy in [-10.0, -10.0, -10.0, 0.0] {
cx.simulate_event(wheel((100.0, 50.0), dy, Modifiers::none()));
}
settle(cx);
let view = *heard(&host, cx).last().expect("a scroll");
assert_eq!(
view.y, 30.0,
"three wheels add up, and an end that moves nothing leaves it"
);
let at = |x: f32, y: f32| point(px(x), px(y));
cx.simulate_mouse_move(at(200.0, 150.0), None, Modifiers::none());
cx.simulate_mouse_down(at(200.0, 150.0), MouseButton::Left, Modifiers::none());
for x in [190.0, 180.0, 170.0, 160.0] {
cx.simulate_mouse_move(at(x, 150.0), Some(MouseButton::Left), Modifiers::none());
}
cx.simulate_mouse_up(at(160.0, 150.0), MouseButton::Left, Modifiers::none());
settle(cx);
let view = *heard(&host, cx).last().expect("a pan");
assert_eq!(
(view.x, view.y),
(40.0, 30.0),
"the drag pans from the press by the pointer's whole way"
);
}
+3
View File
@@ -159,6 +159,9 @@ mod tests {
let view = Viewport::fitting(Frame::new(0.0, 0.0, 800.0, 400.0), (440.0, 440.0), 20.0);
assert_eq!(view.zoom, 0.5);
assert_eq!(view.to_view((400.0, 200.0)), (220.0, 220.0));
let small = Viewport::fitting(Frame::new(0.0, 0.0, 100.0, 50.0), (440.0, 440.0), 20.0);
assert_eq!(small.zoom, 1.0, "no nearer than a zoom of one");
assert_eq!(small.to_view((50.0, 25.0)), (220.0, 220.0));
}
#[test]