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