egui/containers/
scene.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
use core::f32;

use emath::{GuiRounding, Pos2};

use crate::{
    emath::TSTransform, InnerResponse, LayerId, Rangef, Rect, Response, Sense, Ui, UiBuilder, Vec2,
};

/// Creates a transformation that fits a given scene rectangle into the available screen size.
///
/// The resulting visual scene bounds can be larger, due to letterboxing.
///
/// Returns the transformation from `scene` to `global` coordinates.
fn fit_to_rect_in_scene(
    rect_in_global: Rect,
    rect_in_scene: Rect,
    zoom_range: Rangef,
) -> TSTransform {
    // Compute the scale factor to fit the bounding rectangle into the available screen size:
    let scale = rect_in_global.size() / rect_in_scene.size();

    // Use the smaller of the two scales to ensure the whole rectangle fits on the screen:
    let scale = scale.min_elem();

    // Clamp scale to what is allowed
    let scale = zoom_range.clamp(scale);

    // Compute the translation to center the bounding rect in the screen:
    let center_in_global = rect_in_global.center().to_vec2();
    let center_scene = rect_in_scene.center().to_vec2();

    // Set the transformation to scale and then translate to center.
    TSTransform::from_translation(center_in_global - scale * center_scene)
        * TSTransform::from_scaling(scale)
}

/// A container that allows you to zoom and pan.
///
/// This is similar to [`crate::ScrollArea`] but:
/// * Supports zooming
/// * Has no scroll bars
/// * Has no limits on the scrolling
#[derive(Clone, Debug)]
#[must_use = "You should call .show()"]
pub struct Scene {
    zoom_range: Rangef,
    max_inner_size: Vec2,
}

impl Default for Scene {
    fn default() -> Self {
        Self {
            zoom_range: Rangef::new(f32::EPSILON, 1.0),
            max_inner_size: Vec2::splat(1000.0),
        }
    }
}

impl Scene {
    #[inline]
    pub fn new() -> Self {
        Default::default()
    }

    /// Set the allowed zoom range.
    ///
    /// The default zoom range is `0.0..=1.0`,
    /// which mean you zan make things arbitrarily small, but you cannot zoom in past a `1:1` ratio.
    ///
    /// If you want to allow zooming in, you can set the zoom range to `0.0..=f32::INFINITY`.
    /// Note that text rendering becomes blurry when you zoom in: <https://github.com/emilk/egui/issues/4813>.
    #[inline]
    pub fn zoom_range(mut self, zoom_range: impl Into<Rangef>) -> Self {
        self.zoom_range = zoom_range.into();
        self
    }

    /// Set the maximum size of the inner [`Ui`] that will be created.
    #[inline]
    pub fn max_inner_size(mut self, max_inner_size: impl Into<Vec2>) -> Self {
        self.max_inner_size = max_inner_size.into();
        self
    }

    /// `scene_rect` contains the view bounds of the inner [`Ui`].
    ///
    /// `scene_rect` will be mutated by any panning/zooming done by the user.
    /// If `scene_rect` is somehow invalid (e.g. `Rect::ZERO`),
    /// then it will be reset to the inner rect of the inner ui.
    ///
    /// You need to store the `scene_rect` in your state between frames.
    pub fn show<R>(
        &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 ret.response.changed() {
            // Only update if changed, both to avoid numeric drift,
            // and to avoid expanding the scene rect unnecessarily.
            *scene_rect = to_global.inverse() * outer_rect;
        }

        if !scene_rect_was_good {
            // Auto-reset if the trsnsformation goes bad somehow (or started bad).
            *scene_rect = inner_rect;
        }

        ret
    }

    fn show_global_transform<R>(
        &self,
        parent_ui: &mut Ui,
        outer_rect: Rect,
        to_global: &mut TSTransform,
        add_contents: impl FnOnce(&mut Ui) -> R,
    ) -> InnerResponse<R> {
        // Create a new egui paint layer, where we can draw our contents:
        let scene_layer_id = LayerId::new(
            parent_ui.layer_id().order,
            parent_ui.id().with("scene_area"),
        );

        // Put the layer directly on-top of the main layer of the ui:
        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(Sense::click_and_drag()),
        );

        let mut pan_response = local_ui.response();

        // Update the `to_global` transform based on use interaction:
        self.register_pan_and_zoom(&local_ui, &mut pan_response, to_global);

        // Set a correct global clip rect:
        local_ui.set_clip_rect(to_global.inverse() * outer_rect);

        // Add the actual contents to the area:
        let ret = add_contents(&mut local_ui);

        // This ensures we catch clicks/drags/pans anywhere on the background.
        local_ui.force_set_min_rect((to_global.inverse() * outer_rect).round_ui());

        // Tell egui to apply the transform on the layer:
        local_ui
            .ctx()
            .set_transform_layer(scene_layer_id, *to_global);

        InnerResponse {
            response: pan_response,
            inner: ret,
        }
    }

    /// Helper function to handle pan and zoom interactions on a response.
    pub fn register_pan_and_zoom(&self, ui: &Ui, resp: &mut Response, to_global: &mut TSTransform) {
        if resp.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()) {
            if 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);

                // Most of the time we can return early. This is also important to
                // avoid `ui_from_scene` to change slightly due to floating point errors.
                if zoom_delta == 1.0 && pan_delta == Vec2::ZERO {
                    return;
                }

                if zoom_delta != 1.0 {
                    // Zoom in on pointer, but only if we are not zoomed in or out too far.
                    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());

                    // Clamp to exact zoom range.
                    to_global.scaling = self.zoom_range.clamp(to_global.scaling);
                }

                // Pan:
                *to_global = TSTransform::from_translation(pan_delta) * *to_global;
                resp.mark_changed();
            }
        }
    }
}