Skip to main content

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}