Skip to main content

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}