use egui::{
DragPanButtons, InnerResponse, PointerButton, Response, Sense, Ui, UiBuilder, Vec2, Widget,
};
use crate::{
MapMemory, Position, Projector, Tiles, center::Center, position::AdjustedPosition,
tiles::draw_tiles,
};
pub trait Plugin {
fn run(
self: Box<Self>,
ui: &mut Ui,
response: &Response,
projector: &Projector,
map_memory: &MapMemory,
);
}
struct Layer<'a> {
tiles: &'a mut dyn Tiles,
transparency: f32,
}
struct Options {
zoom_gesture_enabled: bool,
drag_pan_buttons: DragPanButtons,
zoom_speed: f64,
double_click_to_zoom: bool,
double_click_to_zoom_out: bool,
zoom_with_ctrl: bool,
panning: bool,
pull_to_my_position_threshold: f32,
}
impl Default for Options {
fn default() -> Self {
Self {
zoom_gesture_enabled: true,
drag_pan_buttons: DragPanButtons::PRIMARY,
zoom_speed: 2.0,
double_click_to_zoom: false,
double_click_to_zoom_out: false,
zoom_with_ctrl: true,
panning: true,
pull_to_my_position_threshold: 0.0,
}
}
}
pub struct Map<'a, 'b, 'c> {
tiles: Option<&'b mut dyn Tiles>,
layers: Vec<Layer<'b>>,
memory: &'a mut MapMemory,
my_position: Position,
plugins: Vec<Box<dyn Plugin + 'c>>,
options: Options,
}
impl<'a, 'b, 'c> Map<'a, 'b, 'c> {
pub fn new(
tiles: Option<&'b mut dyn Tiles>,
memory: &'a mut MapMemory,
my_position: Position,
) -> Self {
Self {
tiles,
layers: Vec::default(),
memory,
my_position,
plugins: Vec::default(),
options: Options::default(),
}
}
pub fn with_plugin(mut self, plugin: impl Plugin + 'c) -> Self {
self.plugins.push(Box::new(plugin));
self
}
pub fn with_layer(mut self, tiles: &'b mut dyn Tiles, transparency: f32) -> Self {
self.layers.push(Layer {
tiles,
transparency,
});
self
}
pub fn zoom_gesture(mut self, enabled: bool) -> Self {
self.options.zoom_gesture_enabled = enabled;
self
}
pub fn drag_pan_buttons(mut self, buttons: DragPanButtons) -> Self {
self.options.drag_pan_buttons = buttons;
self
}
pub fn zoom_speed(mut self, speed: f64) -> Self {
self.options.zoom_speed = speed;
self
}
pub fn double_click_to_zoom(mut self, enabled: bool) -> Self {
self.options.double_click_to_zoom = enabled;
self
}
pub fn double_click_to_zoom_out(mut self, enabled: bool) -> Self {
self.options.double_click_to_zoom_out = enabled;
self
}
pub fn zoom_with_ctrl(mut self, enabled: bool) -> Self {
self.options.zoom_with_ctrl = enabled;
self
}
pub fn panning(mut self, enabled: bool) -> Self {
self.options.panning = enabled;
self
}
pub fn pull_to_my_position_threshold(mut self, threshold: f32) -> Self {
self.options.pull_to_my_position_threshold = threshold;
self
}
pub fn show<R>(
mut self,
ui: &mut Ui,
add_contents: impl FnOnce(&mut Ui, &Response, &Projector, &MapMemory) -> R,
) -> InnerResponse<R> {
let (rect, mut response) =
ui.allocate_exact_size(ui.available_size(), Sense::click_and_drag());
let mut changed = self.handle_gestures(ui, &response);
let delta_time = ui.input(|reader| reader.stable_dt);
let zoom = self.memory.zoom;
changed |= self
.memory
.center_mode
.update_movement(delta_time, zoom.into());
if changed {
response.mark_changed();
ui.request_repaint();
}
let map_center = self.position();
let painter = ui.painter().with_clip_rect(rect);
if let Some(tiles) = self.tiles {
draw_tiles(&painter, map_center, zoom, tiles, 1.0);
}
for layer in self.layers {
draw_tiles(&painter, map_center, zoom, layer.tiles, layer.transparency);
}
let projector = Projector::new(response.rect, self.memory, self.my_position);
for (idx, plugin) in self.plugins.into_iter().enumerate() {
let mut child_ui = ui.new_child(UiBuilder::new().max_rect(rect).id_salt(idx));
plugin.run(&mut child_ui, &response, &projector, self.memory);
}
let mut child_ui = ui.new_child(UiBuilder::new().max_rect(rect).id_salt("inner"));
let inner = add_contents(&mut child_ui, &response, &projector, self.memory);
InnerResponse { inner, response }
}
}
impl Map<'_, '_, '_> {
fn handle_gestures(&mut self, ui: &mut Ui, response: &Response) -> bool {
let zoom_delta = self.zoom_delta(ui, response);
let changed = if (zoom_delta - 1.0).abs() > 0.001
&& ui.ui_contains_pointer()
&& self.options.zoom_gesture_enabled
{
let offset = input_offset(ui, response);
if let Some(offset) = offset {
if self.memory.detached().is_some()
|| offset.length() > self.options.pull_to_my_position_threshold
{
self.memory.center_mode = Center::Exact(
AdjustedPosition::new(self.position()).shift(-offset, self.memory.zoom()),
);
}
}
self.memory
.zoom
.zoom_by((zoom_delta - 1.) * self.options.zoom_speed);
if let Some(offset) = offset {
self.memory.center_mode = self
.memory
.center_mode
.clone()
.shift(offset, self.memory.zoom());
}
true
} else {
self.memory.center_mode.handle_gestures(
response,
self.my_position,
self.options.pull_to_my_position_threshold,
self.options.drag_pan_buttons,
)
};
let panning_enabled =
self.options.panning && (ui.input(|i| i.any_touches()) || self.options.zoom_with_ctrl);
if ui.ui_contains_pointer() && panning_enabled {
let scroll_delta = ui.input(|i| i.smooth_scroll_delta);
if scroll_delta != Vec2::ZERO {
self.memory.center_mode = Center::Exact(
AdjustedPosition::new(self.position()).shift(scroll_delta, self.memory.zoom()),
);
}
}
changed
}
fn zoom_delta(&self, ui: &mut Ui, response: &Response) -> f64 {
let mut zoom_delta = ui.input(|input| input.zoom_delta()) as f64;
if self.options.double_click_to_zoom
&& ui.ui_contains_pointer()
&& response.double_clicked_by(PointerButton::Primary)
{
zoom_delta = 2.0;
}
if self.options.double_click_to_zoom_out
&& ui.ui_contains_pointer()
&& response.double_clicked_by(PointerButton::Secondary)
{
zoom_delta = 0.0;
}
if !self.options.zoom_with_ctrl && zoom_delta == 1.0 {
zoom_delta = 1f64
+ ui.input(|input| {
input.smooth_scroll_delta.y * input.stable_dt.max(input.predicted_dt * 1.5)
}) as f64
/ 4.0;
};
zoom_delta
}
fn position(&self) -> Position {
self.memory.center_mode.position(self.my_position)
}
}
impl Widget for Map<'_, '_, '_> {
fn ui(self, ui: &mut Ui) -> Response {
self.show(ui, |_, _, _, _| ()).response
}
}
fn input_offset(ui: &mut Ui, response: &Response) -> Option<Vec2> {
let mouse_offset = response.hover_pos();
let touch_offset = ui
.input(|input| input.multi_touch())
.map(|multi_touch| multi_touch.center_pos);
touch_offset
.or(mouse_offset)
.map(|pos| pos - response.rect.center())
}