egui/containers/
resize.rs

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