use core::f32;
use egui::emath::{Pos2, TSTransform};
use egui::{
DragPanButtons,
InnerResponse,
LayerId,
PointerButton,
Rangef,
Rect,
Response,
Sense,
Ui,
UiBuilder,
Vec2,
};
fn fit_to_rect_in_scene(
rect_in_global: Rect,
rect_in_scene: Rect,
zoom_range: Rangef,
) -> TSTransform {
let scale = rect_in_global.size() / rect_in_scene.size();
let scale = scale.min_elem();
let scale = zoom_range.clamp(scale);
let center_in_global = rect_in_global.center().to_vec2();
let center_scene = rect_in_scene.center().to_vec2();
TSTransform::from_translation(center_in_global - scale * center_scene)
* TSTransform::from_scaling(scale)
}
fn constrain_transform_to_content(
transform: TSTransform,
view_rect: Rect,
content_rect: Rect,
) -> TSTransform {
if content_rect == Rect::NOTHING || !content_rect.is_finite() {
return transform;
}
let TSTransform {
scaling,
mut translation,
} = transform;
let content_in_global = transform * content_rect;
let view_size = view_rect.size();
let content_size = content_in_global.size();
if content_size.x <= view_size.x {
let center_offset = (view_size.x - content_size.x) * 0.5;
translation.x =
view_rect.min.x + center_offset - content_in_global.min.x + transform.translation.x;
} else {
let left_edge = view_rect.min.x - content_in_global.min.x + transform.translation.x;
let right_edge = view_rect.max.x - content_in_global.max.x + transform.translation.x;
translation.x = translation.x.clamp(right_edge, left_edge);
}
if content_size.y <= view_size.y {
let center_offset = (view_size.y - content_size.y) * 0.5;
translation.y =
view_rect.min.y + center_offset - content_in_global.min.y + transform.translation.y;
} else {
let top_edge = view_rect.min.y - content_in_global.min.y + transform.translation.y;
let bottom_edge = view_rect.max.y - content_in_global.max.y + transform.translation.y;
translation.y = translation.y.clamp(bottom_edge, top_edge);
}
TSTransform {
translation,
scaling,
}
}
#[derive(Clone, Debug)]
#[must_use = "You should call .show()"]
pub struct Scene {
zoom_range: Rangef,
sense: Sense,
max_inner_size: Vec2,
drag_pan_buttons: DragPanButtons,
constrain_panning: bool,
content_rect: Rect,
}
impl Default for Scene {
fn default() -> Self {
Self {
zoom_range: Rangef::new(f32::EPSILON, 1.0),
sense: Sense::click_and_drag(),
max_inner_size: Vec2::splat(1000.0),
drag_pan_buttons: DragPanButtons::all(),
constrain_panning: true,
content_rect: Rect::NOTHING,
}
}
}
#[allow(unused)]
impl Scene {
#[inline]
pub fn new() -> Self {
Default::default()
}
#[inline]
pub fn sense(mut self, sense: Sense) -> Self {
self.sense = sense;
self
}
#[inline]
pub fn zoom_range(mut self, zoom_range: impl Into<Rangef>) -> Self {
self.zoom_range = zoom_range.into();
self
}
#[inline]
pub fn max_inner_size(mut self, max_inner_size: impl Into<Vec2>) -> Self {
self.max_inner_size = max_inner_size.into();
self
}
#[inline]
pub fn drag_pan_buttons(mut self, flags: DragPanButtons) -> Self {
self.drag_pan_buttons = flags;
self
}
#[inline]
pub fn constrain_panning(mut self, constrain: bool) -> Self {
self.constrain_panning = constrain;
self
}
pub fn show<R>(
&mut self,
parent_ui: &mut Ui,
scene_rect: &mut Rect,
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 mut to_global = fit_to_rect_in_scene(outer_rect, *scene_rect, self.zoom_range);
let scene_rect_was_good =
to_global.is_valid() && scene_rect.is_finite() && scene_rect.size() != Vec2::ZERO;
let mut inner_rect = *scene_rect;
let ret = self.show_global_transform(parent_ui, outer_rect, &mut to_global, |ui| {
let r = add_contents(ui);
inner_rect = ui.min_rect();
r
});
if self.constrain_panning && ret.response.changed() {
let constrained_transform =
constrain_transform_to_content(to_global, outer_rect, inner_rect);
if constrained_transform.translation != to_global.translation {
to_global = constrained_transform;
}
}
if ret.response.changed() {
*scene_rect = to_global.inverse() * outer_rect;
}
if !scene_rect_was_good {
let to_global = fit_to_rect_in_scene(outer_rect, inner_rect, self.zoom_range);
*scene_rect = to_global.inverse() * outer_rect;
}
ret
}
fn show_global_transform<R>(
&mut self,
parent_ui: &mut Ui,
outer_rect: Rect,
to_global: &mut TSTransform,
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(Rect::from_min_size(Pos2::ZERO, self.max_inner_size))
.sense(self.sense),
);
let mut pan_response = local_ui.response();
self.register_pan_and_zoom(&local_ui, &mut pan_response, to_global, outer_rect);
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);
self.content_rect = local_ui.min_rect();
InnerResponse {
response: pan_response,
inner: ret,
}
}
pub fn register_pan_and_zoom(
&self,
ui: &Ui,
resp: &mut Response,
to_global: &mut TSTransform,
view_rect: Rect,
) {
let dragged = self.drag_pan_buttons.iter().any(|button| match button {
DragPanButtons::PRIMARY => resp.dragged_by(PointerButton::Primary),
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();
if self.constrain_panning {
*to_global =
constrain_transform_to_content(*to_global, view_rect, self.content_rect);
}
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);
if zoom_delta == 1.0 && pan_delta == Vec2::ZERO {
return;
}
if zoom_delta != 1.0 {
let zoom_delta = zoom_delta.clamp(
self.zoom_range.min / to_global.scaling,
self.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 = self.zoom_range.clamp(to_global.scaling);
}
if self.constrain_panning {
*to_global = constrain_transform_to_content(
TSTransform::from_translation(pan_delta) * *to_global,
view_rect,
self.content_rect,
);
} else {
*to_global = TSTransform::from_translation(pan_delta) * *to_global;
}
resp.mark_changed();
}
}
}