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