use crate::canvas::view::SceneView;
use core::f32;
use egui::containers::DragPanButtons;
use egui::emath::{GuiRounding, TSTransform};
use egui::{
Event, Id, InnerResponse, LayerId, MouseWheelUnit, PointerButton, Pos2, Rangef, Rect, Response,
Sense, Ui, UiBuilder, Vec2,
};
#[derive(Clone, Debug)]
#[must_use = "You should call .show()"]
pub(crate) struct SceneModded {
scene_size: Vec2,
max_zoom: f32,
}
fn drag_pan_buttons() -> DragPanButtons {
DragPanButtons::all() - DragPanButtons::PRIMARY
}
pub(crate) const PAN_MARGIN: f32 = 0.5;
pub(crate) fn pan_bounds(scene: Rect) -> Rect {
scene.expand2(scene.size() * PAN_MARGIN)
}
fn clamp_symmetric(value: f32, middle: f32, radius: f32) -> f32 {
value.clamp(middle - radius.max(0.0), middle + radius.max(0.0))
}
fn scroll_is_a_wheel(ui: &Ui) -> bool {
let remembered = Id::new("scene scroll device");
let seen = ui.input(|input| {
input.events.iter().rev().find_map(|event| match event {
Event::MouseWheel { unit, .. } => Some(*unit != MouseWheelUnit::Point),
_ => None,
})
});
match seen {
Some(wheel) => {
ui.ctx()
.memory_mut(|memory| memory.data.insert_temp(remembered, wheel));
wheel
}
None => ui
.ctx()
.memory(|memory| memory.data.get_temp(remembered).unwrap_or(false)),
}
}
fn wheel_zoom(delta: Vec2, speed: f32) -> f32 {
(speed * (delta.x + delta.y)).exp()
}
impl Default for SceneModded {
fn default() -> Self {
Self {
scene_size: Vec2::splat(1000.0),
max_zoom: 1.0,
}
}
}
impl SceneModded {
#[inline]
pub(crate) fn new() -> Self {
Default::default()
}
#[inline]
pub(crate) fn scene_size(mut self, scene_size: Vec2) -> Self {
self.scene_size = scene_size;
self
}
#[inline]
pub(crate) fn max_zoom(mut self, max_zoom: f32) -> Self {
self.max_zoom = max_zoom;
self
}
pub(crate) fn show<R>(
&self,
parent_ui: &mut Ui,
view: &mut SceneView,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> InnerResponse<R> {
let (outer_rect, _outer_response) =
parent_ui.allocate_exact_size(parent_ui.available_size_before_wrap(), Sense::hover());
let fitted = self.fitted_zoom(outer_rect.size());
let zoom_range = Rangef::new(fitted, self.max_zoom.max(fitted));
let zoom = self.zoom_of(outer_rect.size(), view);
let center = if view.center.is_finite() {
view.center
} else {
self.scene_rect().center()
};
let mut to_global =
TSTransform::from_translation(outer_rect.center().to_vec2() - zoom * center.to_vec2())
* TSTransform::from_scaling(zoom);
let ret = self.show_global_transform(
parent_ui,
outer_rect,
&mut to_global,
zoom_range,
add_contents,
);
view.center = to_global.inverse() * outer_rect.center();
view.zoom = (to_global.scaling > fitted).then_some(to_global.scaling);
ret
}
pub(crate) fn scene_rect(&self) -> Rect {
Rect::from_min_size(Pos2::ZERO, self.scene_size)
}
pub(crate) fn fitted_zoom(&self, canvas: Vec2) -> f32 {
(canvas / self.scene_size).min_elem()
}
pub(crate) fn zoom_of(&self, canvas: Vec2, view: &SceneView) -> f32 {
let fitted = self.fitted_zoom(canvas);
view.zoom
.map_or(fitted, |zoom| zoom.clamp(fitted, self.max_zoom.max(fitted)))
}
pub(crate) fn visible(&self, canvas: Rect, view: &SceneView) -> Rect {
Rect::from_center_size(
view.center,
canvas.size() / self.zoom_of(canvas.size(), view),
)
}
fn clamp_pan(&self, outer_rect: Rect, to_global: &mut TSTransform) {
let bounds = pan_bounds(self.scene_rect());
let visible = to_global.inverse() * outer_rect;
let center = visible.center();
let slack = (bounds.size() - visible.size()) * 0.5;
let clamped = Pos2::new(
clamp_symmetric(center.x, bounds.center().x, slack.x),
clamp_symmetric(center.y, bounds.center().y, slack.y),
);
to_global.translation += to_global.scaling * (center - clamped);
}
fn show_global_transform<R>(
&self,
parent_ui: &mut Ui,
outer_rect: Rect,
to_global: &mut TSTransform,
zoom_range: Rangef,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> InnerResponse<R> {
let scene_layer_id = LayerId::new(
parent_ui.layer_id().order,
parent_ui.id().with("scene_area"),
);
parent_ui
.ctx()
.set_sublayer(parent_ui.layer_id(), scene_layer_id);
let mut local_ui = parent_ui.new_child(
UiBuilder::new()
.layer_id(scene_layer_id)
.max_rect(self.scene_rect())
.sense(Sense::click_and_drag()),
);
let mut pan_response = local_ui.response();
self.register_pan_and_zoom(&local_ui, &mut pan_response, to_global, zoom_range);
self.clamp_pan(outer_rect, to_global);
local_ui.set_clip_rect(to_global.inverse() * outer_rect);
local_ui
.ctx()
.set_transform_layer(scene_layer_id, *to_global);
let ret = add_contents(&mut local_ui);
let min_rect = (to_global.inverse() * outer_rect).round_ui();
local_ui.expand_to_include_rect(min_rect);
InnerResponse {
response: pan_response,
inner: ret,
}
}
fn register_pan_and_zoom(
&self,
ui: &Ui,
resp: &mut Response,
to_global: &mut TSTransform,
zoom_range: Rangef,
) {
let dragged = drag_pan_buttons().iter().any(|button| match button {
DragPanButtons::SECONDARY => resp.dragged_by(PointerButton::Secondary),
DragPanButtons::MIDDLE => resp.dragged_by(PointerButton::Middle),
DragPanButtons::EXTRA_1 => resp.dragged_by(PointerButton::Extra1),
DragPanButtons::EXTRA_2 => resp.dragged_by(PointerButton::Extra2),
_ => false,
});
if dragged {
to_global.translation += to_global.scaling * resp.drag_delta();
resp.mark_changed();
}
if let Some(mouse_pos) = ui.input(|i| i.pointer.latest_pos())
&& resp.contains_pointer()
{
let pointer_in_scene = to_global.inverse() * mouse_pos;
let zoom_delta = ui.ctx().input(|i| i.zoom_delta());
let pan_delta = ui.ctx().input(|i| i.smooth_scroll_delta);
let (zoom_delta, pan_delta) = if scroll_is_a_wheel(ui) {
let speed = ui
.ctx()
.options(|options| options.input_options.scroll_zoom_speed);
(zoom_delta * wheel_zoom(pan_delta, speed), Vec2::ZERO)
} else {
(zoom_delta, pan_delta)
};
if zoom_delta == 1.0 && pan_delta == Vec2::ZERO {
return;
}
if zoom_delta != 1.0 {
let zoom_delta = zoom_delta.clamp(
zoom_range.min / to_global.scaling,
zoom_range.max / to_global.scaling,
);
*to_global = *to_global
* TSTransform::from_translation(pointer_in_scene.to_vec2())
* TSTransform::from_scaling(zoom_delta)
* TSTransform::from_translation(-pointer_in_scene.to_vec2());
to_global.scaling = zoom_range.clamp(to_global.scaling);
}
*to_global = TSTransform::from_translation(pan_delta) * *to_global;
resp.mark_changed();
}
}
}
#[cfg(test)]
mod tests;