agg_gui/widgets/window/builder.rs
1//! Builder methods for [`super::Window`] — extracted into a submodule so
2//! `window.rs` stays under the 800-line guardrail. These are all plain
3//! setter / configuration calls that mutate `self` and return it; they
4//! live in a sibling `impl Window` block that has full access to private
5//! fields by virtue of being inside the parent `window` module.
6
7use std::cell::{Cell, RefCell};
8use std::rc::Rc;
9
10use crate::geometry::{Rect, Size};
11use crate::layout_props::{HAnchor, Insets, VAnchor};
12
13use super::{ClickAwayAction, CloseReason, Window, VISIBILITY_FADE_SECS};
14
15impl Window {
16 /// Treat the window's content as live: invalidate the backbuffer
17 /// every frame so custom paint code reading from a mutating data
18 /// source (network feed, simulation state, sensor stream) is
19 /// rasterised fresh. Automatically skipped when the window is
20 /// collapsed or hidden — no wasted work when the user can't see
21 /// the content.
22 ///
23 /// Use this for diagnostics graphs, telemetry views, or any widget
24 /// whose `paint()` reads from an `Rc<RefCell<…>>` model that the
25 /// framework can't observe. The alternative — composing live UI
26 /// from widgets that auto-invalidate on setter calls
27 /// ([`Label::set_text`](crate::widgets::Label), etc.) — is preferred
28 /// when feasible, but for custom direct-to-`DrawCtx` widgets this
29 /// is the simplest correct fix.
30 ///
31 /// See [`Window::new`] for the full discussion of the back-buffer
32 /// invalidation contract and the canonical "stale pixels" gotcha
33 /// this flag solves.
34 pub fn with_live_content(mut self, live: bool) -> Self {
35 self.live_content = live;
36 self
37 }
38
39 /// Register a callback fired whenever this window requests a raise
40 /// (click-to-front or visibility rising-edge from the sidebar).
41 /// Receives the window title. The demo uses this to feed a shared
42 /// z-order tracker that gets persisted to disk.
43 pub fn on_raised(mut self, cb: impl FnMut(&str) + 'static) -> Self {
44 self.on_raised = Some(Box::new(cb));
45 self
46 }
47
48 pub fn with_bounds(mut self, b: Rect) -> Self {
49 self.pre_collapse_h = b.height;
50 self.bounds = b;
51 if self.maximized {
52 self.pre_maximize_bounds = b;
53 }
54 self
55 }
56 pub fn with_font_size(mut self, size: f64) -> Self {
57 self.font_size = size;
58 self
59 }
60
61 pub fn with_visible_cell(mut self, cell: Rc<Cell<bool>>) -> Self {
62 let visible = cell.get();
63 self.last_visible.set(visible);
64 self.fade_out_active.set(false);
65 self.visibility_anim =
66 crate::animation::Tween::new(if visible { 1.0 } else { 0.0 }, VISIBILITY_FADE_SECS);
67 self.visible_cell = Some(cell);
68 self
69 }
70
71 pub fn with_reset_cell(mut self, cell: Rc<Cell<Option<Rect>>>) -> Self {
72 self.reset_to = Some(cell);
73 self
74 }
75
76 pub fn with_position_cell(mut self, cell: Rc<Cell<Rect>>) -> Self {
77 self.position_cell = Some(cell);
78 self
79 }
80
81 /// Wire the window's canvas-maximized state into external persistence.
82 ///
83 /// Call after [`with_bounds`] when restoring saved state so the current
84 /// bounds become the pre-maximize bounds used by the first layout pass.
85 pub fn with_maximized_cell(mut self, cell: Rc<Cell<bool>>) -> Self {
86 self.maximized = cell.get();
87 if self.maximized {
88 self.pre_maximize_bounds = self.bounds;
89 }
90 self.maximized_cell = Some(cell);
91 self
92 }
93
94 pub fn with_margin(mut self, m: Insets) -> Self {
95 self.base.margin = m;
96 self
97 }
98 pub fn with_h_anchor(mut self, h: HAnchor) -> Self {
99 self.base.h_anchor = h;
100 self
101 }
102 pub fn with_v_anchor(mut self, v: VAnchor) -> Self {
103 self.base.v_anchor = v;
104 self
105 }
106 pub fn with_min_size(mut self, s: Size) -> Self {
107 self.base.min_size = s;
108 self
109 }
110 pub fn with_max_size(mut self, s: Size) -> Self {
111 self.base.max_size = s;
112 self
113 }
114
115 pub fn with_constrain(mut self, constrain: bool) -> Self {
116 self.constrain = constrain;
117 self
118 }
119
120 /// Make this window an app-modal layer while it is visible. The `App`
121 /// routes every pointer and keyboard event into the window's subtree,
122 /// so a floating dialog swallows all input over its bounds instead of
123 /// leaking clicks to widgets underneath. Off by default — only floating
124 /// dialogs (e.g. [`color_wheel_picker_dialog`](crate::color_wheel_picker_dialog))
125 /// opt in; ordinary windows keep normal per-window hit-testing.
126 pub fn with_modal(mut self, modal: bool) -> Self {
127 self.modal = modal;
128 self
129 }
130
131 /// Toggle the window frame (chrome). `true` (default) keeps the full
132 /// title bar + close/maximize buttons + drop shadow + border. `false`
133 /// makes the window *frameless*: no bar, no buttons, no shadow, no border,
134 /// and the content child fills the entire bounds.
135 ///
136 /// Frameless windows keep every interaction hook — modal event routing,
137 /// focus grab, [`with_click_away`](Self::with_click_away), Escape, and
138 /// [`on_close`](Self::on_close) — so a chrome-less [`TextField`](crate::widgets::TextField)
139 /// can be dropped exactly over a value pill and behave as an in-place
140 /// inline editor (Enter commits, click-away / Escape route through the
141 /// same close path as a framed dialog).
142 pub fn with_chrome(mut self, chrome: bool) -> Self {
143 self.chrome = chrome;
144 self
145 }
146
147 /// Opt this window in/out of the generic retained GL-FBO backbuffer.
148 /// Disabling renders directly into the inherited parent target.
149 pub fn with_gl_backbuffer(mut self, enabled: bool) -> Self {
150 self.use_gl_backbuffer = enabled;
151 self.backbuffer.invalidate();
152 self
153 }
154
155 /// Make the window size itself to the content's preferred size every frame.
156 /// Top-left pin: as content grows/shrinks, the title bar stays where it is.
157 pub fn with_auto_size(mut self, auto: bool) -> Self {
158 self.auto_size = auto;
159 self
160 }
161
162 /// Toggle user-dragged resize. `false` hides every edge/corner handle
163 /// and disables resize hit-tests. Default: `true`. Matches egui's
164 /// `Window::resizable(bool)`.
165 pub fn with_resizable(mut self, on: bool) -> Self {
166 self.resizable = on;
167 self
168 }
169
170 /// Drive `resizable` from a shared cell read every `layout()`. Lets a
171 /// demo's checkbox steer the real host window at runtime. Follows the
172 /// [`with_maximized_cell`](Self::with_maximized_cell) precedent.
173 pub fn with_resizable_cell(mut self, cell: Rc<Cell<bool>>) -> Self {
174 self.resizable = cell.get();
175 self.resizable_cell = Some(cell);
176 self
177 }
178
179 /// Drive `auto_size` from a shared cell read every `layout()`.
180 pub fn with_auto_size_cell(mut self, cell: Rc<Cell<bool>>) -> Self {
181 self.auto_size = cell.get();
182 self.auto_size_cell = Some(cell);
183 self
184 }
185
186 /// Drive `collapsible` (whether the collapse chevron is offered) from a
187 /// shared cell read every `layout()`. Matches egui `Window::collapsible`.
188 pub fn with_collapsible_cell(mut self, cell: Rc<Cell<bool>>) -> Self {
189 self.collapsible = cell.get();
190 self.collapsible_cell = Some(cell);
191 self
192 }
193
194 /// Drive the displayed title text from a shared cell read every
195 /// `layout()`. Only the title-bar *label* changes — the window's
196 /// identity string (`title()`, used as the persistence / z-order key)
197 /// is intentionally left fixed so live retitling can't corrupt saved
198 /// state. Mirrors egui's demo, which sets a fixed `Id` precisely
199 /// because it changes the visible title.
200 pub fn with_title_cell(mut self, cell: Rc<RefCell<String>>) -> Self {
201 self.title_cell = Some(cell);
202 self
203 }
204
205 /// Fine-grained axis-locking of the resize handles — pass `(true, false)`
206 /// for a horizontally-only resizable window, etc. Implies
207 /// `with_resizable(true)`. Matches egui's `Window::resizable([h, v])`.
208 pub fn with_resizable_axes(mut self, h: bool, v: bool) -> Self {
209 self.resizable = h || v;
210 self.resizable_h = h;
211 self.resizable_v = v;
212 self
213 }
214
215 /// Lock the window's height to its content's required height.
216 /// The user can grab a vertical resize handle but the next
217 /// layout snaps back — egui's W4 "no scroll, no clip, no
218 /// whitespace" contract. Requires the content tree to expose
219 /// its required height via [`Widget::measure_min_height`]; our
220 /// `FlexColumn`, `Label`, `TextArea`, and `Container::with_fit_height`
221 /// all do.
222 pub fn with_tight_content_fit(mut self, on: bool) -> Self {
223 self.tight_content_fit = on;
224 self
225 }
226
227 /// Floor-only variant of [`with_tight_content_fit`]: refuses to
228 /// shrink past content but allows the user to pull the window
229 /// taller (whitespace below). Used for windows whose content
230 /// includes a flex-fill child like a multiline `TextArea` —
231 /// matches egui's W5 where the TextEdit fills extra height and
232 /// the user can grow the window further.
233 pub fn with_height_floor_to_content(mut self, on: bool) -> Self {
234 self.floor_content_height = on;
235 self
236 }
237
238 /// Wrap the window's content in a built-in vertical [`ScrollView`].
239 /// Matches egui's `Window::vscroll(true)`: lets the user shrink the
240 /// window below content height without the caller having to wrap the
241 /// content in a `ScrollView` manually. Eager — happens at builder
242 /// time so the rest of the layout / event / paint paths see a single
243 /// child as usual. Has no effect when called with `false` (matches
244 /// the default).
245 ///
246 /// Don't combine with [`with_auto_size`]: the ScrollView claims its
247 /// full available height, which would make auto-sizing grow the
248 /// window to the canvas. egui's demo never combines the two flags
249 /// either.
250 pub fn with_vscroll(mut self, vscroll: bool) -> Self {
251 if vscroll {
252 if let Some(content) = self.children.pop() {
253 let scroll = crate::widgets::ScrollView::new(content)
254 .vertical(true)
255 .horizontal(false);
256 self.children.push(Box::new(scroll));
257 }
258 }
259 self
260 }
261
262 /// Register a callback fired whenever the window closes, receiving the
263 /// [`CloseReason`] (× button, Escape, or click-away) so the host can react
264 /// per route — the colour dialog commits a live change on click-away but
265 /// cancels it on Escape / ×.
266 pub fn on_close(mut self, cb: impl FnMut(CloseReason) + 'static) -> Self {
267 self.on_close = Some(Box::new(cb));
268 self
269 }
270
271 /// Set what a modal window does when the pointer is pressed *outside* its
272 /// bounds. Default [`ClickAwayAction::None`] swallows the press and keeps the
273 /// window open (unchanged behaviour for ordinary modal windows);
274 /// [`ClickAwayAction::Close`] dismisses via [`Window::close`] with
275 /// [`CloseReason::ClickAway`] and swallows the press. Only takes effect when
276 /// the window is also [`with_modal(true)`](Self::with_modal).
277 pub fn with_click_away(mut self, action: ClickAwayAction) -> Self {
278 self.click_away = action;
279 self
280 }
281}