1
0
mirror of https://github.com/trezor/trezor-firmware.git synced 2025-08-03 12:28:13 +00:00

refactor(core/ui): reusable timers

This commit is contained in:
matejcik 2024-04-10 11:42:03 +02:00 committed by M1nd3r
parent 680af2cf18
commit 0b7ea1c415
23 changed files with 228 additions and 234 deletions

View File

@ -400,6 +400,42 @@ impl TimerToken {
} }
} }
#[cfg_attr(feature = "debug", derive(ufmt::derive::uDebug))]
pub struct Timer(Option<TimerToken>);
impl Timer {
/// Create a new timer.
pub const fn new() -> Self {
Self(None)
}
/// Start this timer for a given duration.
///
/// Requests the internal timer token to be scheduled to `duration` from
/// now. If the timer was already running, its token is rescheduled.
pub fn start(&mut self, ctx: &mut EventCtx, duration: Duration) {
let token = self.0.get_or_insert_with(|| ctx.next_timer_token());
ctx.register_timer(*token, duration);
}
/// Stop the timer.
///
/// Does not affect scheduling, only clears the internal timer token. This
/// means that _some_ scheduled task might keep running, but this timer
/// will not trigger when that task expires.
pub fn stop(&mut self) {
self.0 = None;
}
/// Check if the timer has expired.
///
/// Returns `true` if the given event is a timer event and the token matches
/// the internal token of this timer.
pub fn is_expired(&self, event: Event) -> bool {
matches!(event, Event::Timer(token) if self.0 == Some(token))
}
}
pub struct EventCtx { pub struct EventCtx {
timers: Vec<(TimerToken, Duration), { Self::MAX_TIMERS }>, timers: Vec<(TimerToken, Duration), { Self::MAX_TIMERS }>,
next_token: u32, next_token: u32,
@ -464,13 +500,6 @@ impl EventCtx {
self.paint_requested = true; self.paint_requested = true;
} }
/// Request a timer event to be delivered after `duration` elapses.
pub fn request_timer(&mut self, duration: Duration) -> TimerToken {
let token = self.next_timer_token();
self.register_timer(token, duration);
token
}
/// Request an animation frame timer to fire as soon as possible. /// Request an animation frame timer to fire as soon as possible.
pub fn request_anim_frame(&mut self) { pub fn request_anim_frame(&mut self) {
if !self.anim_frame_scheduled { if !self.anim_frame_scheduled {
@ -479,6 +508,10 @@ impl EventCtx {
} }
} }
pub fn is_anim_frame(event: Event) -> bool {
matches!(event, Event::Timer(token) if token == Self::ANIM_FRAME_TIMER)
}
pub fn request_repaint_root(&mut self) { pub fn request_repaint_root(&mut self) {
self.root_repaint_requested = true; self.root_repaint_requested = true;
} }

View File

@ -3,7 +3,7 @@ use crate::{
time::{Duration, Instant}, time::{Duration, Instant},
ui::{ ui::{
animation::Animation, animation::Animation,
component::{Component, Event, EventCtx, Never, TimerToken}, component::{Component, Event, EventCtx, Never, Timer},
display::{self, Color, Font}, display::{self, Color, Font},
geometry::{Offset, Rect}, geometry::{Offset, Rect},
shape::{self, Renderer}, shape::{self, Renderer},
@ -24,7 +24,7 @@ enum State {
pub struct Marquee { pub struct Marquee {
area: Rect, area: Rect,
pause_token: Option<TimerToken>, pause_timer: Timer,
min_offset: i16, min_offset: i16,
max_offset: i16, max_offset: i16,
state: State, state: State,
@ -40,7 +40,7 @@ impl Marquee {
pub fn new(text: TString<'static>, font: Font, fg: Color, bg: Color) -> Self { pub fn new(text: TString<'static>, font: Font, fg: Color, bg: Color) -> Self {
Self { Self {
area: Rect::zero(), area: Rect::zero(),
pause_token: None, pause_timer: Timer::new(),
min_offset: 0, min_offset: 0,
max_offset: 0, max_offset: 0,
state: State::Initial, state: State::Initial,
@ -154,53 +154,50 @@ impl Component for Marquee {
let now = Instant::now(); let now = Instant::now();
if let Event::Timer(token) = event { if self.pause_timer.is_expired(event) {
if self.pause_token == Some(token) { match self.state {
match self.state { State::PauseLeft => {
State::PauseLeft => { let anim = Animation::new(self.max_offset, self.min_offset, self.duration, now);
let anim = self.state = State::Right(anim);
Animation::new(self.max_offset, self.min_offset, self.duration, now);
self.state = State::Right(anim);
}
State::PauseRight => {
let anim =
Animation::new(self.min_offset, self.max_offset, self.duration, now);
self.state = State::Left(anim);
}
_ => {}
} }
State::PauseRight => {
let anim = Animation::new(self.min_offset, self.max_offset, self.duration, now);
self.state = State::Left(anim);
}
_ => {}
}
// We have something to paint, so request to be painted in the next pass.
ctx.request_paint();
// There is further progress in the animation, request an animation frame event.
ctx.request_anim_frame();
}
if EventCtx::is_anim_frame(event) {
if self.is_animating() {
// We have something to paint, so request to be painted in the next pass. // We have something to paint, so request to be painted in the next pass.
ctx.request_paint(); ctx.request_paint();
// There is further progress in the animation, request an animation frame event. // There is further progress in the animation, request an animation frame
// event.
ctx.request_anim_frame(); ctx.request_anim_frame();
} }
if token == EventCtx::ANIM_FRAME_TIMER { match self.state {
if self.is_animating() { State::Right(_) => {
// We have something to paint, so request to be painted in the next pass. if self.is_at_right(now) {
ctx.request_paint(); self.pause_timer.start(ctx, self.pause);
// There is further progress in the animation, request an animation frame self.state = State::PauseRight;
// event.
ctx.request_anim_frame();
}
match self.state {
State::Right(_) => {
if self.is_at_right(now) {
self.pause_token = Some(ctx.request_timer(self.pause));
self.state = State::PauseRight;
}
} }
State::Left(_) => {
if self.is_at_left(now) {
self.pause_token = Some(ctx.request_timer(self.pause));
self.state = State::PauseLeft;
}
}
_ => {}
} }
State::Left(_) => {
if self.is_at_left(now) {
self.pause_timer.start(ctx, self.pause);
self.state = State::PauseLeft;
}
}
_ => {}
} }
} }
None None
} }

View File

@ -27,7 +27,7 @@ pub mod text;
pub mod timeout; pub mod timeout;
pub use bar::Bar; pub use bar::Bar;
pub use base::{Child, Component, ComponentExt, Event, EventCtx, Never, TimerToken}; pub use base::{Child, Component, ComponentExt, Event, EventCtx, Never, Timer};
pub use border::Border; pub use border::Border;
pub use button_request::{ButtonRequestExt, SendButtonRequest}; pub use button_request::{ButtonRequestExt, SendButtonRequest};
#[cfg(all(feature = "jpeg", feature = "ui_image_buffer", feature = "micropython"))] #[cfg(all(feature = "jpeg", feature = "ui_image_buffer", feature = "micropython"))]

View File

@ -1,23 +1,22 @@
use crate::{ use crate::{
time::Duration, time::Duration,
ui::{ ui::{
component::{Component, Event, EventCtx, TimerToken}, component::{Component, Event, EventCtx, Timer},
geometry::Rect, geometry::Rect,
shape::Renderer, shape::Renderer,
}, },
}; };
#[derive(Clone)]
pub struct Timeout { pub struct Timeout {
time_ms: u32, time_ms: u32,
timer: Option<TimerToken>, timer: Timer,
} }
impl Timeout { impl Timeout {
pub fn new(time_ms: u32) -> Self { pub fn new(time_ms: u32) -> Self {
Self { Self {
time_ms, time_ms,
timer: None, timer: Timer::new(),
} }
} }
} }
@ -30,19 +29,10 @@ impl Component for Timeout {
} }
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> { fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
match event { if matches!(event, Event::Attach(_)) {
// Set up timer. self.timer.start(ctx, Duration::from_millis(self.time_ms));
Event::Attach(_) => {
self.timer = Some(ctx.request_timer(Duration::from_millis(self.time_ms)));
None
}
// Fire.
Event::Timer(token) if Some(token) == self.timer => {
self.timer = None;
Some(())
}
_ => None,
} }
self.timer.is_expired(event).then_some(())
} }
fn paint(&mut self) {} fn paint(&mut self) {}

View File

@ -28,7 +28,7 @@ use crate::{
time::Duration, time::Duration,
ui::{ ui::{
button_request::ButtonRequest, button_request::ButtonRequest,
component::{base::AttachType, Component, Event, EventCtx, Never, TimerToken}, component::{base::{AttachType, TimerToken}, Component, Event, EventCtx, Never},
constant, display, constant, display,
event::USBEvent, event::USBEvent,
geometry::Rect, geometry::Rect,

View File

@ -21,7 +21,6 @@ use super::{theme, Frame, FrameMsg};
const MAX_XPUBS: usize = 16; const MAX_XPUBS: usize = 16;
#[derive(Clone)]
pub struct AddressDetails { pub struct AddressDetails {
details: Frame<Paragraphs<ParagraphVecShort<'static>>>, details: Frame<Paragraphs<ParagraphVecShort<'static>>>,
xpub_view: Frame<Paragraphs<Paragraph<'static>>>, xpub_view: Frame<Paragraphs<Paragraph<'static>>>,

View File

@ -5,13 +5,12 @@ use crate::{
time::Duration, time::Duration,
ui::{ ui::{
component::{ component::{
Component, ComponentExt, Event, EventCtx, FixedHeightBar, MsgMap, Split, TimerToken, Component, ComponentExt, Event, EventCtx, FixedHeightBar, MsgMap, Split, Timer,
}, },
display::{self, toif::Icon, Color, Font}, display::{self, toif::Icon, Color, Font},
event::TouchEvent, event::TouchEvent,
geometry::{Alignment, Alignment2D, Insets, Offset, Point, Rect}, geometry::{Alignment, Alignment2D, Insets, Offset, Point, Rect},
shape, shape::{self, Renderer},
shape::Renderer,
}, },
}; };
@ -24,7 +23,6 @@ pub enum ButtonMsg {
LongPressed, LongPressed,
} }
#[derive(Clone)]
pub struct Button { pub struct Button {
area: Rect, area: Rect,
touch_expand: Option<Insets>, touch_expand: Option<Insets>,
@ -34,7 +32,7 @@ pub struct Button {
radius: Option<u8>, radius: Option<u8>,
state: State, state: State,
long_press: Option<Duration>, long_press: Option<Duration>,
long_timer: Option<TimerToken>, long_timer: Timer,
haptic: bool, haptic: bool,
} }
@ -54,7 +52,7 @@ impl Button {
radius: None, radius: None,
state: State::Initial, state: State::Initial,
long_press: None, long_press: None,
long_timer: None, long_timer: Timer::new(),
haptic: true, haptic: true,
} }
} }
@ -348,7 +346,7 @@ impl Component for Button {
} }
self.set(ctx, State::Pressed); self.set(ctx, State::Pressed);
if let Some(duration) = self.long_press { if let Some(duration) = self.long_press {
self.long_timer = Some(ctx.request_timer(duration)); self.long_timer.start(ctx, duration);
} }
return Some(ButtonMsg::Pressed); return Some(ButtonMsg::Pressed);
} }
@ -380,13 +378,13 @@ impl Component for Button {
State::Pressed => { State::Pressed => {
// Touch finished outside our area. // Touch finished outside our area.
self.set(ctx, State::Initial); self.set(ctx, State::Initial);
self.long_timer = None; self.long_timer.stop();
return Some(ButtonMsg::Released); return Some(ButtonMsg::Released);
} }
_ => { _ => {
// Touch finished outside our area. // Touch finished outside our area.
self.set(ctx, State::Initial); self.set(ctx, State::Initial);
self.long_timer = None; self.long_timer.stop();
} }
} }
} }
@ -398,28 +396,25 @@ impl Component for Button {
State::Pressed => { State::Pressed => {
// Touch aborted // Touch aborted
self.set(ctx, State::Initial); self.set(ctx, State::Initial);
self.long_timer = None; self.long_timer.stop();
return Some(ButtonMsg::Released); return Some(ButtonMsg::Released);
} }
_ => { _ => {
// Irrelevant touch abort // Irrelevant touch abort
self.set(ctx, State::Initial); self.set(ctx, State::Initial);
self.long_timer = None; self.long_timer.stop();
} }
} }
} }
Event::Timer(token) => { Event::Timer(_) if self.long_timer.is_expired(event) => {
if self.long_timer == Some(token) { if matches!(self.state, State::Pressed) {
self.long_timer = None; #[cfg(feature = "haptic")]
if matches!(self.state, State::Pressed) { if self.haptic {
#[cfg(feature = "haptic")] play(HapticEffect::ButtonPress);
if self.haptic {
play(HapticEffect::ButtonPress);
}
self.set(ctx, State::Initial);
return Some(ButtonMsg::LongPressed);
} }
self.set(ctx, State::Initial);
return Some(ButtonMsg::LongPressed);
} }
} }
_ => {} _ => {}

View File

@ -80,7 +80,6 @@ impl HorizontalSwipe {
} }
} }
#[derive(Clone)]
pub struct Frame<T> { pub struct Frame<T> {
border: Insets, border: Insets,
bounds: Rect, bounds: Rect,

View File

@ -61,7 +61,7 @@ impl AttachAnimation {
} }
const BUTTON_EXPAND_BORDER: i16 = 32; const BUTTON_EXPAND_BORDER: i16 = 32;
#[derive(Clone)]
pub struct Header { pub struct Header {
area: Rect, area: Rect,
title: Label<'static>, title: Label<'static>,

View File

@ -163,7 +163,6 @@ impl HoldToConfirmAnim {
/// Component requesting a hold to confirm action from a user. Most typically /// Component requesting a hold to confirm action from a user. Most typically
/// embedded as a content of a Frame. /// embedded as a content of a Frame.
#[derive(Clone)]
pub struct HoldToConfirm { pub struct HoldToConfirm {
title: Label<'static>, title: Label<'static>,
area: Rect, area: Rect,

View File

@ -5,7 +5,7 @@ use crate::{
translations::TR, translations::TR,
trezorhal::usb::usb_configured, trezorhal::usb::usb_configured,
ui::{ ui::{
component::{Component, Event, EventCtx, TimerToken}, component::{Component, Event, EventCtx, Timer},
display::{image::ImageInfo, Color, Font}, display::{image::ImageInfo, Color, Font},
event::{TouchEvent, USBEvent}, event::{TouchEvent, USBEvent},
geometry::{Alignment, Alignment2D, Offset, Point, Rect}, geometry::{Alignment, Alignment2D, Offset, Point, Rect},
@ -235,8 +235,8 @@ impl AttachAnimation {
} }
struct HideLabelAnimation { struct HideLabelAnimation {
pub timer: Stopwatch, pub stopwatch: Stopwatch,
token: TimerToken, timer: Timer,
animating: bool, animating: bool,
hidden: bool, hidden: bool,
duration: Duration, duration: Duration,
@ -260,8 +260,8 @@ impl HideLabelAnimation {
fn new(label_width: i16) -> Self { fn new(label_width: i16) -> Self {
Self { Self {
timer: Stopwatch::default(), stopwatch: Stopwatch::default(),
token: TimerToken::INVALID, timer: Timer::new(),
animating: false, animating: false,
hidden: false, hidden: false,
duration: Duration::from_millis((label_width as u32 * 300) / 120), duration: Duration::from_millis((label_width as u32 * 300) / 120),
@ -269,19 +269,19 @@ impl HideLabelAnimation {
} }
fn is_active(&self) -> bool { fn is_active(&self) -> bool {
self.timer.is_running_within(self.duration) self.stopwatch.is_running_within(self.duration)
} }
fn reset(&mut self) { fn reset(&mut self) {
self.timer = Stopwatch::default(); self.stopwatch = Stopwatch::default();
} }
fn elapsed(&self) -> Duration { fn elapsed(&self) -> Duration {
self.timer.elapsed() self.stopwatch.elapsed()
} }
fn change_dir(&mut self) { fn change_dir(&mut self) {
let elapsed = self.timer.elapsed(); let elapsed = self.stopwatch.elapsed();
let start = self let start = self
.duration .duration
@ -289,9 +289,9 @@ impl HideLabelAnimation {
.and_then(|e| Instant::now().checked_sub(e)); .and_then(|e| Instant::now().checked_sub(e));
if let Some(start) = start { if let Some(start) = start {
self.timer = Stopwatch::Running(start); self.stopwatch = Stopwatch::Running(start);
} else { } else {
self.timer = Stopwatch::new_started(); self.stopwatch = Stopwatch::new_started();
} }
} }
@ -300,7 +300,7 @@ impl HideLabelAnimation {
return Offset::zero(); return Offset::zero();
} }
let t = self.timer.elapsed().to_millis() as f32 / 1000.0; let t = self.stopwatch.elapsed().to_millis() as f32 / 1000.0;
let pos = if self.hidden { let pos = if self.hidden {
pareen::constant(0.0) pareen::constant(0.0)
@ -329,7 +329,7 @@ impl HideLabelAnimation {
match event { match event {
Event::Attach(AttachType::Initial) => { Event::Attach(AttachType::Initial) => {
ctx.request_anim_frame(); ctx.request_anim_frame();
self.token = ctx.request_timer(Self::HIDE_AFTER); self.timer.start(ctx, Self::HIDE_AFTER);
} }
Event::Attach(AttachType::Resume) => { Event::Attach(AttachType::Resume) => {
self.hidden = resume.hidden; self.hidden = resume.hidden;
@ -341,13 +341,13 @@ impl HideLabelAnimation {
self.animating = resume.animating; self.animating = resume.animating;
if self.animating { if self.animating {
self.timer = Stopwatch::Running(start); self.stopwatch = Stopwatch::Running(start);
ctx.request_anim_frame(); ctx.request_anim_frame();
} else { } else {
self.timer = Stopwatch::new_stopped(); self.stopwatch = Stopwatch::new_stopped();
} }
if !self.animating && !self.hidden { if !self.animating && !self.hidden {
self.token = ctx.request_timer(Self::HIDE_AFTER); self.timer.start(ctx, Self::HIDE_AFTER);
} }
} }
Event::Timer(EventCtx::ANIM_FRAME_TIMER) => { Event::Timer(EventCtx::ANIM_FRAME_TIMER) => {
@ -361,28 +361,26 @@ impl HideLabelAnimation {
ctx.request_paint(); ctx.request_paint();
if !self.hidden { if !self.hidden {
self.token = ctx.request_timer(Self::HIDE_AFTER); self.timer.start(ctx, Self::HIDE_AFTER);
} }
} }
} }
Event::Timer(token) => { Event::Timer(token) if self.timer.is_expired(event) && !animation_disabled() => {
if token == self.token && !animation_disabled() { self.stopwatch.start();
self.timer.start(); ctx.request_anim_frame();
ctx.request_anim_frame(); self.animating = true;
self.animating = true; self.hidden = false;
self.hidden = false;
}
} }
Event::Touch(TouchEvent::TouchStart(_)) => { Event::Touch(TouchEvent::TouchStart(_)) => {
if !self.animating { if !self.animating {
if self.hidden { if self.hidden {
self.timer.start(); self.stopwatch.start();
self.animating = true; self.animating = true;
ctx.request_anim_frame(); ctx.request_anim_frame();
ctx.request_paint(); ctx.request_paint();
} else { } else {
self.token = ctx.request_timer(Self::HIDE_AFTER); self.timer.start(ctx, Self::HIDE_AFTER);
} }
} else if !self.hidden { } else if !self.hidden {
self.change_dir(); self.change_dir();
@ -399,7 +397,7 @@ impl HideLabelAnimation {
HideLabelAnimationState { HideLabelAnimationState {
animating: self.animating, animating: self.animating,
hidden: self.hidden, hidden: self.hidden,
elapsed: self.timer.elapsed().to_millis(), elapsed: self.stopwatch.elapsed().to_millis(),
} }
} }
} }
@ -420,7 +418,7 @@ pub struct Homescreen {
bg_image: ImageBuffer<Rgb565Canvas<'static>>, bg_image: ImageBuffer<Rgb565Canvas<'static>>,
hold_to_lock: bool, hold_to_lock: bool,
loader: Loader, loader: Loader,
delay: Option<TimerToken>, delay: Timer,
attach_animation: AttachAnimation, attach_animation: AttachAnimation,
label_anim: HideLabelAnimation, label_anim: HideLabelAnimation,
} }
@ -458,7 +456,7 @@ impl Homescreen {
bg_image: buf, bg_image: buf,
hold_to_lock, hold_to_lock,
loader: Loader::with_lock_icon().with_durations(LOADER_DURATION, LOADER_DURATION / 3), loader: Loader::with_lock_icon().with_durations(LOADER_DURATION, LOADER_DURATION / 3),
delay: None, delay: Timer::new(),
attach_animation: AttachAnimation::default(), attach_animation: AttachAnimation::default(),
label_anim: HideLabelAnimation::new(label_width), label_anim: HideLabelAnimation::new(label_width),
} }
@ -513,11 +511,11 @@ impl Homescreen {
if self.loader.is_animating() { if self.loader.is_animating() {
self.loader.start_growing(ctx, Instant::now()); self.loader.start_growing(ctx, Instant::now());
} else { } else {
self.delay = Some(ctx.request_timer(LOADER_DELAY)); self.delay.start(ctx, LOADER_DELAY);
} }
} }
Event::Touch(TouchEvent::TouchEnd(_)) => { Event::Touch(TouchEvent::TouchEnd(_)) => {
self.delay = None; self.delay.stop();
let now = Instant::now(); let now = Instant::now();
if self.loader.is_completely_grown(now) { if self.loader.is_completely_grown(now) {
return true; return true;
@ -526,8 +524,7 @@ impl Homescreen {
self.loader.start_shrinking(ctx, now); self.loader.start_shrinking(ctx, now);
} }
} }
Event::Timer(token) if Some(token) == self.delay => { Event::Timer(token) if self.delay.is_expired(event) => {
self.delay = None;
self.loader.start_growing(ctx, Instant::now()); self.loader.start_growing(ctx, Instant::now());
} }
_ => {} _ => {}

View File

@ -1,11 +1,10 @@
use crate::{ use crate::{
time::Duration, time::Duration,
ui::{ ui::{
component::{text::common::TextEdit, Event, EventCtx, TimerToken}, component::{text::common::TextEdit, Event, EventCtx, Timer},
display::{self, Color, Font}, display::{self, Color, Font},
geometry::{Alignment2D, Offset, Point, Rect}, geometry::{Alignment2D, Offset, Point, Rect},
shape, shape::{self, Renderer},
shape::Renderer,
}, },
}; };
@ -17,6 +16,8 @@ pub struct MultiTapKeyboard {
timeout: Duration, timeout: Duration,
/// The currently pending state. /// The currently pending state.
pending: Option<Pending>, pending: Option<Pending>,
/// Timer for clearing the pending state.
timer: Timer,
} }
struct Pending { struct Pending {
@ -25,8 +26,6 @@ struct Pending {
/// Index of the key press (how many times the `key` was pressed, minus /// Index of the key press (how many times the `key` was pressed, minus
/// one). /// one).
press: usize, press: usize,
/// Timer for clearing the pending state.
timer: TimerToken,
} }
impl MultiTapKeyboard { impl MultiTapKeyboard {
@ -35,6 +34,7 @@ impl MultiTapKeyboard {
Self { Self {
timeout: Duration::from_secs(1), timeout: Duration::from_secs(1),
pending: None, pending: None,
timer: Timer::new(),
} }
} }
@ -48,21 +48,17 @@ impl MultiTapKeyboard {
self.pending.as_ref().map(|p| p.press) self.pending.as_ref().map(|p| p.press)
} }
/// Return the token for the currently pending timer.
pub fn pending_timer(&self) -> Option<TimerToken> {
self.pending.as_ref().map(|p| p.timer)
}
/// Returns `true` if `event` is an `Event::Timer` for the currently pending /// Returns `true` if `event` is an `Event::Timer` for the currently pending
/// timer. /// timer.
pub fn is_timeout_event(&self, event: Event) -> bool { pub fn is_timeout_event(&self, event: Event) -> bool {
matches!((event, self.pending_timer()), (Event::Timer(t), Some(pt)) if pt == t) self.timer.is_expired(event)
} }
/// Reset to the empty state. Takes `EventCtx` to request a paint pass (to /// Reset to the empty state. Takes `EventCtx` to request a paint pass (to
/// either hide or show any pending marker our caller might want to draw /// either hide or show any pending marker our caller might want to draw
/// later). /// later).
pub fn clear_pending_state(&mut self, ctx: &mut EventCtx) { pub fn clear_pending_state(&mut self, ctx: &mut EventCtx) {
self.timer.stop();
if self.pending.is_some() { if self.pending.is_some() {
self.pending = None; self.pending = None;
ctx.request_paint(); ctx.request_paint();
@ -97,11 +93,8 @@ impl MultiTapKeyboard {
// transition only happens as a result of an append op, so the painting should // transition only happens as a result of an append op, so the painting should
// be requested by handling the `TextEdit`. // be requested by handling the `TextEdit`.
self.pending = if key_text.len() > 1 { self.pending = if key_text.len() > 1 {
Some(Pending { self.timer.start(ctx, self.timeout);
key, Some(Pending { key, press })
press,
timer: ctx.request_timer(self.timeout),
})
} else { } else {
None None
}; };

View File

@ -8,7 +8,7 @@ use crate::{
component::{ component::{
base::{AttachType, ComponentExt}, base::{AttachType, ComponentExt},
text::TextStyle, text::TextStyle,
Component, Event, EventCtx, Label, Never, Pad, SwipeDirection, TimerToken, Child, Component, Event, EventCtx, Label, Never, Pad, SwipeDirection, Timer,
}, },
display::Font, display::Font,
event::TouchEvent, event::TouchEvent,
@ -256,7 +256,7 @@ pub struct PinKeyboard<'a> {
cancel_btn: Button, cancel_btn: Button,
confirm_btn: Button, confirm_btn: Button,
digit_btns: [(Button, usize); DIGIT_COUNT], digit_btns: [(Button, usize); DIGIT_COUNT],
warning_timer: Option<TimerToken>, warning_timer: Timer,
attach_animation: AttachAnimation, attach_animation: AttachAnimation,
close_animation: CloseAnimation, close_animation: CloseAnimation,
close_confirm: bool, close_confirm: bool,
@ -295,7 +295,7 @@ impl<'a> PinKeyboard<'a> {
.styled(theme::button_pin_confirm()) .styled(theme::button_pin_confirm())
.initially_enabled(false), .initially_enabled(false),
digit_btns: Self::generate_digit_buttons(), digit_btns: Self::generate_digit_buttons(),
warning_timer: None, warning_timer: Timer::new(),
attach_animation: AttachAnimation::default(), attach_animation: AttachAnimation::default(),
close_animation: CloseAnimation::default(), close_animation: CloseAnimation::default(),
close_confirm: false, close_confirm: false,
@ -417,10 +417,10 @@ impl Component for PinKeyboard<'_> {
match event { match event {
// Set up timer to switch off warning prompt. // Set up timer to switch off warning prompt.
Event::Attach(_) if self.major_warning.is_some() => { Event::Attach(_) if self.major_warning.is_some() => {
self.warning_timer = Some(ctx.request_timer(Duration::from_secs(2))); self.warning_timer.start(ctx, Duration::from_secs(2));
} }
// Hide warning, show major prompt. // Hide warning, show major prompt.
Event::Timer(token) if Some(token) == self.warning_timer => { Event::Timer(_) if self.warning_timer.is_expired(event) => {
self.major_warning = None; self.major_warning = None;
self.minor_prompt.request_complete_repaint(ctx); self.minor_prompt.request_complete_repaint(ctx);
ctx.request_paint(); ctx.request_paint();

View File

@ -9,7 +9,6 @@ use super::{HoldToConfirm, TapToConfirm};
/// Component requesting an action from a user. Most typically embedded as a /// Component requesting an action from a user. Most typically embedded as a
/// content of a Frame and promptin "Tap to confirm" or "Hold to XYZ". /// content of a Frame and promptin "Tap to confirm" or "Hold to XYZ".
#[derive(Clone)]
pub enum PromptScreen { pub enum PromptScreen {
Tap(TapToConfirm), Tap(TapToConfirm),
Hold(HoldToConfirm), Hold(HoldToConfirm),

View File

@ -129,7 +129,6 @@ impl StatusAnimation {
/// Component showing status of an operation. Most typically embedded as a /// Component showing status of an operation. Most typically embedded as a
/// content of a Frame and showing success (checkmark with a circle around). /// content of a Frame and showing success (checkmark with a circle around).
#[derive(Clone)]
pub struct StatusScreen { pub struct StatusScreen {
area: Rect, area: Rect,
icon: Icon, icon: Icon,
@ -140,7 +139,6 @@ pub struct StatusScreen {
msg: Label<'static>, msg: Label<'static>,
} }
#[derive(Clone)]
enum DismissType { enum DismissType {
SwipeUp, SwipeUp,
Timeout(Timeout), Timeout(Timeout),

View File

@ -113,7 +113,6 @@ impl TapToConfirmAnim {
/// Component requesting a Tap to confirm action from a user. Most typically /// Component requesting a Tap to confirm action from a user. Most typically
/// embedded as a content of a Frame. /// embedded as a content of a Frame.
#[derive(Clone)]
pub struct TapToConfirm { pub struct TapToConfirm {
area: Rect, area: Rect,
button: Button, button: Button,

View File

@ -172,7 +172,6 @@ impl AttachAnimation {
} }
} }
#[derive(Clone)]
pub struct VerticalMenu { pub struct VerticalMenu {
area: Rect, area: Rect,
/// buttons placed vertically from top to bottom /// buttons placed vertically from top to bottom

View File

@ -4,7 +4,7 @@ use super::{
use crate::{ use crate::{
time::{Duration, Instant}, time::{Duration, Instant},
ui::{ ui::{
component::{base::Event, Component, EventCtx, Pad, TimerToken}, component::{base::Event, Component, EventCtx, Pad, Timer},
event::{ButtonEvent, PhysicalButton}, event::{ButtonEvent, PhysicalButton},
geometry::Rect, geometry::Rect,
shape::Renderer, shape::Renderer,
@ -122,7 +122,7 @@ pub struct ButtonContainer {
/// `ButtonControllerMsg::Triggered` /// `ButtonControllerMsg::Triggered`
long_press_ms: u32, long_press_ms: u32,
/// Timer for sending `ButtonControllerMsg::LongPressed` /// Timer for sending `ButtonControllerMsg::LongPressed`
long_pressed_timer: Option<TimerToken>, long_pressed_timer: Timer,
/// Whether it should even send `ButtonControllerMsg::LongPressed` events /// Whether it should even send `ButtonControllerMsg::LongPressed` events
/// (optional) /// (optional)
send_long_press: bool, send_long_press: bool,
@ -141,7 +141,7 @@ impl ButtonContainer {
button_type: ButtonType::from_button_details(pos, btn_details), button_type: ButtonType::from_button_details(pos, btn_details),
pressed_since: None, pressed_since: None,
long_press_ms: DEFAULT_LONG_PRESS_MS, long_press_ms: DEFAULT_LONG_PRESS_MS,
long_pressed_timer: None, long_pressed_timer: Timer::new(),
send_long_press, send_long_press,
} }
} }
@ -190,7 +190,7 @@ impl ButtonContainer {
Instant::now().saturating_duration_since(since).to_millis() > self.long_press_ms Instant::now().saturating_duration_since(since).to_millis() > self.long_press_ms
}); });
self.pressed_since = None; self.pressed_since = None;
self.long_pressed_timer = None; self.long_pressed_timer.stop();
Some(ButtonControllerMsg::Triggered(self.pos, long_press)) Some(ButtonControllerMsg::Triggered(self.pos, long_press))
} }
ButtonType::HoldToConfirm(_) => { ButtonType::HoldToConfirm(_) => {
@ -216,20 +216,20 @@ impl ButtonContainer {
pub fn got_pressed(&mut self, ctx: &mut EventCtx) { pub fn got_pressed(&mut self, ctx: &mut EventCtx) {
self.pressed_since = Some(Instant::now()); self.pressed_since = Some(Instant::now());
if self.send_long_press { if self.send_long_press {
self.long_pressed_timer = self.long_pressed_timer
Some(ctx.request_timer(Duration::from_millis(self.long_press_ms))); .start(ctx, Duration::from_millis(self.long_press_ms));
} }
} }
/// Reset the pressed information. /// Reset the pressed information.
pub fn reset(&mut self) { pub fn reset(&mut self) {
self.pressed_since = None; self.pressed_since = None;
self.long_pressed_timer = None; self.long_pressed_timer.stop();
} }
/// Whether token matches what we have /// Whether token matches what we have
pub fn is_timer_token(&self, token: TimerToken) -> bool { pub fn is_timer(&self, event: Event) -> bool {
self.long_pressed_timer == Some(token) self.long_pressed_timer.is_expired(event)
} }
/// Registering hold event. /// Registering hold event.
@ -380,14 +380,14 @@ impl ButtonController {
} }
} }
fn handle_long_press_timer_token(&mut self, token: TimerToken) -> Option<ButtonPos> { fn handle_long_press_timers(&mut self, event: Event) -> Option<ButtonPos> {
if self.left_btn.is_timer_token(token) { if self.left_btn.is_timer(event) {
return Some(ButtonPos::Left); return Some(ButtonPos::Left);
} }
if self.middle_btn.is_timer_token(token) { if self.middle_btn.is_timer(event) {
return Some(ButtonPos::Middle); return Some(ButtonPos::Middle);
} }
if self.right_btn.is_timer_token(token) { if self.right_btn.is_timer(event) {
return Some(ButtonPos::Right); return Some(ButtonPos::Right);
} }
None None
@ -572,11 +572,11 @@ impl Component for ButtonController {
event event
} }
// Timer - handle clickable properties and HoldToConfirm expiration // Timer - handle clickable properties and HoldToConfirm expiration
Event::Timer(token) => { Event::Timer(_) => {
if let Some(ignore_btn_delay) = &mut self.ignore_btn_delay { if let Some(ignore_btn_delay) = &mut self.ignore_btn_delay {
ignore_btn_delay.handle_timer_token(token); ignore_btn_delay.handle_timers(event);
} }
if let Some(pos) = self.handle_long_press_timer_token(token) { if let Some(pos) = self.handle_long_press_timers(event) {
return Some(ButtonControllerMsg::LongPressed(pos)); return Some(ButtonControllerMsg::LongPressed(pos));
} }
self.handle_htc_expiration(ctx, event) self.handle_htc_expiration(ctx, event)
@ -624,9 +624,9 @@ struct IgnoreButtonDelay {
/// Whether right button is currently clickable /// Whether right button is currently clickable
right_clickable: bool, right_clickable: bool,
/// Timer for setting the left_clickable /// Timer for setting the left_clickable
left_clickable_timer: Option<TimerToken>, left_clickable_timer: Timer,
/// Timer for setting the right_clickable /// Timer for setting the right_clickable
right_clickable_timer: Option<TimerToken>, right_clickable_timer: Timer,
} }
impl IgnoreButtonDelay { impl IgnoreButtonDelay {
@ -635,8 +635,8 @@ impl IgnoreButtonDelay {
delay: Duration::from_millis(delay_ms), delay: Duration::from_millis(delay_ms),
left_clickable: true, left_clickable: true,
right_clickable: true, right_clickable: true,
left_clickable_timer: None, left_clickable_timer: Timer::new(),
right_clickable_timer: None, right_clickable_timer: Timer::new(),
} }
} }
@ -644,11 +644,11 @@ impl IgnoreButtonDelay {
match pos { match pos {
ButtonPos::Left => { ButtonPos::Left => {
self.left_clickable = true; self.left_clickable = true;
self.left_clickable_timer = None; self.left_clickable_timer.stop();
} }
ButtonPos::Right => { ButtonPos::Right => {
self.right_clickable = true; self.right_clickable = true;
self.right_clickable_timer = None; self.right_clickable_timer.stop();
} }
ButtonPos::Middle => {} ButtonPos::Middle => {}
} }
@ -656,10 +656,10 @@ impl IgnoreButtonDelay {
pub fn handle_button_press(&mut self, ctx: &mut EventCtx, button: PhysicalButton) { pub fn handle_button_press(&mut self, ctx: &mut EventCtx, button: PhysicalButton) {
if matches!(button, PhysicalButton::Left) { if matches!(button, PhysicalButton::Left) {
self.right_clickable_timer = Some(ctx.request_timer(self.delay)); self.right_clickable_timer.start(ctx, self.delay);
} }
if matches!(button, PhysicalButton::Right) { if matches!(button, PhysicalButton::Right) {
self.left_clickable_timer = Some(ctx.request_timer(self.delay)); self.left_clickable_timer.start(ctx, self.delay);
} }
} }
@ -673,22 +673,20 @@ impl IgnoreButtonDelay {
false false
} }
pub fn handle_timer_token(&mut self, token: TimerToken) { pub fn handle_timers(&mut self, event: Event) {
if self.left_clickable_timer == Some(token) { if self.left_clickable_timer.is_expired(event) {
self.left_clickable = false; self.left_clickable = false;
self.left_clickable_timer = None;
} }
if self.right_clickable_timer == Some(token) { if self.right_clickable_timer.is_expired(event) {
self.right_clickable = false; self.right_clickable = false;
self.right_clickable_timer = None;
} }
} }
pub fn reset(&mut self) { pub fn reset(&mut self) {
self.left_clickable = true; self.left_clickable = true;
self.right_clickable = true; self.right_clickable = true;
self.left_clickable_timer = None; self.left_clickable_timer.stop();
self.right_clickable_timer = None; self.right_clickable_timer.stop();
} }
} }
@ -700,7 +698,7 @@ impl IgnoreButtonDelay {
/// Can be started e.g. by holding left/right button. /// Can be started e.g. by holding left/right button.
pub struct AutomaticMover { pub struct AutomaticMover {
/// For requesting timer events repeatedly /// For requesting timer events repeatedly
timer_token: Option<TimerToken>, timer: Timer,
/// Which direction should we go (which button is down) /// Which direction should we go (which button is down)
moving_direction: Option<ButtonPos>, moving_direction: Option<ButtonPos>,
/// How many screens were moved automatically /// How many screens were moved automatically
@ -721,7 +719,7 @@ impl AutomaticMover {
} }
Self { Self {
timer_token: None, timer: Timer::new(),
moving_direction: None, moving_direction: None,
auto_moved_screens: 0, auto_moved_screens: 0,
duration_func: default_duration_func, duration_func: default_duration_func,
@ -760,12 +758,12 @@ impl AutomaticMover {
pub fn start_moving(&mut self, ctx: &mut EventCtx, button: ButtonPos) { pub fn start_moving(&mut self, ctx: &mut EventCtx, button: ButtonPos) {
self.auto_moved_screens = 0; self.auto_moved_screens = 0;
self.moving_direction = Some(button); self.moving_direction = Some(button);
self.timer_token = Some(ctx.request_timer(self.get_auto_move_duration())); self.timer.start(ctx, self.get_auto_move_duration());
} }
pub fn stop_moving(&mut self) { pub fn stop_moving(&mut self) {
self.moving_direction = None; self.moving_direction = None;
self.timer_token = None; self.timer.stop();
} }
} }
@ -781,15 +779,11 @@ impl Component for AutomaticMover {
fn render<'s>(&'s self, _target: &mut impl Renderer<'s>) {} fn render<'s>(&'s self, _target: &mut impl Renderer<'s>) {}
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> { fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
// Moving automatically only when we receive a TimerToken that we have if self.timer.is_expired(event) && self.moving_direction.is_some() {
// requested before // Restart timer and send the appropriate button trigger event
if let Event::Timer(token) = event { self.timer.start(ctx, self.get_auto_move_duration());
if self.timer_token == Some(token) && self.moving_direction.is_some() { self.auto_moved_screens += 1;
// Request new token and send the appropriate button trigger event return self.moving_direction;
self.timer_token = Some(ctx.request_timer(self.get_auto_move_duration()));
self.auto_moved_screens += 1;
return self.moving_direction;
}
} }
None None
} }

View File

@ -5,7 +5,7 @@ use crate::{
time::Duration, time::Duration,
ui::{ ui::{
component::{ component::{
Component, ComponentExt, Event, EventCtx, FixedHeightBar, MsgMap, Split, TimerToken, Component, ComponentExt, Event, EventCtx, FixedHeightBar, MsgMap, Split, Timer,
}, },
display::{self, toif::Icon, Color, Font}, display::{self, toif::Icon, Color, Font},
event::TouchEvent, event::TouchEvent,
@ -31,7 +31,7 @@ pub struct Button {
styles: ButtonStyleSheet, styles: ButtonStyleSheet,
state: State, state: State,
long_press: Option<Duration>, long_press: Option<Duration>,
long_timer: Option<TimerToken>, long_timer: Timer,
haptics: bool, haptics: bool,
} }
@ -48,7 +48,7 @@ impl Button {
styles: theme::button_default(), styles: theme::button_default(),
state: State::Initial, state: State::Initial,
long_press: None, long_press: None,
long_timer: None, long_timer: Timer::new(),
haptics: true, haptics: true,
} }
} }
@ -317,7 +317,7 @@ impl Component for Button {
} }
self.set(ctx, State::Pressed); self.set(ctx, State::Pressed);
if let Some(duration) = self.long_press { if let Some(duration) = self.long_press {
self.long_timer = Some(ctx.request_timer(duration)); self.long_timer.start(ctx, duration)
} }
return Some(ButtonMsg::Pressed); return Some(ButtonMsg::Pressed);
} }
@ -349,21 +349,18 @@ impl Component for Button {
_ => { _ => {
// Touch finished outside our area. // Touch finished outside our area.
self.set(ctx, State::Initial); self.set(ctx, State::Initial);
self.long_timer = None; self.long_timer.stop();
} }
} }
} }
Event::Timer(token) => { Event::Timer(_) if self.long_timer.is_expired(event) => {
if self.long_timer == Some(token) { if matches!(self.state, State::Pressed) {
self.long_timer = None; #[cfg(feature = "haptic")]
if matches!(self.state, State::Pressed) { if self.haptics {
#[cfg(feature = "haptic")] haptic::play(HapticEffect::ButtonPress);
if self.haptics {
haptic::play(HapticEffect::ButtonPress);
}
self.set(ctx, State::Initial);
return Some(ButtonMsg::LongPressed);
} }
self.set(ctx, State::Initial);
return Some(ButtonMsg::LongPressed);
} }
} }
_ => {} _ => {}

View File

@ -7,7 +7,7 @@ use crate::{
translations::TR, translations::TR,
trezorhal::usb::usb_configured, trezorhal::usb::usb_configured,
ui::{ ui::{
component::{Component, Event, EventCtx, Pad, TimerToken}, component::{Component, Event, EventCtx, Pad, Timer},
display::{ display::{
self, self,
image::{ImageInfo, ToifFormat}, image::{ImageInfo, ToifFormat},
@ -58,7 +58,7 @@ pub struct Homescreen {
loader: Loader, loader: Loader,
pad: Pad, pad: Pad,
paint_notification_only: bool, paint_notification_only: bool,
delay: Option<TimerToken>, delay: Timer,
} }
pub enum HomescreenMsg { pub enum HomescreenMsg {
@ -79,7 +79,7 @@ impl Homescreen {
loader: Loader::with_lock_icon().with_durations(LOADER_DURATION, LOADER_DURATION / 3), loader: Loader::with_lock_icon().with_durations(LOADER_DURATION, LOADER_DURATION / 3),
pad: Pad::with_background(theme::BG), pad: Pad::with_background(theme::BG),
paint_notification_only: false, paint_notification_only: false,
delay: None, delay: Timer::new(),
} }
} }
@ -152,11 +152,11 @@ impl Homescreen {
if self.loader.is_animating() { if self.loader.is_animating() {
self.loader.start_growing(ctx, Instant::now()); self.loader.start_growing(ctx, Instant::now());
} else { } else {
self.delay = Some(ctx.request_timer(LOADER_DELAY)); self.delay.start(ctx, LOADER_DELAY);
} }
} }
Event::Touch(TouchEvent::TouchEnd(_)) => { Event::Touch(TouchEvent::TouchEnd(_)) => {
self.delay = None; self.delay.stop();
let now = Instant::now(); let now = Instant::now();
if self.loader.is_completely_grown(now) { if self.loader.is_completely_grown(now) {
return true; return true;
@ -165,8 +165,7 @@ impl Homescreen {
self.loader.start_shrinking(ctx, now); self.loader.start_shrinking(ctx, now);
} }
} }
Event::Timer(token) if Some(token) == self.delay => { Event::Timer(_) if self.delay.is_expired(event) => {
self.delay = None;
self.pad.clear(); self.pad.clear();
self.paint_notification_only = false; self.paint_notification_only = false;
self.loader.start_growing(ctx, Instant::now()); self.loader.start_growing(ctx, Instant::now());

View File

@ -1,7 +1,7 @@
use crate::{ use crate::{
time::Duration, time::Duration,
ui::{ ui::{
component::{text::common::TextEdit, Event, EventCtx, TimerToken}, component::{text::common::TextEdit, Event, EventCtx, Timer},
display::{self, Color, Font}, display::{self, Color, Font},
geometry::{Offset, Point, Rect}, geometry::{Offset, Point, Rect},
shape, shape,
@ -24,7 +24,16 @@ struct Pending {
/// one). /// one).
press: usize, press: usize,
/// Timer for clearing the pending state. /// Timer for clearing the pending state.
timer: TimerToken, timer: Timer,
}
impl Pending {
/// Create a new pending state for a key.
fn start(ctx: &mut EventCtx, key: usize, press: usize, timeout: Duration) -> Self {
let mut timer = Timer::new();
timer.start(ctx, timeout);
Self { key, press, timer }
}
} }
impl MultiTapKeyboard { impl MultiTapKeyboard {
@ -47,14 +56,14 @@ impl MultiTapKeyboard {
} }
/// Return the token for the currently pending timer. /// Return the token for the currently pending timer.
pub fn pending_timer(&self) -> Option<TimerToken> { pub fn pending_timer(&self) -> Option<&Timer> {
self.pending.as_ref().map(|p| p.timer) self.pending.as_ref().map(|p| &p.timer)
} }
/// Returns `true` if `event` is an `Event::Timer` for the currently pending /// Returns `true` if `event` is an `Event::Timer` for the currently pending
/// timer. /// timer.
pub fn is_timeout_event(&self, event: Event) -> bool { pub fn is_timeout_event(&self, event: Event) -> bool {
matches!((event, self.pending_timer()), (Event::Timer(t), Some(pt)) if pt == t) self.pending_timer().map_or(false, |t| t.is_expired(event))
} }
/// Reset to the empty state. Takes `EventCtx` to request a paint pass (to /// Reset to the empty state. Takes `EventCtx` to request a paint pass (to
@ -95,11 +104,7 @@ impl MultiTapKeyboard {
// transition only happens as a result of an append op, so the painting should // transition only happens as a result of an append op, so the painting should
// be requested by handling the `TextEdit`. // be requested by handling the `TextEdit`.
self.pending = if key_text.len() > 1 { self.pending = if key_text.len() > 1 {
Some(Pending { Some(Pending::start(ctx, key, press, self.timeout))
key,
press,
timer: ctx.request_timer(self.timeout),
})
} else { } else {
None None
}; };

View File

@ -7,7 +7,7 @@ use crate::{
ui::{ ui::{
component::{ component::{
base::ComponentExt, text::TextStyle, Child, Component, Event, EventCtx, Label, Maybe, base::ComponentExt, text::TextStyle, Child, Component, Event, EventCtx, Label, Maybe,
Never, Pad, TimerToken, Never, Pad, Timer,
}, },
display::{self, Font}, display::{self, Font},
event::TouchEvent, event::TouchEvent,
@ -54,7 +54,7 @@ pub struct PinKeyboard<'a> {
cancel_btn: Child<Maybe<Button>>, cancel_btn: Child<Maybe<Button>>,
confirm_btn: Child<Button>, confirm_btn: Child<Button>,
digit_btns: [Child<Button>; DIGIT_COUNT], digit_btns: [Child<Button>; DIGIT_COUNT],
warning_timer: Option<TimerToken>, warning_timer: Timer,
} }
impl<'a> PinKeyboard<'a> { impl<'a> PinKeyboard<'a> {
@ -99,7 +99,7 @@ impl<'a> PinKeyboard<'a> {
.initially_enabled(false) .initially_enabled(false)
.into_child(), .into_child(),
digit_btns: Self::generate_digit_buttons(), digit_btns: Self::generate_digit_buttons(),
warning_timer: None, warning_timer: Timer::new(),
} }
} }
@ -201,10 +201,10 @@ impl Component for PinKeyboard<'_> {
match event { match event {
// Set up timer to switch off warning prompt. // Set up timer to switch off warning prompt.
Event::Attach(_) if self.major_warning.is_some() => { Event::Attach(_) if self.major_warning.is_some() => {
self.warning_timer = Some(ctx.request_timer(Duration::from_secs(2))); self.warning_timer.start(ctx, Duration::from_secs(2));
} }
// Hide warning, show major prompt. // Hide warning, show major prompt.
Event::Timer(token) if Some(token) == self.warning_timer => { Event::Timer(_) if self.warning_timer.is_expired(event) => {
self.major_warning = None; self.major_warning = None;
self.textbox_pad.clear(); self.textbox_pad.clear();
self.minor_prompt.request_complete_repaint(ctx); self.minor_prompt.request_complete_repaint(ctx);

View File

@ -297,11 +297,13 @@ class Layout(Generic[T]):
# do not schedule another animation frame if one is already scheduled # do not schedule another animation frame if one is already scheduled
return return
assert token not in self.timers task = self.timers.get(token)
task = timer_task() if task is None:
self.timers[token] = task task = timer_task()
self.timers[token] = task
deadline = utime.ticks_add(utime.ticks_ms(), duration) deadline = utime.ticks_add(utime.ticks_ms(), duration)
loop.schedule(task, deadline=deadline) loop.schedule(task, deadline=deadline, reschedule=True)
def _emit_message(self, msg: Any) -> None: def _emit_message(self, msg: Any) -> None:
"""Process a message coming out of the Rust layout. Set is as a result and shut """Process a message coming out of the Rust layout. Set is as a result and shut