1use std::cell::RefCell;
2use std::collections::HashSet;
3
4use serde::{Deserialize, Serialize};
5
6use geom::{Bounds, Pt2D};
7
8use crate::{Key, ScreenDims, ScreenPt, ScreenRectangle, UpdateType, UserInput};
9
10const DRAG_THRESHOLD: f64 = 5.0;
13
14const PAN_SPEED: f64 = 15.0;
15
16const PANNING_THRESHOLD: f64 = 25.0;
17
18pub struct Canvas {
19 pub cam_x: f64,
22 pub cam_y: f64,
23 pub cam_zoom: f64,
24
25 pub(crate) cursor: ScreenPt,
27 pub(crate) window_has_cursor: bool,
28
29 pub(crate) drag_canvas_from: Option<(ScreenPt, ScreenPt)>,
31 drag_just_ended: bool,
32
33 pub window_width: f64,
34 pub window_height: f64,
35
36 pub map_dims: (f64, f64),
38 pub settings: CanvasSettings,
39
40 pub(crate) covered_areas: RefCell<Vec<ScreenRectangle>>,
42
43 pub(crate) keys_held: HashSet<Key>,
45}
46
47#[derive(Clone, Serialize, Deserialize)]
48pub struct CanvasSettings {
49 pub invert_scroll: bool,
50 pub touchpad_to_move: bool,
51 pub edge_auto_panning: bool,
52 pub keys_to_pan: bool,
53 pub gui_scroll_speed: usize,
54 pub canvas_scroll_speed: usize,
57 pub min_zoom_for_detail: f64,
61}
62
63impl CanvasSettings {
64 pub fn new() -> CanvasSettings {
65 CanvasSettings {
66 invert_scroll: false,
67 touchpad_to_move: false,
68 edge_auto_panning: false,
69 keys_to_pan: false,
70 gui_scroll_speed: 5,
71 canvas_scroll_speed: 10,
72 min_zoom_for_detail: 4.0,
73 }
74 }
75}
76
77impl Canvas {
78 pub(crate) fn new(initial_dims: ScreenDims, settings: CanvasSettings) -> Canvas {
79 Canvas {
80 cam_x: 0.0,
81 cam_y: 0.0,
82 cam_zoom: 1.0,
83
84 cursor: ScreenPt::new(0.0, 0.0),
85 window_has_cursor: true,
86
87 drag_canvas_from: None,
88 drag_just_ended: false,
89
90 window_width: initial_dims.width,
91 window_height: initial_dims.height,
92
93 map_dims: (0.0, 0.0),
94 settings,
95
96 covered_areas: RefCell::new(Vec::new()),
97
98 keys_held: HashSet::new(),
99 }
100 }
101
102 pub fn max_zoom(&self) -> f64 {
103 50.0
104 }
105
106 pub fn min_zoom(&self) -> f64 {
107 let percent_window = 0.8;
108 (percent_window * self.window_width / self.map_dims.0)
109 .min(percent_window * self.window_height / self.map_dims.1)
110 }
111
112 pub fn is_max_zoom(&self) -> bool {
113 self.cam_zoom >= self.max_zoom()
114 }
115
116 pub fn is_min_zoom(&self) -> bool {
117 self.cam_zoom <= self.min_zoom()
118 }
119
120 pub(crate) fn handle_event(&mut self, input: &mut UserInput) -> Option<UpdateType> {
121 if let Some(map_pt) = self.get_cursor_in_map_space() {
123 if self.settings.touchpad_to_move {
124 if let Some((scroll_x, scroll_y)) = input.get_mouse_scroll() {
125 if self.keys_held.contains(&Key::LeftControl) {
126 self.zoom(scroll_y, self.cursor);
127 } else {
128 self.cam_x -= scroll_x * PAN_SPEED;
130 self.cam_y -= scroll_y * PAN_SPEED;
131 }
132 }
133 } else {
134 if input.left_mouse_button_pressed() {
135 self.drag_canvas_from = Some((self.get_cursor(), self.get_cursor()));
136 }
137
138 if let Some((_, scroll)) = input.get_mouse_scroll() {
139 self.zoom(scroll, self.cursor);
140 }
141 }
142
143 if self.settings.keys_to_pan {
144 if input.pressed(Key::LeftArrow) {
145 self.cam_x -= PAN_SPEED;
146 }
147 if input.pressed(Key::RightArrow) {
148 self.cam_x += PAN_SPEED;
149 }
150 if input.pressed(Key::UpArrow) {
151 self.cam_y -= PAN_SPEED;
152 }
153 if input.pressed(Key::DownArrow) {
154 self.cam_y += PAN_SPEED;
155 }
156 if input.pressed(Key::Q) {
157 self.zoom(
158 1.0,
159 ScreenPt::new(self.window_width / 2.0, self.window_height / 2.0),
160 );
161 }
162 if input.pressed(Key::W) {
163 self.zoom(
164 -1.0,
165 ScreenPt::new(self.window_width / 2.0, self.window_height / 2.0),
166 );
167 }
168 }
169
170 if input.left_mouse_double_clicked() {
171 self.zoom(8.0, self.map_to_screen(map_pt));
172 }
173 }
174
175 if let Some((click, orig)) = self.drag_canvas_from {
177 let pt = self.get_cursor();
178 self.cam_x += click.x - pt.x;
179 self.cam_y += click.y - pt.y;
180 self.drag_canvas_from = Some((pt, orig));
181
182 if input.left_mouse_button_released() {
183 let (_, orig) = self.drag_canvas_from.take().unwrap();
184 let dist = ((pt.x - orig.x).powi(2) + (pt.y - orig.y).powi(2)).sqrt();
185 if dist > DRAG_THRESHOLD {
186 self.drag_just_ended = true;
187 }
188 }
189 } else if self.drag_just_ended {
190 self.drag_just_ended = false;
191 } else {
192 let cursor_screen_pt = self.get_cursor().to_pt();
193 let cursor_map_pt = self.screen_to_map(self.get_cursor());
194 let inner_bounds = self.get_inner_bounds();
195 let map_bounds = self.get_map_bounds();
196 if self.settings.edge_auto_panning
197 && !inner_bounds.contains(cursor_screen_pt)
198 && map_bounds.contains(cursor_map_pt)
199 && input.nonblocking_is_update_event().is_some()
200 {
201 let center_pt = self.center_to_screen_pt().to_pt();
202 let displacement_x = cursor_screen_pt.x() - center_pt.x();
203 let displacement_y = cursor_screen_pt.y() - center_pt.y();
204 let displacement_magnitude =
205 f64::sqrt(displacement_x.powf(2.0) + displacement_y.powf(2.0));
206 let displacement_unit_x = displacement_x / displacement_magnitude;
207 let displacement_unit_y = displacement_y / displacement_magnitude;
208 self.cam_x += displacement_unit_x * PAN_SPEED;
210 self.cam_y += displacement_unit_y * PAN_SPEED;
211 return Some(UpdateType::Pan);
212 }
213 }
214 None
215 }
216
217 pub fn center_zoom(&mut self, delta: f64) {
218 self.zoom(delta, self.center_to_screen_pt())
219 }
220
221 pub fn zoom(&mut self, delta: f64, focus: ScreenPt) {
222 let old_zoom = self.cam_zoom;
223 self.cam_zoom = 1.1_f64
225 .powf(old_zoom.log(1.1) + delta * (self.settings.canvas_scroll_speed as f64 / 10.0))
226 .max(self.min_zoom())
227 .min(self.max_zoom());
228
229 self.cam_x = ((self.cam_zoom / old_zoom) * (focus.x + self.cam_x)) - focus.x;
232 self.cam_y = ((self.cam_zoom / old_zoom) * (focus.y + self.cam_y)) - focus.y;
233 }
234
235 pub(crate) fn start_drawing(&self) {
236 self.covered_areas.borrow_mut().clear();
237 }
238
239 pub fn mark_covered_area(&self, rect: ScreenRectangle) {
241 self.covered_areas.borrow_mut().push(rect);
242 }
243
244 pub fn get_cursor(&self) -> ScreenPt {
246 self.cursor
247 }
248
249 pub fn get_cursor_in_screen_space(&self) -> Option<ScreenPt> {
250 if self.window_has_cursor && self.get_cursor_in_map_space().is_none() {
251 Some(self.get_cursor())
252 } else {
253 None
254 }
255 }
256
257 pub fn get_cursor_in_map_space(&self) -> Option<Pt2D> {
258 if self.window_has_cursor {
259 let pt = self.get_cursor();
260
261 for rect in self.covered_areas.borrow().iter() {
262 if rect.contains(pt) {
263 return None;
264 }
265 }
266
267 Some(self.screen_to_map(pt))
268 } else {
269 None
270 }
271 }
272
273 pub fn screen_to_map(&self, pt: ScreenPt) -> Pt2D {
274 Pt2D::new(
275 (pt.x + self.cam_x) / self.cam_zoom,
276 (pt.y + self.cam_y) / self.cam_zoom,
277 )
278 }
279
280 pub fn center_to_screen_pt(&self) -> ScreenPt {
281 ScreenPt::new(self.window_width / 2.0, self.window_height / 2.0)
282 }
283
284 pub fn center_to_map_pt(&self) -> Pt2D {
285 self.screen_to_map(self.center_to_screen_pt())
286 }
287
288 pub fn center_on_map_pt(&mut self, pt: Pt2D) {
289 self.cam_x = (pt.x() * self.cam_zoom) - (self.window_width / 2.0);
290 self.cam_y = (pt.y() * self.cam_zoom) - (self.window_height / 2.0);
291 }
292
293 pub fn map_to_screen(&self, pt: Pt2D) -> ScreenPt {
294 ScreenPt::new(
295 (pt.x() * self.cam_zoom) - self.cam_x,
296 (pt.y() * self.cam_zoom) - self.cam_y,
297 )
298 }
299
300 fn get_inner_bounds(&self) -> Bounds {
302 let mut b = Bounds::new();
303 b.update(ScreenPt::new(PANNING_THRESHOLD, PANNING_THRESHOLD).to_pt());
304 b.update(
305 ScreenPt::new(
306 self.window_width - PANNING_THRESHOLD,
307 self.window_height - PANNING_THRESHOLD,
308 )
309 .to_pt(),
310 );
311 b
312 }
313
314 pub fn get_window_dims(&self) -> ScreenDims {
315 ScreenDims::new(self.window_width, self.window_height)
316 }
317
318 fn get_map_bounds(&self) -> Bounds {
319 let mut b = Bounds::new();
320 b.update(Pt2D::new(0.0, 0.0));
321 b.update(Pt2D::new(self.map_dims.0, self.map_dims.1));
322 b
323 }
324
325 pub fn get_screen_bounds(&self) -> Bounds {
326 let mut b = Bounds::new();
327 b.update(self.screen_to_map(ScreenPt::new(0.0, 0.0)));
328 b.update(self.screen_to_map(ScreenPt::new(self.window_width, self.window_height)));
329 b
330 }
331
332 pub(crate) fn align_window(
333 &self,
334 dims: ScreenDims,
335 horiz: HorizontalAlignment,
336 vert: VerticalAlignment,
337 ) -> ScreenPt {
338 let x1 = match horiz {
339 HorizontalAlignment::Left => 0.0,
340 HorizontalAlignment::LeftInset => INSET,
341 HorizontalAlignment::Center => (self.window_width - dims.width) / 2.0,
342 HorizontalAlignment::Right => self.window_width - dims.width,
343 HorizontalAlignment::RightOf(x) => x,
344 HorizontalAlignment::RightInset => self.window_width - dims.width - INSET,
345 HorizontalAlignment::Percent(pct) => pct * self.window_width,
346 HorizontalAlignment::Centered(x) => x - (dims.width / 2.0),
347 };
348 let y1 = match vert {
349 VerticalAlignment::Top => 0.0,
350 VerticalAlignment::TopInset => INSET,
351 VerticalAlignment::Center => (self.window_height - dims.height) / 2.0,
352 VerticalAlignment::Bottom => self.window_height - dims.height,
353 VerticalAlignment::BottomInset => self.window_height - dims.height - INSET,
354 VerticalAlignment::BottomAboveOSD => self.window_height - dims.height - 60.0,
356 VerticalAlignment::Percent(pct) => pct * self.window_height,
357 VerticalAlignment::Above(y) => y - dims.height,
358 VerticalAlignment::Below(y) => y,
359 };
360 ScreenPt::new(x1, y1)
361 }
362
363 pub fn is_unzoomed(&self) -> bool {
364 self.cam_zoom < self.settings.min_zoom_for_detail
365 }
366
367 pub fn is_zoomed(&self) -> bool {
368 self.cam_zoom >= self.settings.min_zoom_for_detail
369 }
370
371 pub(crate) fn is_dragging(&self) -> bool {
372 if self.drag_just_ended {
375 return true;
376 }
377 if let Some((_, orig)) = self.drag_canvas_from {
378 let pt = self.get_cursor();
379 let dist = ((pt.x - orig.x).powi(2) + (pt.y - orig.y).powi(2)).sqrt();
380 if dist > DRAG_THRESHOLD {
381 return true;
382 }
383 }
384 false
385 }
386}
387
388const INSET: f64 = 16.0;
389
390#[derive(Clone, Copy, Debug, PartialEq)]
391pub enum HorizontalAlignment {
392 Left,
393 LeftInset,
394 Center,
395 Right,
396 RightOf(f64),
397 RightInset,
398 Percent(f64),
399 Centered(f64),
400}
401
402#[derive(Clone, Copy, Debug, PartialEq)]
403pub enum VerticalAlignment {
404 Top,
405 TopInset,
406 Center,
407 Bottom,
408 BottomInset,
409 BottomAboveOSD,
410 Percent(f64),
411 Above(f64),
412 Below(f64),
413}