Skip to main content

agg_gui/widgets/window/
widget_impl.rs

1use super::*;
2
3impl Widget for Window {
4    fn type_name(&self) -> &'static str {
5        "Window"
6    }
7    /// External identity for z-order persistence, inspector lookup, etc.
8    fn id(&self) -> Option<&str> {
9        Some(&self.title)
10    }
11
12    fn is_visible(&self) -> bool {
13        self.requested_visible() || self.fade_out_active.get()
14    }
15
16    /// Collapsed or closed windows should not keep the host loop awake.
17    fn needs_draw(&self) -> bool {
18        if !self.is_visible() || self.collapsed {
19            return false;
20        }
21        self.children().iter().any(|c| c.needs_draw())
22    }
23
24    fn next_draw_deadline(&self) -> Option<web_time::Instant> {
25        if !self.is_visible() || self.collapsed {
26            return None;
27        }
28        let mut best: Option<web_time::Instant> = None;
29        for c in self.children() {
30            if let Some(t) = c.next_draw_deadline() {
31                best = Some(match best {
32                    Some(b) if b <= t => b,
33                    _ => t,
34                });
35            }
36        }
37        best
38    }
39
40    fn bounds(&self) -> Rect {
41        self.bounds
42    }
43
44    fn margin(&self) -> Insets {
45        self.base.margin
46    }
47    fn widget_base(&self) -> Option<&WidgetBase> {
48        Some(&self.base)
49    }
50    fn widget_base_mut(&mut self) -> Option<&mut WidgetBase> {
51        Some(&mut self.base)
52    }
53    fn h_anchor(&self) -> HAnchor {
54        self.base.h_anchor
55    }
56    fn v_anchor(&self) -> VAnchor {
57        self.base.v_anchor
58    }
59    fn min_size(&self) -> Size {
60        self.base.min_size
61    }
62    fn max_size(&self) -> Size {
63        self.base.max_size
64    }
65
66    fn properties(&self) -> Vec<(&'static str, String)> {
67        vec![
68            (
69                "backbuffer_kind",
70                if self.use_gl_backbuffer {
71                    "GlFbo".to_string()
72                } else {
73                    "None".to_string()
74                },
75            ),
76            ("backbuffer_dirty", self.backbuffer.dirty.to_string()),
77            (
78                "backbuffer_repaints",
79                self.backbuffer.repaint_count.to_string(),
80            ),
81            (
82                "backbuffer_composites",
83                self.backbuffer.composite_count.to_string(),
84            ),
85            (
86                "backbuffer_size",
87                format!("{}x{}", self.backbuffer.width, self.backbuffer.height),
88            ),
89        ]
90    }
91
92    /// Pop this window to the top of the parent `Stack` when the
93    /// false→true visibility edge fires (see `layout`).
94    fn take_raise_request(&mut self) -> bool {
95        let pending = self.raise_request.get();
96        self.raise_request.set(false);
97        pending
98    }
99
100    fn set_bounds(&mut self, b: Rect) {
101        if let Some(ref cell) = self.reset_to {
102            if let Some(new_b) = cell.get() {
103                self.bounds = new_b;
104                self.pre_collapse_h = new_b.height;
105                self.collapsed = false;
106                cell.set(None);
107                return;
108            }
109        }
110        if self.bounds.width == 0.0 || self.bounds.height == 0.0 {
111            self.bounds = b;
112            self.pre_collapse_h = b.height;
113        }
114    }
115
116    fn children(&self) -> &[Box<dyn Widget>] {
117        &self.children
118    }
119    fn children_mut(&mut self) -> &mut Vec<Box<dyn Widget>> {
120        &mut self.children
121    }
122
123    fn backbuffer_spec(&mut self) -> BackbufferSpec {
124        if !self.use_gl_backbuffer {
125            return BackbufferSpec::none();
126        }
127        if !self.is_visible() {
128            let alpha = self.visibility_anim.value();
129            if self.requested_visible() || alpha <= 0.001 {
130                return BackbufferSpec::none();
131            }
132        }
133
134        // Live-content windows self-invalidate every frame, except when
135        // collapsed or hidden — no wasted work behind a folded title bar.
136        if self.live_content && !self.collapsed && self.requested_visible() {
137            self.backbuffer.invalidate();
138        }
139
140        let requested_visible = self.requested_visible();
141        self.visibility_anim
142            .set_target(if requested_visible { 1.0 } else { 0.0 });
143        let alpha = self.visibility_anim.tick();
144        if !requested_visible && alpha > 0.001 {
145            self.fade_out_active.set(true);
146        }
147        if !requested_visible && alpha <= 0.001 {
148            self.fade_out_active.set(false);
149        }
150
151        // Frameless windows draw no drop shadow, so they need no outset halo
152        // around the FBO — a shadow-sized border would just waste texture and
153        // offset the field from the pill it must sit exactly over.
154        let (outset_left, outset_bottom, outset_right, outset_top) = if self.chrome {
155            Self::layer_outsets()
156        } else {
157            (0.0, 0.0, 0.0, 0.0)
158        };
159        let rounded_clip = if self.chrome {
160            Some(CORNER_R)
161        } else {
162            Some(3.0)
163        };
164        BackbufferSpec {
165            kind: BackbufferKind::GlFbo,
166            cached: true,
167            alpha,
168            outsets: Insets {
169                left: outset_left,
170                right: outset_right,
171                top: outset_top,
172                bottom: outset_bottom,
173            },
174            rounded_clip,
175        }
176    }
177
178    fn backbuffer_state_mut(&mut self) -> Option<&mut BackbufferState> {
179        Some(&mut self.backbuffer)
180    }
181
182    /// Clip child painting to the content area (below the title bar).
183    /// When collapsed bounds.height == TITLE_H so the content rect has zero height,
184    /// preventing any child from drawing outside the visible title-bar strip.
185    fn clip_children_rect(&self) -> Option<(f64, f64, f64, f64)> {
186        if !self.is_visible() {
187            return None;
188        }
189        let w = self.bounds.width;
190        let content_h = (self.bounds.height - self.chrome_h()).max(0.0);
191        // Clip to content area: y=0 (bottom) up to content_h, full width.
192        Some((0.0, 0.0, w, content_h))
193    }
194
195    fn hit_test(&self, local_pos: Point) -> bool {
196        if !self.requested_visible() {
197            return false;
198        }
199        if self.drag_mode != DragMode::None {
200            return true;
201        }
202        let b = self.bounds();
203        local_pos.x >= 0.0
204            && local_pos.x <= b.width
205            && local_pos.y >= 0.0
206            && local_pos.y <= b.height
207    }
208
209    fn claims_pointer_exclusively(&self, local_pos: Point) -> bool {
210        self.requested_visible()
211            && (self.drag_mode != DragMode::None || self.resize_dir(local_pos).is_some())
212    }
213
214    /// Opt-in modal windows (e.g. the floating colour-wheel picker dialog)
215    /// grab all pointer/keyboard routing while visible.  The `App` walks to
216    /// this subtree before normal hit-testing, so clicks over the window —
217    /// including the chrome close button and any point that would otherwise
218    /// fall through an ancestor whose bounds don't cover the window (the
219    /// `Rebuilder`/`Stack` overlay slot) — are delivered here and never leak
220    /// to widgets painted beneath.  Gated on `requested_visible()` so a closing
221    /// dialog releases the modal grab in the same frame it hides, letting the
222    /// captured press/release pair complete against this window.
223    fn has_active_modal(&self) -> bool {
224        self.modal && self.requested_visible()
225    }
226
227    fn layout(&mut self, available: Size) -> Size {
228        // Pull runtime-driven flag cells (demo checkboxes → live window
229        // behaviour). Read before anything else so this frame's collapse /
230        // resize / auto-size / title decisions all see the current values.
231        if let Some(ref cell) = self.resizable_cell {
232            self.resizable = cell.get();
233        }
234        if let Some(ref cell) = self.auto_size_cell {
235            self.auto_size = cell.get();
236        }
237        if let Some(ref cell) = self.collapsible_cell {
238            self.collapsible = cell.get();
239        }
240        self.title_bar.set_collapsible(self.collapsible);
241        if let Some(ref cell) = self.title_cell {
242            let wanted = cell.borrow();
243            if *wanted != *self.last_applied_title.borrow() {
244                self.title_bar.set_title(&wanted);
245                *self.last_applied_title.borrow_mut() = wanted.clone();
246            }
247        }
248
249        // Drain the title-bar chevron's click flag — the chevron is a
250        // real child widget that flips this `Rc<Cell<bool>>` when the
251        // framework dispatches its MouseDown.  Acting on the flag here
252        // (rather than in our own `on_event`) lets the child consume
253        // the event normally instead of forcing the parent to manual
254        // hit-test the chevron's coordinates.  Gate on `collapsible` so a
255        // stale click can't fold a window whose affordance was just
256        // disabled.
257        if self.title_bar.take_chevron_click() && self.collapsible {
258            self.toggle_collapse();
259            self.last_title_click = None;
260            crate::animation::request_draw();
261        }
262        // Rising-edge visibility detection requests a parent raise.
263        let now_visible = self.requested_visible();
264        // First-layout fit (visibility-cell-managed windows only):
265        // a window restored as already-visible via `visible_cell` misses
266        // the rising-edge branch below (last_visible was seeded to match
267        // the cell), so without this its persisted bounds can land
268        // outside the live viewport — the user sees the sidebar pill
269        // highlighted but no window.  Gating on `visible_cell.is_some()`
270        // keeps the auto-save invariant for plain `with_bounds(...)`
271        // windows whose layout must never mutate persisted state.
272        if now_visible && self.needs_initial_fit.get() && self.visible_cell.is_some() {
273            self.fit_fully_to_canvas(available);
274        }
275        self.needs_initial_fit.set(false);
276        if now_visible && !self.last_visible.get() {
277            self.raise_request.set(true);
278            if let Some(cb) = self.on_raised.as_mut() {
279                cb(&self.title);
280            }
281            // Un-maximize on reopen.  Clicking a sidebar checkbox is "open
282            // this window for use" — the user expects the window to come
283            // up at its normal size, not still stretched to fill the canvas
284            // from the last session's maximise.  Restore `pre_maximize_bounds`
285            // which `toggle_maximize` saved when the user maximised.
286            if self.maximized {
287                self.bounds = self.pre_maximize_bounds;
288                self.maximized = false;
289            }
290            self.fit_fully_to_canvas(available);
291        }
292        if now_visible {
293            self.fade_out_active.set(false);
294            self.visibility_anim.set_target(1.0);
295        } else {
296            self.visibility_anim.set_target(0.0);
297            if self.visibility_anim.tick() <= 0.001 {
298                self.fade_out_active.set(false);
299            }
300        }
301        self.last_visible.set(now_visible);
302
303        if !self.is_visible() {
304            return Size::new(self.bounds.width, self.bounds.height);
305        }
306
307        if self.maximized && available.width > 0.0 && available.height > 0.0 {
308            self.bounds = snap(Rect::new(0.0, 0.0, available.width, available.height));
309            self.pre_collapse_h = self.bounds.height;
310        }
311
312        // Auto-size: measure the child's preferred size, then adopt it as the
313        // new window size (pinning the top edge — Y-up → adjust `bounds.y` so
314        // the title bar stays put when the height changes).  Skip while
315        // collapsed: the user toggled a fixed TITLE_H height.
316        //
317        // We cap the measurement request by `child.max_size()` when finite
318        // (otherwise by the canvas size): flex containers return their given
319        // `available.width` rather than an intrinsic natural width, so without
320        // a cap we'd produce an infinite/canvas-wide window.  Callers wanting
321        // a content-fitted window set `with_max_size(Size::new(w, f64::MAX))`
322        // on their root widget.
323        if self.auto_size && !self.collapsed && !self.maximized {
324            if let Some(child) = self.children.first_mut() {
325                let max_sz = child.max_size();
326                // `Size::MAX` uses `f64::MAX / 2.0` as its sentinel so
327                // widgets can add-without-overflow (see `geometry.rs`).
328                // That value is *technically* finite, so a plain
329                // `.is_finite()` check wrongly treats it as a real cap
330                // and cascades an ~`f64::MAX/2` width down to wrapped
331                // Labels, whose bounds then blow up LCD-backbuffer
332                // allocators to hundreds of GB.  Guard with a sane
333                // threshold: anything ≥ `CAP_SENTINEL` means "no cap,
334                // fall back to viewport-provided bounds".
335                const CAP_SENTINEL: f64 = 1.0e18;
336                // WIDTH is PINNED to the current bounds.width (seeded
337                // by `with_bounds` and preserved across frames).
338                // Why: wrapping Labels inside the content claim their
339                // full available width — if we pass the viewport
340                // width here, the window grows to the canvas on the
341                // first frame and never shrinks back.  egui's
342                // equivalent is `default_width`, which also pins.
343                let cap_w = self.bounds.width.max(MIN_W);
344                let cap_h = if max_sz.height.is_finite() && max_sz.height < CAP_SENTINEL {
345                    max_sz.height
346                } else {
347                    available.height.max(MIN_H)
348                };
349                let pref = child.layout(Size::new(cap_w, cap_h));
350                // Auto-size follows content in BOTH directions — so
351                // the window can also shrink back down when the
352                // inner Resize (or any other sizing widget) narrows.
353                // Lower bound: `MIN_W`.  Upper bound: the parent-
354                // provided `available.width` (main_area / canvas).
355                // Matches egui where auto_sized tracks content size
356                // symmetrically.
357                let new_w = pref.width.max(MIN_W).min(available.width.max(MIN_W));
358                let new_h = (pref.height + TITLE_H).min(cap_h + TITLE_H).max(MIN_H);
359                let top = self.bounds.y + self.bounds.height;
360                self.bounds.width = new_w;
361                self.bounds.height = new_h;
362                self.bounds.y = top - new_h;
363                self.pre_collapse_h = new_h;
364            }
365        }
366
367        // ── Tight-fit pre-pass ───────────────────────────────────
368        //
369        // When `with_tight_content_fit(true)` is set (and we're not
370        // already in the auto_size block above, which handles both
371        // axes), ask the content tree what minimum height it needs
372        // at our current width and SNAP `bounds.height` to that.
373        //
374        // Uses `Widget::measure_min_height` rather than `layout` so
375        // the result is independent of flex distribution — a
376        // flex-fill widget like `TextArea` reports its true wrapped-
377        // content height through `measure_min_height` even though
378        // its `layout` returns the full slot.  This is what makes
379        // egui's "no scroll, no clip, no whitespace" contract work
380        // for windows whose content includes a flex-fill child.
381        if self.tight_content_fit && !self.auto_size && !self.collapsed && !self.maximized {
382            if let Some(child) = self.children.first() {
383                let needed = child.measure_min_height(self.bounds.width);
384                let new_h = (needed + TITLE_H).max(MIN_H);
385                let top = self.bounds.y + self.bounds.height;
386                self.bounds.height = new_h;
387                self.bounds.y = top - new_h;
388                self.last_content_natural_h.set(needed);
389            }
390        }
391
392        // When collapsed, bounds.height == TITLE_H (set during toggle).
393        // Frameless windows reserve no title strip, so content fills bounds.
394        let content_h = (self.bounds.height - self.chrome_h()).max(0.0);
395
396        if let Some(child) = self.children.first_mut() {
397            if !self.collapsed {
398                let desired = child.layout(Size::new(self.bounds.width, content_h));
399                let child_h = if child.v_anchor().is_stretch() {
400                    content_h
401                } else {
402                    desired.height.clamp(
403                        child.min_size().height,
404                        child.max_size().height.min(content_h),
405                    )
406                };
407                let child_y = if child.v_anchor().contains(VAnchor::BOTTOM) {
408                    0.0
409                } else if child.v_anchor().contains(VAnchor::CENTER) {
410                    ((content_h - child_h) * 0.5).max(0.0)
411                } else {
412                    (content_h - child_h).max(0.0)
413                };
414                if (child_h - content_h).abs() > f64::EPSILON {
415                    child.layout(Size::new(self.bounds.width, child_h));
416                }
417                child.set_bounds(Rect::new(0.0, child_y, self.bounds.width, child_h));
418            }
419            // When collapsed the child keeps its last bounds but is not visible
420            // because hit_test returns false for the content area.
421        }
422
423        // Cache the child's required height via `measure_min_height`
424        // so `apply_resize` and the tight-fit floor see a current
425        // value EVEN when the content's `layout` returns the slot
426        // size (the flex-fill case).  `Widget::measure_min_height`
427        // walks the content tree and returns the actual content
428        // requirement at the supplied width.
429        if (self.tight_content_fit || self.floor_content_height) && !self.collapsed {
430            if let Some(child) = self.children.first() {
431                self.last_content_natural_h
432                    .set(child.measure_min_height(self.bounds.width));
433            }
434        }
435
436        // Position the title-bar strip at the top of the window and
437        // give it a layout pass so the title label knows its size.
438        let tb_y = self.bounds.height - TITLE_H;
439        self.title_bar
440            .set_bounds(Rect::new(0.0, tb_y, self.bounds.width, TITLE_H));
441        self.title_bar.layout(Size::new(self.bounds.width, TITLE_H));
442
443        // Record the canvas size — used by drag / resize / collapse clamp
444        // paths that fire on USER ACTION.  We deliberately do NOT clamp
445        // passively at layout time: platforms fire a Resized event with a
446        // transient smaller size during fullscreen/maximize EXIT (Windows
447        // notably), and if we clamped on shrink the auto-save would persist
448        // those transient clamped bounds — the "all windows pushed down to
449        // the same Y on next startup" bug.  Clamping only on user actions
450        // (dragging a window, resize-handle, collapse toggle) keeps saved
451        // state pinned to what the user actually chose.
452        //
453        // If a later OS shrink genuinely leaves a window's title bar out of
454        // reach, the user can drag it back, use "Organize windows" to
455        // retile, or a dedicated "reset positions" command.
456        self.canvas_size = available;
457        if let Some(ref cell) = self.position_cell {
458            // When maximised, persist the UNDERLYING pre-maximise bounds,
459            // not the stretched-to-canvas ones.  The maximized flag itself is
460            // persisted separately so reloads restore the interaction state
461            // without losing the user's last normal-size bounds.
462            let save_bounds = if self.maximized {
463                self.pre_maximize_bounds
464            } else {
465                self.bounds
466            };
467            cell.set(save_bounds);
468        }
469        if let Some(ref cell) = self.maximized_cell {
470            cell.set(self.maximized);
471        }
472
473        // Snap-layout registration — every laid-out window declares
474        // itself as a snap target so peers dragging nearby can pull
475        // toward its edges.  Hidden / maximised windows opt out via
476        // `Snappable::is_snap_target` and are removed from the
477        // thread-local registry so their stale bounds don't yank
478        // anyone around.
479        {
480            use crate::snap::Snappable;
481            if self.is_snap_target() {
482                // Register in canvas-absolute space so peers (whose bounds are
483                // canvas-absolute) snap against our TRUE on-screen edges. A
484                // nested modal dialog's `bounds` are slot-local; lift them by the
485                // cached slot offset (zero for a top-level window). The offset is
486                // captured at modal-paint time and is at worst one frame stale.
487                let (ox, oy) = self.world_offset.get();
488                let abs = Rect::new(
489                    self.bounds.x + ox,
490                    self.bounds.y + oy,
491                    self.bounds.width,
492                    self.bounds.height,
493                );
494                crate::snap::register_target(self.snap_id, abs);
495            } else {
496                crate::snap::unregister_target(self.snap_id);
497            }
498        }
499
500        Size::new(self.bounds.width, self.bounds.height)
501    }
502
503    fn paint(&mut self, ctx: &mut dyn DrawCtx) {
504        super::paint::paint_window(self, ctx);
505    }
506
507    fn paint_overlay(&mut self, ctx: &mut dyn DrawCtx) {
508        super::paint::paint_overlay(self, ctx);
509    }
510
511    fn finish_paint(&mut self, ctx: &mut dyn DrawCtx) {
512        super::paint::finish_paint(self, ctx);
513    }
514
515    /// Modal windows paint via the global-overlay pass (see
516    /// [`paint_global_overlay`](Self::paint_global_overlay)) so an ancestor's
517    /// clip — a scrolled container, the host window's content region — can't
518    /// truncate the dialog. Non-modal windows keep the ordinary inline paint.
519    fn defer_paint_to_overlay(&self) -> bool {
520        self.modal
521    }
522
523    /// Paint a modal window during the clip-free global overlay walk. The walk
524    /// has already translated `ctx` to this window's local origin without
525    /// pushing any ancestor clip, so re-entering the normal paint path here via
526    /// [`paint_subtree_forced`] renders the whole dialog (chrome + content)
527    /// above and outside everything that would otherwise clip it. We first
528    /// clamp the window into the viewport so it can't open partially off-screen.
529    fn paint_global_overlay(&mut self, ctx: &mut dyn DrawCtx) {
530        if !self.modal || !self.is_visible() {
531            return;
532        }
533        let (dx, dy) = super::paint::clamp_modal_into_viewport(self, ctx);
534        ctx.save();
535        if dx != 0.0 || dy != 0.0 {
536            ctx.translate(dx, dy);
537        }
538        crate::widget::paint_subtree_forced(self, ctx);
539        ctx.restore();
540    }
541
542    fn on_event(&mut self, event: &Event) -> EventResult {
543        super::events::on_event(self, event)
544    }
545}