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