egui/containers/
resize.rs

1use crate::{
2    pos2, vec2, Align2, Color32, Context, CursorIcon, Id, NumExt, Rect, Response, Sense, Shape, Ui,
3    UiBuilder, UiKind, UiStackInfo, Vec2, Vec2b,
4};
5
6#[derive(Clone, Copy, Debug)]
7#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
8pub(crate) struct State {
9    /// This is the size that the user has picked by dragging the resize handles.
10    /// This may be smaller and/or larger than the actual size.
11    /// For instance, the user may have tried to shrink too much (not fitting the contents).
12    /// Or the user requested a large area, but the content don't need that much space.
13    pub(crate) desired_size: Vec2,
14
15    /// Actual size of content last frame
16    last_content_size: Vec2,
17
18    /// Externally requested size (e.g. by Window) for the next frame
19    pub(crate) requested_size: Option<Vec2>,
20}
21
22impl State {
23    pub fn load(ctx: &Context, id: Id) -> Option<Self> {
24        ctx.data_mut(|d| d.get_persisted(id))
25    }
26
27    pub fn store(self, ctx: &Context, id: Id) {
28        ctx.data_mut(|d| d.insert_persisted(id, self));
29    }
30}
31
32/// A region that can be resized by dragging the bottom right corner.
33#[derive(Clone, Copy, Debug)]
34#[must_use = "You should call .show()"]
35pub struct Resize {
36    id: Option<Id>,
37    id_salt: Option<Id>,
38
39    /// If false, we are no enabled
40    resizable: Vec2b,
41
42    pub(crate) min_size: Vec2,
43    pub(crate) max_size: Vec2,
44
45    default_size: Vec2,
46
47    with_stroke: bool,
48}
49
50impl Default for Resize {
51    fn default() -> Self {
52        Self {
53            id: None,
54            id_salt: None,
55            resizable: Vec2b::TRUE,
56            min_size: Vec2::splat(16.0),
57            max_size: Vec2::splat(f32::INFINITY),
58            default_size: vec2(320.0, 128.0), // TODO(emilk): preferred size of [`Resize`] area.
59            with_stroke: true,
60        }
61    }
62}
63
64impl Resize {
65    /// Assign an explicit and globally unique id.
66    #[inline]
67    pub fn id(mut self, id: Id) -> Self {
68        self.id = Some(id);
69        self
70    }
71
72    /// A source for the unique [`Id`], e.g. `.id_source("second_resize_area")` or `.id_source(loop_index)`.
73    #[inline]
74    #[deprecated = "Renamed id_salt"]
75    pub fn id_source(self, id_salt: impl std::hash::Hash) -> Self {
76        self.id_salt(id_salt)
77    }
78
79    /// A source for the unique [`Id`], e.g. `.id_salt("second_resize_area")` or `.id_salt(loop_index)`.
80    #[inline]
81    pub fn id_salt(mut self, id_salt: impl std::hash::Hash) -> Self {
82        self.id_salt = Some(Id::new(id_salt));
83        self
84    }
85
86    /// Preferred / suggested width. Actual width will depend on contents.
87    ///
88    /// Examples:
89    /// * if the contents is text, this will decide where we break long lines.
90    /// * if the contents is a canvas, this decides the width of it,
91    /// * if the contents is some buttons, this is ignored and we will auto-size.
92    #[inline]
93    pub fn default_width(mut self, width: f32) -> Self {
94        self.default_size.x = width;
95        self
96    }
97
98    /// Preferred / suggested height. Actual height will depend on contents.
99    ///
100    /// Examples:
101    /// * if the contents is a [`crate::ScrollArea`] then this decides the maximum size.
102    /// * if the contents is a canvas, this decides the height of it,
103    /// * if the contents is text and buttons, then the `default_height` is ignored
104    ///   and the height is picked automatically..
105    #[inline]
106    pub fn default_height(mut self, height: f32) -> Self {
107        self.default_size.y = height;
108        self
109    }
110
111    #[inline]
112    pub fn default_size(mut self, default_size: impl Into<Vec2>) -> Self {
113        self.default_size = default_size.into();
114        self
115    }
116
117    /// Won't shrink to smaller than this
118    #[inline]
119    pub fn min_size(mut self, min_size: impl Into<Vec2>) -> Self {
120        self.min_size = min_size.into();
121        self
122    }
123
124    /// Won't shrink to smaller than this
125    #[inline]
126    pub fn min_width(mut self, min_width: f32) -> Self {
127        self.min_size.x = min_width;
128        self
129    }
130
131    /// Won't shrink to smaller than this
132    #[inline]
133    pub fn min_height(mut self, min_height: f32) -> Self {
134        self.min_size.y = min_height;
135        self
136    }
137
138    /// Won't expand to larger than this
139    #[inline]
140    pub fn max_size(mut self, max_size: impl Into<Vec2>) -> Self {
141        self.max_size = max_size.into();
142        self
143    }
144
145    /// Won't expand to larger than this
146    #[inline]
147    pub fn max_width(mut self, max_width: f32) -> Self {
148        self.max_size.x = max_width;
149        self
150    }
151
152    /// Won't expand to larger than this
153    #[inline]
154    pub fn max_height(mut self, max_height: f32) -> Self {
155        self.max_size.y = max_height;
156        self
157    }
158
159    /// Can you resize it with the mouse?
160    ///
161    /// Note that a window can still auto-resize.
162    ///
163    /// Default is `true`.
164    #[inline]
165    pub fn resizable(mut self, resizable: impl Into<Vec2b>) -> Self {
166        self.resizable = resizable.into();
167        self
168    }
169
170    #[inline]
171    pub fn is_resizable(&self) -> Vec2b {
172        self.resizable
173    }
174
175    /// Not manually resizable, just takes the size of its contents.
176    /// Text will not wrap, but will instead make your window width expand.
177    pub fn auto_sized(self) -> Self {
178        self.min_size(Vec2::ZERO)
179            .default_size(Vec2::splat(f32::INFINITY))
180            .resizable(false)
181    }
182
183    #[inline]
184    pub fn fixed_size(mut self, size: impl Into<Vec2>) -> Self {
185        let size = size.into();
186        self.default_size = size;
187        self.min_size = size;
188        self.max_size = size;
189        self.resizable = Vec2b::FALSE;
190        self
191    }
192
193    #[inline]
194    pub fn with_stroke(mut self, with_stroke: bool) -> Self {
195        self.with_stroke = with_stroke;
196        self
197    }
198}
199
200struct Prepared {
201    id: Id,
202    corner_id: Option<Id>,
203    state: State,
204    content_ui: Ui,
205}
206
207impl Resize {
208    fn begin(&self, ui: &mut Ui) -> Prepared {
209        let position = ui.available_rect_before_wrap().min;
210        let id = self.id.unwrap_or_else(|| {
211            let id_salt = self.id_salt.unwrap_or_else(|| Id::new("resize"));
212            ui.make_persistent_id(id_salt)
213        });
214
215        let mut state = State::load(ui.ctx(), id).unwrap_or_else(|| {
216            ui.ctx().request_repaint(); // counter frame delay
217
218            let default_size = self
219                .default_size
220                .at_least(self.min_size)
221                .at_most(self.max_size)
222                .at_most(
223                    ui.ctx().screen_rect().size() - ui.spacing().window_margin.sum(), // hack for windows
224                )
225                .round_ui();
226
227            State {
228                desired_size: default_size,
229                last_content_size: vec2(0.0, 0.0),
230                requested_size: None,
231            }
232        });
233
234        state.desired_size = state
235            .desired_size
236            .at_least(self.min_size)
237            .at_most(self.max_size)
238            .round_ui();
239
240        let mut user_requested_size = state.requested_size.take();
241
242        let corner_id = self.resizable.any().then(|| id.with("__resize_corner"));
243
244        if let Some(corner_id) = corner_id {
245            if let Some(corner_response) = ui.ctx().read_response(corner_id) {
246                if let Some(pointer_pos) = corner_response.interact_pointer_pos() {
247                    // Respond to the interaction early to avoid frame delay.
248                    user_requested_size =
249                        Some(pointer_pos - position + 0.5 * corner_response.rect.size());
250                }
251            }
252        }
253
254        if let Some(user_requested_size) = user_requested_size {
255            state.desired_size = user_requested_size;
256        } else {
257            // We are not being actively resized, so auto-expand to include size of last frame.
258            // This prevents auto-shrinking if the contents contain width-filling widgets (separators etc)
259            // but it makes a lot of interactions with [`Window`]s nicer.
260            state.desired_size = state.desired_size.max(state.last_content_size);
261        }
262
263        state.desired_size = state
264            .desired_size
265            .at_least(self.min_size)
266            .at_most(self.max_size);
267
268        // ------------------------------
269
270        let inner_rect = Rect::from_min_size(position, state.desired_size);
271
272        let mut content_clip_rect = inner_rect.expand(ui.visuals().clip_rect_margin);
273
274        // If we pull the resize handle to shrink, we want to TRY to shrink it.
275        // After laying out the contents, we might be much bigger.
276        // In those cases we don't want the clip_rect to be smaller, because
277        // then we will clip the contents of the region even thought the result gets larger. This is simply ugly!
278        // So we use the memory of last_content_size to make the clip rect large enough.
279        content_clip_rect.max = content_clip_rect.max.max(
280            inner_rect.min + state.last_content_size + Vec2::splat(ui.visuals().clip_rect_margin),
281        );
282
283        content_clip_rect = content_clip_rect.intersect(ui.clip_rect()); // Respect parent region
284
285        let mut content_ui = ui.new_child(
286            UiBuilder::new()
287                .ui_stack_info(UiStackInfo::new(UiKind::Resize))
288                .max_rect(inner_rect),
289        );
290        content_ui.set_clip_rect(content_clip_rect);
291
292        Prepared {
293            id,
294            corner_id,
295            state,
296            content_ui,
297        }
298    }
299
300    pub fn show<R>(self, ui: &mut Ui, add_contents: impl FnOnce(&mut Ui) -> R) -> R {
301        let mut prepared = self.begin(ui);
302        let ret = add_contents(&mut prepared.content_ui);
303        self.end(ui, prepared);
304        ret
305    }
306
307    fn end(self, ui: &mut Ui, prepared: Prepared) {
308        let Prepared {
309            id,
310            corner_id,
311            mut state,
312            content_ui,
313        } = prepared;
314
315        state.last_content_size = content_ui.min_size();
316
317        // ------------------------------
318
319        let mut size = state.last_content_size;
320        for d in 0..2 {
321            if self.with_stroke || self.resizable[d] {
322                // We show how large we are,
323                // so we must follow the contents:
324
325                state.desired_size[d] = state.desired_size[d].max(state.last_content_size[d]);
326
327                // We are as large as we look
328                size[d] = state.desired_size[d];
329            } else {
330                // Probably a window.
331                size[d] = state.last_content_size[d];
332            }
333        }
334        ui.advance_cursor_after_rect(Rect::from_min_size(content_ui.min_rect().min, size));
335
336        // ------------------------------
337
338        let corner_response = if let Some(corner_id) = corner_id {
339            // We do the corner interaction last to place it on top of the content:
340            let corner_size = Vec2::splat(ui.visuals().resize_corner_size);
341            let corner_rect = Rect::from_min_size(
342                content_ui.min_rect().left_top() + size - corner_size,
343                corner_size,
344            );
345            Some(ui.interact(corner_rect, corner_id, Sense::drag()))
346        } else {
347            None
348        };
349
350        // ------------------------------
351
352        if self.with_stroke && corner_response.is_some() {
353            let rect = Rect::from_min_size(content_ui.min_rect().left_top(), state.desired_size);
354            let rect = rect.expand(2.0); // breathing room for content
355            ui.painter().add(Shape::rect_stroke(
356                rect,
357                3.0,
358                ui.visuals().widgets.noninteractive.bg_stroke,
359                epaint::StrokeKind::Inside,
360            ));
361        }
362
363        if let Some(corner_response) = corner_response {
364            paint_resize_corner(ui, &corner_response);
365
366            if corner_response.hovered() || corner_response.dragged() {
367                ui.ctx().set_cursor_icon(CursorIcon::ResizeNwSe);
368            }
369        }
370
371        state.store(ui.ctx(), id);
372
373        #[cfg(debug_assertions)]
374        if ui.ctx().style().debug.show_resize {
375            ui.ctx().debug_painter().debug_rect(
376                Rect::from_min_size(content_ui.min_rect().left_top(), state.desired_size),
377                Color32::GREEN,
378                "desired_size",
379            );
380            ui.ctx().debug_painter().debug_rect(
381                Rect::from_min_size(content_ui.min_rect().left_top(), state.last_content_size),
382                Color32::LIGHT_BLUE,
383                "last_content_size",
384            );
385        }
386    }
387}
388
389use emath::GuiRounding as _;
390use epaint::Stroke;
391
392pub fn paint_resize_corner(ui: &Ui, response: &Response) {
393    let stroke = ui.style().interact(response).fg_stroke;
394    paint_resize_corner_with_style(ui, &response.rect, stroke.color, Align2::RIGHT_BOTTOM);
395}
396
397pub fn paint_resize_corner_with_style(
398    ui: &Ui,
399    rect: &Rect,
400    color: impl Into<Color32>,
401    corner: Align2,
402) {
403    let painter = ui.painter();
404    let cp = corner
405        .pos_in_rect(rect)
406        .round_to_pixels(ui.pixels_per_point());
407    let mut w = 2.0;
408    let stroke = Stroke {
409        width: 1.0, // Set width to 1.0 to prevent overlapping
410        color: color.into(),
411    };
412
413    while w <= rect.width() && w <= rect.height() {
414        painter.line_segment(
415            [
416                pos2(cp.x - w * corner.x().to_sign(), cp.y),
417                pos2(cp.x, cp.y - w * corner.y().to_sign()),
418            ],
419            stroke,
420        );
421        w += 4.0;
422    }
423}