widgetry/
canvas.rs

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
10// Click and release counts as a normal click, not a drag, if the distance between click and
11// release is less than this.
12const DRAG_THRESHOLD: f64 = 5.0;
13
14const PAN_SPEED: f64 = 15.0;
15
16const PANNING_THRESHOLD: f64 = 25.0;
17
18pub struct Canvas {
19    // All of these f64's are in screen-space, so do NOT use Pt2D.
20    // Public for saving/loading... should probably do better
21    pub cam_x: f64,
22    pub cam_y: f64,
23    pub cam_zoom: f64,
24
25    // TODO Should this become Option<ScreenPt>?
26    pub(crate) cursor: ScreenPt,
27    pub(crate) window_has_cursor: bool,
28
29    // Only for drags starting on the map. Only used to pan the map. (Last event, original)
30    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    // TODO Proper API for setting these
37    pub map_dims: (f64, f64),
38    pub settings: CanvasSettings,
39
40    // TODO Bit weird and hacky to mutate inside of draw() calls.
41    pub(crate) covered_areas: RefCell<Vec<ScreenRectangle>>,
42
43    // Kind of just widgetry state awkwardly stuck here...
44    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    // TODO Ideally this would be an f64, but elsewhere we use it in a Spinner. Until we override
55    // the Display trait to do some rounding, floating point increments render pretty horribly.
56    pub canvas_scroll_speed: usize,
57    /// Some map-space elements are drawn differently when unzoomed and zoomed. This specifies the canvas
58    /// zoom level where they switch. The concept of "unzoomed" and "zoomed" is used by
59    /// `ToggleZoomed`.
60    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        // Can't start dragging or zooming on top of covered area
122        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                        // Woo, inversion is different for the two. :P
129                        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 we start the drag on the map and move the mouse off the map, keep dragging.
176        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                // Add displacement along each axis
209                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        // By popular request, some limits ;)
224        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        // Make screen_to_map of the focus point still point to the same thing after
230        // zooming.
231        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    // TODO Only public for the OSD. :(
240    pub fn mark_covered_area(&self, rect: ScreenRectangle) {
241        self.covered_areas.borrow_mut().push(rect);
242    }
243
244    // Might be hovering anywhere.
245    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    // the inner bound tells us whether auto-panning should or should not take place
301    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            // TODO Hack
355            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        // This could be called before or after handle_event. So we need to repeat the threshold
373        // check here! Alternatively, we could this upfront in runner.
374        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}