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 h_anchor(&self) -> HAnchor {
172        self.base.h_anchor
173    }
174    fn v_anchor(&self) -> VAnchor {
175        self.base.v_anchor
176    }
177    fn min_size(&self) -> Size {
178        self.min_size
179    }
180    fn max_size(&self) -> Size {
181        self.max_size
182    }
183
184    fn layout(&mut self, available: Size) -> Size {
185        let _ = available; // Intentionally ignored — see below.
186                           // Pick up the latest cell value each frame so external writes
187                           // (e.g. persistence restore) propagate into layout.
188        if let Some(c) = &self.size_cell {
189            self.current_size = Some(c.get());
190        }
191        let target = self.current_size.unwrap_or(self.default_size);
192        // Clamp only to the explicit min_size / max_size hints — NOT
193        // to `available`.  A `Resize` widget is the user's "I want
194        // exactly this much space" contract: if the user drags it
195        // bigger than its parent's current slot, the widget still
196        // reports the bigger size so an auto-sized ancestor can grow
197        // to fit on the next layout pass.  Matches egui, where the
198        // surrounding Window expands when the inner Resize demands
199        // more width or height.
200        let w_target = target.width.clamp(self.min_size.width, self.max_size.width);
201        let h_target = target
202            .height
203            .clamp(self.min_size.height, self.max_size.height);
204
205        // Content-bound floor: measure the child at the requested
206        // target, and if its natural size is larger (e.g. wrapped
207        // text at a narrower width produces taller content), enforce
208        // content-natural as the minimum.  The user can never drag
209        // the Resize smaller than its content fits — matches egui.
210        let natural = if let Some(child) = self.children.first_mut() {
211            child.layout(Size::new(w_target, h_target))
212        } else {
213            Size::new(0.0, 0.0)
214        };
215        let w = w_target.max(natural.width);
216        let h = h_target.max(natural.height);
217        let size = Size::new(w, h);
218        self.current_size = Some(size);
219        self.bounds = Rect::new(0.0, 0.0, w, h);
220
221        if let Some(child) = self.children.first_mut() {
222            // Re-layout if enforcement inflated either axis.
223            if (w - w_target).abs() > 0.5 || (h - h_target).abs() > 0.5 {
224                child.layout(size);
225            }
226            child.set_bounds(Rect::new(0.0, 0.0, w, h));
227        }
228        size
229    }
230
231    fn paint(&mut self, ctx: &mut dyn DrawCtx) {
232        let v = ctx.visuals();
233        let w = self.bounds.width;
234        let h = self.bounds.height;
235
236        // 1-px outline so users see the resizable region even before
237        // they grab the handle.  Matches egui's `Resize` subtle frame.
238        ctx.set_stroke_color(v.widget_stroke);
239        ctx.set_line_width(1.0);
240        ctx.begin_path();
241        ctx.rounded_rect(0.5, 0.5, (w - 1.0).max(0.0), (h - 1.0).max(0.0), 3.0);
242        ctx.stroke();
243
244        // SE grip — three stacked diagonals in the bottom-right corner
245        // (Y-up: y = 0 is the bottom edge).  Highlight when hovered or
246        // actively dragging so the interaction is obvious.
247        let grip_color = if self.dragging {
248            v.window_resize_active
249        } else if self.hover_handle {
250            v.window_resize_hover
251        } else {
252            v.widget_stroke
253        };
254        ctx.set_stroke_color(grip_color);
255        ctx.set_line_width(1.5);
256        let m = 3.0_f64;
257        for i in 1..=3_i32 {
258            let off = i as f64 * 4.0 + m;
259            ctx.begin_path();
260            ctx.move_to(w - off, m);
261            ctx.line_to(w - m, off);
262            ctx.stroke();
263        }
264        // `h` used only by the stroke above; mark silenced for any
265        // future refactor that comments out the outline.
266        let _ = h;
267    }
268
269    fn on_event(&mut self, event: &Event) -> EventResult {
270        match event {
271            Event::MouseMove { pos } => {
272                if self.dragging {
273                    // Use APP-LEVEL world coords.  Widget-local and
274                    // parent-relative positions both shift between
275                    // events here because we're typically nested
276                    // inside an auto-sized `Window` whose layout
277                    // ripples each frame as our size changes, moving
278                    // every ancestor frame in the tree.  World coords
279                    // come from `App` via a thread-local set by the
280                    // same entry point that dispatched this event, so
281                    // they're stable against ancestor reshuffling.
282                    let world = crate::widget::current_mouse_world().unwrap_or_else(|| {
283                        Point::new(pos.x + self.bounds.x, pos.y + self.bounds.y)
284                    });
285                    let dx = world.x - self.drag_start_world.x;
286                    let dy = world.y - self.drag_start_world.y;
287                    // SE handle semantics:
288                    //   cursor right  → width  grows  (+dx)
289                    //   cursor down   → height grows  (in Y-up, down = dy<0 → -dy)
290                    let new_w = (self.drag_start_size.width + dx)
291                        .clamp(self.min_size.width, self.max_size.width);
292                    let new_h = (self.drag_start_size.height - dy)
293                        .clamp(self.min_size.height, self.max_size.height);
294                    let new_sz = Size::new(new_w, new_h);
295                    self.current_size = Some(new_sz);
296                    if let Some(c) = &self.size_cell {
297                        c.set(new_sz);
298                    }
299                    set_cursor_icon(CursorIcon::ResizeNwSe);
300                    crate::animation::request_draw();
301                    return EventResult::Consumed;
302                }
303                let was = self.hover_handle;
304                self.hover_handle = self.in_handle(*pos);
305                if self.hover_handle {
306                    set_cursor_icon(CursorIcon::ResizeNwSe);
307                }
308                if was != self.hover_handle {
309                    crate::animation::request_draw();
310                    return EventResult::Consumed;
311                }
312                EventResult::Ignored
313            }
314            Event::MouseDown {
315                pos,
316                button: MouseButton::Left | MouseButton::Middle,
317                ..
318            } if self.in_handle(*pos) => {
319                self.dragging = true;
320                // Snapshot the world cursor pos at drag start.  If
321                // unavailable (a unit test dispatching events directly
322                // without going through `App`), fall back to parent-
323                // relative — widget-local drag semantics work when no
324                // ancestor layout ripple is happening.
325                self.drag_start_world = crate::widget::current_mouse_world()
326                    .unwrap_or_else(|| Point::new(pos.x + self.bounds.x, pos.y + self.bounds.y));
327                self.drag_start_size = Size::new(self.bounds.width, self.bounds.height);
328                set_cursor_icon(CursorIcon::ResizeNwSe);
329                crate::animation::request_draw();
330                EventResult::Consumed
331            }
332            Event::MouseUp { .. } if self.dragging => {
333                self.dragging = false;
334                crate::animation::request_draw();
335                EventResult::Consumed
336            }
337            _ => EventResult::Ignored,
338        }
339    }
340
341    fn hit_test(&self, local_pos: Point) -> bool {
342        local_pos.x >= 0.0
343            && local_pos.x <= self.bounds.width
344            && local_pos.y >= 0.0
345            && local_pos.y <= self.bounds.height
346    }
347
348    fn properties(&self) -> Vec<(&'static str, String)> {
349        let s = self.current_size();
350        vec![
351            ("current_w", format!("{:.1}", s.width)),
352            ("current_h", format!("{:.1}", s.height)),
353            ("min_w", format!("{:.1}", self.min_size.width)),
354            ("max_w", format!("{:.1}", self.max_size.width)),
355            ("dragging", self.dragging.to_string()),
356        ]
357    }
358}