open_gpui_canvas/
geometry.rs1use open_gpui::{Bounds, Pixels, Point, Size};
2use serde::{Deserialize, Serialize};
3use thiserror::Error;
4
5#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
6pub struct CanvasViewport {
7 pub origin: Point<Pixels>,
8 pub zoom: f32,
9}
10
11impl Default for CanvasViewport {
12 fn default() -> Self {
13 Self {
14 origin: Point::default(),
15 zoom: 1.0,
16 }
17 }
18}
19
20impl CanvasViewport {
21 pub fn new(origin: Point<Pixels>, zoom: f32) -> Result<Self, TransformError> {
22 if !zoom.is_finite() || zoom <= 0.0 {
23 return Err(TransformError::InvalidZoom(zoom));
24 }
25
26 Ok(Self { origin, zoom })
27 }
28
29 pub fn pan_by(&mut self, delta: Point<Pixels>) {
30 self.origin += delta;
31 }
32
33 pub fn set_zoom(&mut self, zoom: f32) -> Result<(), TransformError> {
34 if !zoom.is_finite() || zoom <= 0.0 {
35 return Err(TransformError::InvalidZoom(zoom));
36 }
37
38 self.zoom = zoom;
39 Ok(())
40 }
41
42 pub fn set_zoom_around_view_point(
43 &mut self,
44 zoom: f32,
45 view_point: Point<Pixels>,
46 ) -> Result<(), TransformError> {
47 let document_point = self.view_to_document(view_point);
48 self.set_zoom(zoom)?;
49 self.origin = document_point - (view_point * (1.0 / self.zoom));
50 Ok(())
51 }
52
53 pub fn zoom_by_around_view_point(
54 &mut self,
55 factor: f32,
56 view_point: Point<Pixels>,
57 ) -> Result<(), TransformError> {
58 if !factor.is_finite() || factor <= 0.0 {
59 return Err(TransformError::InvalidZoomFactor(factor));
60 }
61
62 self.set_zoom_around_view_point(self.zoom * factor, view_point)
63 }
64
65 pub fn document_to_view(&self, point: Point<Pixels>) -> Point<Pixels> {
66 (point - self.origin) * self.zoom
67 }
68
69 pub fn view_to_document(&self, point: Point<Pixels>) -> Point<Pixels> {
70 (point * (1.0 / self.zoom)) + self.origin
71 }
72
73 pub fn document_bounds_to_view(&self, bounds: Bounds<Pixels>) -> Bounds<Pixels> {
74 Bounds::new(
75 self.document_to_view(bounds.origin),
76 Size::new(
77 bounds.size.width * self.zoom,
78 bounds.size.height * self.zoom,
79 ),
80 )
81 }
82
83 pub fn view_bounds_to_document(&self, bounds: Bounds<Pixels>) -> Bounds<Pixels> {
84 Bounds::new(
85 self.view_to_document(bounds.origin),
86 Size::new(
87 bounds.size.width * (1.0 / self.zoom),
88 bounds.size.height * (1.0 / self.zoom),
89 ),
90 )
91 }
92
93 pub fn visible_document_bounds(&self, view_size: Size<Pixels>) -> Bounds<Pixels> {
94 self.view_bounds_to_document(Bounds::new(Point::default(), view_size))
95 }
96}
97
98#[derive(Debug, Error, PartialEq)]
99pub enum TransformError {
100 #[error("invalid canvas zoom `{0}`")]
101 InvalidZoom(f32),
102 #[error("invalid canvas zoom factor `{0}`")]
103 InvalidZoomFactor(f32),
104}
105
106#[cfg(test)]
107mod tests {
108 use super::*;
109 use open_gpui::{point, px};
110
111 #[test]
112 fn round_trips_between_view_and_document_space() {
113 let viewport = CanvasViewport::new(point(px(100.0), px(50.0)), 2.0).unwrap();
114 let document = point(px(110.0), px(70.0));
115 let view = viewport.document_to_view(document);
116
117 assert_eq!(view, point(px(20.0), px(40.0)));
118 assert_eq!(viewport.view_to_document(view), document);
119 }
120
121 #[test]
122 fn rejects_non_positive_zoom() {
123 assert_eq!(
124 CanvasViewport::new(point(px(0.0), px(0.0)), 0.0).unwrap_err(),
125 TransformError::InvalidZoom(0.0)
126 );
127 }
128
129 #[test]
130 fn zooms_around_view_point_without_moving_document_anchor() {
131 let mut viewport = CanvasViewport::new(point(px(100.0), px(50.0)), 1.0).unwrap();
132 let view_point = point(px(20.0), px(10.0));
133 let document_point = viewport.view_to_document(view_point);
134
135 viewport
136 .set_zoom_around_view_point(2.0, view_point)
137 .unwrap();
138
139 assert_eq!(viewport.view_to_document(view_point), document_point);
140 assert_eq!(viewport.zoom, 2.0);
141 }
142
143 #[test]
144 fn rejects_invalid_zoom_factor() {
145 let mut viewport = CanvasViewport::default();
146
147 assert_eq!(
148 viewport
149 .zoom_by_around_view_point(0.0, point(px(0.0), px(0.0)))
150 .unwrap_err(),
151 TransformError::InvalidZoomFactor(0.0)
152 );
153 }
154
155 #[test]
156 fn returns_visible_document_bounds() {
157 let viewport = CanvasViewport::new(point(px(100.0), px(50.0)), 2.0).unwrap();
158
159 let bounds = viewport.visible_document_bounds(open_gpui::size(px(200.0), px(100.0)));
160
161 assert_eq!(bounds.origin, point(px(100.0), px(50.0)));
162 assert_eq!(bounds.size, open_gpui::size(px(100.0), px(50.0)));
163 }
164}