agg_gui/widgets/resize.rs
1//! `Resize` — a nested, user-draggable resizable container.
2//!
3//! Egui-parity port of `egui::Resize`. Wraps a single child widget
4//! and exposes a bottom-right grip the user can drag to resize the
5//! subregion independently of its surrounding layout. Used inside
6//! the Window Resize Test's "↔ auto-sized" window to show a resize-
7//! within-auto-size behaviour (the outer window fits its content; the
8//! inner `Resize` has its own draggable handle).
9//!
10//! # Coordinate conventions
11//!
12//! The widget is pure Y-up: local `(0, 0)` is bottom-left. The SE
13//! grip sits at `(w - HANDLE, 0) .. (w, HANDLE)` — bottom-right in
14//! screen space.
15//!
16//! # Drag bookkeeping
17//!
18//! A drag captures the mouse position in **parent-relative** coords
19//! (`local + bounds.xy`) rather than widget-local. That keeps the
20//! drag stable even when the parent's layout shifts the widget's
21//! `bounds.y` because the widget's own height just changed — a
22//! common situation inside a `FlexColumn`, where stacking widgets
23//! push later siblings down when earlier ones grow.
24
25use std::cell::Cell;
26use std::rc::Rc;
27
28use crate::cursor::{set_cursor_icon, CursorIcon};
29use crate::draw_ctx::DrawCtx;
30use crate::event::{Event, EventResult, MouseButton};
31use crate::geometry::{Point, Rect, Size};
32use crate::layout_props::{HAnchor, Insets, VAnchor, WidgetBase};
33use crate::widget::Widget;
34
35/// Width (and height) of the bottom-right drag grip, in logical pixels.
36const HANDLE: f64 = 14.0;
37
38pub struct Resize {
39 bounds: Rect,
40 /// Always exactly one child — the wrapped content.
41 children: Vec<Box<dyn Widget>>,
42 base: WidgetBase,
43
44 /// Current size the user has dragged to. `None` before the first
45 /// drag → layout uses `default_size` clamped against available.
46 current_size: Option<Size>,
47 min_size: Size,
48 max_size: Size,
49 default_size: Size,
50
51 /// Optional external cell that mirrors `current_size` for
52 /// persistence / inspection. Written each time the user drags.
53 size_cell: Option<Rc<Cell<Size>>>,
54
55 // ── drag state ────────────────────────────────────────────────
56 dragging: bool,
57 hover_handle: bool,
58 /// Mouse position in APP-LEVEL world coords at drag start. Using
59 /// world (not widget-local or parent-relative) coords is required
60 /// because a nested `Resize` inside an auto-sized `Window` has
61 /// ancestor bounds that shift each frame as layout ripples — so
62 /// widget-local event positions shift even when the user's cursor
63 /// is stationary. World coords are the only invariant reference.
64 drag_start_world: Point,
65 drag_start_size: Size,
66}
67
68impl Resize {
69 /// Wrap `child` in a user-resizable container. Defaults: 200×150
70 /// initial size, 80×40 min, 1000×800 max — override with the
71 /// builder methods below. The defaults are deliberately "sane
72 /// demo-friendly" values; match egui's `Resize::default().show(...)`.
73 pub fn new(child: Box<dyn Widget>) -> Self {
74 Self {
75 bounds: Rect::default(),
76 children: vec![child],
77 base: WidgetBase::new(),
78 current_size: None,
79 min_size: Size::new(80.0, 40.0),
80 // Generous default max — we want `Resize` to be able to
81 // grow up to whatever the surrounding layout allows.
82 // Override via `with_max_size_hint` to impose a tighter
83 // cap; this value is way beyond any realistic screen.
84 max_size: Size::new(8000.0, 6000.0),
85 default_size: Size::new(200.0, 150.0),
86 size_cell: None,
87 dragging: false,
88 hover_handle: false,
89 drag_start_world: Point::ORIGIN,
90 drag_start_size: Size::new(0.0, 0.0),
91 }
92 }
93
94 pub fn with_default_size(mut self, s: Size) -> Self {
95 self.default_size = s;
96 self
97 }
98 pub fn with_min_size_hint(mut self, s: Size) -> Self {
99 self.min_size = s;
100 self
101 }
102 pub fn with_max_size_hint(mut self, s: Size) -> Self {
103 self.max_size = s;
104 self
105 }
106
107 /// Bind the current size to a shared `Cell<Size>`. Reads during
108 /// layout (so callers can programmatically drive size), writes
109 /// during drag (so callers can persist user-chosen geometry).
110 pub fn with_size_cell(mut self, cell: Rc<Cell<Size>>) -> Self {
111 // Seed so the first layout picks up any persisted value.
112 self.current_size = Some(cell.get());
113 self.size_cell = Some(cell);
114 self
115 }
116
117 pub fn with_margin(mut self, m: Insets) -> Self {
118 self.base.margin = m;
119 self
120 }
121 pub fn with_h_anchor(mut self, h: HAnchor) -> Self {
122 self.base.h_anchor = h;
123 self
124 }
125 pub fn with_v_anchor(mut self, v: VAnchor) -> Self {
126 self.base.v_anchor = v;
127 self
128 }
129
130 /// Public accessor for tests and inspector integrations.
131 pub fn current_size(&self) -> Size {
132 self.current_size.unwrap_or(self.default_size)
133 }
134
135 /// Widget-local rect of the SE drag grip (Y-up: bottom-right).
136 fn handle_rect(&self) -> Rect {
137 Rect::new(
138 (self.bounds.width - HANDLE).max(0.0),
139 0.0,
140 HANDLE.min(self.bounds.width),
141 HANDLE.min(self.bounds.height),
142 )
143 }
144
145 fn in_handle(&self, p: Point) -> bool {
146 let h = self.handle_rect();
147 p.x >= h.x && p.x <= h.x + h.width && p.y >= h.y && p.y <= h.y + h.height
148 }
149}
150
151impl Widget for Resize {
152 fn type_name(&self) -> &'static str {
153 "Resize"
154 }
155 fn bounds(&self) -> Rect {
156 self.bounds
157 }
158 fn set_bounds(&mut self, b: Rect) {
159 self.bounds = b;
160 }
161 fn children(&self) -> &[Box<dyn Widget>] {
162 &self.children
163 }
164 fn children_mut(&mut self) -> &mut Vec<Box<dyn Widget>> {
165 &mut self.children
166 }
167
168 fn margin(&self) -> Insets {
169 self.base.margin
170 }
171 fn widget_base(&self) -> Option<&WidgetBase> {
172 Some(&self.base)
173 }
174 fn widget_base_mut(&mut self) -> Option<&mut WidgetBase> {
175 Some(&mut self.base)
176 }
177 fn h_anchor(&self) -> HAnchor {
178 self.base.h_anchor
179 }
180 fn v_anchor(&self) -> VAnchor {
181 self.base.v_anchor
182 }
183 fn min_size(&self) -> Size {
184 self.min_size
185 }
186 fn max_size(&self) -> Size {
187 self.max_size
188 }
189
190 fn layout(&mut self, available: Size) -> Size {
191 let _ = available; // Intentionally ignored — see below.
192 // Pick up the latest cell value each frame so external writes
193 // (e.g. persistence restore) propagate into layout.
194 if let Some(c) = &self.size_cell {
195 self.current_size = Some(c.get());
196 }
197 let target = self.current_size.unwrap_or(self.default_size);
198 // Clamp only to the explicit min_size / max_size hints — NOT
199 // to `available`. A `Resize` widget is the user's "I want
200 // exactly this much space" contract: if the user drags it
201 // bigger than its parent's current slot, the widget still
202 // reports the bigger size so an auto-sized ancestor can grow
203 // to fit on the next layout pass. Matches egui, where the
204 // surrounding Window expands when the inner Resize demands
205 // more width or height.
206 let w_target = target.width.clamp(self.min_size.width, self.max_size.width);
207 let h_target = target
208 .height
209 .clamp(self.min_size.height, self.max_size.height);
210
211 // Content-bound floor: measure the child at the requested
212 // target, and if its natural size is larger (e.g. wrapped
213 // text at a narrower width produces taller content), enforce
214 // content-natural as the minimum. The user can never drag
215 // the Resize smaller than its content fits — matches egui.
216 let natural = if let Some(child) = self.children.first_mut() {
217 child.layout(Size::new(w_target, h_target))
218 } else {
219 Size::new(0.0, 0.0)
220 };
221 let w = w_target.max(natural.width);
222 let h = h_target.max(natural.height);
223 let size = Size::new(w, h);
224 self.current_size = Some(size);
225 self.bounds = Rect::new(0.0, 0.0, w, h);
226
227 if let Some(child) = self.children.first_mut() {
228 // Re-layout if enforcement inflated either axis.
229 if (w - w_target).abs() > 0.5 || (h - h_target).abs() > 0.5 {
230 child.layout(size);
231 }
232 child.set_bounds(Rect::new(0.0, 0.0, w, h));
233 }
234 size
235 }
236
237 fn paint(&mut self, ctx: &mut dyn DrawCtx) {
238 let v = ctx.visuals();
239 let w = self.bounds.width;
240 let h = self.bounds.height;
241
242 // 1-px outline so users see the resizable region even before
243 // they grab the handle. Matches egui's `Resize` subtle frame.
244 ctx.set_stroke_color(v.widget_stroke);
245 ctx.set_line_width(1.0);
246 ctx.begin_path();
247 ctx.rounded_rect(0.5, 0.5, (w - 1.0).max(0.0), (h - 1.0).max(0.0), 3.0);
248 ctx.stroke();
249
250 // SE grip — three stacked diagonals in the bottom-right corner
251 // (Y-up: y = 0 is the bottom edge). Highlight when hovered or
252 // actively dragging so the interaction is obvious.
253 let grip_color = if self.dragging {
254 v.window_resize_active
255 } else if self.hover_handle {
256 v.window_resize_hover
257 } else {
258 v.widget_stroke
259 };
260 ctx.set_stroke_color(grip_color);
261 ctx.set_line_width(1.5);
262 let m = 3.0_f64;
263 for i in 1..=3_i32 {
264 let off = i as f64 * 4.0 + m;
265 ctx.begin_path();
266 ctx.move_to(w - off, m);
267 ctx.line_to(w - m, off);
268 ctx.stroke();
269 }
270 // `h` used only by the stroke above; mark silenced for any
271 // future refactor that comments out the outline.
272 let _ = h;
273 }
274
275 fn on_event(&mut self, event: &Event) -> EventResult {
276 match event {
277 Event::MouseMove { pos } => {
278 if self.dragging {
279 // Use APP-LEVEL world coords. Widget-local and
280 // parent-relative positions both shift between
281 // events here because we're typically nested
282 // inside an auto-sized `Window` whose layout
283 // ripples each frame as our size changes, moving
284 // every ancestor frame in the tree. World coords
285 // come from `App` via a thread-local set by the
286 // same entry point that dispatched this event, so
287 // they're stable against ancestor reshuffling.
288 let world = crate::widget::current_mouse_world().unwrap_or_else(|| {
289 Point::new(pos.x + self.bounds.x, pos.y + self.bounds.y)
290 });
291 let dx = world.x - self.drag_start_world.x;
292 let dy = world.y - self.drag_start_world.y;
293 // SE handle semantics:
294 // cursor right → width grows (+dx)
295 // cursor down → height grows (in Y-up, down = dy<0 → -dy)
296 let new_w = (self.drag_start_size.width + dx)
297 .clamp(self.min_size.width, self.max_size.width);
298 let new_h = (self.drag_start_size.height - dy)
299 .clamp(self.min_size.height, self.max_size.height);
300 let new_sz = Size::new(new_w, new_h);
301 self.current_size = Some(new_sz);
302 if let Some(c) = &self.size_cell {
303 c.set(new_sz);
304 }
305 set_cursor_icon(CursorIcon::ResizeNwSe);
306 crate::animation::request_draw();
307 return EventResult::Consumed;
308 }
309 let was = self.hover_handle;
310 self.hover_handle = self.in_handle(*pos);
311 if self.hover_handle {
312 set_cursor_icon(CursorIcon::ResizeNwSe);
313 }
314 if was != self.hover_handle {
315 crate::animation::request_draw();
316 return EventResult::Consumed;
317 }
318 EventResult::Ignored
319 }
320 Event::MouseDown {
321 pos,
322 button: MouseButton::Left | MouseButton::Middle,
323 ..
324 } if self.in_handle(*pos) => {
325 self.dragging = true;
326 // Snapshot the world cursor pos at drag start. If
327 // unavailable (a unit test dispatching events directly
328 // without going through `App`), fall back to parent-
329 // relative — widget-local drag semantics work when no
330 // ancestor layout ripple is happening.
331 self.drag_start_world = crate::widget::current_mouse_world()
332 .unwrap_or_else(|| Point::new(pos.x + self.bounds.x, pos.y + self.bounds.y));
333 self.drag_start_size = Size::new(self.bounds.width, self.bounds.height);
334 set_cursor_icon(CursorIcon::ResizeNwSe);
335 crate::animation::request_draw();
336 EventResult::Consumed
337 }
338 Event::MouseUp { .. } if self.dragging => {
339 self.dragging = false;
340 crate::animation::request_draw();
341 EventResult::Consumed
342 }
343 _ => EventResult::Ignored,
344 }
345 }
346
347 fn hit_test(&self, local_pos: Point) -> bool {
348 local_pos.x >= 0.0
349 && local_pos.x <= self.bounds.width
350 && local_pos.y >= 0.0
351 && local_pos.y <= self.bounds.height
352 }
353
354 fn properties(&self) -> Vec<(&'static str, String)> {
355 let s = self.current_size();
356 vec![
357 ("current_w", format!("{:.1}", s.width)),
358 ("current_h", format!("{:.1}", s.height)),
359 ("min_w", format!("{:.1}", self.min_size.width)),
360 ("max_w", format!("{:.1}", self.max_size.width)),
361 ("dragging", self.dragging.to_string()),
362 ]
363 }
364}