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 pub(crate) desired_size: Vec2,
14
15 last_content_size: Vec2,
17
18 pub(crate) requested_size: Option<Vec2>,
20
21 #[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#[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 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), with_stroke: true,
67 }
68 }
69}
70
71impl Resize {
72 #[inline]
74 pub fn id(mut self, id: Id) -> Self {
75 self.id = Some(id);
76 self
77 }
78
79 #[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 #[inline]
93 pub fn default_width(mut self, width: f32) -> Self {
94 self.default_size.x = width;
95 self
96 }
97
98 #[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 #[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 #[inline]
126 pub fn min_width(mut self, min_width: f32) -> Self {
127 self.min_size.x = min_width;
128 self
129 }
130
131 #[inline]
133 pub fn min_height(mut self, min_height: f32) -> Self {
134 self.min_size.y = min_height;
135 self
136 }
137
138 #[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 #[inline]
147 pub fn max_width(mut self, max_width: f32) -> Self {
148 self.max_size.x = max_width;
149 self
150 }
151
152 #[inline]
154 pub fn max_height(mut self, max_height: f32) -> Self {
155 self.max_size.y = max_height;
156 self
157 }
158
159 #[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 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(); 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(), )
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 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 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 state.desired_size = state.desired_size.max(state.last_content_size);
270 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 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 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()); 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 state.min_content_width = Some(content_ui.min_size().x);
344 } else {
345 state.last_content_size = content_ui.min_size();
346 }
347
348 let mut size = state.last_content_size;
351 for d in 0..2 {
352 if self.with_stroke || self.resizable[d] {
353 state.desired_size[d] = state.desired_size[d].max(state.last_content_size[d]);
357
358 size[d] = state.desired_size[d];
360 } else {
361 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 let corner_response = if let Some(corner_id) = corner_id {
370 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 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); 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, 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}