agg_gui/widgets/window.rs
1//! `Window` — a floating, draggable, resizable panel with a title bar.
2//!
3//! # Usage
4//!
5//! Create a `Window` and place it as the **last** child of a [`Stack`] so it
6//! paints on top of everything and receives hit-test priority.
7//!
8//! ```ignore
9//! let win = Window::new("Inspector", font, Box::new(my_content));
10//! Stack::new()
11//! .add(Box::new(main_ui))
12//! .add(Box::new(win))
13//! ```
14//!
15//! # Features
16//!
17//! - **Drag** — click-drag the title bar to move the window.
18//! - **Resize** — drag any of the 8 edges/corners to resize; min size 120×80.
19//! - **Collapse** — click the chevron on the left of the title bar to collapse
20//! to title-bar-only height (click again to expand).
21//! - **Maximize** — double-click the title bar (or click the maximize button)
22//! to toggle between maximised and restored size.
23//! - **Close** — click the × button; syncs with an optional shared `visible_cell`.
24//!
25//! # ⚠ Backbuffer caching gotcha — read before adding a custom Window
26//!
27//! `Window` retains its painted pixels in a GL FBO (or CPU bitmap) and only
28//! re-rasterises on widget setter mutations (`Label::set_text`, hover changes,
29//! etc.). Custom paint code that reads from an `Rc<RefCell<…>>` model the
30//! framework can't observe — telemetry graphs, sensor streams, simulation
31//! views — will blit stale pixels forever unless you tell the window to
32//! invalidate. Two ways:
33//!
34//! - `.with_live_content(true)` — Window self-invalidates every frame
35//! (auto-skipped when collapsed or hidden). Use for streaming data.
36//! - [`Window::invalidate_backbuffer`] — manual flag from the data-arrival
37//! path. Use when invalidation is sparse and you want frame-skip when
38//! nothing changed.
39//!
40//! See [`Window::new`] for the full discussion.
41//!
42//! # Coordinate notes (Y-up)
43//!
44//! `bounds` stores the window's position in its **parent's** coordinate space.
45//! The title bar is at the **top** of the window, i.e. local Y ∈
46//! `[height − TITLE_H .. height]`. The content area fills local Y ∈ `[0 .. height − TITLE_H]`.
47
48use std::cell::{Cell, RefCell};
49use std::rc::Rc;
50use std::sync::Arc;
51
52use web_time::Instant;
53
54use crate::cursor::{set_cursor_icon, CursorIcon};
55use crate::draw_ctx::DrawCtx;
56use crate::event::{Event, EventResult, Key, MouseButton};
57use crate::geometry::{Point, Rect, Size};
58use crate::layout_props::{HAnchor, Insets, VAnchor, WidgetBase};
59use crate::text::Font;
60use crate::widget::{BackbufferKind, BackbufferSpec, BackbufferState, Widget};
61use crate::widgets::window_title_bar::{TitleBarView, WindowTitleBar};
62
63/// Round all four components of a Rect to the nearest integer so widgets
64/// are always placed on exact pixel boundaries (crisp bitmap blits, no blur).
65fn snap(r: Rect) -> Rect {
66 Rect::new(r.x.round(), r.y.round(), r.width.round(), r.height.round())
67}
68
69const TITLE_H: f64 = 28.0;
70const CORNER_R: f64 = 8.0;
71const SHADOW_BLUR: f64 = 14.0;
72const SHADOW_DX: f64 = 2.0;
73const SHADOW_DY: f64 = 6.0;
74const VISIBILITY_FADE_SECS: f64 = 0.18;
75const CLOSE_R: f64 = 6.0;
76const CLOSE_PAD: f64 = 10.0;
77const MAX_PAD: f64 = CLOSE_PAD + CLOSE_R * 2.0 + 4.0; // 26 px
78const RESIZE_EDGE: f64 = 6.0; // px from the edge that counts as a resize zone
79const MIN_W: f64 = 120.0;
80const MIN_H: f64 = 80.0;
81const DBL_CLICK_MS: u128 = 500; // double-click detection window
82
83/// Which edge(s) are being dragged during a resize operation.
84#[derive(Clone, Copy, Debug, PartialEq)]
85pub(crate) enum ResizeDir {
86 N,
87 NE,
88 E,
89 SE,
90 S,
91 SW,
92 W,
93 NW,
94}
95
96/// Interaction mode for the current drag.
97#[derive(Clone, Copy, Debug, PartialEq)]
98enum DragMode {
99 None,
100 Move,
101 Resize(ResizeDir),
102}
103
104/// A floating panel with a draggable/resizable title bar and a single content child.
105pub struct Window {
106 bounds: Rect,
107 children: Vec<Box<dyn Widget>>, // always exactly 1: the content
108 base: WidgetBase,
109
110 font_size: f64,
111
112 visible: bool,
113 visible_cell: Option<Rc<Cell<bool>>>,
114 visibility_anim: crate::animation::Tween,
115 fade_out_active: Cell<bool>,
116 backbuffer: BackbufferState,
117 use_gl_backbuffer: bool,
118 reset_to: Option<Rc<Cell<Option<Rect>>>>,
119 position_cell: Option<Rc<Cell<Rect>>>,
120 maximized_cell: Option<Rc<Cell<bool>>>,
121 /// Runtime-driven flag cells (egui `Window::resizable/collapsible`,
122 /// plus our `auto_size` addition). Read each `layout()` so a demo's
123 /// checkboxes can steer the real host window. `None` means the flag is
124 /// fixed at its builder value.
125 resizable_cell: Option<Rc<Cell<bool>>>,
126 auto_size_cell: Option<Rc<Cell<bool>>>,
127 collapsible_cell: Option<Rc<Cell<bool>>>,
128 /// Live title text. Applied to the title-bar label only — the window's
129 /// identity (`self.title`, used as the persistence / z-order key) is
130 /// deliberately left untouched so retitling can't corrupt saved state.
131 title_cell: Option<Rc<RefCell<String>>>,
132 /// Last title string pushed into the title bar, so we only re-set (and
133 /// invalidate the label's glyph cache) when the text actually changes.
134 last_applied_title: RefCell<String>,
135
136 /// Snapshot of `is_visible()` from the previous `layout()` call. Used
137 /// to detect the false→true transition (demo toggled on in the
138 /// sidebar) so we can request the parent `Stack` raise us to the top.
139 last_visible: Cell<bool>,
140 /// `true` until the first `layout()` runs. A window restored as
141 /// already-visible (e.g. saved-state inspector open) misses the
142 /// rising-edge fit-to-canvas pass, so without this one-shot trigger
143 /// its persisted bounds can land outside a smaller live viewport
144 /// (mobile portrait, resized window, etc.) and the user sees the
145 /// sidebar toggle highlighted but no window. Cleared after the
146 /// first layout completes.
147 needs_initial_fit: Cell<bool>,
148 /// Set to `true` on a visibility rising edge; read + cleared by
149 /// `take_raise_request` on the next parent-layout pass.
150 raise_request: Cell<bool>,
151
152 collapsed: bool,
153 /// Whether the collapse affordance is offered. Driven at runtime by
154 /// `collapsible_cell` when wired. Matches egui `Window::collapsible`.
155 collapsible: bool,
156 /// Height before collapsing, so we can restore it.
157 pre_collapse_h: f64,
158
159 drag_mode: DragMode,
160 /// Cursor world position when drag started.
161 drag_start_world: Point,
162 /// Window bounds when drag started.
163 drag_start_bounds: Rect,
164
165 close_hovered: bool,
166 on_close: Option<Box<dyn FnMut(CloseReason)>>,
167
168 /// What an outside pointer press does to this (modal) window. `None` swallows
169 /// it and stays open; `Close` dismisses via [`Window::close`] with
170 /// [`CloseReason::ClickAway`]. Only consulted while `modal` is set.
171 click_away: ClickAwayAction,
172
173 /// Whether the window is currently maximized (fills the full canvas).
174 maximized: bool,
175 /// Bounds saved before maximizing so we can restore them.
176 pre_maximize_bounds: Rect,
177 maximize_hovered: bool,
178
179 /// Which resize edge/corner the cursor is currently hovering over.
180 /// Cleared to None when the cursor moves into the interior.
181 hover_dir: Option<ResizeDir>,
182
183 /// Time of last left-click in the title bar — for double-click collapse.
184 last_title_click: Option<Instant>,
185
186 /// Title-bar sub-widget — owns the bar fill, separator, chevron,
187 /// title label, maximize/close buttons. Painted manually from
188 /// `paint()` so `clip_children_rect` can keep content clipped to the
189 /// body area. Display state is written into `title_state` every
190 /// layout pass; the sub-widget reads it at paint time.
191 title_bar: WindowTitleBar,
192 title_state: Rc<RefCell<TitleBarView>>,
193
194 /// Canvas size supplied by the last `layout()` call; used for clamping.
195 canvas_size: Size,
196 /// When true, the window is kept fully inside the canvas bounds during drag/resize.
197 constrain: bool,
198
199 /// When true, the window declares itself an app-modal layer while visible
200 /// (see [`Widget::has_active_modal`]). The `App` then routes *all* pointer
201 /// and keyboard events into this window's subtree, so a floating dialog
202 /// (e.g. the colour-wheel picker) swallows every click over its bounds
203 /// instead of leaking them to widgets painted underneath. Opt-in — plain
204 /// windows leave this `false` and hit-test normally.
205 modal: bool,
206
207 /// When true, the window bounds adopt the content's preferred size each
208 /// layout pass (width + height). Keeps the title-bar top edge pinned so
209 /// the window appears to grow/shrink downward. User resize is disabled
210 /// while auto-size is active (dragging still works).
211 auto_size: bool,
212
213 /// Whether the user can resize the window by dragging its edges. When
214 /// `false`, no resize handles are active regardless of `resizable_h` /
215 /// `resizable_v` — matches egui's `.resizable(false)`. Defaults to
216 /// `true` to preserve existing behaviour for call sites that don't
217 /// explicitly opt out.
218 resizable: bool,
219 /// Fine-grained axis control, used when `resizable` is `true`.
220 resizable_h: bool,
221 resizable_v: bool,
222 /// Content-bound resize floor + ceiling. When `true`, the
223 /// window's height is locked to its content's required height
224 /// each layout (snap pre-pass) AND `apply_resize` refuses to
225 /// drag it smaller than content. Matches egui's no-scroll-no-
226 /// clip-no-whitespace W4 contract. Off by default.
227 tight_content_fit: bool,
228 /// Floor-only variant of [`tight_content_fit`]. Same minimum-
229 /// height enforcement, but allows the user to grow the window
230 /// past the content (whitespace below). Used by W5 where a
231 /// `TextArea` flex-fills extra space and the user can pull the
232 /// window taller than the wrapped text. Off by default.
233 floor_content_height: bool,
234 /// Most recently observed content required height (via
235 /// `Widget::measure_min_height`). Updated each layout pass so
236 /// `apply_resize` and the tight-fit pre-pass see a current value
237 /// even when the content tree contains a flex-fill widget.
238 last_content_natural_h: Cell<f64>,
239 /// True between `paint()` and `finish_paint()` when GL compositing opened
240 /// a foreground layer for body/title/children. The shadow stays outside.
241 foreground_layer_active: Cell<bool>,
242
243 /// When `true`, the window's backbuffer is invalidated on every
244 /// frame the window is visible-and-expanded, forcing the content
245 /// widget's `paint()` to run fresh. See [`with_live_content`] and
246 /// the constructor doc-comment for when to set this.
247 live_content: bool,
248
249 /// Window title string — stored so external callers (z-order
250 /// persistence, inspector display, etc.) can identify this window
251 /// without going through the inner `title_bar` sub-widget.
252 title: String,
253 /// Optional callback invoked whenever this window requests a raise
254 /// (click-to-front or visibility rising-edge from the sidebar).
255 /// Receives the window title. Used by the demo's z-order tracker
256 /// to record "most recently raised" so the stacking order survives
257 /// a save/restore round-trip.
258 on_raised: Option<Box<dyn FnMut(&str)>>,
259
260 /// Identity for the snap-layout system. Minted once at
261 /// construction from a process-wide counter and never changes —
262 /// `Snappable` uses it to skip self-matches in the snap engine's
263 /// target list.
264 snap_id: crate::snap::SnapId,
265
266 /// Canvas-absolute origin offset of the slot this window nests in —
267 /// i.e. `canvas_absolute_origin − bounds.origin`, in logical Y-up units.
268 /// Zero for a top-level window (bounds already canvas-absolute); non-zero
269 /// for a modal dialog nested in an overlay slot, where `bounds` are
270 /// slot-local.
271 ///
272 /// The snap engine and its registry both work in canvas-absolute space, but
273 /// `apply_move_snap` runs during `on_event` with no `DrawCtx` to derive the
274 /// ancestor offset. So we cache it here at modal-paint time (see
275 /// [`paint::clamp_modal_into_viewport`]) where the accumulated root
276 /// transform is available, and the snap path folds it into both the snap
277 /// candidate and the registered target rect. The cached value is at worst
278 /// one frame stale relative to the drag; the host slot rarely moves while
279 /// the user drags the dialog, so this is acceptable.
280 world_offset: Cell<(f64, f64)>,
281}
282
283impl Window {
284 /// Create a new window with the given title, font, and content widget.
285 ///
286 /// Default position: `(60, 60)` with `size = (360, 280)`. Call
287 /// [`with_bounds`] to override.
288 ///
289 /// Windows keep a retained backbuffer. Live content must either call
290 /// [`Window::invalidate_backbuffer`] when external data changes or use
291 /// [`Window::with_live_content`] to force repaint while visible.
292 pub fn new(title: impl Into<String>, font: Arc<Font>, content: Box<dyn Widget>) -> Self {
293 let font_size = 13.0;
294 let title_str: String = title.into();
295 let title_state = Rc::new(RefCell::new(TitleBarView::default_visuals()));
296 let title_bar = WindowTitleBar::new(&title_str, Arc::clone(&font), Rc::clone(&title_state));
297 Self {
298 bounds: Rect::new(60.0, 60.0, 360.0, 280.0),
299 children: vec![content],
300 base: WidgetBase::new(),
301 font_size,
302 visible: true,
303 visible_cell: None,
304 visibility_anim: crate::animation::Tween::new(1.0, VISIBILITY_FADE_SECS),
305 fade_out_active: Cell::new(false),
306 backbuffer: BackbufferState::new(),
307 use_gl_backbuffer: true,
308 reset_to: None,
309 position_cell: None,
310 maximized_cell: None,
311 resizable_cell: None,
312 auto_size_cell: None,
313 collapsible_cell: None,
314 title_cell: None,
315 last_applied_title: RefCell::new(title_str.clone()),
316 // Seed `last_visible` to `true` (matches `visible` above) so a
317 // window that's open on first frame doesn't spuriously request
318 // a raise before the user has interacted with it.
319 last_visible: Cell::new(true),
320 needs_initial_fit: Cell::new(true),
321 raise_request: Cell::new(false),
322 collapsed: false,
323 collapsible: true,
324 pre_collapse_h: 280.0,
325 drag_mode: DragMode::None,
326 drag_start_world: Point::ORIGIN,
327 drag_start_bounds: Rect::default(),
328 close_hovered: false,
329 on_close: None,
330 click_away: ClickAwayAction::None,
331 maximized: false,
332 pre_maximize_bounds: Rect::new(60.0, 60.0, 360.0, 280.0),
333 maximize_hovered: false,
334 hover_dir: None,
335 last_title_click: None,
336 title_bar,
337 title_state,
338 // Seed as "unknown" so `layout()`'s shrink-detect guard
339 // (`had_prior = prev.w > 0 && prev.h > 0`) correctly skips the
340 // clamp on the very first layout pass. The old default
341 // `(1280, 720)` was treated as prior, so the first-frame
342 // transition from 1280×720 → <smaller> incorrectly looked like
343 // an OS-window shrink and pulled saved Y-up positions down into
344 // the transient canvas. Real-value `canvas_size` is populated
345 // by `layout()` before any drag/resize/collapse hit-test runs.
346 canvas_size: Size::new(0.0, 0.0),
347 constrain: true,
348 modal: false,
349 auto_size: false,
350 resizable: true,
351 resizable_h: true,
352 resizable_v: true,
353 tight_content_fit: false,
354 floor_content_height: false,
355 last_content_natural_h: Cell::new(0.0),
356 foreground_layer_active: Cell::new(false),
357 title: title_str,
358 on_raised: None,
359 live_content: false,
360 snap_id: crate::snap::next_snap_id(),
361 world_offset: Cell::new((0.0, 0.0)),
362 }
363 }
364
365 /// Returns the window title as it was passed to [`Window::new`].
366 pub fn title(&self) -> &str {
367 &self.title
368 }
369
370 /// Force the window's retained backbuffer to re-rasterise on the next
371 /// paint pass. Use this when the content widget reads from a live
372 /// data source (network feed, animation curve, simulation state)
373 /// that the framework can't observe. Otherwise the cached pixels
374 /// blit unchanged and your live data never reaches the screen.
375 ///
376 /// Pair with [`Window::with_live_content`] for streaming data that
377 /// changes every frame: that flag self-invalidates here automatically
378 /// (and skips when collapsed/hidden).
379 ///
380 /// See [`Window::new`] for the full discussion of when this matters
381 /// and the alternative ("compose live UI out of widgets that
382 /// invalidate on data change") that avoids needing to call this at
383 /// all.
384 pub fn invalidate_backbuffer(&mut self) {
385 self.backbuffer.invalidate();
386 }
387
388 fn requested_visible(&self) -> bool {
389 if let Some(ref cell) = self.visible_cell {
390 cell.get()
391 } else {
392 self.visible
393 }
394 }
395
396 fn layer_outsets() -> (f64, f64, f64, f64) {
397 let left = (SHADOW_BLUR - SHADOW_DX).max(0.0).ceil();
398 let bottom = (SHADOW_BLUR + SHADOW_DY).ceil();
399 let right = (SHADOW_BLUR + SHADOW_DX).ceil();
400 let top = (SHADOW_BLUR - SHADOW_DY).max(0.0).ceil();
401 (left, bottom, right, top)
402 }
403
404 fn clamp_to_canvas(&mut self) {
405 if !self.constrain {
406 return;
407 }
408 // A modal window is constrained to the whole app VIEWPORT rather than the
409 // (often tiny) overlay slot it nests in: `clamp_modal_into_viewport`
410 // pulls it fully on-screen during the clip-free global-overlay paint and
411 // folds any correction back into `bounds`, so a drag can travel across
412 // the entire app and the paint clamp keeps it visible. Applying the
413 // slot-based clamp here would re-cage it to the parent slot — the exact
414 // "locked to a dimension less than the app" bug we're fixing.
415 if self.modal {
416 return;
417 }
418 let cw = self.canvas_size.width;
419 let ch = self.canvas_size.height;
420 // **Policy: keep the TITLE BAR grabbable**, not the whole window.
421 // Horizontally we keep at least `MIN_H_VISIBLE` pixels of the title
422 // bar inside the canvas so the user can always drag the window back
423 // on-screen. Vertically (Y-up) we keep the FULL title bar inside
424 // the canvas — the body may extend above/below, but the drag handle
425 // is always fully reachable. This matches how native OS window
426 // managers constrain child windows against their host monitor.
427 const MIN_H_VISIBLE: f64 = 40.0;
428
429 let min_x = MIN_H_VISIBLE - self.bounds.width;
430 let max_x = (cw - MIN_H_VISIBLE).max(min_x);
431 self.bounds.x = self.bounds.x.clamp(min_x, max_x).round();
432
433 // Title bar Y range in parent coords: [bounds.y + h - TITLE_H, bounds.y + h].
434 // Full title bar visible → `bounds.y >= TITLE_H - h` AND `bounds.y <= ch - h`.
435 // `bounds.height` collapses to `TITLE_H` when the user folds the
436 // window, so the collapsed case naturally falls out of the same math.
437 let min_y = TITLE_H - self.bounds.height;
438 let max_y = (ch - self.bounds.height).max(min_y);
439 self.bounds.y = self.bounds.y.clamp(min_y, max_y).round();
440 }
441
442 fn fit_fully_to_canvas(&mut self, available: Size) {
443 if !self.constrain || available.width <= 1.0 || available.height <= 1.0 {
444 return;
445 }
446 let max_w = available.width.max(MIN_W);
447 let max_h = available.height.max(TITLE_H);
448 self.bounds.width = self.bounds.width.clamp(MIN_W.min(max_w), max_w).round();
449 self.bounds.height = self.bounds.height.clamp(TITLE_H, max_h).round();
450 self.bounds.x = self
451 .bounds
452 .x
453 .clamp(0.0, (available.width - self.bounds.width).max(0.0))
454 .round();
455 self.bounds.y = self
456 .bounds
457 .y
458 .clamp(0.0, (available.height - self.bounds.height).max(0.0))
459 .round();
460 self.pre_collapse_h = self.bounds.height;
461 if self.maximized {
462 self.pre_maximize_bounds = self.bounds;
463 }
464 }
465
466 /// Close the window: hide it, sync the optional `visible_cell`, and fire
467 /// `on_close`. Shared by the title-bar × button, the Escape key (modal
468 /// windows), and any programmatic close so every route runs the same
469 /// teardown — critically the `on_close` hook, which a modal dialog uses to
470 /// unwind in-flight state (e.g. cancelling a live colour preview). Before
471 /// this was factored out, only the × button ran it, so an Escape/close of
472 /// the colour dialog left its preview session dangling.
473 ///
474 /// `reason` distinguishes the dismissal route (× button, Escape, click-away)
475 /// so `on_close` can react differently — the colour dialog commits a live
476 /// change on click-away but cancels it on Escape / ×.
477 fn close(&mut self, reason: CloseReason) {
478 self.visible = false;
479 self.visibility_anim.set_target(0.0);
480 if let Some(ref cell) = self.visible_cell {
481 cell.set(false);
482 }
483 if let Some(cb) = self.on_close.as_mut() {
484 cb(reason);
485 }
486 crate::animation::request_draw();
487 }
488
489 pub fn show(&mut self) {
490 self.visible = true;
491 self.fade_out_active.set(false);
492 self.visibility_anim.set_target(1.0);
493 crate::animation::request_draw();
494 }
495 pub fn hide(&mut self) {
496 self.visible = false;
497 self.visibility_anim.set_target(0.0);
498 crate::animation::request_draw();
499 }
500 pub fn toggle(&mut self) {
501 if self.visible {
502 self.hide();
503 } else {
504 self.show();
505 }
506 }
507 /// Current visibility — honours an optional shared `visible_cell` when
508 /// wired (sidebar toggles, programmatic show/hide). The inherent
509 /// `self.visible` field is a fallback for windows that aren't wired to
510 /// a cell. Must match the Widget-trait impl below so rising-edge
511 /// detection in `layout()` observes sidebar toggles.
512 pub fn is_visible(&self) -> bool {
513 self.requested_visible() || self.fade_out_active.get()
514 }
515
516 fn title_bar_bottom(&self) -> f64 {
517 self.bounds.height - TITLE_H
518 }
519
520 fn in_title_bar(&self, local: Point) -> bool {
521 local.y >= self.title_bar_bottom()
522 && local.y <= self.bounds.height
523 && local.x >= 0.0
524 && local.x <= self.bounds.width
525 }
526
527 fn close_center(&self) -> Point {
528 Point::new(
529 self.bounds.width - CLOSE_PAD,
530 self.bounds.height - TITLE_H * 0.5,
531 )
532 }
533
534 fn in_close_button(&self, local: Point) -> bool {
535 let c = self.close_center();
536 let dx = local.x - c.x;
537 let dy = local.y - c.y;
538 dx * dx + dy * dy <= (CLOSE_R + 3.0) * (CLOSE_R + 3.0)
539 }
540
541 fn maximize_center(&self) -> Point {
542 Point::new(
543 self.bounds.width - MAX_PAD,
544 self.bounds.height - TITLE_H * 0.5,
545 )
546 }
547
548 fn in_maximize_button(&self, local: Point) -> bool {
549 let c = self.maximize_center();
550 let dx = local.x - c.x;
551 let dy = local.y - c.y;
552 dx * dx + dy * dy <= (CLOSE_R + 3.0) * (CLOSE_R + 3.0)
553 }
554
555 /// Toggle collapsed <-> expanded, keeping the top edge of the window
556 /// fixed in place. Factored out of the event path so both the chevron
557 /// click and any future keyboard shortcut go through the same math.
558 fn toggle_collapse(&mut self) {
559 let top = self.bounds.y + self.bounds.height;
560 if self.collapsed {
561 self.bounds.height = self.pre_collapse_h;
562 self.bounds.y = (top - self.pre_collapse_h).round();
563 self.collapsed = false;
564 } else {
565 self.pre_collapse_h = self.bounds.height;
566 self.bounds.height = TITLE_H;
567 self.bounds.y = (top - TITLE_H).round();
568 self.collapsed = true;
569 }
570 self.clamp_to_canvas();
571 }
572
573 fn toggle_maximize(&mut self) {
574 if self.maximized {
575 self.bounds = self.pre_maximize_bounds;
576 self.maximized = false;
577 } else {
578 self.pre_maximize_bounds = self.bounds;
579 self.bounds = snap(Rect::new(
580 0.0,
581 0.0,
582 self.canvas_size.width,
583 self.canvas_size.height,
584 ));
585 self.maximized = true;
586 }
587 if let Some(ref cell) = self.maximized_cell {
588 cell.set(self.maximized);
589 }
590 }
591
592 /// Return the resize direction for `local`, or `None` if the point is in
593 /// the interior (or the window is collapsed).
594 fn resize_dir(&self, local: Point) -> Option<ResizeDir> {
595 if self.collapsed || self.auto_size {
596 return None;
597 }
598 if !self.resizable {
599 return None;
600 }
601 let w = self.bounds.width;
602 let h = self.bounds.height;
603 let x = local.x;
604 let y = local.y;
605
606 // Outside the window altogether.
607 if x < 0.0 || x > w || y < 0.0 || y > h {
608 return None;
609 }
610
611 // Mask each edge to the axes the window is allowed to resize on.
612 let on_n = self.resizable_v && y > h - RESIZE_EDGE;
613 let on_s = self.resizable_v && y < RESIZE_EDGE;
614 let on_w = self.resizable_h && x < RESIZE_EDGE;
615 let on_e = self.resizable_h && x > w - RESIZE_EDGE;
616
617 match (on_n, on_e, on_s, on_w) {
618 (true, true, _, _) => Some(ResizeDir::NE),
619 (true, _, _, true) => Some(ResizeDir::NW),
620 (_, _, true, true) => Some(ResizeDir::SW),
621 (_, true, true, _) => Some(ResizeDir::SE),
622 (true, _, _, _) => Some(ResizeDir::N),
623 (_, true, _, _) => Some(ResizeDir::E),
624 (_, _, true, _) => Some(ResizeDir::S),
625 (_, _, _, true) => Some(ResizeDir::W),
626 _ => None,
627 }
628 }
629
630 /// Effective minimum height for this resize pass. Honours
631 /// either `tight_content_fit` (lock + floor) or
632 /// `floor_content_height` (floor only) so a window whose content
633 /// has a natural height > MIN_H can never be dragged smaller
634 /// than its content.
635 fn effective_min_h(&self) -> f64 {
636 if self.tight_content_fit || self.floor_content_height {
637 let content_min = self.last_content_natural_h.get() + TITLE_H;
638 MIN_H.max(content_min)
639 } else {
640 MIN_H
641 }
642 }
643
644 /// Apply a mouse-world-space delta to bounds according to the resize direction.
645 fn apply_resize(&mut self, world_pos: Point) {
646 let dx = world_pos.x - self.drag_start_world.x;
647 let dy = world_pos.y - self.drag_start_world.y;
648 let sb = self.drag_start_bounds;
649 let min_h = self.effective_min_h();
650
651 let (mut x, mut y, mut w, mut h) = (sb.x, sb.y, sb.width, sb.height);
652
653 if let DragMode::Resize(dir) = self.drag_mode {
654 match dir {
655 ResizeDir::N => {
656 h = (sb.height + dy).max(min_h);
657 }
658 ResizeDir::S => {
659 y = sb.y + dy;
660 h = (sb.height - dy).max(min_h);
661 if h == min_h {
662 y = sb.y + sb.height - min_h;
663 }
664 }
665 ResizeDir::E => {
666 w = (sb.width + dx).max(MIN_W);
667 }
668 ResizeDir::W => {
669 x = sb.x + dx;
670 w = (sb.width - dx).max(MIN_W);
671 if w == MIN_W {
672 x = sb.x + sb.width - MIN_W;
673 }
674 }
675 ResizeDir::NE => {
676 w = (sb.width + dx).max(MIN_W);
677 h = (sb.height + dy).max(min_h);
678 }
679 ResizeDir::NW => {
680 x = sb.x + dx;
681 w = (sb.width - dx).max(MIN_W);
682 if w == MIN_W {
683 x = sb.x + sb.width - MIN_W;
684 }
685 h = (sb.height + dy).max(min_h);
686 }
687 ResizeDir::SE => {
688 w = (sb.width + dx).max(MIN_W);
689 y = sb.y + dy;
690 h = (sb.height - dy).max(min_h);
691 if h == min_h {
692 y = sb.y + sb.height - min_h;
693 }
694 }
695 ResizeDir::SW => {
696 x = sb.x + dx;
697 w = (sb.width - dx).max(MIN_W);
698 if w == MIN_W {
699 x = sb.x + sb.width - MIN_W;
700 }
701 y = sb.y + dy;
702 h = (sb.height - dy).max(min_h);
703 if h == min_h {
704 y = sb.y + sb.height - min_h;
705 }
706 }
707 }
708 }
709
710 self.bounds = snap(Rect::new(x, y, w, h));
711 self.clamp_to_canvas();
712 }
713}
714
715/// Map a resize direction to the appropriate OS cursor icon.
716fn resize_cursor(dir: ResizeDir) -> CursorIcon {
717 match dir {
718 ResizeDir::N => CursorIcon::ResizeNorth,
719 ResizeDir::S => CursorIcon::ResizeSouth,
720 ResizeDir::E => CursorIcon::ResizeEast,
721 ResizeDir::W => CursorIcon::ResizeWest,
722 ResizeDir::NE => CursorIcon::ResizeNorthEast,
723 ResizeDir::NW => CursorIcon::ResizeNorthWest,
724 ResizeDir::SE => CursorIcon::ResizeSouthEast,
725 ResizeDir::SW => CursorIcon::ResizeSouthWest,
726 }
727}
728
729mod builder;
730pub mod chrome;
731mod close;
732mod paint;
733mod snap_glue;
734mod widget_impl;
735
736pub use close::{ClickAwayAction, CloseReason};
737
738pub use chrome::{
739 paint_chevron, paint_chrome_body, paint_chrome_border, paint_chrome_shadow,
740 paint_chrome_title_bar, ChromeStyle,
741};