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::Rgb;
12use crate::env::Env;
13use crate::geometry::{Rect, Size, clamp_u16};
14use crate::icons::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}
38
39impl PaintCx<'_> {
40 /// The id of the widget being painted.
41 #[must_use]
42 pub fn id(&self) -> WidgetId {
43 self.id
44 }
45
46 /// The environment.
47 #[must_use]
48 pub fn env(&self) -> &Env {
49 self.env
50 }
51
52 /// Layout properties of the widget being painted.
53 #[must_use]
54 pub fn layout(&self) -> LayoutProps {
55 self.layout
56 }
57
58 /// Time since the runtime started; drives animations.
59 #[must_use]
60 pub fn now(&self) -> Duration {
61 self.now
62 }
63
64 /// Whether the pointer is over this widget.
65 #[must_use]
66 pub fn is_hovered(&self) -> bool {
67 self.interaction.hovered == Some(self.id)
68 }
69
70 /// The widget that has keyboard focus, if any.
71 #[must_use]
72 pub fn focused(&self) -> Option<WidgetId> {
73 self.interaction.focused
74 }
75
76 /// The pointer cell wherever it is on screen, unlike [`PaintCx::pointer`].
77 #[must_use]
78 pub fn pointer_anywhere(&self) -> Option<(i32, i32)> {
79 self.interaction.pointer
80 }
81
82 /// Once this frame is painted, moves keyboard focus to the first focusable widget inside
83 /// `ancestor`, unless focus is already inside. Used by layers that take focus when they open.
84 pub fn request_focus_within(&mut self, ancestor: WidgetId) {
85 self.frame.focus_request = Some(FocusRequest::Within(ancestor));
86 }
87
88 /// Once this frame is painted, moves keyboard focus to `id` if that widget is focusable.
89 /// Used by layers to give focus back when they close.
90 pub fn request_focus(&mut self, id: WidgetId) {
91 self.frame.focus_request = Some(FocusRequest::Exact(id));
92 }
93
94 /// Makes this widget a dismissable layer for this frame: a pointer press outside it and its
95 /// descendants sends it [`Event::PointerOutside`](crate::event::Event::PointerOutside) and
96 /// then still reaches what was pressed, unless that is the widget whose press opened the
97 /// layer (then the press only closes it); an Esc key no focused widget used is sent to it.
98 /// Layers registered later are on top and are asked first. Dismissable layers share the
99 /// layer stack with modal layers ([`PaintCx::open_layer`]): a press on a modal layer above a
100 /// dismissable one lands inside the modal layer, so the dismissable layer beneath stays.
101 pub fn register_dismissable(&mut self) {
102 self.frame.layers.push(LayerEntry { id: self.id, modal: false, surface: None });
103 }
104
105 /// Whether this widget has keyboard focus.
106 #[must_use]
107 pub fn is_focused(&self) -> bool {
108 self.interaction.focused == Some(self.id)
109 }
110
111 /// Whether this widget has focus that should be shown loudly: it was reached with the
112 /// keyboard rather than clicked. Buttons and cards breathe their pillar only then, so a
113 /// clicked button stays calm under the pointer.
114 #[must_use]
115 pub fn is_focus_visible(&self) -> bool {
116 self.is_focused() && !self.interaction.focus_by_pointer
117 }
118
119 /// The pointer cell, when the pointer is over this widget.
120 #[must_use]
121 pub fn pointer(&self) -> Option<(i32, i32)> {
122 self.interaction.pointer.filter(|_| self.is_hovered())
123 }
124
125 /// Whether this widget is flashing after being activated. Schedules the frame that ends
126 /// the flash.
127 pub fn is_pressed(&mut self) -> bool {
128 let Some((id, at)) = self.interaction.pressed else {
129 return false;
130 };
131 let end = at + self.env.theme().motion().flash;
132 if id != self.id || self.now >= end {
133 return false;
134 }
135 self.frame.schedule(end);
136 true
137 }
138
139 /// Hover, focus and pressed states of a pressable widget such as a button, card, tab or
140 /// toggle: like [`states`](Self::states), but focus counts only when it is
141 /// [visible](Self::is_focus_visible), so a clicked control stays calm under the pointer.
142 pub fn pressable_states(&mut self) -> Vec<State> {
143 let visible = self.is_focus_visible();
144 let mut states = self.states();
145 if !visible {
146 states.retain(|state| *state != State::Focus);
147 }
148 states
149 }
150
151 /// Hover, focus and pressed states of this widget.
152 pub fn states(&mut self) -> Vec<State> {
153 let mut states = Vec::new();
154 if self.is_hovered() {
155 states.push(State::Hover);
156 }
157 if self.is_focused() {
158 states.push(State::Focus);
159 }
160 if self.is_pressed() {
161 states.push(State::Pressed);
162 }
163 states
164 }
165
166 /// The theme style of `widget.variant` in `states`, evaluated for this frame. Animated
167 /// styles schedule the next frame.
168 pub fn style(&mut self, widget: &str, variant: Option<&str>, states: &[State]) -> WidgetStyle {
169 let props = self.env.theme().style(widget, variant, states);
170 let style = WidgetStyle::new(props, self.pulse_phase());
171 if style.is_animated() && !self.env.reduced_motion() {
172 self.request_frame_in(PULSE_FRAME);
173 }
174 style
175 }
176
177 /// A theme colour token such as `"accent"`; black when the token does not exist.
178 #[must_use]
179 pub fn color(&self, token: &str) -> Rgb {
180 self.env.theme().color(token).unwrap_or(Rgb::new(0, 0, 0))
181 }
182
183 /// Asks for another frame after `delay`.
184 pub fn request_frame_in(&mut self, delay: Duration) {
185 self.frame.schedule(self.now + delay);
186 }
187
188 /// Whether the user asked for reduced motion; animations should show their end state.
189 #[must_use]
190 pub fn reduced_motion(&self) -> bool {
191 self.env.reduced_motion()
192 }
193
194 /// A value of this widget that moves towards `target` over `duration`. The value named
195 /// `name` starts at its first target without animating; later target changes animate from
196 /// wherever the value is. Schedules frames while it moves; returns `target` at once when
197 /// motion is reduced.
198 pub fn animate(&mut self, name: &'static str, target: f32, duration: Duration, easing: Easing) -> f32 {
199 if self.env.reduced_motion() {
200 return target;
201 }
202 let now = self.now;
203 let persistent = self.scope.is_some();
204 let tween = self.memory.get::<Tweens>(self.id, persistent).drive(name, target, now, duration, easing);
205 if tween.is_running(now) {
206 self.request_frame_in(ANIMATION_FRAME);
207 }
208 tween.value(now)
209 }
210
211 /// Eased progress from 0 to 1 of something that started at `start` (a time from
212 /// [`PaintCx::now`] or [`EventCx::now`](super::EventCx::now)) and takes `duration`. Schedules frames until it
213 /// completes; is 1 at once when motion is reduced.
214 pub fn progress_since(&mut self, start: Duration, duration: Duration, easing: Easing) -> f32 {
215 if self.env.reduced_motion() || duration.is_zero() {
216 return 1.0;
217 }
218 let elapsed = self.now.saturating_sub(start);
219 if elapsed >= duration {
220 return 1.0;
221 }
222 self.request_frame_in(ANIMATION_FRAME);
223 easing.apply(elapsed.as_secs_f32() / duration.as_secs_f32())
224 }
225
226 /// Where a repeating animation of length `period` is, `0.0..1.0`. Schedules smooth frames;
227 /// always 0 when motion is reduced.
228 pub fn cycle(&mut self, period: Duration) -> f32 {
229 let period = period.as_millis().max(1);
230 if self.env.reduced_motion() {
231 return 0.0;
232 }
233 self.request_frame_in(ANIMATION_FRAME);
234 (self.now.as_millis() % period) as f32 / period as f32
235 }
236
237 /// How many whole `interval`s have passed, for animations that jump between frames such as
238 /// spinners. Schedules a frame exactly at the next step; always 0 when motion is reduced.
239 pub fn ticks(&mut self, interval: Duration) -> u128 {
240 let interval_ms = interval.as_millis().max(1);
241 if self.env.reduced_motion() {
242 return 0;
243 }
244 let elapsed = self.now.as_millis();
245 let into = u64::try_from(elapsed % interval_ms).unwrap_or(0);
246 self.request_frame_in(Duration::from_millis(u64::try_from(interval_ms).unwrap_or(u64::MAX) - into));
247 elapsed / interval_ms
248 }
249
250 /// Where the theme pulse is, `0.0..1.0`; always 0 when motion is reduced.
251 #[must_use]
252 pub fn pulse_phase(&self) -> f32 {
253 if self.env.reduced_motion() {
254 return 0.0;
255 }
256 let period = self.env.theme().motion().pulse_period.as_secs_f64();
257 let phase = self.now.as_secs_f64().rem_euclid(period) / period;
258 // `phase` is in 0..1, which f32 represents closely enough for colour blending.
259 phase as f32
260 }
261
262 /// The visible area of this widget.
263 #[must_use]
264 pub fn clip(&self) -> Rect {
265 self.clip
266 }
267
268 /// Whether pasting would insert text, as far as the runtime knows; see `probe_clipboard` on
269 /// [`EventCx`](super::EventCx).
270 pub(crate) fn can_paste(&self) -> bool {
271 self.interaction.can_paste
272 }
273
274 /// Makes `rect` clickable for this widget. Later registrations are on top.
275 pub fn register_hit(&mut self, rect: Rect) {
276 self.register_hit_as(rect, self.id);
277 }
278
279 /// Makes `rect` clickable for `id`, a target the runtime handles itself (such as a toast)
280 /// rather than a widget in the view.
281 pub(crate) fn register_hit_as(&mut self, rect: Rect, id: WidgetId) {
282 if let Some(visible) = self.visible_part(rect) {
283 self.frame.hits.push((visible, id));
284 }
285 }
286
287 /// Keeps mouse text selection from starting in `rect`, even inside a selectable area, e.g.
288 /// for a secret shown in a selectable log.
289 pub fn unselectable(&mut self, rect: Rect) {
290 if let Some(visible) = self.visible_part(rect) {
291 self.frame.unselectable.push(visible);
292 }
293 }
294
295 /// Makes the visible part of `rect` a text selection region of this widget: a mouse drag
296 /// that starts inside it, and that no widget uses, selects text, and the selection stays
297 /// within this widget. Nothing is selectable unless a widget or
298 /// [`NodeMut::selectable`](crate::widget::NodeMut::selectable) asks; widgets whose text is
299 /// content to copy (code, documents, terminal output) call this while painting. The
300 /// innermost region under a press wins.
301 pub fn selectable(&mut self, rect: Rect) {
302 if let Some(visible) = self.visible_part(rect) {
303 self.frame.selectable.push((visible, self.id));
304 }
305 }
306
307 /// Marks the cells of `rect` as decoration rather than content, such as a scrollbar a widget
308 /// draws itself: a clean copy of a text selection leaves them out, a raw copy keeps them.
309 /// [`PaintCx::pillar`] marks its cell by itself.
310 pub fn decoration(&mut self, rect: Rect) {
311 if let Some(visible) = self.visible_part(rect) {
312 self.frame.decorations.push(visible);
313 }
314 }
315
316 /// Asks for [`MouseKind::Moved`](crate::event::MouseKind::Moved) events in this frame: the
317 /// pointer moving with no button held over this widget's hit area, or over a child of it.
318 /// Other widgets never see plain moves. Widgets that follow the pointer without a button,
319 /// such as an embedded terminal whose program asked for every motion,
320 /// call this while painting, and only while they need it.
321 pub fn track_pointer_moves(&mut self) {
322 self.frame.pointer_moves.push(self.id);
323 }
324
325 /// Adds this widget to the keyboard focus order.
326 pub fn register_focusable(&mut self) {
327 self.frame.focusable.push(self.id);
328 }
329
330 /// Paints this widget's overlay after the rest of the view.
331 pub fn request_overlay(&mut self, anchor: Rect) {
332 self.frame.overlays.push((self.id, anchor));
333 }
334
335 /// Makes this widget a modal layer for this frame and returns the time the layer opened.
336 ///
337 /// Call it from [`Widget::paint_overlay`](crate::widget::Widget::paint_overlay) every frame
338 /// the layer is shown, before painting what is inside it. Modal and dismissable layers
339 /// ([`PaintCx::register_dismissable`]) share one stack in paint order; the rules of a modal
340 /// layer are:
341 ///
342 /// - **Focus stays inside.** Each frame the layer asks, like
343 /// [`PaintCx::request_focus_within`], for focus to be inside it; a request made later in
344 /// the frame by a widget inside the layer (a popover opening) wins. Tab and Shift+Tab
345 /// cycle only through the layer's widgets.
346 /// - **Input stops at the layer.** Keys and pointer events bubble from their target up to
347 /// the topmost modal layer and no further; a press outside it lands on the layer itself.
348 /// Paint a hit area over the whole screen and use presses on it, so nothing beneath
349 /// reacts and no text selection starts there.
350 /// - **Shortcuts pause.** Application keymap actions (and global ones the runtime passes to
351 /// the application) do not run; quit, focus moves, debug, copy and paste still do. Key
352 /// listeners outside the layer are not heard.
353 /// - **Above it:** dismissable layers opened inside it, later modal layers, and the
354 /// runtime's toasts, which stay clickable.
355 /// - **Focus comes back.** When the layer is no longer painted, the runtime requests focus
356 /// for the widget that had it when the layer opened.
357 ///
358 /// The opening time stays the same for as long as the layer is shown, which makes it the
359 /// start of an entrance animation, even for layers inside persistent pages.
360 pub fn open_layer(&mut self) -> Duration {
361 self.frame.layers.push(LayerEntry { id: self.id, modal: true, surface: None });
362 self.request_focus_within(self.id);
363 self.interaction.layers.iter().find(|layer| layer.id == self.id).map_or(self.now, |layer| layer.opened)
364 }
365
366 /// Tells the runtime where the surface of the modal layer this widget opened with
367 /// [`PaintCx::open_layer`] sits, so toasts can keep clear of it.
368 pub(crate) fn set_layer_surface(&mut self, surface: Rect) {
369 let id = self.id;
370 if let Some(layer) = self.frame.layers.iter_mut().rev().find(|layer| layer.id == id) {
371 layer.surface = Some(surface);
372 }
373 }
374
375 /// The areas of the modal layers open in this frame: each layer's surface, or all of
376 /// `screen` for a layer that did not say where its surface is.
377 pub(crate) fn modal_surfaces(&self, screen: Rect) -> Vec<Rect> {
378 self.frame.layers.iter().filter(|layer| layer.modal).map(|layer| layer.surface.unwrap_or(screen)).collect()
379 }
380
381 /// Delivers `chord` to this widget even when it is not focused, for as long as the widget
382 /// is painted: after the focused widgets had their chance and before the keymap. Repeats
383 /// and releases of the key (which keyboards report while it is held) are delivered too,
384 /// including repeats of Enter and Space that focused widgets never see. Inside a modal
385 /// layer only listeners within the topmost layer hear keys.
386 pub fn listen_key(&mut self, chord: KeyChord) {
387 self.frame.listeners.push((chord, self.id));
388 }
389
390 /// Blends the text and background colours already drawn in `rect` towards `color` by
391 /// `amount` (0 keeps them, 1 replaces them), e.g. to dim the screen behind a dialog. Cells
392 /// drawn with reduced colour depth take `color` once `amount` passes one half.
393 pub fn tint(&mut self, rect: Rect, color: Rgb, amount: f32) {
394 let amount = amount.clamp(0.0, 1.0);
395 if amount <= 0.0 {
396 return;
397 }
398 let depth = self.env.depth();
399 let blend = |current: Color| match current {
400 Color::Rgb(r, g, b) => Some(to_color(Rgb::new(r, g, b).mix(color, amount), depth)),
401 _ if amount > 0.5 => Some(to_color(color, depth)),
402 _ => None,
403 };
404 self.each_cell(rect, |cell| {
405 if let Some(fg) = blend(cell.fg) {
406 cell.fg = fg;
407 }
408 if let Some(bg) = blend(cell.bg) {
409 cell.bg = bg;
410 }
411 });
412 }
413
414 /// This widget's state of type `T`.
415 pub fn memory<T: Default + 'static>(&mut self) -> &mut T {
416 self.memory.get::<T>(self.id, self.scope.is_some())
417 }
418
419 /// Fills `rect` with `color`, keeping text.
420 pub fn fill(&mut self, rect: Rect, color: Rgb) {
421 let bg = to_color(color, self.env.depth());
422 self.each_cell(rect, |cell| cell.bg = bg);
423 }
424
425 /// Puts `color` behind the cells of `rect`, keeping their glyphs. A cell whose text would no
426 /// longer read on it (contrast below 3:1) takes `readable` instead, so faint text stays legible
427 /// under a highlight such as a text selection.
428 pub(crate) fn fill_keeping_text_readable(&mut self, rect: Rect, color: Rgb, readable: Rgb) {
429 let depth = self.env.depth();
430 let (bg, text) = (to_color(color, depth), to_color(readable, depth));
431 self.each_cell(rect, |cell| {
432 cell.bg = bg;
433 if let Color::Rgb(r, g, b) = cell.fg
434 && cell.symbol().trim() != ""
435 && Rgb::new(r, g, b).contrast_ratio(color) < 3.0
436 {
437 cell.fg = text;
438 }
439 });
440 }
441
442 /// Clears `rect` to spaces on `color`.
443 pub fn clear(&mut self, rect: Rect, color: Rgb) {
444 // An empty cell reads as a space and equals a cell holding one; copying a prepared blank
445 // cell is cheaper than resetting, writing and styling every cell.
446 let mut blank = Cell::EMPTY;
447 blank.bg = to_color(color, self.env.depth());
448 self.each_cell(rect, |cell| cell.clone_from(&blank));
449 }
450
451 /// Draws the theme's pillar (`[icons] pillar`, e.g. `▌`) at `(x, y)` in `color`, over whatever
452 /// surface is already there. A blank glyph, as in ASCII mode, becomes a cell of `color`. The
453 /// cell is [decoration](PaintCx::decoration): clean copies of a text selection skip it.
454 pub fn pillar(&mut self, x: i32, y: i32, color: Rgb) {
455 self.decoration(Rect::new(x, y, 1, 1));
456 let env = self.env;
457 let glyph = env.icons().glyph(PILLAR);
458 if glyph.trim().is_empty() {
459 self.fill(Rect::new(x, y, 1, 1), color);
460 } else {
461 self.text(x, y, &glyph, CellStyle::fg(color), 1);
462 }
463 }
464
465 /// Draws `text` starting at `(x, y)`, at most `max` cells wide, clipped to the visible area.
466 /// Returns the number of cells the text occupies (before clipping).
467 pub fn text(&mut self, x: i32, y: i32, text: &str, style: CellStyle, max: u16) -> u16 {
468 let paint = style.for_depth(self.env.depth());
469 let clip = self.clip;
470 let limit = x + i32::from(max);
471 let mut column = x;
472 // Printable ASCII is one cell per byte. The byte after the last one that fits is checked
473 // too: a combining mark there would join the last character drawn.
474 let drawn = text.len().min(usize::from(max));
475 if text.get(..text.len().min(drawn + 1)).is_some_and(text::is_printable_ascii) {
476 for index in 0..drawn {
477 if clip.contains(column, y)
478 && let Some(cell) = self.cell_mut(column, y)
479 {
480 cell.set_symbol(&text[index..=index]);
481 paint.apply(cell);
482 }
483 column += 1;
484 }
485 return clamp_u16(column - x);
486 }
487 for grapheme in text.graphemes(true) {
488 let width = i32::from(text::grapheme_width(grapheme));
489 if width == 0 {
490 continue;
491 }
492 if column + width > limit {
493 break;
494 }
495 for offset in 0..width {
496 let cx = column + offset;
497 if !clip.contains(cx, y) {
498 continue;
499 }
500 let whole = clip.contains(column, y) && clip.contains(column + width - 1, y);
501 if let Some(cell) = self.cell_mut(cx, y) {
502 let symbol = match (offset, whole) {
503 (0, true) => grapheme,
504 (_, true) => "",
505 _ => " ",
506 };
507 cell.set_symbol(symbol);
508 paint.apply(cell);
509 }
510 }
511 column += width;
512 }
513 clamp_u16(column - x)
514 }
515
516 /// Paints a child node into `rect`, applying its padding.
517 pub fn paint_child<M: 'static>(&mut self, node: &Node<M>, rect: Rect) {
518 let saved = (self.id, self.layout, self.clip, self.scope);
519 self.id = node.id;
520 self.layout = node.layout;
521 if node.persistent {
522 self.scope = Some(node.id);
523 }
524 self.clip = saved.2.intersect(rect);
525 self.frame.rects.insert(node.id, rect);
526 self.frame.parents.insert(node.id, saved.0);
527 if let Key::Named(name) = &node.key {
528 self.frame.names.insert(node.id, name.clone());
529 }
530 if let Some(scope) = self.scope {
531 self.frame.scopes.insert(node.id, scope);
532 }
533 self.memory.touch(node.id, self.scope.is_some());
534 match node.selectable {
535 Some(true) => self.selectable(rect),
536 Some(false) => self.unselectable(rect),
537 None => {}
538 }
539 if node.widget.focusable() {
540 self.register_focusable();
541 }
542 node.widget.paint(self, rect.inset(node.layout.padding));
543 (self.id, self.layout, self.clip, self.scope) = saved;
544 }
545
546 /// Paints a child node that does not take keyboard focus itself, not even when it is
547 /// normally focusable. For composite widgets that take focus as one control and pass keys on
548 /// to the child with [`EventCx::forward`](super::EventCx::forward), such as a settings row's switch.
549 pub fn paint_child_unfocusable<M: 'static>(&mut self, node: &Node<M>, rect: Rect) {
550 let registered = self.frame.focusable.len();
551 self.paint_child(node, rect);
552 self.frame.focusable.truncate(registered);
553 }
554
555 /// The pointer cell, when the pointer is over this widget or over a widget inside it, for
556 /// containers whose rows light up while the pointer is on a control within them.
557 #[must_use]
558 pub fn pointer_within(&self) -> Option<(i32, i32)> {
559 self.interaction.pointer.filter(|_| self.interaction.hovered_chain.contains(&self.id))
560 }
561
562 /// Whether keyboard focus is on this widget or on a widget painted inside it so far in this
563 /// frame. Paint children before asking, e.g. to brighten a field label while its control has
564 /// focus.
565 #[must_use]
566 pub fn has_focus_within(&self) -> bool {
567 self.interaction.focused.is_some_and(|focused| self.frame.is_within(focused, self.id))
568 }
569
570 /// Measures a child node with the same rules as layout.
571 pub fn measure_child<M: 'static>(&mut self, node: &Node<M>, available: Size) -> Size {
572 MeasureCx::for_frame(self.env, &mut self.frame.measures).measure_child(node, available)
573 }
574
575 /// Runs `paint` with drawing limited to `rect` (and the current visible area).
576 pub fn with_clip(&mut self, rect: Rect, paint: impl FnOnce(&mut Self)) {
577 let saved = self.clip;
578 self.clip = saved.intersect(rect);
579 paint(self);
580 self.clip = saved;
581 }
582
583 /// The part of `rect` inside the visible area, when there is one.
584 fn visible_part(&self, rect: Rect) -> Option<Rect> {
585 Some(rect.intersect(self.clip)).filter(|visible| !visible.is_empty())
586 }
587
588 /// Runs `change` on every cell of `rect` inside the visible area and the screen.
589 pub(super) fn each_cell(&mut self, rect: Rect, mut change: impl FnMut(&mut Cell)) {
590 let area = rect.intersect(self.clip);
591 for y in area.y..area.bottom() {
592 for x in area.x..area.right() {
593 if let Some(cell) = self.cell_mut(x, y) {
594 change(cell);
595 }
596 }
597 }
598 }
599
600 /// The screen cell at `(x, y)`, when it is on screen.
601 fn cell_mut(&mut self, x: i32, y: i32) -> Option<&mut Cell> {
602 self.buf.cell_mut((u16::try_from(x).ok()?, u16::try_from(y).ok()?))
603 }
604}
605
606/// Frame interval while something moves: about 60 frames a second.
607pub(crate) const ANIMATION_FRAME: Duration = Duration::from_millis(16);
608
609/// How often animated theme colours are redrawn.
610pub(crate) const PULSE_FRAME: Duration = Duration::from_millis(50);