denise_ui/motion.rs
1//! How fast the tree animates, and how a widget says what it is waiting for.
2//!
3//! Two different things used to be spelled the same way. A spinner asking for
4//! "another frame in 16 ms" and a carousel asking for "the next page in eight
5//! seconds" both came back as a millisecond deadline, so nothing could tell a
6//! **sample rate** from a **duration** — and a setting that halved one would
7//! have silently halved the other. [`Wake`] separates them, and [`Motion`] is
8//! the one knob that sets the rate for everything.
9
10/// How often the tree looks at whatever is animating.
11///
12/// One setting for every moving thing in the tree: spinners, knobs crossing,
13/// carousel slides, layout tweens, toast fades. It is a **sample rate**, not a
14/// duration — halving it makes animation coarser, never slower. A toggle still
15/// crosses in 120 ms and a carousel still advances after eight seconds,
16/// whatever this says.
17///
18/// ```
19/// # use denise::{Size, theme};
20/// # use denise_ui::{Motion, Ui};
21/// # enum Msg { Noop }
22/// # let mut ui: Ui<Msg> = Ui::new(Size::new(1920, 1080), theme::DARK);
23/// ui.set_motion(Motion::Every(33)); // 30 fps: half the wakes, half the cost
24/// ui.set_motion(Motion::None); // reduced motion, or a tight power budget
25/// ```
26///
27/// # Why this and not a constant per widget
28///
29/// It used to be a constant per widget — four of them, all saying 16 or 50, all
30/// private. That is one decision copied four times and reachable from nowhere,
31/// and it is the wrong number in two directions at once: a desktop wants sixty
32/// frames a second because a rotating arc at twenty reads as a stutter, and a
33/// battery-powered panel wants the arc to cost a third as much. The gallery on a
34/// Pi 3A+ is 4.20% of a core at 16 ms and 1.37% at 50, for as long as one
35/// spinner is on screen.
36///
37/// So the widget says *that* it is moving and the tree says *when* to look —
38/// which also means a custom widget gets the setting for free, without knowing
39/// it exists.
40#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
41pub enum Motion {
42 /// Sample every animation in flight this often, in milliseconds.
43 ///
44 /// Clamped to at least 1 ms: zero would ask the event loop never to sleep,
45 /// which is not a frame rate but a busy loop.
46 Every(u64),
47 /// Do not animate. Transitions land at their end state immediately, and
48 /// nothing in the tree asks to be woken for movement.
49 ///
50 /// This is the `prefers-reduced-motion` answer, and the right setting on
51 /// hardware where any animation is a bad trade. It stops **motion**, not
52 /// **schedules**: a tooltip still appears after its dwell, a toast still
53 /// goes after its hold, a carousel still advances — those are deadlines,
54 /// and a deadline is not a frame rate.
55 None,
56}
57
58impl Motion {
59 /// Sixty frames a second, the default.
60 ///
61 /// Sixty rather than twenty because a rotating arc is the animation least
62 /// forgiving of a low rate: a caret can blink twice a second and a knob can
63 /// cross in eight frames, but a ring turning in visible steps reads as a
64 /// stutter rather than as a style.
65 ///
66 /// The spinner said twenty for a while, on the argument that twenty is above
67 /// the rate at which a rotation stops reading as separate positions and
68 /// costs a third of the wakes. The first half of that turned out to be wrong
69 /// by eye: asked for twenty and *given* twenty, the arc is visibly steppy. It
70 /// had never actually been tried, because until the desktop backend started
71 /// honouring `next_wake_ms` the loop free-ran at 60 Hz and quietly delivered
72 /// sixty.
73 ///
74 /// The second half was right, and is why sixty is affordable: the drawing is
75 /// not the expense — the #17 bench puts a spinner-sized arc at about three
76 /// microseconds — the **wake** is, and each wake ends in a present. That was
77 /// costing 16 MB of copying on macOS until `denise-winit` started handing the
78 /// compositor an `IOSurface`; a present there is now free, a DRM page flip
79 /// always was, and win32 blits the damage rectangle.
80 ///
81 /// Which is also why it is a default and not a constant. Sixty wakes a
82 /// second for a widget that can keep a device awake indefinitely is a small
83 /// cost on a desktop and a real one on a battery.
84 pub const DEFAULT_INTERVAL_MS: u64 = 16;
85
86 /// The sampling interval in milliseconds, or `None` under [`Motion::None`].
87 #[inline]
88 pub const fn interval_ms(self) -> Option<u64> {
89 match self {
90 // `max(1)` rather than a rejected value: a caller asking for zero
91 // wants "as fast as possible", and the fastest this can honestly
92 // promise is one millisecond.
93 Self::Every(ms) => Some(if ms == 0 { 1 } else { ms }),
94 Self::None => None,
95 }
96 }
97
98 /// Whether anything is allowed to move.
99 #[inline]
100 pub const fn animates(self) -> bool {
101 matches!(self, Self::Every(_))
102 }
103}
104
105impl Default for Motion {
106 /// [`Motion::Every`] at [`Motion::DEFAULT_INTERVAL_MS`].
107 fn default() -> Self {
108 Self::Every(Self::DEFAULT_INTERVAL_MS)
109 }
110}
111
112/// When a widget wants [`Widget::animate`](crate::Widget::animate) called again.
113///
114/// The distinction this type exists for:
115///
116/// - [`Wake::Animating`] is a **rate**. The widget is mid-movement and wants to
117/// be looked at as often as the tree looks at movement — so [`Motion`] decides
118/// how often, and turning it down costs the animation resolution and nothing
119/// else.
120/// - [`Wake::At`] is a **deadline**. Something happens at that reading of the
121/// clock: a carousel advances, a caret flips, a toast expires. [`Motion`] does
122/// not touch it, because quantising a schedule to a frame rate would be a bug.
123///
124/// Both were `Option<u64>` before, and the difference was invisible.
125#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
126pub enum Wake {
127 /// Nothing more to do. The widget drops out of the animating set, which is
128 /// the only way out of it — see [`Widget::animate`](crate::Widget::animate).
129 #[default]
130 Never,
131 /// Again at the tree's animation rate, because this widget is moving.
132 Animating,
133 /// At this reading of the application's clock, whatever the rate is.
134 At(u64),
135}