1 use crate::{Key, ScreenDims, ScreenPt, ScreenRectangle, UpdateType, UserInput}; 2 use abstutil::Timer; 3 use geom::{Bounds, Pt2D}; 4 use serde::{Deserialize, Serialize}; 5 use std::cell::RefCell; 6 7 // Click and release counts as a normal click, not a drag, if the distance between click and 8 // release is less than this. 9 const DRAG_THRESHOLD: f64 = 5.0; 10 11 const PAN_SPEED: f64 = 15.0; 12 13 const PANNING_THRESHOLD: f64 = 25.0; 14 15 pub struct Canvas { 16 // All of these f64's are in screen-space, so do NOT use Pt2D. 17 // Public for saving/loading... should probably do better 18 pub cam_x: f64, 19 pub cam_y: f64, 20 pub cam_zoom: f64, 21 22 // TODO Should this become Option<ScreenPt>? 23 pub(crate) cursor: ScreenPt, 24 pub(crate) window_has_cursor: bool, 25 26 // Only for drags starting on the map. Only used to pan the map. (Last event, original) 27 pub(crate) drag_canvas_from: Option<(ScreenPt, ScreenPt)>, 28 pub(crate) drag_just_ended: bool, 29 30 pub window_width: f64, 31 pub window_height: f64, 32 33 // TODO Proper API for setting these 34 pub map_dims: (f64, f64), 35 pub invert_scroll: bool, 36 pub touchpad_to_move: bool, 37 pub edge_auto_panning: bool, 38 pub keys_to_pan: bool, 39 pub gui_scroll_speed: usize, 40 41 // TODO Bit weird and hacky to mutate inside of draw() calls. 42 pub(crate) covered_areas: RefCell<Vec<ScreenRectangle>>, 43 44 // Kind of just widgetry state awkwardly stuck here... 45 pub(crate) lctrl_held: bool, 46 pub(crate) lshift_held: bool, 47 } 48 49 impl Canvas { new(initial_dims: ScreenDims) -> Canvas50 pub(crate) fn new(initial_dims: ScreenDims) -> Canvas { 51 Canvas { 52 cam_x: 0.0, 53 cam_y: 0.0, 54 cam_zoom: 1.0, 55 56 cursor: ScreenPt::new(0.0, 0.0), 57 window_has_cursor: true, 58 59 drag_canvas_from: None, 60 drag_just_ended: false, 61 62 window_width: initial_dims.width, 63 window_height: initial_dims.height, 64 65 map_dims: (0.0, 0.0), 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 72 covered_areas: RefCell::new(Vec::new()), 73 74 lctrl_held: false, 75 lshift_held: false, 76 } 77 } 78 min_zoom(&self) -> f6479 pub fn min_zoom(&self) -> f64 { 80 let percent_window = 0.8; 81 (percent_window * self.window_width / self.map_dims.0) 82 .min(percent_window * self.window_height / self.map_dims.1) 83 } 84 handle_event(&mut self, input: &mut UserInput) -> Option<UpdateType>85 pub(crate) fn handle_event(&mut self, input: &mut UserInput) -> Option<UpdateType> { 86 // Can't start dragging or zooming on top of covered area 87 if self.get_cursor_in_map_space().is_some() { 88 if self.touchpad_to_move { 89 if let Some((scroll_x, scroll_y)) = input.get_mouse_scroll() { 90 if self.lctrl_held { 91 self.zoom(scroll_y, self.cursor); 92 } else { 93 // Woo, inversion is different for the two. :P 94 self.cam_x += scroll_x * PAN_SPEED; 95 self.cam_y -= scroll_y * PAN_SPEED; 96 } 97 } 98 } else { 99 if input.left_mouse_button_pressed() { 100 self.drag_canvas_from = Some((self.get_cursor(), self.get_cursor())); 101 } 102 103 if let Some((_, scroll)) = input.get_mouse_scroll() { 104 self.zoom(scroll, self.cursor); 105 } 106 } 107 108 if self.keys_to_pan { 109 if input.key_pressed(Key::LeftArrow) { 110 self.cam_x -= PAN_SPEED; 111 } 112 if input.key_pressed(Key::RightArrow) { 113 self.cam_x += PAN_SPEED; 114 } 115 if input.key_pressed(Key::UpArrow) { 116 self.cam_y -= PAN_SPEED; 117 } 118 if input.key_pressed(Key::DownArrow) { 119 self.cam_y += PAN_SPEED; 120 } 121 if input.key_pressed(Key::Q) { 122 self.zoom( 123 1.0, 124 ScreenPt::new(self.window_width / 2.0, self.window_height / 2.0), 125 ); 126 } 127 if input.key_pressed(Key::W) { 128 self.zoom( 129 -1.0, 130 ScreenPt::new(self.window_width / 2.0, self.window_height / 2.0), 131 ); 132 } 133 } 134 } 135 136 // If we start the drag on the map and move the mouse off the map, keep dragging. 137 if let Some((click, orig)) = self.drag_canvas_from { 138 let pt = self.get_cursor(); 139 self.cam_x += click.x - pt.x; 140 self.cam_y += click.y - pt.y; 141 self.drag_canvas_from = Some((pt, orig)); 142 143 if input.left_mouse_button_released() { 144 let (_, orig) = self.drag_canvas_from.take().unwrap(); 145 let dist = ((pt.x - orig.x).powi(2) + (pt.y - orig.y).powi(2)).sqrt(); 146 if dist > DRAG_THRESHOLD { 147 self.drag_just_ended = true; 148 } 149 } 150 } else if self.drag_just_ended { 151 self.drag_just_ended = false; 152 } else { 153 let cursor_screen_pt = self.get_cursor().to_pt(); 154 let cursor_map_pt = self.screen_to_map(self.get_cursor()); 155 let inner_bounds = self.get_inner_bounds(); 156 let map_bounds = self.get_map_bounds(); 157 if self.edge_auto_panning 158 && !inner_bounds.contains(cursor_screen_pt) 159 && map_bounds.contains(cursor_map_pt) 160 && input.nonblocking_is_update_event().is_some() 161 { 162 let center_pt = self.center_to_screen_pt().to_pt(); 163 let displacement_x = cursor_screen_pt.x() - center_pt.x(); 164 let displacement_y = cursor_screen_pt.y() - center_pt.y(); 165 let displacement_magnitude = 166 f64::sqrt(displacement_x.powf(2.0) + displacement_y.powf(2.0)); 167 let displacement_unit_x = displacement_x / displacement_magnitude; 168 let displacement_unit_y = displacement_y / displacement_magnitude; 169 // Add displacement along each axis 170 self.cam_x += displacement_unit_x * PAN_SPEED; 171 self.cam_y += displacement_unit_y * PAN_SPEED; 172 return Some(UpdateType::Pan); 173 } 174 } 175 None 176 } 177 zoom(&mut self, delta: f64, focus: ScreenPt)178 fn zoom(&mut self, delta: f64, focus: ScreenPt) { 179 let old_zoom = self.cam_zoom; 180 // By popular request, some limits ;) 181 self.cam_zoom = 1.1_f64 182 .powf(old_zoom.log(1.1) + delta) 183 .max(self.min_zoom()) 184 .min(150.0); 185 186 // Make screen_to_map of the focus point still point to the same thing after 187 // zooming. 188 self.cam_x = ((self.cam_zoom / old_zoom) * (focus.x + self.cam_x)) - focus.x; 189 self.cam_y = ((self.cam_zoom / old_zoom) * (focus.y + self.cam_y)) - focus.y; 190 } 191 start_drawing(&self)192 pub(crate) fn start_drawing(&self) { 193 self.covered_areas.borrow_mut().clear(); 194 } 195 196 // TODO Only public for the OSD. :( mark_covered_area(&self, rect: ScreenRectangle)197 pub fn mark_covered_area(&self, rect: ScreenRectangle) { 198 self.covered_areas.borrow_mut().push(rect); 199 } 200 201 // Might be hovering anywhere. get_cursor(&self) -> ScreenPt202 pub fn get_cursor(&self) -> ScreenPt { 203 self.cursor 204 } 205 get_cursor_in_screen_space(&self) -> Option<ScreenPt>206 pub fn get_cursor_in_screen_space(&self) -> Option<ScreenPt> { 207 if self.window_has_cursor && self.get_cursor_in_map_space().is_none() { 208 Some(self.get_cursor()) 209 } else { 210 None 211 } 212 } 213 get_cursor_in_map_space(&self) -> Option<Pt2D>214 pub fn get_cursor_in_map_space(&self) -> Option<Pt2D> { 215 if self.window_has_cursor { 216 let pt = self.get_cursor(); 217 218 for rect in self.covered_areas.borrow().iter() { 219 if rect.contains(pt) { 220 return None; 221 } 222 } 223 224 Some(self.screen_to_map(pt)) 225 } else { 226 None 227 } 228 } 229 screen_to_map(&self, pt: ScreenPt) -> Pt2D230 pub fn screen_to_map(&self, pt: ScreenPt) -> Pt2D { 231 Pt2D::new( 232 (pt.x + self.cam_x) / self.cam_zoom, 233 (pt.y + self.cam_y) / self.cam_zoom, 234 ) 235 } 236 center_to_screen_pt(&self) -> ScreenPt237 pub fn center_to_screen_pt(&self) -> ScreenPt { 238 ScreenPt::new(self.window_width / 2.0, self.window_height / 2.0) 239 } 240 center_to_map_pt(&self) -> Pt2D241 pub fn center_to_map_pt(&self) -> Pt2D { 242 self.screen_to_map(self.center_to_screen_pt()) 243 } 244 center_on_map_pt(&mut self, pt: Pt2D)245 pub fn center_on_map_pt(&mut self, pt: Pt2D) { 246 self.cam_x = (pt.x() * self.cam_zoom) - (self.window_width / 2.0); 247 self.cam_y = (pt.y() * self.cam_zoom) - (self.window_height / 2.0); 248 } 249 map_to_screen(&self, pt: Pt2D) -> ScreenPt250 pub fn map_to_screen(&self, pt: Pt2D) -> ScreenPt { 251 ScreenPt::new( 252 (pt.x() * self.cam_zoom) - self.cam_x, 253 (pt.y() * self.cam_zoom) - self.cam_y, 254 ) 255 } 256 257 //the inner bound tells us whether auto-panning should or should not take place get_inner_bounds(&self) -> Bounds258 fn get_inner_bounds(&self) -> Bounds { 259 let mut b = Bounds::new(); 260 b.update(ScreenPt::new(PANNING_THRESHOLD, PANNING_THRESHOLD).to_pt()); 261 b.update( 262 ScreenPt::new( 263 self.window_width - PANNING_THRESHOLD, 264 self.window_height - PANNING_THRESHOLD, 265 ) 266 .to_pt(), 267 ); 268 b 269 } 270 get_window_dims(&self) -> ScreenDims271 pub fn get_window_dims(&self) -> ScreenDims { 272 ScreenDims::new(self.window_width, self.window_height) 273 } 274 get_map_bounds(&self) -> Bounds275 fn get_map_bounds(&self) -> Bounds { 276 let mut b = Bounds::new(); 277 b.update(Pt2D::new(0.0, 0.0)); 278 b.update(Pt2D::new(self.map_dims.0, self.map_dims.1)); 279 b 280 } 281 get_screen_bounds(&self) -> Bounds282 pub fn get_screen_bounds(&self) -> Bounds { 283 let mut b = Bounds::new(); 284 b.update(self.screen_to_map(ScreenPt::new(0.0, 0.0))); 285 b.update(self.screen_to_map(ScreenPt::new(self.window_width, self.window_height))); 286 b 287 } 288 save_camera_state(&self, map_name: &str)289 pub fn save_camera_state(&self, map_name: &str) { 290 let state = CameraState { 291 cam_x: self.cam_x, 292 cam_y: self.cam_y, 293 cam_zoom: self.cam_zoom, 294 }; 295 abstutil::write_json(abstutil::path_camera_state(map_name), &state); 296 } 297 298 // True if this succeeds load_camera_state(&mut self, map_name: &str) -> bool299 pub fn load_camera_state(&mut self, map_name: &str) -> bool { 300 match abstutil::maybe_read_json::<CameraState>( 301 abstutil::path_camera_state(map_name), 302 &mut Timer::throwaway(), 303 ) { 304 Ok(ref loaded) => { 305 self.cam_x = loaded.cam_x; 306 self.cam_y = loaded.cam_y; 307 self.cam_zoom = loaded.cam_zoom; 308 true 309 } 310 _ => false, 311 } 312 } 313 align_window( &self, dims: ScreenDims, horiz: HorizontalAlignment, vert: VerticalAlignment, ) -> ScreenPt314 pub(crate) fn align_window( 315 &self, 316 dims: ScreenDims, 317 horiz: HorizontalAlignment, 318 vert: VerticalAlignment, 319 ) -> ScreenPt { 320 let x1 = match horiz { 321 HorizontalAlignment::Left => 0.0, 322 HorizontalAlignment::Center => (self.window_width - dims.width) / 2.0, 323 HorizontalAlignment::Right => self.window_width - dims.width, 324 HorizontalAlignment::Percent(pct) => pct * self.window_width, 325 HorizontalAlignment::Centered(x) => x - (dims.width / 2.0), 326 }; 327 let y1 = match vert { 328 VerticalAlignment::Top => 0.0, 329 VerticalAlignment::Center => (self.window_height - dims.height) / 2.0, 330 VerticalAlignment::Bottom => self.window_height - dims.height, 331 // TODO Hack 332 VerticalAlignment::BottomAboveOSD => self.window_height - dims.height - 60.0, 333 VerticalAlignment::Percent(pct) => pct * self.window_height, 334 VerticalAlignment::Above(y) => y - dims.height, 335 VerticalAlignment::Below(y) => y, 336 }; 337 ScreenPt::new(x1, y1) 338 } 339 } 340 341 #[derive(Clone, Copy)] 342 pub enum HorizontalAlignment { 343 Left, 344 Center, 345 Right, 346 Percent(f64), 347 Centered(f64), 348 } 349 350 #[derive(Clone, Copy)] 351 pub enum VerticalAlignment { 352 Top, 353 Center, 354 Bottom, 355 BottomAboveOSD, 356 Percent(f64), 357 Above(f64), 358 Below(f64), 359 } 360 361 #[derive(Serialize, Deserialize, Debug)] 362 pub struct CameraState { 363 cam_x: f64, 364 cam_y: f64, 365 cam_zoom: f64, 366 } 367