Skip to main content

egui/containers/
resize.rs

1use crate::{
2    Align2, AsIdSalt, Color32, Context, CursorIcon, Id, IdSalt, NumExt as _, Rect, Response, Sense,
3    Shape, Ui, UiBuilder, UiKind, UiStackInfo, Vec2, Vec2b, pos2, vec2,
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    /// Minimum content width measured by a sizing pass at the start of the current
22    /// interactive resize. We clamp `desired_size.x` against this for the rest of
23    /// the drag so the user can't shrink the window past what the content actually
24    /// needs. Reset to `None` whenever a drag is not in progress.
25    #[cfg_attr(feature = "serde", serde(default))]
26    min_content_width: Option<f32>,
27}
28
29impl State {
30    pub fn load(ctx: &Context, id: Id) -> Option<Self> {
31        ctx.data_mut(|d| d.get_persisted(id))
32    }
33
34    pub fn store(self, ctx: &Context, id: Id) {
35        ctx.data_mut(|d| d.insert_persisted(id, self));
36    }
37}
38
39/// A region that can be resized by dragging the bottom right corner.
40#[derive(Clone, Copy, Debug)]
41#[must_use = "You should call .show()"]
42pub struct Resize {
43    id: Option<Id>,
44    id_salt: Option<IdSalt>,
45
46    /// If false, we are no enabled
47    resizable: Vec2b,
48
49    pub(crate) min_size: Vec2,
50    pub(crate) max_size: Vec2,
51
52    pub(crate) default_size: Vec2,
53
54    with_stroke: bool,
55}
56
57impl Default for Resize {
58    fn default() -> Self {
59        Self {
60            id: None,
61            id_salt: None,
62            resizable: Vec2b::TRUE,
63            min_size: Vec2::splat(16.0),
64            max_size: Vec2::splat(f32::INFINITY),
65            default_size: vec2(320.0, 128.0), // TODO(emilk): preferred size of [`Resize`] area.
66            with_stroke: true,
67        }
68    }
69}
70
71impl Resize {
72    /// Assign an explicit and globally unique id.
73    #[inline]
74    pub fn id(mut self, id: Id) -> Self {
75        self.id = Some(id);
76        self
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 AsIdSalt) -> Self {
82        self.id_salt = Some(IdSalt::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    sizing_pass: bool,
206}
207
208impl Resize {
209    fn begin(&self, ui: &mut Ui) -> Prepared {
210        let position = ui.available_rect_before_wrap().min;
211        let id = self.id.unwrap_or_else(|| {
212            let id_salt = self.id_salt.unwrap_or_else(|| IdSalt::new("resize"));
213            ui.make_persistent_id(id_salt)
214        });
215
216        let mut state = State::load(ui.ctx(), id).unwrap_or_else(|| {
217            ui.request_repaint(); // counter frame delay
218
219            let default_size = self
220                .default_size
221                .at_least(self.min_size)
222                .at_most(self.max_size)
223                .at_most(
224                    ui.ctx().content_rect().size() - ui.spacing().window_margin.sum(), // hack for windows
225                )
226                .round_ui();
227
228            State {
229                desired_size: default_size,
230                last_content_size: vec2(0.0, 0.0),
231                requested_size: None,
232                min_content_width: None,
233            }
234        });
235
236        state.desired_size = state
237            .desired_size
238            .at_least(self.min_size)
239            .at_most(self.max_size)
240            .round_ui();
241
242        let mut user_requested_size = state.requested_size.take();
243
244        let corner_id = self.resizable.any().then(|| id.with("__resize_corner"));
245
246        if let Some(corner_id) = corner_id
247            && let Some(corner_response) = ui.ctx().read_response(corner_id)
248            && let Some(pointer_pos) = corner_response.interact_pointer_pos()
249        {
250            // Respond to the interaction early to avoid frame delay.
251            user_requested_size = Some(pointer_pos - position + 0.5 * corner_response.rect.size());
252        }
253
254        let is_actively_resizing = user_requested_size.is_some();
255
256        // Drag just started: we don't yet know what the content's minimum width is.
257        // Run a one-frame sizing pass below to discover it.
258        let needs_sizing_pass = is_actively_resizing && state.min_content_width.is_none();
259
260        if let Some(mut user_requested_size) = user_requested_size {
261            if let Some(min_width) = state.min_content_width {
262                user_requested_size.x = user_requested_size.x.at_least(min_width);
263            }
264            state.desired_size = user_requested_size;
265        } else {
266            // We are not being actively resized, so auto-expand to include size of last frame.
267            // This prevents auto-shrinking if the contents contain width-filling widgets (separators etc)
268            // but it makes a lot of interactions with [`Window`]s nicer.
269            state.desired_size = state.desired_size.max(state.last_content_size);
270            // Drag ended (if any). Forget the cached min so the next drag re-measures it,
271            // in case content changed.
272            state.min_content_width = None;
273        }
274
275        state.desired_size = state
276            .desired_size
277            .at_least(self.min_size)
278            .at_most(self.max_size);
279
280        // ------------------------------
281
282        // For the sizing pass, offer the tightest possible rect so widgets shrink to
283        // their natural minimum. We render the frame invisibly and discard it so the
284        // user never sees the squished layout; the measured min then clamps drags.
285        let inner_rect = if needs_sizing_pass {
286            ui.ctx().request_discard("Resize sizing pass");
287            Rect::from_min_size(position, Vec2::new(self.min_size.x, state.desired_size.y))
288        } else {
289            Rect::from_min_size(position, state.desired_size)
290        };
291
292        let mut content_clip_rect = inner_rect;
293
294        // If we pull the resize handle to shrink, we want to TRY to shrink it.
295        // After laying out the contents, we might be much bigger.
296        // In those cases we don't want the clip_rect to be smaller, because
297        // then we will clip the contents of the region even thought the result gets larger. This is simply ugly!
298        // So we use the memory of last_content_size to make the clip rect large enough.
299        content_clip_rect.max = content_clip_rect
300            .max
301            .max(inner_rect.min + state.last_content_size);
302
303        content_clip_rect = content_clip_rect.intersect(ui.clip_rect()); // Respect parent region
304
305        let mut ui_builder = UiBuilder::new()
306            .ui_stack_info(UiStackInfo::new(UiKind::Resize))
307            .max_rect(inner_rect);
308        if needs_sizing_pass {
309            ui_builder = ui_builder.sizing_pass();
310        }
311        let mut content_ui = ui.new_child(ui_builder);
312        content_ui.set_clip_rect(content_clip_rect);
313
314        Prepared {
315            id,
316            corner_id,
317            state,
318            content_ui,
319            sizing_pass: needs_sizing_pass,
320        }
321    }
322
323    pub fn show<R>(self, ui: &mut Ui, add_contents: impl FnOnce(&mut Ui) -> R) -> R {
324        let mut prepared = self.begin(ui);
325        let ret = add_contents(&mut prepared.content_ui);
326        self.end(ui, prepared);
327        ret
328    }
329
330    fn end(self, ui: &mut Ui, prepared: Prepared) {
331        let Prepared {
332            id,
333            corner_id,
334            mut state,
335            content_ui,
336            sizing_pass,
337        } = prepared;
338
339        if sizing_pass {
340            // Remember the measured minimum so we can clamp the user's drag on subsequent frames.
341            // Don't touch `last_content_size`, it should keep reflecting the previously
342            // rendered content.
343            state.min_content_width = Some(content_ui.min_size().x);
344        } else {
345            state.last_content_size = content_ui.min_size();
346        }
347
348        // ------------------------------
349
350        let mut size = state.last_content_size;
351        for d in 0..2 {
352            if self.with_stroke || self.resizable[d] {
353                // We show how large we are,
354                // so we must follow the contents:
355
356                state.desired_size[d] = state.desired_size[d].max(state.last_content_size[d]);
357
358                // We are as large as we look
359                size[d] = state.desired_size[d];
360            } else {
361                // Probably a window.
362                size[d] = state.last_content_size[d];
363            }
364        }
365        ui.advance_cursor_after_rect(Rect::from_min_size(content_ui.min_rect().min, size));
366
367        // ------------------------------
368
369        let corner_response = if let Some(corner_id) = corner_id {
370            // We do the corner interaction last to place it on top of the content:
371            let corner_size = Vec2::splat(ui.visuals().resize_corner_size);
372            let corner_rect = Rect::from_min_size(
373                content_ui.min_rect().left_top() + size - corner_size,
374                corner_size,
375            );
376            Some(ui.interact(corner_rect, corner_id, Sense::drag()))
377        } else {
378            None
379        };
380
381        // ------------------------------
382
383        if self.with_stroke && corner_response.is_some() {
384            let rect = Rect::from_min_size(content_ui.min_rect().left_top(), state.desired_size);
385            let rect = rect.expand(2.0); // breathing room for content
386            ui.painter().add(Shape::rect_stroke(
387                rect,
388                3.0,
389                ui.visuals().widgets.noninteractive.bg_stroke,
390                epaint::StrokeKind::Inside,
391            ));
392        }
393
394        if let Some(corner_response) = corner_response {
395            paint_resize_corner(ui, &corner_response);
396
397            if corner_response.hovered() || corner_response.dragged() {
398                ui.set_cursor_icon(CursorIcon::ResizeNwSe);
399            }
400        }
401
402        state.store(ui.ctx(), id);
403
404        #[cfg(debug_assertions)]
405        if ui.global_style().debug.show_resize {
406            ui.debug_painter().debug_rect(
407                Rect::from_min_size(content_ui.min_rect().left_top(), state.desired_size),
408                Color32::GREEN,
409                "desired_size",
410            );
411            ui.debug_painter().debug_rect(
412                Rect::from_min_size(content_ui.min_rect().left_top(), state.last_content_size),
413                Color32::LIGHT_BLUE,
414                "last_content_size",
415            );
416        }
417    }
418}
419
420use emath::GuiRounding as _;
421use epaint::Stroke;
422
423pub fn paint_resize_corner(ui: &Ui, response: &Response) {
424    let stroke = ui.style().interact(response).fg_stroke;
425    paint_resize_corner_with_style(ui, &response.rect, stroke.color, Align2::RIGHT_BOTTOM);
426}
427
428pub fn paint_resize_corner_with_style(
429    ui: &Ui,
430    rect: &Rect,
431    color: impl Into<Color32>,
432    corner: Align2,
433) {
434    let painter = ui.painter();
435    let cp = corner
436        .pos_in_rect(rect)
437        .round_to_pixels(ui.pixels_per_point());
438    let mut w = 2.0;
439    let stroke = Stroke {
440        width: 1.0, // Set width to 1.0 to prevent overlapping
441        color: color.into(),
442    };
443
444    while w <= rect.width() && w <= rect.height() {
445        painter.line_segment(
446            [
447                pos2(cp.x - w * corner.x().to_sign(), cp.y),
448                pos2(cp.x, cp.y - w * corner.y().to_sign()),
449            ],
450            stroke,
451        );
452        w += 4.0;
453    }
454}