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}