Skip to main content

qframe/widget/context/
paint.rs

1//! The painting context.
2
3use std::time::Duration;
4
5use ratatui_core::buffer::{Buffer, Cell};
6use ratatui_core::style::Color;
7use unicode_segmentation::UnicodeSegmentation;
8
9use super::MeasureCx;
10use super::frame::{FocusRequest, Frame, Interaction, LayerEntry};
11use crate::color::{ColorDepth, Rgb};
12use crate::env::Env;
13use crate::geometry::{Rect, Size, clamp_u16};
14use crate::icons::{GlyphMode, PILLAR};
15use crate::keymap::KeyChord;
16use crate::motion::{Easing, Tweens};
17use crate::style::{CellStyle, WidgetStyle, to_color};
18use crate::text;
19use crate::theme::State;
20use crate::widget::memory::Memory;
21use crate::widget::{Key, LayoutProps, Node, PointerShape, WidgetId};
22
23/// Painting context: draws into the frame, clipped to the widget's visible area.
24pub struct PaintCx<'a> {
25    pub(crate) buf: &'a mut Buffer,
26    pub(crate) env: &'a Env,
27    pub(crate) frame: &'a mut Frame,
28    pub(crate) memory: &'a mut Memory,
29    pub(crate) interaction: &'a Interaction,
30    pub(crate) now: Duration,
31    pub(crate) clip: Rect,
32    pub(crate) id: WidgetId,
33    pub(crate) layout: LayoutProps,
34    pub(crate) scope: Option<WidgetId>,
35    /// How long no input has arrived, for views a widget builds while it paints.
36    pub(crate) idle: Duration,
37    /// Whether the widgets painted now hold the focus of the container painting them, which
38    /// forwards its keys to them without their taking focus themselves.
39    pub(crate) focus_lent: bool,
40}
41
42impl PaintCx<'_> {
43    /// The id of the widget being painted.
44    #[must_use]
45    pub fn id(&self) -> WidgetId {
46        self.id
47    }
48
49    /// The environment.
50    #[must_use]
51    pub fn env(&self) -> &Env {
52        self.env
53    }
54
55    /// Layout properties of the widget being painted.
56    #[must_use]
57    pub fn layout(&self) -> LayoutProps {
58        self.layout
59    }
60
61    /// Time since the runtime started; drives animations.
62    #[must_use]
63    pub fn now(&self) -> Duration {
64        self.now
65    }
66
67    /// Whether the pointer is over this widget.
68    #[must_use]
69    pub fn is_hovered(&self) -> bool {
70        self.interaction.hovered == Some(self.id)
71    }
72
73    /// The widget that has keyboard focus, if any.
74    #[must_use]
75    pub fn focused(&self) -> Option<WidgetId> {
76        self.interaction.focused
77    }
78
79    /// The pointer cell wherever it is on screen, unlike [`PaintCx::pointer`].
80    #[must_use]
81    pub fn pointer_anywhere(&self) -> Option<(i32, i32)> {
82        self.interaction.pointer
83    }
84
85    /// Once this frame is painted, moves keyboard focus to the first focusable widget inside
86    /// `ancestor`, unless focus is already inside. Used by layers that take focus when they open.
87    pub fn request_focus_within(&mut self, ancestor: WidgetId) {
88        self.frame.focus_request = Some(FocusRequest::Within(ancestor));
89    }
90
91    /// Once this frame is painted, moves keyboard focus to `id` if that widget is focusable.
92    /// Used by layers to give focus back when they close.
93    pub fn request_focus(&mut self, id: WidgetId) {
94        self.frame.focus_request = Some(FocusRequest::Exact(id));
95    }
96
97    /// Makes this widget a dismissable layer for this frame: a pointer press outside it and its
98    /// descendants sends it [`Event::PointerOutside`](crate::event::Event::PointerOutside) and
99    /// then still reaches what was pressed, unless that is the widget whose press opened the
100    /// layer (then the press only closes it); an Esc key no focused widget used is sent to it.
101    /// Layers registered later are on top and are asked first. Dismissable layers share the
102    /// layer stack with modal layers ([`PaintCx::open_layer`]): a press on a modal layer above a
103    /// dismissable one lands inside the modal layer, so the dismissable layer beneath stays.
104    pub fn register_dismissable(&mut self) {
105        self.frame.layers.push(LayerEntry { id: self.id, modal: false, surface: None });
106    }
107
108    /// Whether this widget has keyboard focus.
109    #[must_use]
110    pub fn is_focused(&self) -> bool {
111        self.focus_lent || self.interaction.focused == Some(self.id)
112    }
113
114    /// Whether this widget has focus that should be shown loudly: it was reached with the
115    /// keyboard rather than clicked. Buttons and cards breathe their pillar only then, so a
116    /// clicked button stays calm under the pointer.
117    #[must_use]
118    pub fn is_focus_visible(&self) -> bool {
119        self.is_focused() && !self.interaction.focus_by_pointer
120    }
121
122    /// The pointer cell, when the pointer is over this widget.
123    #[must_use]
124    pub fn pointer(&self) -> Option<(i32, i32)> {
125        self.interaction.pointer.filter(|_| self.is_hovered())
126    }
127
128    /// Whether this widget is flashing after being activated. Schedules the frame that ends
129    /// the flash.
130    pub fn is_pressed(&mut self) -> bool {
131        let Some((id, at)) = self.interaction.pressed else {
132            return false;
133        };
134        let end = at + self.env.theme().motion().flash;
135        if id != self.id || self.now >= end {
136            return false;
137        }
138        self.frame.schedule(end);
139        true
140    }
141
142    /// Hover, focus and pressed states of a pressable widget such as a button, card, tab or
143    /// toggle: like [`states`](Self::states), but focus counts only when it is
144    /// [visible](Self::is_focus_visible), so a clicked control stays calm under the pointer.
145    pub fn pressable_states(&mut self) -> Vec<State> {
146        let visible = self.is_focus_visible();
147        let mut states = self.states();
148        if !visible {
149            states.retain(|state| *state != State::Focus);
150        }
151        states
152    }
153
154    /// Hover, focus and pressed states of this widget.
155    pub fn states(&mut self) -> Vec<State> {
156        let mut states = Vec::new();
157        if self.is_hovered() {
158            states.push(State::Hover);
159        }
160        if self.is_focused() {
161            states.push(State::Focus);
162        }
163        if self.is_pressed() {
164            states.push(State::Pressed);
165        }
166        states
167    }
168
169    /// The theme style of `widget.variant` in `states`, evaluated for this frame. Animated
170    /// styles schedule the next frame.
171    pub fn style(&mut self, widget: &str, variant: Option<&str>, states: &[State]) -> WidgetStyle {
172        let props = self.env.theme().style(widget, variant, states);
173        let style = WidgetStyle::new(props, self.pulse_phase());
174        if style.is_animated() && !self.env.reduced_motion() {
175            self.request_frame_in(PULSE_FRAME);
176        }
177        style
178    }
179
180    /// A theme colour token such as `"accent"`; black when the token does not exist.
181    #[must_use]
182    pub fn color(&self, token: &str) -> Rgb {
183        self.env.theme().color(token).unwrap_or(Rgb::new(0, 0, 0))
184    }
185
186    /// Asks for another frame after `delay`.
187    pub fn request_frame_in(&mut self, delay: Duration) {
188        self.frame.schedule(self.now + delay);
189    }
190
191    /// Whether the user asked for reduced motion; animations should show their end state.
192    #[must_use]
193    pub fn reduced_motion(&self) -> bool {
194        self.env.reduced_motion()
195    }
196
197    /// A value of this widget that moves towards `target` over `duration`. The value named
198    /// `name` starts at its first target without animating; later target changes animate from
199    /// wherever the value is. Schedules frames while it moves; returns `target` at once when
200    /// motion is reduced.
201    pub fn animate(&mut self, name: &'static str, target: f32, duration: Duration, easing: Easing) -> f32 {
202        if self.env.reduced_motion() {
203            return target;
204        }
205        let now = self.now;
206        let persistent = self.scope.is_some();
207        let tween = self.memory.get::<Tweens>(self.id, persistent).drive(name, target, now, duration, easing);
208        if tween.is_running(now) {
209            self.request_frame_in(ANIMATION_FRAME);
210        }
211        tween.value(now)
212    }
213
214    /// Eased progress from 0 to 1 of something that started at `start` (a time from
215    /// [`PaintCx::now`] or [`EventCx::now`](super::EventCx::now)) and takes `duration`. Schedules frames until it
216    /// completes; is 1 at once when motion is reduced.
217    pub fn progress_since(&mut self, start: Duration, duration: Duration, easing: Easing) -> f32 {
218        if self.env.reduced_motion() || duration.is_zero() {
219            return 1.0;
220        }
221        let elapsed = self.now.saturating_sub(start);
222        if elapsed >= duration {
223            return 1.0;
224        }
225        self.request_frame_in(ANIMATION_FRAME);
226        easing.apply(elapsed.as_secs_f32() / duration.as_secs_f32())
227    }
228
229    /// Where a repeating animation of length `period` is, `0.0..1.0`. Schedules smooth frames;
230    /// always 0 when motion is reduced.
231    pub fn cycle(&mut self, period: Duration) -> f32 {
232        let period = period.as_millis().max(1);
233        if self.env.reduced_motion() {
234            return 0.0;
235        }
236        self.request_frame_in(ANIMATION_FRAME);
237        (self.now.as_millis() % period) as f32 / period as f32
238    }
239
240    /// How many whole `interval`s have passed, for animations that jump between frames such as
241    /// spinners. Schedules a frame exactly at the next step; always 0 when motion is reduced.
242    pub fn ticks(&mut self, interval: Duration) -> u128 {
243        let interval_ms = interval.as_millis().max(1);
244        if self.env.reduced_motion() {
245            return 0;
246        }
247        let elapsed = self.now.as_millis();
248        let into = u64::try_from(elapsed % interval_ms).unwrap_or(0);
249        self.request_frame_in(Duration::from_millis(u64::try_from(interval_ms).unwrap_or(u64::MAX) - into));
250        elapsed / interval_ms
251    }
252
253    /// Where the theme pulse is, `0.0..1.0`; always 0 when motion is reduced.
254    #[must_use]
255    pub fn pulse_phase(&self) -> f32 {
256        if self.env.reduced_motion() {
257            return 0.0;
258        }
259        let period = self.env.theme().motion().pulse_period.as_secs_f64();
260        let phase = self.now.as_secs_f64().rem_euclid(period) / period;
261        // `phase` is in 0..1, which f32 represents closely enough for colour blending.
262        phase as f32
263    }
264
265    /// The visible area of this widget.
266    #[must_use]
267    pub fn clip(&self) -> Rect {
268        self.clip
269    }
270
271    /// Whether pasting would insert text, as far as the runtime knows; see `probe_clipboard` on
272    /// [`EventCx`](super::EventCx).
273    pub(crate) fn can_paste(&self) -> bool {
274        self.interaction.can_paste
275    }
276
277    /// Makes `rect` clickable for this widget. Later registrations are on top.
278    pub fn register_hit(&mut self, rect: Rect) {
279        self.register_hit_as(rect, self.id);
280    }
281
282    /// Makes `rect` clickable for `id`, a target the runtime handles itself (such as a toast)
283    /// rather than a widget in the view.
284    pub(crate) fn register_hit_as(&mut self, rect: Rect, id: WidgetId) {
285        if let Some(visible) = self.visible_part(rect) {
286            self.frame.hits.push((visible, id));
287        }
288    }
289
290    /// Asks for the pointer to take `shape` while it is over `rect`, for this frame, such as a
291    /// resize arrow over a window's edge. The last shape asked for over a cell wins, but only
292    /// from this widget or one around the widget the pointer is on: a surface painted on top
293    /// hides the shapes of what lies beneath it. A widget that asks for a shape over part of
294    /// itself asks for [`PointerShape::Default`] over the whole of it first. While this widget
295    /// holds the pointer (a drag it captured) and the pointer is outside every area it asked
296    /// for, the first shape it asked for in the frame holds, so a widget in the middle of a
297    /// drag asks for the drag's shape first and the pointer keeps it however far it goes.
298    ///
299    /// The runtime tells the terminal only when the shape under the pointer changes, and only a
300    /// terminal known to understand it (see [`PointerShape`]); elsewhere asking costs nothing.
301    pub fn pointer_shape(&mut self, rect: Rect, shape: PointerShape) {
302        if let Some(visible) = self.visible_part(rect) {
303            self.frame.pointer_shapes.push((visible, shape, self.id));
304        }
305    }
306
307    /// Keeps mouse text selection from starting in `rect`, even inside a selectable area, e.g.
308    /// for a secret shown in a selectable log.
309    pub fn unselectable(&mut self, rect: Rect) {
310        if let Some(visible) = self.visible_part(rect) {
311            self.frame.unselectable.push(visible);
312        }
313    }
314
315    /// Makes the visible part of `rect` a text selection region of this widget: a mouse drag
316    /// that starts inside it, and that no widget uses, selects text, and the selection stays
317    /// within this widget. Nothing is selectable unless a widget or
318    /// [`NodeMut::selectable`](crate::widget::NodeMut::selectable) asks; widgets whose text is
319    /// content to copy (code, documents, terminal output) call this while painting. The
320    /// innermost region under a press wins.
321    pub fn selectable(&mut self, rect: Rect) {
322        if let Some(visible) = self.visible_part(rect) {
323            self.frame.selectable.push((visible, self.id));
324        }
325    }
326
327    /// Marks the cells of `rect` as decoration rather than content, such as a scrollbar a widget
328    /// draws itself: a clean copy of a text selection leaves them out, a raw copy keeps them.
329    /// [`PaintCx::pillar`] marks its cell by itself.
330    pub fn decoration(&mut self, rect: Rect) {
331        if let Some(visible) = self.visible_part(rect) {
332            self.frame.decorations.push(visible);
333        }
334    }
335
336    /// Asks for [`MouseKind::Moved`](crate::event::MouseKind::Moved) events in this frame: the
337    /// pointer moving with no button held over this widget's hit area, or over a child of it.
338    /// Other widgets never see plain moves. Widgets that follow the pointer without a button,
339    /// such as an embedded terminal whose program asked for every motion,
340    /// call this while painting, and only while they need it.
341    pub fn track_pointer_moves(&mut self) {
342        self.frame.pointer_moves.push(self.id);
343    }
344
345    /// Shows this widget a mouse press inside it before the widgets inside it, for this frame:
346    /// a window takes an alt-drag on its body even from a terminal whose program reads the
347    /// mouse. The press arrives with [`EventCx::is_preview`](super::EventCx::is_preview) set;
348    /// using it keeps it from everything else, otherwise it goes on as usual. Outer widgets
349    /// see it first.
350    pub(crate) fn preview_presses(&mut self) {
351        self.frame.press_previews.push(self.id);
352    }
353
354    /// Adds this widget to the keyboard focus order.
355    pub fn register_focusable(&mut self) {
356        self.frame.focusable.push(self.id);
357    }
358
359    /// Paints this widget's overlay after the rest of the view.
360    pub fn request_overlay(&mut self, anchor: Rect) {
361        self.frame.overlays.push((self.id, anchor));
362    }
363
364    /// Makes this widget a modal layer for this frame and returns the time the layer opened.
365    ///
366    /// Call it from [`Widget::paint_overlay`](crate::widget::Widget::paint_overlay) every frame
367    /// the layer is shown, before painting what is inside it. Modal and dismissable layers
368    /// ([`PaintCx::register_dismissable`]) share one stack in paint order; the rules of a modal
369    /// layer are:
370    ///
371    /// - **Focus stays inside.** Each frame the layer asks, like
372    ///   [`PaintCx::request_focus_within`], for focus to be inside it; a request made later in
373    ///   the frame by a widget inside the layer (a popover opening) wins. Tab and Shift+Tab
374    ///   cycle only through the layer's widgets.
375    /// - **Input stops at the layer.** Keys and pointer events bubble from their target up to
376    ///   the topmost modal layer and no further; a press outside it lands on the layer itself.
377    ///   Paint a hit area over the whole screen and use presses on it, so nothing beneath
378    ///   reacts and no text selection starts there.
379    /// - **Shortcuts pause.** Application keymap actions (and global ones the runtime passes to
380    ///   the application) do not run; quit, focus moves, debug, copy and paste still do. Key
381    ///   listeners outside the layer are not heard.
382    /// - **Above it:** dismissable layers opened inside it, later modal layers, and the
383    ///   runtime's toasts, which stay clickable.
384    /// - **Focus comes back.** When the layer is no longer painted, the runtime requests focus
385    ///   for the widget that had it when the layer opened.
386    ///
387    /// The opening time stays the same for as long as the layer is shown, which makes it the
388    /// start of an entrance animation, even for layers inside persistent pages.
389    pub fn open_layer(&mut self) -> Duration {
390        self.frame.layers.push(LayerEntry { id: self.id, modal: true, surface: None });
391        self.request_focus_within(self.id);
392        self.interaction.layers.iter().find(|layer| layer.id == self.id).map_or(self.now, |layer| layer.opened)
393    }
394
395    /// Tells the runtime where the surface of the modal layer this widget opened with
396    /// [`PaintCx::open_layer`] sits, so toasts can keep clear of it.
397    pub(crate) fn set_layer_surface(&mut self, surface: Rect) {
398        let id = self.id;
399        if let Some(layer) = self.frame.layers.iter_mut().rev().find(|layer| layer.id == id) {
400            layer.surface = Some(surface);
401        }
402    }
403
404    /// The areas of the modal layers open in this frame: each layer's surface, or all of
405    /// `screen` for a layer that did not say where its surface is.
406    pub(crate) fn modal_surfaces(&self, screen: Rect) -> Vec<Rect> {
407        self.frame.layers.iter().filter(|layer| layer.modal).map(|layer| layer.surface.unwrap_or(screen)).collect()
408    }
409
410    /// Delivers `chord` to this widget even when it is not focused, for as long as the widget
411    /// is painted: after the focused widgets had their chance and before the keymap. Repeats
412    /// and releases of the key (which keyboards report while it is held) are delivered too,
413    /// including repeats of Enter and Space that focused widgets never see. Inside a modal
414    /// layer only listeners within the topmost layer hear keys.
415    pub fn listen_key(&mut self, chord: KeyChord) {
416        self.frame.listeners.push((chord, self.id));
417    }
418
419    /// Says that this widget takes typed text, as a text field or a terminal does. Call it on
420    /// every paint.
421    ///
422    /// Keys given to such a widget are never guessed to be a held key: every Enter and Space
423    /// reaches it however soon it follows the one before, and so do the repeats a terminal with
424    /// the kitty keyboard protocol reports. Text that arrives in one read (from a terminal
425    /// multiplexer, a slow connection or dictation) has its keys closer together than a person
426    /// types, and every one of them is text.
427    ///
428    /// Other widgets never see an Enter or Space the terminal reports as a repeat, nor one that
429    /// follows the same key within 100 ms with no other key between, so a held key presses a
430    /// button once on terminals that send a hold as fast presses.
431    ///
432    /// A widget painted with focus lent by its container, which forwards its keys to it, marks
433    /// that container as well.
434    pub fn takes_text(&mut self) {
435        self.frame.text_takers.push(self.id);
436        if self.focus_lent
437            && let Some(focused) = self.interaction.focused
438            && focused != self.id
439            && self.frame.is_within(self.id, focused)
440        {
441            self.frame.text_takers.push(focused);
442        }
443    }
444
445    /// Asks the nearest [`ScrollView`](crate::widgets::ScrollView) around this widget to scroll
446    /// just enough to show `rect`, a part of this widget's area, e.g. a line a code view jumps
447    /// to. The view glides there, or jumps when motion is reduced. Ask once when what should
448    /// be shown changes, not every frame, or the user could not scroll away from it.
449    pub fn reveal(&mut self, rect: Rect) {
450        self.frame.reveals.push((self.id, rect));
451    }
452
453    /// Blends the text and background colours already drawn in `rect` towards `color` by
454    /// `amount` (0 keeps them, 1 replaces them), e.g. to dim the screen behind a dialog. Every
455    /// frame is painted in full colour and reduced to the terminal's palette only once complete,
456    /// so the blend holds at every colour depth; a cell drawn directly in a palette colour cannot
457    /// be blended and takes `color` once `amount` passes one half. Over a picture a terminal
458    /// draws itself, an `Image` on a kitty terminal, the blend is recorded, so the picture
459    /// shows dimmed beneath it rather than hidden.
460    pub fn tint(&mut self, rect: Rect, color: Rgb, amount: f32) {
461        let amount = amount.clamp(0.0, 1.0);
462        if amount <= 0.0 {
463            return;
464        }
465        let blend = |current: Color| match current {
466            Color::Rgb(r, g, b) => Some(to_color(Rgb::new(r, g, b).mix(color, amount))),
467            _ if amount > 0.5 => Some(to_color(color)),
468            _ => None,
469        };
470        // A picture beneath learns it was dimmed from this record, not from its colours.
471        #[cfg(feature = "image")]
472        if !self.frame.pictures.is_empty() {
473            let reached = rect.intersect(self.clip);
474            if !reached.is_empty() {
475                let over = self.frame.pictures.len();
476                self.frame.dims.push(crate::widgets::image::Dim::new(reached, color, amount, over));
477            }
478        }
479        self.each_cell(rect, |cell| {
480            if let Some(fg) = blend(cell.fg) {
481                cell.fg = fg;
482            }
483            if let Some(bg) = blend(cell.bg) {
484                cell.bg = bg;
485            }
486        });
487    }
488
489    /// Blends the background of `rect` towards `color` by `amount`, keeping every glyph and its
490    /// colour, e.g. the tone a [`Ghost`](crate::widgets::Ghost) lays over the ground. Below true
491    /// colour a faint blend over each cell would round back to the cell's own colour, so the
492    /// cells take the colour mixed into the theme's canvas instead, and text that would no longer
493    /// read on it is brightened.
494    pub(crate) fn tint_ground(&mut self, rect: Rect, color: Rgb, amount: f32) {
495        let amount = amount.clamp(0.0, 1.0);
496        if amount <= 0.0 {
497            return;
498        }
499        if self.env.depth() != ColorDepth::TrueColor {
500            let ground = self.color("canvas").mix(color, amount);
501            let readable = self.color("text");
502            self.fill_keeping_text_readable(rect, ground, readable);
503            return;
504        }
505        self.each_cell(rect, |cell| {
506            if let Color::Rgb(r, g, b) = cell.bg {
507                cell.bg = to_color(Rgb::new(r, g, b).mix(color, amount));
508            }
509        });
510    }
511
512    /// This widget's state of type `T`.
513    pub fn memory<T: Default + 'static>(&mut self) -> &mut T {
514        self.memory.get::<T>(self.id, self.scope.is_some())
515    }
516
517    /// Fills `rect` with `color`, keeping text.
518    pub fn fill(&mut self, rect: Rect, color: Rgb) {
519        let bg = to_color(color);
520        self.each_cell(rect, |cell| cell.bg = bg);
521    }
522
523    /// Puts `color` behind the cells of `rect`, keeping their glyphs. A cell whose text would no
524    /// longer read on it (contrast below 3:1) takes `readable` instead, so faint text stays legible
525    /// under a highlight such as a text selection.
526    pub(crate) fn fill_keeping_text_readable(&mut self, rect: Rect, color: Rgb, readable: Rgb) {
527        let (bg, text) = (to_color(color), to_color(readable));
528        self.each_cell(rect, |cell| {
529            cell.bg = bg;
530            if let Color::Rgb(r, g, b) = cell.fg
531                && cell.symbol().trim() != ""
532                && Rgb::new(r, g, b).contrast_ratio(color) < 3.0
533            {
534                cell.fg = text;
535            }
536        });
537    }
538
539    /// Clears `rect` to spaces on `color`.
540    pub fn clear(&mut self, rect: Rect, color: Rgb) {
541        // An empty cell reads as a space and equals a cell holding one; copying a prepared blank
542        // cell is cheaper than resetting, writing and styling every cell.
543        let mut blank = Cell::EMPTY;
544        blank.bg = to_color(color);
545        // Cells inside the rectangle are all replaced; only a wide character crossing its left
546        // or right edge would be cut in half.
547        let area = rect.intersect(self.clip);
548        if !area.is_empty() {
549            for y in area.y..area.bottom() {
550                self.release(area.x, y);
551                self.release(area.right() - 1, y);
552            }
553        }
554        self.each_cell(rect, |cell| cell.clone_from(&blank));
555    }
556
557    /// Draws the theme's pillar (`[icons] pillar`, e.g. `▌`) at `(x, y)` in `color`, over whatever
558    /// surface is already there. A blank glyph, as in ASCII mode, becomes a cell of `color`. The
559    /// cell is [decoration](PaintCx::decoration): clean copies of a text selection skip it.
560    pub fn pillar(&mut self, x: i32, y: i32, color: Rgb) {
561        self.decoration(Rect::new(x, y, 1, 1));
562        let env = self.env;
563        let glyph = env.icons().glyph(PILLAR);
564        if glyph.trim().is_empty() {
565            self.fill(Rect::new(x, y, 1, 1), color);
566        } else {
567            self.text(x, y, &glyph, CellStyle::fg(color), 1);
568        }
569    }
570
571    /// Draws `text` starting at `(x, y)`, at most `max` cells wide, clipped to the visible area.
572    /// Returns the number of cells the text occupies (before clipping).
573    ///
574    /// In ASCII glyph mode an [`ELLIPSIS`](text::ELLIPSIS) is drawn as
575    /// [`ASCII_ELLIPSIS`](text::ASCII_ELLIPSIS), in the same single cell. Every widget cuts text
576    /// with [`text::truncate`] or [`text::truncate_middle`] and draws it through here, so this one
577    /// place gives every cut an ASCII mark; a `…` written by the application itself is changed
578    /// too, since an ASCII terminal could not show it either way.
579    pub fn text(&mut self, x: i32, y: i32, text: &str, style: CellStyle, max: u16) -> u16 {
580        let paint = style.paint();
581        let clip = self.clip;
582        let limit = x + i32::from(max);
583        let mut column = x;
584        // Printable ASCII is one cell per byte. The byte after the last one that fits is checked
585        // too: a combining mark there would join the last character drawn.
586        let drawn = text.len().min(usize::from(max));
587        if text.get(..text.len().min(drawn + 1)).is_some_and(text::is_printable_ascii) {
588            for index in 0..drawn {
589                if clip.contains(column, y) {
590                    self.release(column, y);
591                }
592                if clip.contains(column, y)
593                    && let Some(cell) = self.cell_mut(column, y)
594                {
595                    cell.set_symbol(&text[index..=index]);
596                    paint.apply(cell);
597                }
598                column += 1;
599            }
600            return clamp_u16(column - x);
601        }
602        let ascii = self.env.icons().mode() == GlyphMode::Ascii;
603        for grapheme in text.graphemes(true) {
604            let width = i32::from(text::grapheme_width(grapheme));
605            if width == 0 {
606                continue;
607            }
608            if column + width > limit {
609                break;
610            }
611            // Every cell of the grapheme is released before any is written, so its own second
612            // half is not mistaken for part of a character it cut.
613            for offset in 0..width {
614                if clip.contains(column + offset, y) {
615                    self.release(column + offset, y);
616                }
617            }
618            for offset in 0..width {
619                let cx = column + offset;
620                if !clip.contains(cx, y) {
621                    continue;
622                }
623                let whole = clip.contains(column, y) && clip.contains(column + width - 1, y);
624                if let Some(cell) = self.cell_mut(cx, y) {
625                    let symbol = match (offset, whole) {
626                        // A cell cannot hold a control character: a terminal would act on it
627                        // rather than show it. It keeps the cell it is measured at, blank.
628                        (0, true) if grapheme.chars().any(char::is_control) => " ",
629                        (0, true) if ascii && grapheme == text::ELLIPSIS => text::ASCII_ELLIPSIS,
630                        (0, true) => grapheme,
631                        (_, true) => "",
632                        _ => " ",
633                    };
634                    cell.set_symbol(symbol);
635                    paint.apply(cell);
636                }
637            }
638            column += width;
639        }
640        clamp_u16(column - x)
641    }
642
643    /// Paints a child node into `rect`, applying its padding.
644    pub fn paint_child<M: 'static>(&mut self, node: &Node<M>, rect: Rect) {
645        self.paint_child_spilling(node, rect, rect);
646    }
647
648    /// Paints a child node into `rect` like [`PaintCx::paint_child`], letting it draw anywhere in
649    /// `visible` (which holds `rect`), e.g. a placed window's shadow just past its edges.
650    pub(crate) fn paint_child_spilling<M: 'static>(&mut self, node: &Node<M>, rect: Rect, visible: Rect) {
651        let saved = (self.id, self.layout, self.clip, self.scope);
652        self.id = node.id;
653        self.layout = node.layout;
654        if node.persistent {
655            self.scope = Some(node.id);
656        }
657        self.clip = saved.2.intersect(visible);
658        self.frame.rects.insert(node.id, rect);
659        self.frame.parents.insert(node.id, saved.0);
660        if let Key::Named(name) = &node.key {
661            self.frame.names.insert(node.id, name.clone());
662        }
663        if let Some(scope) = self.scope {
664            self.frame.scopes.insert(node.id, scope);
665        }
666        self.memory.touch(node.id, self.scope.is_some());
667        match node.selectable {
668            Some(true) => self.selectable(rect),
669            Some(false) => self.unselectable(rect),
670            None => {}
671        }
672        if node.widget.focusable() {
673            self.register_focusable();
674        }
675        node.widget.paint(self, rect.inset(node.layout.padding));
676        (self.id, self.layout, self.clip, self.scope) = saved;
677    }
678
679    /// Paints a child node that does not take keyboard focus itself, not even when it is
680    /// normally focusable. For composite widgets that take focus as one control and pass keys on
681    /// to the child with [`EventCx::forward`](super::EventCx::forward), such as a settings row's switch.
682    pub fn paint_child_unfocusable<M: 'static>(&mut self, node: &Node<M>, rect: Rect) {
683        let registered = self.frame.focusable.len();
684        self.paint_child(node, rect);
685        self.frame.focusable.truncate(registered);
686    }
687
688    /// Paints a child like [`paint_child_unfocusable`](Self::paint_child_unfocusable) while
689    /// `focused` lends it this container's focus: a container that forwards its keys to the
690    /// child's widgets paints them focused, so they draw their focus and keep what focus keeps,
691    /// such as the half-typed part of a time.
692    pub(crate) fn paint_child_lending_focus<M: 'static>(&mut self, node: &Node<M>, rect: Rect, focused: bool) {
693        let lent = self.focus_lent;
694        self.focus_lent = lent || focused;
695        self.paint_child_unfocusable(node, rect);
696        self.focus_lent = lent;
697    }
698
699    /// The pointer cell, when the pointer is over this widget or over a widget inside it, for
700    /// containers whose rows light up while the pointer is on a control within them.
701    #[must_use]
702    pub fn pointer_within(&self) -> Option<(i32, i32)> {
703        self.interaction.pointer.filter(|_| self.interaction.hovered_chain.contains(&self.id))
704    }
705
706    /// Whether keyboard focus is on this widget or on a widget painted inside it so far in this
707    /// frame. Paint children before asking, e.g. to brighten a field label while its control has
708    /// focus.
709    #[must_use]
710    pub fn has_focus_within(&self) -> bool {
711        self.interaction.focused.is_some_and(|focused| self.frame.is_within(focused, self.id))
712    }
713
714    /// Measures a child node with the same rules as layout.
715    pub fn measure_child<M: 'static>(&mut self, node: &Node<M>, available: Size) -> Size {
716        MeasureCx::for_frame(self.env, &mut self.frame.measures).measure_child(node, available)
717    }
718
719    /// Runs `paint` with drawing limited to `rect` (and the current visible area).
720    pub fn with_clip(&mut self, rect: Rect, paint: impl FnOnce(&mut Self)) {
721        let saved = self.clip;
722        self.clip = saved.intersect(rect);
723        paint(self);
724        self.clip = saved;
725    }
726
727    /// The part of `rect` inside the visible area, when there is one.
728    fn visible_part(&self, rect: Rect) -> Option<Rect> {
729        Some(rect.intersect(self.clip)).filter(|visible| !visible.is_empty())
730    }
731
732    /// Runs `change` on every cell of `rect` inside the visible area and the screen.
733    pub(super) fn each_cell(&mut self, rect: Rect, mut change: impl FnMut(&mut Cell)) {
734        let area = rect.intersect(self.clip);
735        for y in area.y..area.bottom() {
736            for x in area.x..area.right() {
737                if let Some(cell) = self.cell_mut(x, y) {
738                    change(cell);
739                }
740            }
741        }
742    }
743
744    /// Runs `paint` on every cell of `rect` inside the visible area and the screen, with its
745    /// column and row counted from the corner of `rect`, for a widget that writes a block of cells
746    /// itself. A wide character crossing the left or right edge is released first, as
747    /// [`PaintCx::clear`] does.
748    #[cfg(feature = "image")]
749    pub(crate) fn each_cell_within(&mut self, rect: Rect, mut paint: impl FnMut(u16, u16, &mut Cell)) {
750        let area = rect.intersect(self.clip);
751        if area.is_empty() {
752            return;
753        }
754        for y in area.y..area.bottom() {
755            self.release(area.x, y);
756            self.release(area.right() - 1, y);
757            for x in area.x..area.right() {
758                let (column, row) = (clamp_u16(x - rect.x), clamp_u16(y - rect.y));
759                if let Some(cell) = self.cell_mut(x, y) {
760                    paint(column, row, cell);
761                }
762            }
763        }
764    }
765
766    /// Keeps wide characters whole before a new symbol lands on `(x, y)`.
767    ///
768    /// A terminal draws a double-width character from its first cell across the next one, and the
769    /// buffer holds it as the glyph followed by an empty second half. Replacing only one half, as a
770    /// dialog's pillar or edge drawn over text does, leaves a pair the terminal cannot show: the
771    /// glyph spills over the new symbol, or the rest of the row shifts by a column. So when the
772    /// cell belongs to a wide character that reaches beyond it, every other cell of that
773    /// character becomes a blank in the colours it already had. The cell may lie outside the
774    /// visible area: a layer's edge decides what happens to the character it cuts.
775    fn release(&mut self, x: i32, y: i32) {
776        // The character this cell is the second half of: the nearest non-empty cell to the left.
777        let mut lead = None;
778        for back in 1..=MAX_GLYPH_CELLS {
779            let Some(cell) = self.cell_mut(x - back, y) else { break };
780            let symbol = cell.symbol();
781            if symbol.is_empty() {
782                continue;
783            }
784            let cells = i32::from(text::width(symbol));
785            if cells > back {
786                lead = Some((x - back, cells));
787            }
788            break;
789        }
790        if let Some((start, cells)) = lead {
791            self.blank_cells(start, start + cells, y);
792        }
793        let own = self.cell_mut(x, y).map_or(1, |cell| i32::from(text::width(cell.symbol())));
794        if own > 1 {
795            self.blank_cells(x + 1, x + own, y);
796        }
797    }
798
799    /// Turns the cells from `start` up to `end` on row `y` into spaces, keeping their colours.
800    fn blank_cells(&mut self, start: i32, end: i32, y: i32) {
801        for x in start..end {
802            if let Some(cell) = self.cell_mut(x, y) {
803                cell.set_symbol(" ");
804            }
805        }
806    }
807
808    /// The screen cell at `(x, y)`, when it is on screen.
809    fn cell_mut(&mut self, x: i32, y: i32) -> Option<&mut Cell> {
810        self.buf.cell_mut((u16::try_from(x).ok()?, u16::try_from(y).ok()?))
811    }
812}
813
814/// The most cells one grapheme covers on screen; how far to look left for the start of the
815/// character a cell belongs to.
816const MAX_GLYPH_CELLS: i32 = 4;
817
818/// Frame interval while something moves: about 60 frames a second.
819pub(crate) const ANIMATION_FRAME: Duration = Duration::from_millis(16);
820
821/// How often animated theme colours are redrawn.
822pub(crate) const PULSE_FRAME: Duration = Duration::from_millis(50);