Files
Ely-GPUI-Components/src/canvas/view.rs
T
ZacharyZhang-NYandClaude Opus 5.5 ea99d30e3b 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>
2026-09-27 07:15:01 -04:00

175 lines
5.8 KiB
Rust

/// A box on the canvas, in canvas units.
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct Frame {
pub x: f32,
pub y: f32,
pub w: f32,
pub h: f32,
}
impl Frame {
pub fn new(x: f32, y: f32, w: f32, h: f32) -> Self {
assert!(w >= 0.0 && h >= 0.0, "a frame of {w} by {h}");
Self { x, y, w, h }
}
pub fn right(&self) -> f32 {
self.x + self.w
}
pub fn bottom(&self) -> f32 {
self.y + self.h
}
pub fn center(&self) -> (f32, f32) {
(self.x + self.w / 2.0, self.y + self.h / 2.0)
}
/// The least frame that holds both.
pub fn union(&self, other: &Frame) -> Frame {
let (x, y) = (self.x.min(other.x), self.y.min(other.y));
Frame::new(
x,
y,
self.right().max(other.right()) - x,
self.bottom().max(other.bottom()) - y,
)
}
/// The frame spanned by two corners, in any order.
pub fn spanning((ax, ay): (f32, f32), (bx, by): (f32, f32)) -> Frame {
Frame::new(ax.min(bx), ay.min(by), (ax - bx).abs(), (ay - by).abs())
}
/// Whether it lies wholly inside `other`.
pub fn within(&self, other: &Frame) -> bool {
self.x >= other.x
&& self.y >= other.y
&& self.right() <= other.right()
&& self.bottom() <= other.bottom()
}
pub fn contains(&self, (x, y): (f32, f32)) -> bool {
x >= self.x && x <= self.right() && y >= self.y && y <= self.bottom()
}
}
/// Where a canvas looks: the canvas point at the view's top left, and its zoom.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Viewport {
pub x: f32,
pub y: f32,
pub zoom: f32,
}
impl Viewport {
/// The farthest out and the nearest in a canvas zooms.
pub const ZOOMS: (f32, f32) = (0.1, 8.0);
pub fn new(x: f32, y: f32, zoom: f32) -> Self {
let (least, most) = Self::ZOOMS;
assert!(
(least..=most).contains(&zoom),
"zoom {zoom} past {least} to {most}"
);
Self { x, y, zoom }
}
/// A canvas point in view pixels from the view's top left.
pub fn to_view(&self, (x, y): (f32, f32)) -> (f32, f32) {
((x - self.x) * self.zoom, (y - self.y) * self.zoom)
}
/// A point in the view as a canvas point.
pub fn to_canvas(&self, (x, y): (f32, f32)) -> (f32, f32) {
(x / self.zoom + self.x, y / self.zoom + self.y)
}
/// Zoomed by `factor` about a view point, which keeps its canvas point; held inside the zooms.
pub fn zoomed(self, factor: f32, about: (f32, f32)) -> Viewport {
let (least, most) = Self::ZOOMS;
let zoom = (self.zoom * factor).clamp(least, most);
let (x, y) = self.to_canvas(about);
Viewport::new(x - about.0 / zoom, y - about.1 / zoom, zoom)
}
/// Moved so the content slides by a view distance.
pub fn panned(self, (dx, dy): (f32, f32)) -> Viewport {
Viewport::new(self.x - dx / self.zoom, self.y - dy / self.zoom, self.zoom)
}
/// Shows `frame` whole and centered in a view of `size`, `margin` view pixels clear each side, no nearer than a zoom of one.
pub fn fitting(frame: Frame, (w, h): (f32, f32), margin: f32) -> Viewport {
let (least, _) = Self::ZOOMS;
let room = ((w - margin * 2.0).max(1.0), (h - margin * 2.0).max(1.0));
let zoom = (room.0 / frame.w.max(1.0))
.min(room.1 / frame.h.max(1.0))
.clamp(least, 1.0);
let (cx, cy) = frame.center();
Viewport::new(cx - w / 2.0 / zoom, cy - h / 2.0 / zoom, zoom)
}
}
/// The canvas step between marks at `zoom`: the least of one, two or five times a power of ten that sets marks `least` view pixels apart or more.
pub(crate) fn step(zoom: f32, least: f32) -> f32 {
let raw = least / zoom;
let power = 10f32.powf(raw.log10().floor());
[1.0, 2.0, 5.0, 10.0]
.into_iter()
.map(|each| each * power)
.find(|each| *each >= raw)
.expect("ten times the power passes the raw step")
}
/// Canvas marks every `step` from `from` through `to`.
pub(crate) fn marks(from: f32, to: f32, step: f32) -> Vec<f32> {
let first = (from / step).ceil() as i64;
let last = (to / step).floor() as i64;
(first..=last).map(|ix| ix as f32 * step).collect()
}
#[cfg(test)]
mod tests {
use super::{Frame, Viewport, marks, step};
#[test]
fn a_point_keeps_its_place_as_the_view_zooms_about_it() {
let view = Viewport::new(100.0, 50.0, 1.0);
let about = (200.0, 120.0);
let held = view.to_canvas(about);
let near = view.zoomed(2.0, about);
assert_eq!(near.zoom, 2.0);
assert_eq!(near.to_canvas(about), held);
assert_eq!(
view.zoomed(1000.0, about).zoom,
8.0,
"held at the nearest zoom"
);
}
#[test]
fn a_pan_slides_the_content_with_the_pointer() {
let view = Viewport::new(0.0, 0.0, 2.0).panned((40.0, -20.0));
assert_eq!((view.x, view.y), (-20.0, 10.0));
assert_eq!(view.to_view((0.0, 0.0)), (40.0, -20.0));
}
#[test]
fn a_fit_shows_the_frame_whole_and_centered() {
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]
fn marks_step_by_one_two_or_five_and_stay_apart() {
assert_eq!(step(1.0, 16.0), 20.0);
assert_eq!(step(0.5, 16.0), 50.0);
assert_eq!(step(4.0, 16.0), 5.0);
assert_eq!(marks(-15.0, 45.0, 20.0), [0.0, 20.0, 40.0]);
}
}