1use core::f32;
2
3use emath::{GuiRounding, Pos2};
4
5use crate::{
6 emath::TSTransform, InnerResponse, LayerId, Rangef, Rect, Response, Sense, Ui, UiBuilder, Vec2,
7};
8
9fn fit_to_rect_in_scene(
15 rect_in_global: Rect,
16 rect_in_scene: Rect,
17 zoom_range: Rangef,
18) -> TSTransform {
19 let scale = rect_in_global.size() / rect_in_scene.size();
21
22 let scale = scale.min_elem();
24
25 let scale = zoom_range.clamp(scale);
27
28 let center_in_global = rect_in_global.center().to_vec2();
30 let center_scene = rect_in_scene.center().to_vec2();
31
32 TSTransform::from_translation(center_in_global - scale * center_scene)
34 * TSTransform::from_scaling(scale)
35}
36
37#[derive(Clone, Debug)]
44#[must_use = "You should call .show()"]
45pub struct Scene {
46 zoom_range: Rangef,
47 max_inner_size: Vec2,
48}
49
50impl Default for Scene {
51 fn default() -> Self {
52 Self {
53 zoom_range: Rangef::new(f32::EPSILON, 1.0),
54 max_inner_size: Vec2::splat(1000.0),
55 }
56 }
57}
58
59impl Scene {
60 #[inline]
61 pub fn new() -> Self {
62 Default::default()
63 }
64
65 #[inline]
73 pub fn zoom_range(mut self, zoom_range: impl Into<Rangef>) -> Self {
74 self.zoom_range = zoom_range.into();
75 self
76 }
77
78 #[inline]
80 pub fn max_inner_size(mut self, max_inner_size: impl Into<Vec2>) -> Self {
81 self.max_inner_size = max_inner_size.into();
82 self
83 }
84
85 pub fn show<R>(
93 &self,
94 parent_ui: &mut Ui,
95 scene_rect: &mut Rect,
96 add_contents: impl FnOnce(&mut Ui) -> R,
97 ) -> InnerResponse<R> {
98 let (outer_rect, _outer_response) =
99 parent_ui.allocate_exact_size(parent_ui.available_size_before_wrap(), Sense::hover());
100
101 let mut to_global = fit_to_rect_in_scene(outer_rect, *scene_rect, self.zoom_range);
102
103 let scene_rect_was_good =
104 to_global.is_valid() && scene_rect.is_finite() && scene_rect.size() != Vec2::ZERO;
105
106 let mut inner_rect = *scene_rect;
107
108 let ret = self.show_global_transform(parent_ui, outer_rect, &mut to_global, |ui| {
109 let r = add_contents(ui);
110 inner_rect = ui.min_rect();
111 r
112 });
113
114 if ret.response.changed() {
115 *scene_rect = to_global.inverse() * outer_rect;
118 }
119
120 if !scene_rect_was_good {
121 *scene_rect = inner_rect;
123 }
124
125 ret
126 }
127
128 fn show_global_transform<R>(
129 &self,
130 parent_ui: &mut Ui,
131 outer_rect: Rect,
132 to_global: &mut TSTransform,
133 add_contents: impl FnOnce(&mut Ui) -> R,
134 ) -> InnerResponse<R> {
135 let scene_layer_id = LayerId::new(
137 parent_ui.layer_id().order,
138 parent_ui.id().with("scene_area"),
139 );
140
141 parent_ui
143 .ctx()
144 .set_sublayer(parent_ui.layer_id(), scene_layer_id);
145
146 let mut local_ui = parent_ui.new_child(
147 UiBuilder::new()
148 .layer_id(scene_layer_id)
149 .max_rect(Rect::from_min_size(Pos2::ZERO, self.max_inner_size))
150 .sense(Sense::click_and_drag()),
151 );
152
153 let mut pan_response = local_ui.response();
154
155 self.register_pan_and_zoom(&local_ui, &mut pan_response, to_global);
157
158 local_ui.set_clip_rect(to_global.inverse() * outer_rect);
160
161 let ret = add_contents(&mut local_ui);
163
164 local_ui.force_set_min_rect((to_global.inverse() * outer_rect).round_ui());
166
167 local_ui
169 .ctx()
170 .set_transform_layer(scene_layer_id, *to_global);
171
172 InnerResponse {
173 response: pan_response,
174 inner: ret,
175 }
176 }
177
178 pub fn register_pan_and_zoom(&self, ui: &Ui, resp: &mut Response, to_global: &mut TSTransform) {
180 if resp.dragged() {
181 to_global.translation += to_global.scaling * resp.drag_delta();
182 resp.mark_changed();
183 }
184
185 if let Some(mouse_pos) = ui.input(|i| i.pointer.latest_pos()) {
186 if resp.contains_pointer() {
187 let pointer_in_scene = to_global.inverse() * mouse_pos;
188 let zoom_delta = ui.ctx().input(|i| i.zoom_delta());
189 let pan_delta = ui.ctx().input(|i| i.smooth_scroll_delta);
190
191 if zoom_delta == 1.0 && pan_delta == Vec2::ZERO {
194 return;
195 }
196
197 if zoom_delta != 1.0 {
198 let zoom_delta = zoom_delta.clamp(
200 self.zoom_range.min / to_global.scaling,
201 self.zoom_range.max / to_global.scaling,
202 );
203
204 *to_global = *to_global
205 * TSTransform::from_translation(pointer_in_scene.to_vec2())
206 * TSTransform::from_scaling(zoom_delta)
207 * TSTransform::from_translation(-pointer_in_scene.to_vec2());
208
209 to_global.scaling = self.zoom_range.clamp(to_global.scaling);
211 }
212
213 *to_global = TSTransform::from_translation(pan_delta) * *to_global;
215 resp.mark_changed();
216 }
217 }
218 }
219}