Skip to main content

denise_ui/widgets/
button.rs

1//! A pressable, focusable, message-emitting rectangle.
2
3use alloc::string::String;
4
5use denise::Pen;
6use denise::icon::Icon;
7use denise::{ElementState, InputEvent, KeyCode, Radius, Rect, Role};
8use denise_text::TextStyle;
9
10use crate::widget::{
11    Animation, Event, EventCtx, Handled, MeasureCtx, Measured, Offer, PaintCtx, VisualState, Widget,
12};
13use crate::widgets::describe::{
14    Describe, DynDescribe, Group, Mismatch, Payload, Property, PropertyKind, RADII, ROLES, Value,
15};
16use crate::widgets::style::{Align, draw_aligned, focus_ring, interactive_pair};
17
18/// A button that emits a message when it is activated.
19///
20/// Activation is a release *inside* the button, or Enter/Space while it holds
21/// focus. A press that is dragged off and released elsewhere is cancelled, which
22/// is what makes a touchscreen usable — a finger that lands on the wrong control
23/// can be slid away rather than committing.
24#[derive(Clone, Debug)]
25pub struct Button<M> {
26    label: String,
27    message: Option<M>,
28    role: Role,
29    radius: Radius,
30    style: TextStyle,
31    no_focus: bool,
32    /// How long a hold waits, and how fast it goes after that. `None` is a
33    /// button that does not repeat, which is nearly all of them.
34    repeat: Option<Repeat>,
35    /// When the finger went down, while it is still down.
36    held_since: Option<u64>,
37    /// The last repeat already counted, as a count rather than a time so that
38    /// arithmetic on a clock that jumped cannot produce a burst.
39    counted: u32,
40    /// Repeats owed to whoever asks next.
41    pending: u32,
42    /// A small second label in the top-right corner. Empty for most buttons.
43    corner: String,
44    /// A drawn shape in place of the label. `None` for most buttons.
45    icon: Option<&'static Icon>,
46    /// Whether this button reports how long it has been held.
47    watches_hold: bool,
48    /// How long the current press has lasted, as of the last `animate`.
49    held_ms: u64,
50}
51
52/// A button's press-and-hold schedule.
53#[derive(Clone, Copy, Debug, PartialEq, Eq)]
54struct Repeat {
55    delay_ms: u64,
56    interval_ms: u64,
57}
58
59/// The most repeats one frame may hand over.
60///
61/// A loop that blocked — a page arriving over a slow link, a display waking,
62/// a snapshot ticking straight past a second — comes back with a clock that has
63/// jumped, and counting from the press would then report every repeat that gap
64/// covered. Truthfully, and uselessly: nobody watching a frozen screen meant to
65/// delete two hundred characters, and a keyboard that empties a field because
66/// the network hiccuped is worse than one that does not repeat at all.
67///
68/// Past this the missed repeats are dropped rather than queued, so a stall
69/// costs the repeats it swallowed and nothing more. Four is about a quarter of
70/// a second at the on-screen keyboard's interval: enough that an ordinary
71/// stutter is invisible, little enough that a real stall is obvious.
72const MAX_CATCH_UP: u32 = 4;
73
74/// How much of a button's shorter side an icon takes, as a percentage.
75///
76/// Slightly over half. A glyph in a 48-pixel key occupies about this much once
77/// its own side bearings are counted, so an icon at the same share sits in a row
78/// of lettered keys without looking like a different size of thing.
79const ICON_SHARE: i32 = 55;
80
81impl<M> Button<M> {
82    /// A primary button carrying `message`.
83    pub fn new(label: impl Into<String>, message: M) -> Self {
84        Self {
85            label: label.into(),
86            message: Some(message),
87            role: Role::Primary,
88            radius: Radius::Field,
89            style: TextStyle::built_in(16),
90            no_focus: false,
91            repeat: None,
92            held_since: None,
93            counted: 0,
94            pending: 0,
95            corner: String::new(),
96            icon: None,
97            watches_hold: false,
98            held_ms: 0,
99        }
100    }
101
102    /// A button that emits nothing. Useful as a disabled affordance, or when the
103    /// application only cares about focus.
104    pub fn inert(label: impl Into<String>) -> Self {
105        Self {
106            label: label.into(),
107            message: None,
108            role: Role::Primary,
109            radius: Radius::Field,
110            style: TextStyle::built_in(16),
111            no_focus: false,
112            repeat: None,
113            held_since: None,
114            counted: 0,
115            pending: 0,
116            corner: String::new(),
117            icon: None,
118            watches_hold: false,
119            held_ms: 0,
120        }
121    }
122
123    /// Presses without touching focus: the button takes none, and costs none.
124    ///
125    /// An ordinary button takes focus when pressed, which is right for a button
126    /// somebody tabs to and wrong for a key on an on-screen keyboard — that key
127    /// is pressed *while* a field is being typed into, and the field has to keep
128    /// the caret. Making the key merely unfocusable is not enough either, since
129    /// pressing an unfocusable node is what drops focus and commits a field.
130    ///
131    /// So this asks for neither. The button still presses, still paints pressed,
132    /// still emits its message; Tab skips it, and the focus ring never moves.
133    ///
134    /// ```
135    /// # use denise::{Rect, Size, theme};
136    /// # use denise_ui::{Ui, widgets::{Button, TextInput}};
137    /// # #[derive(Clone, Debug)] enum Msg { Key(char) }
138    /// # let mut ui: Ui<Msg> = Ui::new(Size::new(800, 480), theme::DARK);
139    /// # let root = ui.root();
140    /// # let field = ui.add(root, TextInput::new(), Rect::new(0, 0, 200, 40)).unwrap();
141    /// # ui.focus(Some(field));
142    /// ui.add(root, Button::new("q", Msg::Key('q')).no_focus(), Rect::new(0, 100, 40, 40));
143    /// assert_eq!(ui.focused(), Some(field));
144    /// ```
145    pub fn no_focus(mut self) -> Self {
146        self.no_focus = true;
147        self
148    }
149
150    /// Emits again, and again, while a finger stays on it.
151    ///
152    /// A repeating button **acts on press rather than on release**, which is the
153    /// only way it could work: repeats have to start while the finger is still
154    /// down. That is a real change in feel and the reason this is opt-in — an
155    /// ordinary button emits on release precisely so that sliding off it before
156    /// letting go cancels the press, and a repeating one gives that up.
157    ///
158    /// Reach for it where holding means *more of the same*: Backspace on an
159    /// on-screen keyboard, a stepper's arrows, a scrollbar's ends. Not for
160    /// anything whose second press means something different from its first.
161    ///
162    /// `delay_ms` is the pause before the first repeat — long enough that an
163    /// ordinary tap never triggers one — and `interval_ms` the gap between the
164    /// rest. The repeats are counted, not emitted: the button has no message
165    /// channel while it is animating, so whoever owns it collects them with
166    /// [`take_repeats`](Self::take_repeats) once a frame.
167    ///
168    /// Costs nothing when nothing is held. The button asks the tree to wake it
169    /// only between a press and its release, and answers
170    /// [`Wake::Never`](crate::Wake::Never) the moment the finger goes.
171    pub fn with_repeat(mut self, delay_ms: u64, interval_ms: u64) -> Self {
172        self.repeat = Some(Repeat {
173            delay_ms,
174            interval_ms: interval_ms.max(1),
175        });
176        self
177    }
178
179    /// A shape drawn in place of the label.
180    ///
181    /// For the button whose meaning is a picture — a Backspace key, a
182    /// scrollbar's arrow — and specifically for the case where that picture is
183    /// not reliably in the font. An [`Icon`] is filled polygons this crate
184    /// draws itself, so it is the same on a machine with no fonts installed at
185    /// all as it is on one with DejaVu.
186    ///
187    /// It takes the label's place rather than sitting beside it, and it is
188    /// drawn in the same content colour the label would have used, so it
189    /// follows the theme and the button's state without being told. Keep the
190    /// label anyway: it is what [`label`](Self::label) still reports, which is
191    /// what a test and an accessibility pass read.
192    ///
193    /// Sized from the button rather than fixed, and kept square — the shortest
194    /// side decides, so a wide key gets a centred square icon rather than a
195    /// stretched one.
196    #[must_use]
197    pub fn with_icon(mut self, icon: &'static Icon) -> Self {
198        self.icon = Some(icon);
199        self
200    }
201
202    /// Removes or replaces the icon.
203    pub fn set_icon(&mut self, icon: Option<&'static Icon>) {
204        self.icon = icon;
205    }
206
207    /// The shape drawn in place of the label, if any.
208    #[inline]
209    pub const fn icon(&self) -> Option<&'static Icon> {
210        self.icon
211    }
212
213    /// A small second label in the top-right corner.
214    ///
215    /// What a key on a real keyboard has printed above the character it types:
216    /// the `!` over the `1`, the `?` over the `+`. It says what the *other*
217    /// state of this button would give, which is the whole reason a keyboard
218    /// prints it — you cannot discover Shift by pressing Shift if pressing it
219    /// is what changes the legend.
220    ///
221    /// Drawn at two thirds the label's size in the same content colour, so it
222    /// reads as an annotation rather than as a second button. Empty is the
223    /// normal case and costs nothing.
224    #[must_use]
225    pub fn with_corner(mut self, corner: impl Into<String>) -> Self {
226        self.corner = corner.into();
227        self
228    }
229
230    /// Replaces the corner label.
231    pub fn set_corner(&mut self, corner: impl Into<String>) {
232        self.corner = corner.into();
233    }
234
235    /// What is printed in the corner, if anything.
236    #[inline]
237    pub fn corner(&self) -> &str {
238        &self.corner
239    }
240
241    /// Repeats owed since this was last called, and clears the tally.
242    ///
243    /// Zero unless [`with_repeat`](Self::with_repeat) was asked for and a finger
244    /// has been resting on the button for longer than its delay.
245    ///
246    /// Reach for [`repeats_pending`](Self::repeats_pending) first when polling
247    /// several buttons: taking needs `&mut`, and getting one out of the tree
248    /// costs a repaint of the node whether or not anything had changed.
249    pub fn take_repeats(&mut self) -> u32 {
250        core::mem::take(&mut self.pending)
251    }
252
253    /// Reports how long a finger has been resting on it.
254    ///
255    /// The other half of press-and-hold. [`with_repeat`](Self::with_repeat)
256    /// answers "again, and again"; this answers "how long", which is what a
257    /// gesture that fires *once* after a delay needs — a key offering its
258    /// alternates, a button revealing a menu.
259    ///
260    /// Costs the same as repeating and no more: the button asks the tree to
261    /// wake it only between a press and its release, so a screen nobody is
262    /// touching schedules nothing. Read it with [`held_ms`](Self::held_ms).
263    #[must_use]
264    pub const fn watching_hold(mut self) -> Self {
265        self.watches_hold = true;
266        self
267    }
268
269    /// How long the current press has lasted, in milliseconds.
270    ///
271    /// `None` when nothing is on it. Updated on each tick while held, so it is
272    /// as fresh as the last one — which for a wake-driven tree means as fresh
273    /// as whatever asked to be woken.
274    ///
275    /// A free read: unlike `Ui::widget_mut`, looking does not repaint.
276    #[inline]
277    pub const fn held_ms(&self) -> Option<u64> {
278        if self.held_since.is_some() {
279            Some(self.held_ms)
280        } else {
281            None
282        }
283    }
284
285    /// Repeats owed, without taking them.
286    ///
287    /// The read that costs nothing. `Ui::widget_mut` damages the node it hands
288    /// out — it cannot know whether the caller changed anything — so polling a
289    /// keyboard's sixty keys through it repaints the whole keyboard on every
290    /// frame. This is how a caller finds the one key that owes something before
291    /// asking for it mutably.
292    #[inline]
293    pub const fn repeats_pending(&self) -> u32 {
294        self.pending
295    }
296
297    /// Whether a finger is on it now.
298    #[inline]
299    pub const fn is_held(&self) -> bool {
300        self.held_since.is_some()
301    }
302
303    /// Sets the colour role. The content colour comes from the theme's pairing, so
304    /// the label stays readable whichever role and theme are chosen.
305    pub fn with_role(mut self, role: Role) -> Self {
306        self.role = role;
307        self
308    }
309
310    /// Sets the corner rounding token.
311    pub fn with_radius(mut self, radius: Radius) -> Self {
312        self.radius = radius;
313        self
314    }
315
316    /// Sets the font and size.
317    pub fn with_style(mut self, style: TextStyle) -> Self {
318        self.style = style;
319        self
320    }
321
322    /// Sets the size, keeping the font.
323    pub fn with_size(mut self, size_px: u16) -> Self {
324        self.style.size_px = size_px;
325        self
326    }
327
328    /// The font and size the label draws in.
329    #[inline]
330    pub const fn style(&self) -> TextStyle {
331        self.style
332    }
333
334    /// The current label.
335    #[inline]
336    pub fn label(&self) -> &str {
337        &self.label
338    }
339
340    /// Replaces the label.
341    pub fn set_label(&mut self, label: impl Into<String>) {
342        self.label = label.into();
343    }
344
345    /// Replaces the message emitted on activation.
346    pub fn set_message(&mut self, message: Option<M>) {
347        self.message = message;
348    }
349
350    /// The colour role it is drawn in.
351    ///
352    /// Worth reading before writing: `Ui::widget_mut` repaints whatever it
353    /// hands out, so a caller restyling a row of buttons at once should skip
354    /// the ones already right.
355    #[inline]
356    pub const fn role(&self) -> Role {
357        self.role
358    }
359
360    /// Replaces the colour role.
361    ///
362    /// What a list of buttons uses to show which one is selected, since a role
363    /// survives a theme change and a colour does not.
364    pub fn set_role(&mut self, role: Role) {
365        self.role = role;
366    }
367
368    /// Replaces the font and size.
369    ///
370    /// For an application that registers a font after building its tree, which is
371    /// the ordinary case: the tree has to exist before anyone knows whether the
372    /// font file was there.
373    pub fn set_style(&mut self, style: TextStyle) {
374        self.style = style;
375    }
376
377    /// Width this button needs for its label plus comfortable padding.
378    ///
379    /// Takes the engine because with a proportional font the answer is not the
380    /// character count times anything, and guessing is how a button ends up one
381    /// letter too narrow in the language it was not tested in.
382    pub fn preferred_width(&self, engine: &mut denise_text::TextEngine) -> i32 {
383        let text = engine.measure_line(self.style, &self.label);
384        text + i32::from(self.style.size_px) * 3 / 2
385    }
386}
387
388impl<M: Clone + 'static> Widget<M> for Button<M> {
389    fn describe(&self) -> Option<&dyn DynDescribe> {
390        Some(self)
391    }
392
393    fn describe_mut(&mut self) -> Option<&mut dyn DynDescribe> {
394        Some(self)
395    }
396    fn measure(&self, ctx: &mut MeasureCtx<'_>, _offered: Offer) -> Measured {
397        // Width from the label; height from the theme, because a button is a
398        // field-sized target and that is the theme's number rather than this
399        // widget's.
400        Measured::both(
401            self.preferred_width(ctx.text),
402            ctx.theme.metrics.size_field.max(1),
403        )
404    }
405
406    fn paint(&self, ctx: &mut PaintCtx<'_>, canvas: &mut Pen<'_>) {
407        let radius = ctx.theme.radius(self.radius);
408        let (background, content) = interactive_pair(ctx.theme, self.role, ctx.state);
409        canvas.fill_rounded_rect(ctx.bounds, radius, background);
410        if ctx.state.contains(VisualState::FOCUSED) {
411            focus_ring(ctx.theme, ctx.bounds, radius, canvas);
412        }
413        // The corner first, so that a label wide enough to reach it is drawn
414        // over the annotation rather than under it — the character somebody is
415        // about to type matters more than the one they are not.
416        if !self.corner.is_empty() {
417            let size = (i32::from(self.style.size_px) * 2 / 3).max(1) as u16;
418            let small = TextStyle {
419                size_px: size,
420                ..self.style
421            };
422            let pad = i32::from(size) / 3;
423            let corner = Rect::new(
424                ctx.bounds.x,
425                ctx.bounds.y + pad,
426                (ctx.bounds.width - pad).max(0),
427                i32::from(size) + pad,
428            );
429            draw_aligned(
430                canvas,
431                ctx.text,
432                small,
433                corner,
434                (Align::End, Align::Start),
435                &self.corner,
436                content,
437            );
438        }
439        // An icon takes the label's place. Square and centred, from the shorter
440        // side, so a wide key gets a centred picture rather than a stretched
441        // one; the corner legend above is untouched, since a key can have both.
442        if let Some(icon) = self.icon {
443            let side = (ctx.bounds.width.min(ctx.bounds.height) * ICON_SHARE / 100).max(1);
444            let box_ = Rect::new(
445                ctx.bounds.x + (ctx.bounds.width - side) / 2,
446                ctx.bounds.y + (ctx.bounds.height - side) / 2,
447                side,
448                side,
449            );
450            canvas.draw_icon(icon, box_, content, background);
451            return;
452        }
453
454        // The label sits low when something shares the key with it, so the two
455        // do not collide and the row of characters still reads as a row.
456        let vertical = if self.corner.is_empty() {
457            Align::Center
458        } else {
459            Align::End
460        };
461        let inset = if self.corner.is_empty() {
462            ctx.bounds
463        } else {
464            let pad = i32::from(self.style.size_px) / 4;
465            Rect::new(
466                ctx.bounds.x,
467                ctx.bounds.y,
468                ctx.bounds.width,
469                (ctx.bounds.height - pad).max(0),
470            )
471        };
472        draw_aligned(
473            canvas,
474            ctx.text,
475            self.style,
476            inset,
477            (Align::Center, vertical),
478            &self.label,
479            content,
480        );
481    }
482
483    fn on_event(&mut self, event: &Event<'_>, ctx: &mut EventCtx<'_, M>) -> Handled {
484        // A repeating button lives on the down edge instead of the up one, and
485        // has to, since the repeats begin while the finger is still there.
486        if self.repeat.is_some() || self.watches_hold {
487            match event {
488                Event::Input(
489                    InputEvent::PointerButton {
490                        state: ElementState::Down,
491                        position,
492                        ..
493                    }
494                    | InputEvent::TouchDown { position, .. },
495                ) if ctx.bounds.contains(*position) => {
496                    self.held_since = Some(ctx.now_ms);
497                    self.counted = 0;
498                    self.held_ms = 0;
499                    // The wake that carries the hold. Asked for here and given
500                    // back by `animate` the moment the finger leaves, so a tree
501                    // nobody is touching schedules nothing.
502                    ctx.request_animation();
503                    // A repeating button acts on the press; one that merely
504                    // watches the hold does not, so that it still emits on
505                    // release the way every other button does — the gesture is
506                    // "hold for something else", not "act sooner".
507                    if self.repeat.is_some()
508                        && let Some(message) = self.message.clone()
509                    {
510                        ctx.emit(message);
511                    }
512                    return if self.repeat.is_some() {
513                        Handled::Yes
514                    } else {
515                        Handled::No
516                    };
517                }
518                // Every way a press can end. The tree dispatches the release to
519                // whichever node was pressed even when the finger has wandered
520                // off it, and `PressCancelled` covers the ends that no pointer
521                // event describes.
522                Event::Input(InputEvent::PointerButton {
523                    state: ElementState::Up,
524                    ..
525                })
526                | Event::Input(InputEvent::TouchUp { .. })
527                | Event::PressCancelled => {
528                    self.held_since = None;
529                    self.held_ms = 0;
530                    // Whatever was owed is dropped with the press: a repeat
531                    // nobody collected before the finger lifted is one the user
532                    // did not wait for.
533                    self.pending = 0;
534                    // A repeating button has already acted; one that only
535                    // watches must fall through, or it would never emit at all.
536                    if self.repeat.is_some() {
537                        return Handled::Yes;
538                    }
539                }
540                _ => {}
541            }
542        }
543
544        let activated = match event {
545            Event::Input(InputEvent::PointerButton {
546                state: ElementState::Up,
547                position,
548                ..
549            }) => ctx.bounds.contains(*position),
550            Event::Input(InputEvent::TouchUp {
551                position,
552                cancelled: false,
553                ..
554            }) => ctx.bounds.contains(*position),
555            Event::Input(InputEvent::Key {
556                code: KeyCode::Enter | KeyCode::Space | KeyCode::NumpadEnter,
557                state: ElementState::Down,
558                repeat: false,
559                ..
560            }) => ctx.state.contains(VisualState::FOCUSED),
561            _ => return Handled::No,
562        };
563        if !activated {
564            return Handled::No;
565        }
566        if let Some(message) = self.message.clone() {
567            ctx.emit(message);
568        }
569        Handled::Yes
570    }
571
572    /// Counts the repeats a held finger has earned, and asks for the next wake.
573    ///
574    /// Counted from the press rather than accumulated from the last tick, so a
575    /// clock that jumped — a loop that blocked, a snapshot ticking straight past
576    /// a second — yields the repeats that time actually covered and no more.
577    fn animate(&mut self, now_ms: u64) -> Animation {
578        let Some(since) = self.held_since else {
579            return Animation::NONE;
580        };
581        self.held_ms = now_ms.saturating_sub(since);
582        let Some(repeat) = self.repeat else {
583            // Watching the hold and nothing else: come back at the rate the
584            // tree animates at, which is what makes `held_ms` current without
585            // this widget inventing a cadence of its own.
586            return Animation {
587                repaint: false,
588                next: crate::Wake::Animating,
589            };
590        };
591        let held = self.held_ms;
592        let Some(after_delay) = held.checked_sub(repeat.delay_ms) else {
593            // Still inside the initial pause: nothing owed, come back when it
594            // is over.
595            return Animation::due_at(since + repeat.delay_ms);
596        };
597        // The first repeat lands *at* the delay, so a hold of exactly the delay
598        // owes one.
599        let due = u32::try_from(after_delay / repeat.interval_ms + 1).unwrap_or(u32::MAX);
600        let owed = due.saturating_sub(self.counted).min(MAX_CATCH_UP);
601        self.pending = self.pending.saturating_add(owed);
602        // Counted up to `due` rather than to what was handed over, so the
603        // repeats a stall swallowed are gone rather than owed.
604        self.counted = due;
605        Animation::due_at(
606            since
607                .saturating_add(repeat.delay_ms)
608                .saturating_add(u64::from(due).saturating_mul(repeat.interval_ms)),
609        )
610    }
611
612    /// A held key under reduced motion still repeats.
613    ///
614    /// `Motion::None` is about movement, not about clocks — the same rule the
615    /// carousel's auto-advance follows. Backspace that stopped deleting because
616    /// somebody turned animations off would be a bug wearing a setting's
617    /// clothes.
618    fn snap(&mut self, now_ms: u64) -> Animation {
619        self.animate(now_ms)
620    }
621
622    fn accepts_pointer(&self) -> bool {
623        true
624    }
625
626    fn focusable(&self) -> bool {
627        !self.no_focus
628    }
629
630    fn preserves_focus(&self) -> bool {
631        self.no_focus
632    }
633}
634
635impl<M> Describe for Button<M> {
636    const KIND: &'static str = "button";
637    const DOC: &'static str = "A rectangle somebody presses to make something happen.";
638    const GROUP: Group = Group::Input;
639    const ICON: &'static denise::icon::Icon = &super::icons::BUTTON;
640
641    const PROPERTIES: &'static [Property] = &[
642        Property::new("text", PropertyKind::Text, "The label."),
643        Property::new(
644            "on-press",
645            PropertyKind::Message(Payload::None),
646            "The message emitted on activation. Omitted, the button is inert — it draws and does not emit.",
647        ),
648        Property::new(
649            "role",
650            PropertyKind::Enum(ROLES),
651            "Colour role. The label's colour comes from the theme's pairing, so it stays readable whichever role is chosen.",
652        ),
653        Property::new(
654            "radius",
655            PropertyKind::Enum(RADII),
656            "Corner rounding token. The theme decides the pixels.",
657        ),
658        Property::new(
659            "size",
660            PropertyKind::Int { min: 6, max: 96 },
661            "Text size in logical pixels.",
662        )
663        .in_pixels(),
664        Property::new(
665            "corner",
666            PropertyKind::Text,
667            "A small legend in the corner, as the keyboard's globe key carries its layout.",
668        ),
669        Property::new(
670            "no-focus",
671            PropertyKind::Bool,
672            "The button never takes focus, so pressing it does not steal the caret from a field.",
673        ),
674        Property::new(
675            "repeat-delay",
676            PropertyKind::Int {
677                min: 100,
678                max: 2000,
679            },
680            "Milliseconds held before the press repeats. Set without `repeat-interval`, the interval becomes this same value, so a lone half of the pair is a button that repeats steadily rather than one that ignores the setting.",
681        ),
682        Property::new(
683            "repeat-interval",
684            PropertyKind::Int { min: 10, max: 1000 },
685            "Milliseconds between repeats. Set without `repeat-delay`, the delay becomes this same value, by the rule `repeat-delay` describes.",
686        ),
687        Property::new(
688            "watch-hold",
689            PropertyKind::Bool,
690            "Report how long the button has been held, for a long-press.",
691        ),
692    ];
693
694    fn get(&self, name: &str) -> Option<Value> {
695        Some(match name {
696            "text" => Value::text(self.label.as_str()),
697            // The message is the application's, and this crate has never seen
698            // its type. See the `describe` module docs.
699            "on-press" => return None,
700            "role" => Value::role(self.role),
701            "radius" => Value::radius(self.radius),
702            "size" => Value::Int(i32::from(self.style.size_px)),
703            "corner" => Value::text(self.corner.as_str()),
704            "no-focus" => Value::Bool(self.no_focus),
705            // One field carries both halves, so a button that does not repeat
706            // reports neither rather than reporting a schedule it does not have.
707            "repeat-delay" => Value::Int(millis(self.repeat?.delay_ms)),
708            "repeat-interval" => Value::Int(millis(self.repeat?.interval_ms)),
709            "watch-hold" => Value::Bool(self.watches_hold),
710            _ => return None,
711        })
712    }
713
714    fn apply(&mut self, name: &str, value: Value) -> Result<(), Mismatch> {
715        match name {
716            "text" => self.label = value.as_text()?,
717            "on-press" => return Err(Mismatch::Supplied),
718            "role" => self.role = value.as_role()?,
719            "radius" => self.radius = value.as_radius()?,
720            "size" => self.style.size_px = value.as_size()?,
721            "corner" => self.corner = value.as_text()?,
722            "no-focus" => self.no_focus = value.as_bool()?,
723            // The two halves arrive one property at a time, and either may be
724            // the only one a file mentions. Whichever comes first supplies the
725            // other, so a lone `repeat-delay=400` is a button that repeats every
726            // 400 ms after 400 ms rather than one that quietly does not repeat.
727            "repeat-delay" => {
728                let delay_ms = value.as_millis()?;
729                self.repeat = Some(match self.repeat {
730                    Some(repeat) => Repeat { delay_ms, ..repeat },
731                    None => Repeat {
732                        delay_ms,
733                        interval_ms: delay_ms.max(1),
734                    },
735                });
736            }
737            "repeat-interval" => {
738                // Never zero, exactly as `with_repeat` insists: an interval of
739                // nothing is a repeat every frame forever.
740                let interval_ms = value.as_millis()?.max(1);
741                self.repeat = Some(match self.repeat {
742                    Some(repeat) => Repeat {
743                        interval_ms,
744                        ..repeat
745                    },
746                    None => Repeat {
747                        delay_ms: interval_ms,
748                        interval_ms,
749                    },
750                });
751            }
752            "watch-hold" => self.watches_hold = value.as_bool()?,
753            _ => return Err(Mismatch::Unknown),
754        }
755        Ok(())
756    }
757}
758
759/// A duration reported to an inspector, saturating rather than wrapping.
760///
761/// The schedule is `u64` because a clock is; an editor's spinbox is not, and a
762/// delay of half a million years is not worth a wider `Value` variant.
763fn millis(ms: u64) -> i32 {
764    i32::try_from(ms).unwrap_or(i32::MAX)
765}