1use egui::{
2 DragPanButtons, InnerResponse, PointerButton, Response, Sense, Ui, UiBuilder, Vec2, Widget,
3};
4
5use crate::{
6 MapMemory, Position, Projector, Tiles, center::Center, position::AdjustedPosition,
7 tiles::draw_tiles,
8};
9
10pub trait Plugin {
13 fn run(
23 self: Box<Self>,
24 ui: &mut Ui,
25 response: &Response,
26 projector: &Projector,
27 map_memory: &MapMemory,
28 );
29}
30
31struct Layer<'a> {
32 tiles: &'a mut dyn Tiles,
33 transparency: f32,
34}
35
36struct Options {
37 zoom_gesture_enabled: bool,
38 drag_pan_buttons: DragPanButtons,
39 zoom_speed: f64,
40 double_click_to_zoom: bool,
41 double_click_to_zoom_out: bool,
42 zoom_with_ctrl: bool,
43 panning: bool,
44 pull_to_my_position_threshold: f32,
45}
46
47impl Default for Options {
48 fn default() -> Self {
49 Self {
50 zoom_gesture_enabled: true,
51 drag_pan_buttons: DragPanButtons::PRIMARY,
52 zoom_speed: 2.0,
53 double_click_to_zoom: false,
54 double_click_to_zoom_out: false,
55 zoom_with_ctrl: true,
56 panning: true,
57 pull_to_my_position_threshold: 0.0,
58 }
59 }
60}
61
62pub struct Map<'a, 'b, 'c> {
83 tiles: Option<&'b mut dyn Tiles>,
84 layers: Vec<Layer<'b>>,
85 memory: &'a mut MapMemory,
86 my_position: Position,
87 plugins: Vec<Box<dyn Plugin + 'c>>,
88 options: Options,
89}
90
91impl<'a, 'b, 'c> Map<'a, 'b, 'c> {
92 pub fn new(
93 tiles: Option<&'b mut dyn Tiles>,
94 memory: &'a mut MapMemory,
95 my_position: Position,
96 ) -> Self {
97 Self {
98 tiles,
99 layers: Vec::default(),
100 memory,
101 my_position,
102 plugins: Vec::default(),
103 options: Options::default(),
104 }
105 }
106
107 pub fn with_plugin(mut self, plugin: impl Plugin + 'c) -> Self {
109 self.plugins.push(Box::new(plugin));
110 self
111 }
112
113 pub fn with_layer(mut self, tiles: &'b mut dyn Tiles, transparency: f32) -> Self {
115 self.layers.push(Layer {
116 tiles,
117 transparency,
118 });
119 self
120 }
121
122 pub fn zoom_gesture(mut self, enabled: bool) -> Self {
127 self.options.zoom_gesture_enabled = enabled;
128 self
129 }
130
131 pub fn drag_pan_buttons(mut self, buttons: DragPanButtons) -> Self {
133 self.options.drag_pan_buttons = buttons;
134 self
135 }
136
137 pub fn zoom_speed(mut self, speed: f64) -> Self {
140 self.options.zoom_speed = speed;
141 self
142 }
143
144 pub fn double_click_to_zoom(mut self, enabled: bool) -> Self {
146 self.options.double_click_to_zoom = enabled;
147 self
148 }
149
150 pub fn double_click_to_zoom_out(mut self, enabled: bool) -> Self {
152 self.options.double_click_to_zoom_out = enabled;
153 self
154 }
155
156 pub fn zoom_with_ctrl(mut self, enabled: bool) -> Self {
166 self.options.zoom_with_ctrl = enabled;
167 self
168 }
169
170 pub fn panning(mut self, enabled: bool) -> Self {
174 self.options.panning = enabled;
175 self
176 }
177
178 pub fn pull_to_my_position_threshold(mut self, threshold: f32) -> Self {
183 self.options.pull_to_my_position_threshold = threshold;
184 self
185 }
186
187 pub fn show<R>(
189 mut self,
190 ui: &mut Ui,
191 add_contents: impl FnOnce(&mut Ui, &Response, &Projector, &MapMemory) -> R,
192 ) -> InnerResponse<R> {
193 let (rect, mut response) =
194 ui.allocate_exact_size(ui.available_size(), Sense::click_and_drag());
195
196 let mut changed = self.handle_gestures(ui, &response);
197 let delta_time = ui.input(|reader| reader.stable_dt);
198 let zoom = self.memory.zoom;
199 changed |= self
200 .memory
201 .center_mode
202 .update_movement(delta_time, zoom.into());
203
204 if changed {
205 response.mark_changed();
206 ui.request_repaint();
207 }
208
209 let map_center = self.position();
210 let painter = ui.painter().with_clip_rect(rect);
211
212 if let Some(tiles) = self.tiles {
213 draw_tiles(&painter, map_center, zoom, tiles, 1.0);
214 }
215
216 for layer in self.layers {
217 draw_tiles(&painter, map_center, zoom, layer.tiles, layer.transparency);
218 }
219
220 let projector = Projector::new(response.rect, self.memory, self.my_position);
222 for (idx, plugin) in self.plugins.into_iter().enumerate() {
223 let mut child_ui = ui.new_child(UiBuilder::new().max_rect(rect).id_salt(idx));
224 plugin.run(&mut child_ui, &response, &projector, self.memory);
225 }
226
227 let mut child_ui = ui.new_child(UiBuilder::new().max_rect(rect).id_salt("inner"));
228 let inner = add_contents(&mut child_ui, &response, &projector, self.memory);
229
230 InnerResponse { inner, response }
231 }
232}
233
234impl Map<'_, '_, '_> {
235 fn handle_gestures(&mut self, ui: &mut Ui, response: &Response) -> bool {
237 let zoom_delta = self.zoom_delta(ui, response);
238
239 let changed = if (zoom_delta - 1.0).abs() > 0.001
242 && ui.ui_contains_pointer()
243 && self.options.zoom_gesture_enabled
244 {
245 let offset = input_offset(ui, response);
247
248 if let Some(offset) = offset {
253 if self.memory.detached().is_some()
255 || offset.length() > self.options.pull_to_my_position_threshold
256 {
257 self.memory.center_mode = Center::Exact(
258 AdjustedPosition::new(self.position()).shift(-offset, self.memory.zoom()),
259 );
260 }
261 }
262
263 self.memory
266 .zoom
267 .zoom_by((zoom_delta - 1.) * self.options.zoom_speed);
268
269 if let Some(offset) = offset {
270 self.memory.center_mode = self
271 .memory
272 .center_mode
273 .clone()
274 .shift(offset, self.memory.zoom());
275 }
276
277 true
278 } else {
279 self.memory.center_mode.handle_gestures(
280 response,
281 self.my_position,
282 self.options.pull_to_my_position_threshold,
283 self.options.drag_pan_buttons,
284 )
285 };
286
287 let panning_enabled =
289 self.options.panning && (ui.input(|i| i.any_touches()) || self.options.zoom_with_ctrl);
290
291 if ui.ui_contains_pointer() && panning_enabled {
292 let scroll_delta = ui.input(|i| i.smooth_scroll_delta);
294 if scroll_delta != Vec2::ZERO {
295 self.memory.center_mode = Center::Exact(
296 AdjustedPosition::new(self.position()).shift(scroll_delta, self.memory.zoom()),
297 );
298 }
299 }
300
301 changed
302 }
303
304 fn zoom_delta(&self, ui: &mut Ui, response: &Response) -> f64 {
306 let mut zoom_delta = ui.input(|input| input.zoom_delta()) as f64;
307
308 if self.options.double_click_to_zoom
309 && ui.ui_contains_pointer()
310 && response.double_clicked_by(PointerButton::Primary)
311 {
312 zoom_delta = 2.0;
313 }
314
315 if self.options.double_click_to_zoom_out
316 && ui.ui_contains_pointer()
317 && response.double_clicked_by(PointerButton::Secondary)
318 {
319 zoom_delta = 0.0;
320 }
321
322 if !self.options.zoom_with_ctrl && zoom_delta == 1.0 {
323 zoom_delta = 1f64
327 + ui.input(|input| {
328 input.smooth_scroll_delta.y * input.stable_dt.max(input.predicted_dt * 1.5)
329 }) as f64
330 / 4.0;
331 };
332
333 zoom_delta
334 }
335
336 fn position(&self) -> Position {
338 self.memory.center_mode.position(self.my_position)
339 }
340}
341
342impl Widget for Map<'_, '_, '_> {
343 fn ui(self, ui: &mut Ui) -> Response {
344 self.show(ui, |_, _, _, _| ()).response
345 }
346}
347
348fn input_offset(ui: &mut Ui, response: &Response) -> Option<Vec2> {
350 let mouse_offset = response.hover_pos();
351 let touch_offset = ui
352 .input(|input| input.multi_touch())
353 .map(|multi_touch| multi_touch.center_pos);
354
355 touch_offset
357 .or(mouse_offset)
358 .map(|pos| pos - response.rect.center())
359}