Skip to main content

qframe/widgets/
window.rs

1//! Windows: a surface placed freely in a stack, with a one-row title strip, that tells the
2//! application how the pointer moves, resizes, minimizes, maximizes and closes it.
3
4use crate::color::{ColorDepth, Rgb};
5use crate::event::{Event, MouseButton, MouseEvent, MouseKind};
6use crate::geometry::{Rect, Size, clamp_u16};
7use crate::icons::Glyph;
8use crate::style::CellStyle;
9use crate::text;
10use crate::theme::State;
11use crate::widget::{Axis, Container, EventCx, Flex, Length, MeasureCx, Node, PaintCx, PointerShape, Widget};
12
13use super::{click, close_mark};
14
15/// Cells the marks take at the right end of the title row: minimize, maximize and close.
16const MARKS: u16 = close_mark::WIDTH * 3;
17
18/// Cells between the name and the subtitle.
19const TITLE_GAP: u16 = 2;
20
21/// The fewest cells a shortened subtitle keeps; below that it is left out, since an ellipsis and
22/// two letters say nothing.
23const MIN_SUBTITLE: u16 = 4;
24
25/// How far the ground under a shadow is darkened, in percent, when the theme does not say.
26const DEFAULT_SHADOW: u16 = 45;
27
28/// What the pointer did to a [`Window`], sent through [`Window::on_event`].
29///
30/// Deltas count cells since the last message of the same drag, so an application adds them to
31/// the window's rectangle as they come. What it allows (a smallest size, staying on screen) is
32/// its own decision; the window reports the pointer, not a new rectangle.
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub enum WindowEvent {
35    /// A press anywhere in a window that is not [focused](Window::focused), sent before whatever
36    /// else the press does: raise the window and give it focus.
37    Focus,
38    /// The window was dragged by its title, or with alt and the left button anywhere, by `dx`
39    /// columns and `dy` rows.
40    Move {
41        /// Columns to the right; negative is to the left.
42        dx: i32,
43        /// Rows down; negative is up.
44        dy: i32,
45    },
46    /// An edge or a corner was dragged: the left or right column, the bottom row, one of the four
47    /// corners, or with alt and the right button the edge or corner nearest to the press. A drag
48    /// on the left or top side moves that side: the application moves the window by the delta
49    /// and changes its size by the opposite, so the other side stays where it was (see
50    /// [`WindowEdge::left`] and [`WindowEdge::top`]).
51    Resize {
52        /// The edge or corner that moves.
53        edge: WindowEdge,
54        /// Columns the edge's side moves to the right; 0 for the top and bottom edges.
55        dx: i32,
56        /// Rows the edge's side moves down; 0 for the left and right edges.
57        dy: i32,
58    },
59    /// The minimize mark was clicked.
60    Minimize,
61    /// The maximize mark was clicked, or the title double-clicked.
62    ToggleMaximize,
63    /// The close mark was clicked.
64    Close,
65    /// A move or a resize ended: the button came up after at least one [`WindowEvent::Move`] or
66    /// [`WindowEvent::Resize`]. This is where snapping to an edge and a ghost drag land, and
67    /// where a size is saved.
68    Dropped,
69}
70
71/// An edge or a corner of a window being resized, see [`WindowEvent::Resize`].
72#[derive(Debug, Clone, Copy, PartialEq, Eq)]
73pub enum WindowEdge {
74    /// The left edge.
75    Left,
76    /// The right edge.
77    Right,
78    /// The top edge.
79    Top,
80    /// The bottom edge.
81    Bottom,
82    /// The top left corner.
83    TopLeft,
84    /// The top right corner.
85    TopRight,
86    /// The bottom left corner.
87    BottomLeft,
88    /// The bottom right corner.
89    BottomRight,
90}
91
92impl WindowEdge {
93    /// Whether the left side moves: the left edge and the corners beside it. The window's `x`
94    /// then moves by `dx` and its width by `-dx`.
95    #[must_use]
96    pub fn left(self) -> bool {
97        matches!(self, Self::Left | Self::TopLeft | Self::BottomLeft)
98    }
99
100    /// Whether the right side moves; the width then changes by `dx`.
101    #[must_use]
102    pub fn right(self) -> bool {
103        matches!(self, Self::Right | Self::TopRight | Self::BottomRight)
104    }
105
106    /// Whether the top side moves. The window's `y` then moves by `dy` and its height by `-dy`.
107    #[must_use]
108    pub fn top(self) -> bool {
109        matches!(self, Self::Top | Self::TopLeft | Self::TopRight)
110    }
111
112    /// Whether the bottom side moves; the height then changes by `dy`.
113    #[must_use]
114    pub fn bottom(self) -> bool {
115        matches!(self, Self::Bottom | Self::BottomLeft | Self::BottomRight)
116    }
117
118    /// The pointer's resize arrow over this edge or corner.
119    fn pointer_shape(self) -> PointerShape {
120        match self {
121            Self::Left | Self::Right => PointerShape::EwResize,
122            Self::Top | Self::Bottom => PointerShape::NsResize,
123            Self::TopLeft | Self::BottomRight => PointerShape::NwseResize,
124            Self::TopRight | Self::BottomLeft => PointerShape::NeswResize,
125        }
126    }
127}
128
129/// Whether an edge moves a given side, such as [`WindowEdge::left`].
130type SideTest = fn(WindowEdge) -> bool;
131
132/// Builds a message from what the pointer did to the window.
133type EventMessage<Msg> = Box<dyn Fn(WindowEvent) -> Msg>;
134
135/// A window: a title strip one row tall above a body, with no border lines, for applications
136/// that put surfaces where the user drags them, such as a desktop or a tool box. Place it with
137/// [`View::place`](crate::widget::View::place) inside a stack; the body is built with
138/// [`View::add_with`](crate::widget::View::add_with).
139///
140/// The title strip shows the icon and the name, then a faint subtitle (a program's own title, a
141/// folder). A window that is [`focused`](Self::focused) is one tone raised, its name bright, and
142/// the pillar `▌` runs down its whole left edge; others sit one tone lower with a quieter name.
143/// On a narrow window the subtitle shortens first, then the name, each with `…`. The body keeps
144/// the pillar column and one cell after it on the left, and the right column and the bottom row
145/// free for the handles.
146///
147/// With no options the window is only a surface. [`on_event`](Self::on_event) makes it one the
148/// pointer moves: the three marks near the right end of the title (minimize, maximize or restore,
149/// close) light up together under the pointer like every close mark; dragging the title moves
150/// the window and double-clicking it maximizes. Every side resizes with a plain drag: the left
151/// column, the right column and the bottom row are handles, with their four corners. The top
152/// side is the title strip, which moves the window, so only its two end cells are handles, the
153/// top left and top right corners; the marks sit left of the right column to leave that corner
154/// free. Handles brighten under the pointer and take the accent while dragged, like a
155/// splitter's boundary, and on terminals that can change the pointer's shape the pointer turns
156/// into the matching resize arrow over them (see
157/// [`PaintCx::pointer_shape`](crate::widget::PaintCx::pointer_shape)). Alt with the left button
158/// drags the window from anywhere, alt with the right button resizes it from the nearest edge
159/// or corner, the top edge too. A drag belongs to the window until the button comes up, which
160/// arrives as [`WindowEvent::Dropped`], wherever the pointer goes. The title, the
161/// marks, the handles and alt drags are the window's even when the body holds a
162/// [`Terminal`](super::Terminal) whose program reads the mouse; other presses in the body reach
163/// the body. The window reports all of it through [`WindowEvent`]s and changes nothing itself:
164/// stacking order, focus, size limits and snapping are the application's.
165///
166/// [`shadow`](Self::shadow) darkens one column right of the window and one row below it. It is
167/// not drawn with reduced motion or in 16 colours.
168///
169/// Style keys: `window` (`bg`, `pillar`), `window-title` (`bg`, `fg`, `bold`),
170/// `window-subtitle` (`fg`), all with `focus` for a focused window; `close-mark` for the marks
171/// (`active` while focused, `hover`); `split-handle` for the handles (`hover`, `active` while
172/// dragged); `window-shadow` (`scrim`, `strength` in percent).
173pub struct Window<Msg> {
174    title: String,
175    subtitle: Option<String>,
176    icon: Option<Glyph>,
177    focused: bool,
178    maximized: bool,
179    shadow: bool,
180    on_event: Option<EventMessage<Msg>>,
181    /// The body: one column holding what [`View::add_with`](crate::widget::View::add_with) built.
182    body: Vec<Node<Msg>>,
183}
184
185impl<Msg: 'static> Window<Msg> {
186    /// A window named `title`, unfocused, with an empty body.
187    #[must_use]
188    pub fn new(title: impl Into<String>) -> Self {
189        Self {
190            title: title.into(),
191            subtitle: None,
192            icon: None,
193            focused: false,
194            maximized: false,
195            shadow: false,
196            on_event: None,
197            body: vec![body(Vec::new())],
198        }
199    }
200
201    /// A faint second title after the name, such as the title a program set or its folder.
202    #[must_use]
203    pub fn subtitle(mut self, subtitle: impl Into<String>) -> Self {
204        self.subtitle = Some(subtitle.into());
205        self
206    }
207
208    /// The glyph before the name: an icon key such as `"folder"`, or a [`Glyph::literal`].
209    #[must_use]
210    pub fn icon(mut self, glyph: impl Into<Glyph>) -> Self {
211        self.icon = Some(glyph.into());
212        self
213    }
214
215    /// Whether this is the window the user works in: raised one tone, bright name and the pillar
216    /// down its left edge. A press on a window that is not focused sends [`WindowEvent::Focus`].
217    #[must_use]
218    pub fn focused(mut self, focused: bool) -> Self {
219        self.focused = focused;
220        self
221    }
222
223    /// Whether the window fills its desktop; the maximize mark then offers to restore it.
224    #[must_use]
225    pub fn maximized(mut self, maximized: bool) -> Self {
226        self.maximized = maximized;
227        self
228    }
229
230    /// Darkens one column right of the window and one row below it, as if it floated; drawn in
231    /// the cell a placed child may reach past its rectangle. Not drawn with reduced motion or in
232    /// 16 colours.
233    #[must_use]
234    pub fn shadow(mut self, shadow: bool) -> Self {
235        self.shadow = shadow;
236        self
237    }
238
239    /// Makes the window one the pointer moves, resizes and closes, and shows its marks; `message`
240    /// turns each [`WindowEvent`] into the application's message.
241    #[must_use]
242    pub fn on_event(mut self, message: impl Fn(WindowEvent) -> Msg + 'static) -> Self {
243        self.on_event = Some(Box::new(message));
244        self
245    }
246}
247
248fn body<Msg: 'static>(children: Vec<Node<Msg>>) -> Node<Msg> {
249    let mut column = Node::new(Flex::new(Axis::Column, children), 0);
250    column.layout.width = Length::Fill(1);
251    column.layout.height = Length::Fill(1);
252    column
253}
254
255impl<Msg: 'static> Container<Msg> for Window<Msg> {
256    fn set_children(&mut self, children: Vec<Node<Msg>>) {
257        self.body[0] = body(children);
258    }
259}
260
261/// A mark in the title row, left to right.
262#[derive(Debug, Clone, Copy, PartialEq, Eq)]
263enum Mark {
264    Minimize,
265    Maximize,
266    Close,
267}
268
269impl Mark {
270    const ALL: [Self; 3] = [Self::Minimize, Self::Maximize, Self::Close];
271
272    fn event(self) -> WindowEvent {
273        match self {
274            Self::Minimize => WindowEvent::Minimize,
275            Self::Maximize => WindowEvent::ToggleMaximize,
276            Self::Close => WindowEvent::Close,
277        }
278    }
279}
280
281/// The part of a window a cell belongs to.
282#[derive(Debug, Clone, Copy, PartialEq, Eq)]
283enum Part {
284    Title,
285    Mark(Mark),
286    Handle(WindowEdge),
287    Body,
288}
289
290/// What a held button is doing to the window.
291#[derive(Debug, Clone, Copy, PartialEq, Eq)]
292enum Grab {
293    /// Moving it; the pointer was last at `last`.
294    Move { button: MouseButton, last: (i32, i32) },
295    /// Resizing it by `edge`.
296    Resize { button: MouseButton, edge: WindowEdge, last: (i32, i32) },
297    /// Held on a mark, which acts when released over it.
298    Mark(Mark),
299}
300
301#[derive(Debug, Default)]
302struct WindowMemory {
303    grab: Option<Grab>,
304    /// Whether the held grab has moved the window or an edge, so the release is a drop.
305    moved: bool,
306    /// When the title was last pressed without being dragged, to tell a double click.
307    title_press: Option<std::time::Duration>,
308}
309
310impl<Msg: 'static> Window<Msg> {
311    fn interactive(&self) -> bool {
312        self.on_event.is_some()
313    }
314
315    /// Where the body's content goes: after the pillar column and a cell, above the bottom row
316    /// and left of the right column.
317    fn content(area: Rect) -> Rect {
318        Rect::new(area.x + 2, area.y + 1, area.width.saturating_sub(3), area.height.saturating_sub(2))
319    }
320
321    /// Where the marks start: left of the right column, which is the right edge's handle all the
322    /// way up to the title row's corner.
323    fn marks_x(area: Rect) -> i32 {
324        area.right() - 1 - i32::from(MARKS)
325    }
326
327    /// The part of the window at `(x, y)`, or `None` outside it.
328    ///
329    /// On a window the pointer moves, every side is a handle: the left column, the right column
330    /// and the bottom row, with their corners. The top side is the title strip, which moves the
331    /// window, so only its two end cells are handles, the top corners; the cells between them are
332    /// the title and the marks.
333    fn part_at(&self, area: Rect, x: i32, y: i32) -> Option<Part> {
334        if !area.contains(x, y) {
335            return None;
336        }
337        if !self.interactive() {
338            return Some(if y == area.y { Part::Title } else { Part::Body });
339        }
340        let (left, right) = (x == area.x, x == area.right() - 1);
341        let (top, bottom) = (y == area.y, y == area.bottom() - 1);
342        Some(match (left, right, top, bottom) {
343            (true, _, true, _) => Part::Handle(WindowEdge::TopLeft),
344            (_, true, true, _) => Part::Handle(WindowEdge::TopRight),
345            (_, _, true, _) => {
346                let marks = Self::marks_x(area);
347                if x >= marks {
348                    let index = usize::try_from((x - marks) / i32::from(close_mark::WIDTH)).unwrap_or(0);
349                    Part::Mark(Mark::ALL[index.min(2)])
350                } else {
351                    Part::Title
352                }
353            }
354            (true, _, _, true) => Part::Handle(WindowEdge::BottomLeft),
355            (_, true, _, true) => Part::Handle(WindowEdge::BottomRight),
356            (true, _, _, _) => Part::Handle(WindowEdge::Left),
357            (_, true, _, _) => Part::Handle(WindowEdge::Right),
358            (_, _, _, true) => Part::Handle(WindowEdge::Bottom),
359            _ => Part::Body,
360        })
361    }
362
363    /// The edge or corner nearest to `(x, y)`: the corners and edges each take a third of the
364    /// window, and in the middle third the closest side wins, counting a row as two columns
365    /// since a cell is about twice as tall as it is wide.
366    fn nearest_edge(area: Rect, x: i32, y: i32) -> WindowEdge {
367        let third = |offset: i32, length: u16| (offset * 3 / i32::from(length.max(1))).clamp(0, 2);
368        match (third(x - area.x, area.width), third(y - area.y, area.height)) {
369            (0, 0) => WindowEdge::TopLeft,
370            (1, 0) => WindowEdge::Top,
371            (2, 0) => WindowEdge::TopRight,
372            (0, 1) => WindowEdge::Left,
373            (2, 1) => WindowEdge::Right,
374            (0, 2) => WindowEdge::BottomLeft,
375            (1, 2) => WindowEdge::Bottom,
376            (2, 2) => WindowEdge::BottomRight,
377            _ => {
378                let sides = [
379                    (x - area.x, WindowEdge::Left),
380                    (area.right() - 1 - x, WindowEdge::Right),
381                    (2 * (y - area.y), WindowEdge::Top),
382                    (2 * (area.bottom() - 1 - y), WindowEdge::Bottom),
383                ];
384                sides.into_iter().min_by_key(|(distance, _)| *distance).map_or(WindowEdge::Right, |(_, edge)| edge)
385            }
386        }
387    }
388
389    fn send(&self, cx: &mut EventCx<'_, Msg>, event: WindowEvent) {
390        if let Some(message) = &self.on_event {
391            cx.emit(message(event));
392        }
393    }
394
395    fn press(&self, cx: &mut EventCx<'_, Msg>, mouse: MouseEvent, button: MouseButton) -> bool {
396        let area = cx.area();
397        let Some(part) = self.part_at(area, mouse.x, mouse.y) else {
398            return false;
399        };
400        if !self.focused {
401            self.send(cx, WindowEvent::Focus);
402        }
403        let at = (mouse.x, mouse.y);
404        let now = cx.now();
405        let memory = cx.memory::<WindowMemory>();
406        let grab = match (mouse.mods.alt, button, part) {
407            (true, MouseButton::Left, _) => Grab::Move { button, last: at },
408            (true, MouseButton::Right, _) => {
409                Grab::Resize { button, edge: Self::nearest_edge(area, mouse.x, mouse.y), last: at }
410            }
411            // Other buttons on the window's own parts do nothing yet, but they are the window's.
412            (_, MouseButton::Left, Part::Body) | (_, MouseButton::Right | MouseButton::Middle, _) => {
413                return part != Part::Body;
414            }
415            (_, MouseButton::Left, Part::Mark(mark)) => Grab::Mark(mark),
416            (_, MouseButton::Left, Part::Handle(edge)) => Grab::Resize { button, edge, last: at },
417            (_, MouseButton::Left, Part::Title) => {
418                if memory.title_press.is_some_and(|last| click::is_double(last, now)) {
419                    memory.title_press = None;
420                    memory.grab = None;
421                    cx.capture_pointer();
422                    self.send(cx, WindowEvent::ToggleMaximize);
423                    return true;
424                }
425                memory.title_press = Some(now);
426                Grab::Move { button, last: at }
427            }
428        };
429        if !matches!(grab, Grab::Move { .. }) || part != Part::Title {
430            memory.title_press = None;
431        }
432        memory.grab = Some(grab);
433        memory.moved = false;
434        cx.capture_pointer();
435        true
436    }
437
438    fn drag(&self, cx: &mut EventCx<'_, Msg>, mouse: MouseEvent, button: MouseButton) -> bool {
439        let at = (mouse.x, mouse.y);
440        let memory = cx.memory::<WindowMemory>();
441        let event = match memory.grab {
442            Some(Grab::Move { button: held, last }) if held == button => {
443                memory.grab = Some(Grab::Move { button, last: at });
444                let (dx, dy) = (at.0 - last.0, at.1 - last.1);
445                if (dx, dy) != (0, 0) {
446                    memory.title_press = None;
447                }
448                ((dx, dy) != (0, 0)).then_some(WindowEvent::Move { dx, dy })
449            }
450            Some(Grab::Resize { button: held, edge, last }) if held == button => {
451                memory.grab = Some(Grab::Resize { button, edge, last: at });
452                let dx = if edge.left() || edge.right() { at.0 - last.0 } else { 0 };
453                let dy = if edge.top() || edge.bottom() { at.1 - last.1 } else { 0 };
454                ((dx, dy) != (0, 0)).then_some(WindowEvent::Resize { edge, dx, dy })
455            }
456            Some(Grab::Mark(_)) => None,
457            _ => return false,
458        };
459        if let Some(event) = event {
460            cx.memory::<WindowMemory>().moved = true;
461            self.send(cx, event);
462        }
463        true
464    }
465
466    fn release(&self, cx: &mut EventCx<'_, Msg>, mouse: MouseEvent) -> bool {
467        let area = cx.area();
468        let memory = cx.memory::<WindowMemory>();
469        let (Some(grab), moved) = (memory.grab.take(), memory.moved) else {
470            return false;
471        };
472        memory.moved = false;
473        match grab {
474            Grab::Mark(mark) if self.part_at(area, mouse.x, mouse.y) == Some(Part::Mark(mark)) => {
475                self.send(cx, mark.event());
476            }
477            Grab::Move { .. } | Grab::Resize { .. } if moved => self.send(cx, WindowEvent::Dropped),
478            _ => {}
479        }
480        true
481    }
482
483    /// Darkens the column right of `area` and the row below it.
484    fn paint_shadow(cx: &mut PaintCx<'_>, area: Rect) {
485        let depth = cx.env().depth();
486        if depth == ColorDepth::Ansi16 || cx.reduced_motion() {
487            return;
488        }
489        let style = cx.style("window-shadow", None, &[]);
490        let scrim = style.color("scrim").unwrap_or_else(|| cx.color("canvas"));
491        let strength = f32::from(style.cells("strength").unwrap_or(DEFAULT_SHADOW).min(100)) / 100.0;
492        let rects = [
493            Rect::new(area.right(), area.y + 1, 1, area.height.saturating_sub(1)),
494            Rect::new(area.x + 1, area.bottom(), area.width, 1),
495        ];
496        for rect in rects {
497            if depth == ColorDepth::TrueColor {
498                cx.tint(rect, scrim, strength);
499            } else {
500                // Palette cells cannot be blended; the shadow darkens the canvas instead.
501                let ground = cx.color("canvas").mix(scrim, strength);
502                cx.fill(rect, ground);
503            }
504        }
505    }
506
507    /// The icon, name and subtitle that fit in `room` cells: the subtitle shortens first, then it
508    /// goes, then the name shortens.
509    fn fit_title(&self, icon: Option<&str>, room: u16) -> (Option<String>, String, Option<String>) {
510        let lead = icon.map_or(0, |glyph| text::width(glyph).saturating_add(1));
511        let name = text::width(&self.title);
512        let icon = icon.filter(|glyph| text::width(glyph) <= room).map(str::to_owned);
513        let before = lead.saturating_add(name).saturating_add(TITLE_GAP);
514        if let Some(subtitle) = self.subtitle.as_deref().filter(|subtitle| !subtitle.is_empty()) {
515            let left = room.saturating_sub(before);
516            if before <= room && left >= MIN_SUBTITLE.min(text::width(subtitle)) {
517                return (icon, self.title.clone(), Some(text::truncate(subtitle, left).into_owned()));
518            }
519        }
520        (icon, text::truncate(&self.title, room.saturating_sub(lead)).into_owned(), None)
521    }
522
523    /// The title strip's colour and the styles of the name and the subtitle. In 16 colours the
524    /// surface tones share two greys, too few to tell the focused window's strip from the others,
525    /// so the strip takes the accent on the focused window and a grey on the others, with dark
526    /// text on both.
527    fn title_look(&self, cx: &mut PaintCx<'_>, states: &[State], ground: Rgb) -> (Rgb, CellStyle, CellStyle) {
528        let name = cx.style("window-title", None, states).text();
529        let subtitle = cx.style("window-subtitle", None, states).text();
530        if cx.env().depth() != ColorDepth::Ansi16 {
531            return (name.bg.unwrap_or(ground), CellStyle { bg: None, ..name }, CellStyle { bg: None, ..subtitle });
532        }
533        let strip = cx.color(if self.focused { "accent" } else { "muted" });
534        let ink = Some(cx.color("ink"));
535        (strip, CellStyle { bg: None, fg: ink, ..name }, CellStyle { bg: None, fg: ink, ..subtitle })
536    }
537
538    fn paint_title(&self, cx: &mut PaintCx<'_>, area: Rect, style: CellStyle, subtitle_style: CellStyle) {
539        let marks = if self.interactive() { MARKS } else { 0 };
540        let start = area.x + 1;
541        // One cell after the name's start for the pillar, one before the marks, and on a window
542        // with marks the right column, which is the top right corner's handle.
543        let corner = i32::from(self.interactive());
544        let room = clamp_u16(i32::from(area.width) - 2 - corner - i32::from(marks));
545        let icon = self.icon.as_ref().map(|icon| icon.resolve(cx.env().icons()).into_owned());
546        let (icon, name, subtitle) = self.fit_title(icon.as_deref(), room);
547        let text_style = style;
548        let mut x = start;
549        if let Some(icon) = icon {
550            let width = cx.text(x, area.y, &icon, text_style, room);
551            x += i32::from(width) + 1;
552        }
553        let limit = clamp_u16(i32::from(room) - (x - start));
554        let width = cx.text(x, area.y, &name, text_style, limit);
555        x += i32::from(width) + i32::from(TITLE_GAP);
556        if let Some(subtitle) = subtitle {
557            let limit = clamp_u16(i32::from(room) - (x - start));
558            cx.text(x, area.y, &subtitle, subtitle_style, limit);
559        }
560        if self.interactive() {
561            let restore = if self.maximized { "window-restore" } else { "window-maximize" };
562            let marks_x = Self::marks_x(area);
563            for (index, key) in ["window-minimize", restore, "close"].into_iter().enumerate() {
564                let offset = i32::try_from(index).unwrap_or(0) * i32::from(close_mark::WIDTH);
565                close_mark::paint_glyph(cx, marks_x + offset, area.y, self.focused, key);
566            }
567        }
568    }
569
570    /// Asks for a resize arrow over every handle: the sides first, then the corners over them.
571    fn ask_pointer_shapes(cx: &mut PaintCx<'_>, area: Rect) {
572        let (right, bottom) = (area.right() - 1, area.bottom() - 1);
573        let handles = [
574            (Rect::new(area.x, area.y, 1, area.height), WindowEdge::Left),
575            (Rect::new(right, area.y, 1, area.height), WindowEdge::Right),
576            (Rect::new(area.x, bottom, area.width, 1), WindowEdge::Bottom),
577            (Rect::new(area.x, area.y, 1, 1), WindowEdge::TopLeft),
578            (Rect::new(right, area.y, 1, 1), WindowEdge::TopRight),
579            (Rect::new(area.x, bottom, 1, 1), WindowEdge::BottomLeft),
580            (Rect::new(right, bottom, 1, 1), WindowEdge::BottomRight),
581        ];
582        for (rect, edge) in handles {
583            cx.pointer_shape(rect, edge.pointer_shape());
584        }
585    }
586
587    /// Lights the sides under the pointer or while dragged: the left or right column, the bottom
588    /// row, and for the top side, which has no row of its own, its two corner cells.
589    fn paint_handles(&self, cx: &mut PaintCx<'_>, area: Rect) {
590        if area.height < 2 || area.width < 2 {
591            return;
592        }
593        let hovered = cx.pointer().and_then(|(x, y)| match self.part_at(area, x, y) {
594            Some(Part::Handle(edge)) => Some(edge),
595            _ => None,
596        });
597        let dragged = match cx.memory::<WindowMemory>().grab {
598            Some(Grab::Resize { edge, .. }) => Some(edge),
599            _ => None,
600        };
601        let right = area.right() - 1;
602        let handles: [(Rect, SideTest); 5] = [
603            (Rect::new(area.x, area.y, 1, area.height), WindowEdge::left),
604            (Rect::new(right, area.y, 1, area.height), WindowEdge::right),
605            (Rect::new(area.x, area.bottom() - 1, area.width, 1), WindowEdge::bottom),
606            (Rect::new(area.x, area.y, 1, 1), WindowEdge::top),
607            (Rect::new(right, area.y, 1, 1), WindowEdge::top),
608        ];
609        for (rect, moves) in handles {
610            let state = if dragged.is_some_and(moves) {
611                State::Active
612            } else if hovered.is_some_and(moves) {
613                State::Hover
614            } else {
615                continue;
616            };
617            let style = cx.style("split-handle", None, &[state]).text();
618            if let Some(bg) = style.bg {
619                cx.fill(rect, bg);
620            }
621        }
622    }
623}
624
625impl<Msg: 'static> Widget<Msg> for Window<Msg> {
626    fn measure(&self, _cx: &mut MeasureCx<'_>, available: Size) -> Size {
627        available
628    }
629
630    fn paint(&self, cx: &mut PaintCx<'_>, area: Rect) {
631        if area.is_empty() {
632            return;
633        }
634        let states: &[State] = if self.focused { &[State::Focus] } else { &[] };
635        if self.shadow {
636            Self::paint_shadow(cx, area);
637        }
638        cx.register_hit(area);
639        let grab = if self.interactive() {
640            cx.preview_presses();
641            let grab = cx.memory::<WindowMemory>().grab;
642            // The window's own pointer first, so what the body asks for over itself wins, and the
643            // arrow of a resize in progress, which holds wherever the drag takes the pointer.
644            let shape = match grab {
645                Some(Grab::Resize { edge, .. }) => edge.pointer_shape(),
646                _ => PointerShape::Default,
647            };
648            cx.pointer_shape(area, shape);
649            grab
650        } else {
651            None
652        };
653        let surface = cx.style("window", None, states);
654        let ground = surface.text().bg.unwrap_or_else(|| cx.color("surface"));
655        let (strip, name, subtitle) = self.title_look(cx, states, ground);
656        cx.clear(area, ground);
657        cx.clear(area.row(0), strip);
658        if let Some(pillar) = surface.color("pillar").filter(|_| self.focused) {
659            for y in area.y..area.bottom() {
660                cx.pillar(area.x, y, pillar);
661            }
662        }
663        self.paint_title(cx, area, name, subtitle);
664        cx.paint_child(&self.body[0], Self::content(area));
665        if self.interactive() {
666            self.paint_handles(cx, area);
667            if grab.is_none() {
668                Self::ask_pointer_shapes(cx, area);
669            }
670        }
671    }
672
673    fn event(&self, cx: &mut EventCx<'_, Msg>, event: &Event) -> bool {
674        if !self.interactive() {
675            return false;
676        }
677        let Event::Mouse(mouse) = event else {
678            return false;
679        };
680        match mouse.kind {
681            // Presses are read before the body sees them; a press the body left bubbles here
682            // afterwards and stays the body's.
683            MouseKind::Down(button) if cx.is_preview() => self.press(cx, *mouse, button),
684            MouseKind::Drag(button) => self.drag(cx, *mouse, button),
685            MouseKind::Up(_) => self.release(cx, *mouse),
686            _ => false,
687        }
688    }
689
690    fn children(&self) -> &[Node<Msg>] {
691        &self.body
692    }
693
694    fn children_mut(&mut self) -> &mut [Node<Msg>] {
695        &mut self.body
696    }
697}
698
699#[cfg(test)]
700mod tests {
701    use super::{Mark, Part, Window, WindowEdge};
702    use crate::geometry::Rect;
703
704    #[test]
705    fn part_at_names_every_side_and_corner_and_leaves_the_title_between_the_top_corners() {
706        let window: Window<()> = Window::new("notes").on_event(|_| ());
707        let area = Rect::new(0, 0, 20, 5);
708        let part = |x, y| window.part_at(area, x, y);
709        assert_eq!(part(0, 0), Some(Part::Handle(WindowEdge::TopLeft)));
710        assert_eq!(part(19, 0), Some(Part::Handle(WindowEdge::TopRight)));
711        assert_eq!(part(0, 4), Some(Part::Handle(WindowEdge::BottomLeft)));
712        assert_eq!(part(19, 4), Some(Part::Handle(WindowEdge::BottomRight)));
713        assert_eq!(part(0, 2), Some(Part::Handle(WindowEdge::Left)));
714        assert_eq!(part(19, 2), Some(Part::Handle(WindowEdge::Right)));
715        assert_eq!(part(7, 4), Some(Part::Handle(WindowEdge::Bottom)));
716        assert_eq!((part(1, 0), part(9, 0)), (Some(Part::Title), Some(Part::Title)));
717        assert_eq!(part(10, 0), Some(Part::Mark(Mark::Minimize)), "the marks end left of the right column");
718        assert_eq!(part(18, 0), Some(Part::Mark(Mark::Close)));
719        assert_eq!(part(7, 2), Some(Part::Body));
720        assert_eq!(part(20, 2), None);
721        let still: Window<()> = Window::new("still");
722        assert_eq!((still.part_at(area, 0, 2), still.part_at(area, 19, 0)), (Some(Part::Body), Some(Part::Title)));
723    }
724}