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, 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 /// Keeps mouse text selection from starting in `rect`, even inside a selectable area, e.g.
291 /// for a secret shown in a selectable log.
292 pub fn unselectable(&mut self, rect: Rect) {
293 if let Some(visible) = self.visible_part(rect) {
294 self.frame.unselectable.push(visible);
295 }
296 }
297
298 /// Makes the visible part of `rect` a text selection region of this widget: a mouse drag
299 /// that starts inside it, and that no widget uses, selects text, and the selection stays
300 /// within this widget. Nothing is selectable unless a widget or
301 /// [`NodeMut::selectable`](crate::widget::NodeMut::selectable) asks; widgets whose text is
302 /// content to copy (code, documents, terminal output) call this while painting. The
303 /// innermost region under a press wins.
304 pub fn selectable(&mut self, rect: Rect) {
305 if let Some(visible) = self.visible_part(rect) {
306 self.frame.selectable.push((visible, self.id));
307 }
308 }
309
310 /// Marks the cells of `rect` as decoration rather than content, such as a scrollbar a widget
311 /// draws itself: a clean copy of a text selection leaves them out, a raw copy keeps them.
312 /// [`PaintCx::pillar`] marks its cell by itself.
313 pub fn decoration(&mut self, rect: Rect) {
314 if let Some(visible) = self.visible_part(rect) {
315 self.frame.decorations.push(visible);
316 }
317 }
318
319 /// Asks for [`MouseKind::Moved`](crate::event::MouseKind::Moved) events in this frame: the
320 /// pointer moving with no button held over this widget's hit area, or over a child of it.
321 /// Other widgets never see plain moves. Widgets that follow the pointer without a button,
322 /// such as an embedded terminal whose program asked for every motion,
323 /// call this while painting, and only while they need it.
324 pub fn track_pointer_moves(&mut self) {
325 self.frame.pointer_moves.push(self.id);
326 }
327
328 /// Shows this widget a mouse press inside it before the widgets inside it, for this frame:
329 /// a window takes an alt-drag on its body even from a terminal whose program reads the
330 /// mouse. The press arrives with [`EventCx::is_preview`](super::EventCx::is_preview) set;
331 /// using it keeps it from everything else, otherwise it goes on as usual. Outer widgets
332 /// see it first.
333 pub(crate) fn preview_presses(&mut self) {
334 self.frame.press_previews.push(self.id);
335 }
336
337 /// Adds this widget to the keyboard focus order.
338 pub fn register_focusable(&mut self) {
339 self.frame.focusable.push(self.id);
340 }
341
342 /// Paints this widget's overlay after the rest of the view.
343 pub fn request_overlay(&mut self, anchor: Rect) {
344 self.frame.overlays.push((self.id, anchor));
345 }
346
347 /// Makes this widget a modal layer for this frame and returns the time the layer opened.
348 ///
349 /// Call it from [`Widget::paint_overlay`](crate::widget::Widget::paint_overlay) every frame
350 /// the layer is shown, before painting what is inside it. Modal and dismissable layers
351 /// ([`PaintCx::register_dismissable`]) share one stack in paint order; the rules of a modal
352 /// layer are:
353 ///
354 /// - **Focus stays inside.** Each frame the layer asks, like
355 /// [`PaintCx::request_focus_within`], for focus to be inside it; a request made later in
356 /// the frame by a widget inside the layer (a popover opening) wins. Tab and Shift+Tab
357 /// cycle only through the layer's widgets.
358 /// - **Input stops at the layer.** Keys and pointer events bubble from their target up to
359 /// the topmost modal layer and no further; a press outside it lands on the layer itself.
360 /// Paint a hit area over the whole screen and use presses on it, so nothing beneath
361 /// reacts and no text selection starts there.
362 /// - **Shortcuts pause.** Application keymap actions (and global ones the runtime passes to
363 /// the application) do not run; quit, focus moves, debug, copy and paste still do. Key
364 /// listeners outside the layer are not heard.
365 /// - **Above it:** dismissable layers opened inside it, later modal layers, and the
366 /// runtime's toasts, which stay clickable.
367 /// - **Focus comes back.** When the layer is no longer painted, the runtime requests focus
368 /// for the widget that had it when the layer opened.
369 ///
370 /// The opening time stays the same for as long as the layer is shown, which makes it the
371 /// start of an entrance animation, even for layers inside persistent pages.
372 pub fn open_layer(&mut self) -> Duration {
373 self.frame.layers.push(LayerEntry { id: self.id, modal: true, surface: None });
374 self.request_focus_within(self.id);
375 self.interaction.layers.iter().find(|layer| layer.id == self.id).map_or(self.now, |layer| layer.opened)
376 }
377
378 /// Tells the runtime where the surface of the modal layer this widget opened with
379 /// [`PaintCx::open_layer`] sits, so toasts can keep clear of it.
380 pub(crate) fn set_layer_surface(&mut self, surface: Rect) {
381 let id = self.id;
382 if let Some(layer) = self.frame.layers.iter_mut().rev().find(|layer| layer.id == id) {
383 layer.surface = Some(surface);
384 }
385 }
386
387 /// The areas of the modal layers open in this frame: each layer's surface, or all of
388 /// `screen` for a layer that did not say where its surface is.
389 pub(crate) fn modal_surfaces(&self, screen: Rect) -> Vec<Rect> {
390 self.frame.layers.iter().filter(|layer| layer.modal).map(|layer| layer.surface.unwrap_or(screen)).collect()
391 }
392
393 /// Delivers `chord` to this widget even when it is not focused, for as long as the widget
394 /// is painted: after the focused widgets had their chance and before the keymap. Repeats
395 /// and releases of the key (which keyboards report while it is held) are delivered too,
396 /// including repeats of Enter and Space that focused widgets never see. Inside a modal
397 /// layer only listeners within the topmost layer hear keys.
398 pub fn listen_key(&mut self, chord: KeyChord) {
399 self.frame.listeners.push((chord, self.id));
400 }
401
402 /// Asks the nearest [`ScrollView`](crate::widgets::ScrollView) around this widget to scroll
403 /// just enough to show `rect`, a part of this widget's area, e.g. a line a code view jumps
404 /// to. The view glides there, or jumps when motion is reduced. Ask once when what should
405 /// be shown changes, not every frame, or the user could not scroll away from it.
406 pub fn reveal(&mut self, rect: Rect) {
407 self.frame.reveals.push((self.id, rect));
408 }
409
410 /// Blends the text and background colours already drawn in `rect` towards `color` by
411 /// `amount` (0 keeps them, 1 replaces them), e.g. to dim the screen behind a dialog. Every
412 /// frame is painted in full colour and reduced to the terminal's palette only once complete,
413 /// so the blend holds at every colour depth; a cell drawn directly in a palette colour cannot
414 /// be blended and takes `color` once `amount` passes one half.
415 pub fn tint(&mut self, rect: Rect, color: Rgb, amount: f32) {
416 let amount = amount.clamp(0.0, 1.0);
417 if amount <= 0.0 {
418 return;
419 }
420 let blend = |current: Color| match current {
421 Color::Rgb(r, g, b) => Some(to_color(Rgb::new(r, g, b).mix(color, amount))),
422 _ if amount > 0.5 => Some(to_color(color)),
423 _ => None,
424 };
425 self.each_cell(rect, |cell| {
426 if let Some(fg) = blend(cell.fg) {
427 cell.fg = fg;
428 }
429 if let Some(bg) = blend(cell.bg) {
430 cell.bg = bg;
431 }
432 });
433 }
434
435 /// Blends the background of `rect` towards `color` by `amount`, keeping every glyph and its
436 /// colour, e.g. the tone a [`Ghost`](crate::widgets::Ghost) lays over the ground. Below true
437 /// colour a faint blend over each cell would round back to the cell's own colour, so the
438 /// cells take the colour mixed into the theme's canvas instead, and text that would no longer
439 /// read on it is brightened.
440 pub(crate) fn tint_ground(&mut self, rect: Rect, color: Rgb, amount: f32) {
441 let amount = amount.clamp(0.0, 1.0);
442 if amount <= 0.0 {
443 return;
444 }
445 if self.env.depth() != ColorDepth::TrueColor {
446 let ground = self.color("canvas").mix(color, amount);
447 let readable = self.color("text");
448 self.fill_keeping_text_readable(rect, ground, readable);
449 return;
450 }
451 self.each_cell(rect, |cell| {
452 if let Color::Rgb(r, g, b) = cell.bg {
453 cell.bg = to_color(Rgb::new(r, g, b).mix(color, amount));
454 }
455 });
456 }
457
458 /// This widget's state of type `T`.
459 pub fn memory<T: Default + 'static>(&mut self) -> &mut T {
460 self.memory.get::<T>(self.id, self.scope.is_some())
461 }
462
463 /// Fills `rect` with `color`, keeping text.
464 pub fn fill(&mut self, rect: Rect, color: Rgb) {
465 let bg = to_color(color);
466 self.each_cell(rect, |cell| cell.bg = bg);
467 }
468
469 /// Puts `color` behind the cells of `rect`, keeping their glyphs. A cell whose text would no
470 /// longer read on it (contrast below 3:1) takes `readable` instead, so faint text stays legible
471 /// under a highlight such as a text selection.
472 pub(crate) fn fill_keeping_text_readable(&mut self, rect: Rect, color: Rgb, readable: Rgb) {
473 let (bg, text) = (to_color(color), to_color(readable));
474 self.each_cell(rect, |cell| {
475 cell.bg = bg;
476 if let Color::Rgb(r, g, b) = cell.fg
477 && cell.symbol().trim() != ""
478 && Rgb::new(r, g, b).contrast_ratio(color) < 3.0
479 {
480 cell.fg = text;
481 }
482 });
483 }
484
485 /// Clears `rect` to spaces on `color`.
486 pub fn clear(&mut self, rect: Rect, color: Rgb) {
487 // An empty cell reads as a space and equals a cell holding one; copying a prepared blank
488 // cell is cheaper than resetting, writing and styling every cell.
489 let mut blank = Cell::EMPTY;
490 blank.bg = to_color(color);
491 // Cells inside the rectangle are all replaced; only a wide character crossing its left
492 // or right edge would be cut in half.
493 let area = rect.intersect(self.clip);
494 if !area.is_empty() {
495 for y in area.y..area.bottom() {
496 self.release(area.x, y);
497 self.release(area.right() - 1, y);
498 }
499 }
500 self.each_cell(rect, |cell| cell.clone_from(&blank));
501 }
502
503 /// Draws the theme's pillar (`[icons] pillar`, e.g. `▌`) at `(x, y)` in `color`, over whatever
504 /// surface is already there. A blank glyph, as in ASCII mode, becomes a cell of `color`. The
505 /// cell is [decoration](PaintCx::decoration): clean copies of a text selection skip it.
506 pub fn pillar(&mut self, x: i32, y: i32, color: Rgb) {
507 self.decoration(Rect::new(x, y, 1, 1));
508 let env = self.env;
509 let glyph = env.icons().glyph(PILLAR);
510 if glyph.trim().is_empty() {
511 self.fill(Rect::new(x, y, 1, 1), color);
512 } else {
513 self.text(x, y, &glyph, CellStyle::fg(color), 1);
514 }
515 }
516
517 /// Draws `text` starting at `(x, y)`, at most `max` cells wide, clipped to the visible area.
518 /// Returns the number of cells the text occupies (before clipping).
519 ///
520 /// In ASCII glyph mode an [`ELLIPSIS`](text::ELLIPSIS) is drawn as
521 /// [`ASCII_ELLIPSIS`](text::ASCII_ELLIPSIS), in the same single cell. Every widget cuts text
522 /// with [`text::truncate`] or [`text::truncate_middle`] and draws it through here, so this one
523 /// place gives every cut an ASCII mark; a `…` written by the application itself is changed
524 /// too, since an ASCII terminal could not show it either way.
525 pub fn text(&mut self, x: i32, y: i32, text: &str, style: CellStyle, max: u16) -> u16 {
526 let paint = style.paint();
527 let clip = self.clip;
528 let limit = x + i32::from(max);
529 let mut column = x;
530 // Printable ASCII is one cell per byte. The byte after the last one that fits is checked
531 // too: a combining mark there would join the last character drawn.
532 let drawn = text.len().min(usize::from(max));
533 if text.get(..text.len().min(drawn + 1)).is_some_and(text::is_printable_ascii) {
534 for index in 0..drawn {
535 if clip.contains(column, y) {
536 self.release(column, y);
537 }
538 if clip.contains(column, y)
539 && let Some(cell) = self.cell_mut(column, y)
540 {
541 cell.set_symbol(&text[index..=index]);
542 paint.apply(cell);
543 }
544 column += 1;
545 }
546 return clamp_u16(column - x);
547 }
548 let ascii = self.env.icons().mode() == GlyphMode::Ascii;
549 for grapheme in text.graphemes(true) {
550 let width = i32::from(text::grapheme_width(grapheme));
551 if width == 0 {
552 continue;
553 }
554 if column + width > limit {
555 break;
556 }
557 // Every cell of the grapheme is released before any is written, so its own second
558 // half is not mistaken for part of a character it cut.
559 for offset in 0..width {
560 if clip.contains(column + offset, y) {
561 self.release(column + offset, y);
562 }
563 }
564 for offset in 0..width {
565 let cx = column + offset;
566 if !clip.contains(cx, y) {
567 continue;
568 }
569 let whole = clip.contains(column, y) && clip.contains(column + width - 1, y);
570 if let Some(cell) = self.cell_mut(cx, y) {
571 let symbol = match (offset, whole) {
572 // A cell cannot hold a control character: a terminal would act on it
573 // rather than show it. It keeps the cell it is measured at, blank.
574 (0, true) if grapheme.chars().any(char::is_control) => " ",
575 (0, true) if ascii && grapheme == text::ELLIPSIS => text::ASCII_ELLIPSIS,
576 (0, true) => grapheme,
577 (_, true) => "",
578 _ => " ",
579 };
580 cell.set_symbol(symbol);
581 paint.apply(cell);
582 }
583 }
584 column += width;
585 }
586 clamp_u16(column - x)
587 }
588
589 /// Paints a child node into `rect`, applying its padding.
590 pub fn paint_child<M: 'static>(&mut self, node: &Node<M>, rect: Rect) {
591 self.paint_child_spilling(node, rect, rect);
592 }
593
594 /// Paints a child node into `rect` like [`PaintCx::paint_child`], letting it draw anywhere in
595 /// `visible` (which holds `rect`), e.g. a placed window's shadow just past its edges.
596 pub(crate) fn paint_child_spilling<M: 'static>(&mut self, node: &Node<M>, rect: Rect, visible: Rect) {
597 let saved = (self.id, self.layout, self.clip, self.scope);
598 self.id = node.id;
599 self.layout = node.layout;
600 if node.persistent {
601 self.scope = Some(node.id);
602 }
603 self.clip = saved.2.intersect(visible);
604 self.frame.rects.insert(node.id, rect);
605 self.frame.parents.insert(node.id, saved.0);
606 if let Key::Named(name) = &node.key {
607 self.frame.names.insert(node.id, name.clone());
608 }
609 if let Some(scope) = self.scope {
610 self.frame.scopes.insert(node.id, scope);
611 }
612 self.memory.touch(node.id, self.scope.is_some());
613 match node.selectable {
614 Some(true) => self.selectable(rect),
615 Some(false) => self.unselectable(rect),
616 None => {}
617 }
618 if node.widget.focusable() {
619 self.register_focusable();
620 }
621 node.widget.paint(self, rect.inset(node.layout.padding));
622 (self.id, self.layout, self.clip, self.scope) = saved;
623 }
624
625 /// Paints a child node that does not take keyboard focus itself, not even when it is
626 /// normally focusable. For composite widgets that take focus as one control and pass keys on
627 /// to the child with [`EventCx::forward`](super::EventCx::forward), such as a settings row's switch.
628 pub fn paint_child_unfocusable<M: 'static>(&mut self, node: &Node<M>, rect: Rect) {
629 let registered = self.frame.focusable.len();
630 self.paint_child(node, rect);
631 self.frame.focusable.truncate(registered);
632 }
633
634 /// Paints a child like [`paint_child_unfocusable`](Self::paint_child_unfocusable) while
635 /// `focused` lends it this container's focus: a container that forwards its keys to the
636 /// child's widgets paints them focused, so they draw their focus and keep what focus keeps,
637 /// such as the half-typed part of a time.
638 pub(crate) fn paint_child_lending_focus<M: 'static>(&mut self, node: &Node<M>, rect: Rect, focused: bool) {
639 let lent = self.focus_lent;
640 self.focus_lent = lent || focused;
641 self.paint_child_unfocusable(node, rect);
642 self.focus_lent = lent;
643 }
644
645 /// The pointer cell, when the pointer is over this widget or over a widget inside it, for
646 /// containers whose rows light up while the pointer is on a control within them.
647 #[must_use]
648 pub fn pointer_within(&self) -> Option<(i32, i32)> {
649 self.interaction.pointer.filter(|_| self.interaction.hovered_chain.contains(&self.id))
650 }
651
652 /// Whether keyboard focus is on this widget or on a widget painted inside it so far in this
653 /// frame. Paint children before asking, e.g. to brighten a field label while its control has
654 /// focus.
655 #[must_use]
656 pub fn has_focus_within(&self) -> bool {
657 self.interaction.focused.is_some_and(|focused| self.frame.is_within(focused, self.id))
658 }
659
660 /// Measures a child node with the same rules as layout.
661 pub fn measure_child<M: 'static>(&mut self, node: &Node<M>, available: Size) -> Size {
662 MeasureCx::for_frame(self.env, &mut self.frame.measures).measure_child(node, available)
663 }
664
665 /// Runs `paint` with drawing limited to `rect` (and the current visible area).
666 pub fn with_clip(&mut self, rect: Rect, paint: impl FnOnce(&mut Self)) {
667 let saved = self.clip;
668 self.clip = saved.intersect(rect);
669 paint(self);
670 self.clip = saved;
671 }
672
673 /// The part of `rect` inside the visible area, when there is one.
674 fn visible_part(&self, rect: Rect) -> Option<Rect> {
675 Some(rect.intersect(self.clip)).filter(|visible| !visible.is_empty())
676 }
677
678 /// Runs `change` on every cell of `rect` inside the visible area and the screen.
679 pub(super) fn each_cell(&mut self, rect: Rect, mut change: impl FnMut(&mut Cell)) {
680 let area = rect.intersect(self.clip);
681 for y in area.y..area.bottom() {
682 for x in area.x..area.right() {
683 if let Some(cell) = self.cell_mut(x, y) {
684 change(cell);
685 }
686 }
687 }
688 }
689
690 /// Keeps wide characters whole before a new symbol lands on `(x, y)`.
691 ///
692 /// A terminal draws a double-width character from its first cell across the next one, and the
693 /// buffer holds it as the glyph followed by an empty second half. Replacing only one half, as a
694 /// dialog's pillar or edge drawn over text does, leaves a pair the terminal cannot show: the
695 /// glyph spills over the new symbol, or the rest of the row shifts by a column. So when the
696 /// cell belongs to a wide character that reaches beyond it, every other cell of that
697 /// character becomes a blank in the colours it already had. The cell may lie outside the
698 /// visible area: a layer's edge decides what happens to the character it cuts.
699 fn release(&mut self, x: i32, y: i32) {
700 // The character this cell is the second half of: the nearest non-empty cell to the left.
701 let mut lead = None;
702 for back in 1..=MAX_GLYPH_CELLS {
703 let Some(cell) = self.cell_mut(x - back, y) else { break };
704 let symbol = cell.symbol();
705 if symbol.is_empty() {
706 continue;
707 }
708 let cells = i32::from(text::width(symbol));
709 if cells > back {
710 lead = Some((x - back, cells));
711 }
712 break;
713 }
714 if let Some((start, cells)) = lead {
715 self.blank_cells(start, start + cells, y);
716 }
717 let own = self.cell_mut(x, y).map_or(1, |cell| i32::from(text::width(cell.symbol())));
718 if own > 1 {
719 self.blank_cells(x + 1, x + own, y);
720 }
721 }
722
723 /// Turns the cells from `start` up to `end` on row `y` into spaces, keeping their colours.
724 fn blank_cells(&mut self, start: i32, end: i32, y: i32) {
725 for x in start..end {
726 if let Some(cell) = self.cell_mut(x, y) {
727 cell.set_symbol(" ");
728 }
729 }
730 }
731
732 /// The screen cell at `(x, y)`, when it is on screen.
733 fn cell_mut(&mut self, x: i32, y: i32) -> Option<&mut Cell> {
734 self.buf.cell_mut((u16::try_from(x).ok()?, u16::try_from(y).ok()?))
735 }
736}
737
738/// The most cells one grapheme covers on screen; how far to look left for the start of the
739/// character a cell belongs to.
740const MAX_GLYPH_CELLS: i32 = 4;
741
742/// Frame interval while something moves: about 60 frames a second.
743pub(crate) const ANIMATION_FRAME: Duration = Duration::from_millis(16);
744
745/// How often animated theme colours are redrawn.
746pub(crate) const PULSE_FRAME: Duration = Duration::from_millis(50);