1
0
mirror of https://github.com/trezor/trezor-firmware.git synced 2025-01-23 13:51:00 +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 3de77db2b8
commit adaf0b7871
29 changed files with 280 additions and 271 deletions

View File

@ -402,6 +402,11 @@ pub struct TimerToken(u32);
impl TimerToken { impl TimerToken {
/// Value of an invalid (or missing) token. /// Value of an invalid (or missing) token.
pub const INVALID: TimerToken = TimerToken(0); pub const INVALID: TimerToken = TimerToken(0);
/// Reserved value of the animation frame timer.
pub const ANIM_FRAME: TimerToken = TimerToken(1);
/// Starting token value
const STARTING_TOKEN: u32 = 2;
pub const fn from_raw(raw: u32) -> Self { pub const fn from_raw(raw: u32) -> Self {
Self(raw) Self(raw)
@ -410,11 +415,77 @@ impl TimerToken {
pub const fn into_raw(self) -> u32 { pub const fn into_raw(self) -> u32 {
self.0 self.0
} }
pub fn next_token() -> Self {
static mut NEXT_TOKEN: TimerToken = Self(TimerToken::STARTING_TOKEN);
// SAFETY: we are in single-threaded environment
let token = unsafe { NEXT_TOKEN };
let next = {
if token.0 == u32::MAX {
TimerToken(Self::STARTING_TOKEN)
} else {
TimerToken(token.0 + 1)
}
};
// SAFETY: we are in single-threaded environment
unsafe { NEXT_TOKEN = next };
token
}
}
#[cfg_attr(feature = "debug", derive(ufmt::derive::uDebug))]
pub struct Timer {
token: TimerToken,
running: bool,
}
impl Timer {
/// Create a new timer.
pub const fn new() -> Self {
Self {
token: TimerToken::INVALID,
running: false,
}
}
/// 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) {
if self.token == TimerToken::INVALID {
self.token = TimerToken::next_token();
}
self.running = true;
ctx.register_timer(self.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.running = false;
}
/// 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 expire(&mut self, event: Event) -> bool {
if self.running && event == Event::Timer(self.token) {
self.running = false;
true
} else {
false
}
}
} }
pub struct EventCtx { pub struct EventCtx {
timers: Vec<(TimerToken, Duration), { Self::MAX_TIMERS }>, timers: Vec<(TimerToken, Duration), { Self::MAX_TIMERS }>,
next_token: u32,
place_requested: bool, place_requested: bool,
paint_requested: bool, paint_requested: bool,
anim_frame_scheduled: bool, anim_frame_scheduled: bool,
@ -433,17 +504,12 @@ impl EventCtx {
/// How long into the future we should schedule the animation frame timer. /// How long into the future we should schedule the animation frame timer.
const ANIM_FRAME_DURATION: Duration = Duration::from_millis(1); const ANIM_FRAME_DURATION: Duration = Duration::from_millis(1);
// 0 == `TimerToken::INVALID`,
// 1 == `Self::ANIM_FRAME_TIMER`.
const STARTING_TIMER_TOKEN: u32 = 2;
/// Maximum amount of timers requested in one event tick. /// Maximum amount of timers requested in one event tick.
const MAX_TIMERS: usize = 4; const MAX_TIMERS: usize = 4;
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
timers: Vec::new(), timers: Vec::new(),
next_token: Self::STARTING_TIMER_TOKEN,
place_requested: false, place_requested: false,
paint_requested: false, paint_requested: false,
anim_frame_scheduled: false, anim_frame_scheduled: false,
@ -476,13 +542,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 {
@ -491,6 +550,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;
} }
@ -551,10 +614,7 @@ impl EventCtx {
#[cfg(feature = "ui_debug")] #[cfg(feature = "ui_debug")]
assert!(self.button_request.is_none()); assert!(self.button_request.is_none());
// replace self with a new instance, keeping only the fields we care about // replace self with a new instance, keeping only the fields we care about
*self = Self { *self = Self::new();
next_token: self.next_token,
..Self::new()
}
} }
fn register_timer(&mut self, token: TimerToken, duration: Duration) { fn register_timer(&mut self, token: TimerToken, duration: Duration) {
@ -566,18 +626,6 @@ impl EventCtx {
} }
} }
fn next_timer_token(&mut self) -> TimerToken {
let token = TimerToken(self.next_token);
// We start again from the beginning if the token counter overflows. This would
// probably happen in case of a bug and a long-running session. Let's risk the
// collisions in such case.
self.next_token = self
.next_token
.checked_add(1)
.unwrap_or(Self::STARTING_TIMER_TOKEN);
token
}
pub fn set_transition_out(&mut self, attach_type: AttachType) { pub fn set_transition_out(&mut self, attach_type: AttachType) {
self.transition_out = Some(attach_type); self.transition_out = Some(attach_type);
} }

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,17 +154,14 @@ impl Component for Marquee {
let now = Instant::now(); let now = Instant::now();
if let Event::Timer(token) = event { if self.pause_timer.expire(event) {
if self.pause_token == Some(token) {
match self.state { match self.state {
State::PauseLeft => { State::PauseLeft => {
let anim = let anim = Animation::new(self.max_offset, self.min_offset, self.duration, now);
Animation::new(self.max_offset, self.min_offset, self.duration, now);
self.state = State::Right(anim); self.state = State::Right(anim);
} }
State::PauseRight => { State::PauseRight => {
let anim = let anim = Animation::new(self.min_offset, self.max_offset, self.duration, now);
Animation::new(self.min_offset, self.max_offset, self.duration, now);
self.state = State::Left(anim); self.state = State::Left(anim);
} }
_ => {} _ => {}
@ -175,7 +172,7 @@ impl Component for Marquee {
ctx.request_anim_frame(); ctx.request_anim_frame();
} }
if token == EventCtx::ANIM_FRAME_TIMER { if EventCtx::is_anim_frame(event) {
if self.is_animating() { 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();
@ -187,20 +184,20 @@ impl Component for Marquee {
match self.state { match self.state {
State::Right(_) => { State::Right(_) => {
if self.is_at_right(now) { if self.is_at_right(now) {
self.pause_token = Some(ctx.request_timer(self.pause)); self.pause_timer.start(ctx, self.pause);
self.state = State::PauseRight; self.state = State::PauseRight;
} }
} }
State::Left(_) => { State::Left(_) => {
if self.is_at_left(now) { if self.is_at_left(now) {
self.pause_token = Some(ctx.request_timer(self.pause)); self.pause_timer.start(ctx, self.pause);
self.state = State::PauseLeft; 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, FlowMsg, Never, TimerToken}; pub use base::{Child, Component, ComponentExt, Event, EventCtx, FlowMsg, 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.expire(event).then_some(())
} }
fn paint(&mut self) {} fn paint(&mut self) {}

View File

@ -29,7 +29,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

@ -4,12 +4,11 @@ use crate::{
strutil::TString, strutil::TString,
time::Duration, time::Duration,
ui::{ ui::{
component::{Component, Event, EventCtx, TimerToken}, component::{Component, Event, EventCtx, 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,
util::split_two_lines, util::split_two_lines,
}, },
}; };
@ -23,7 +22,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>,
@ -33,7 +31,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,
} }
@ -53,7 +51,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,
} }
} }
@ -312,7 +310,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);
} }
@ -344,13 +342,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();
} }
} }
} }
@ -363,20 +361,18 @@ 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.expire(event) => {
if self.long_timer == Some(token) {
self.long_timer = None;
if matches!(self.state, State::Pressed) { if matches!(self.state, State::Pressed) {
#[cfg(feature = "haptic")] #[cfg(feature = "haptic")]
if self.haptic { if self.haptic {
@ -386,7 +382,6 @@ impl Component for Button {
return Some(ButtonMsg::LongPressed); return Some(ButtonMsg::LongPressed);
} }
} }
}
_ => {} _ => {}
}; };
None None

View File

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

View File

@ -59,7 +59,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.expire(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.expire(event) => {
self.delay = None;
self.loader.start_growing(ctx, Instant::now()); self.loader.start_growing(ctx, Instant::now());
} }
_ => {} _ => {}

View File

@ -93,7 +93,7 @@ impl Component for Bip39Input {
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> { fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
self.button_suggestion.event(ctx, event); self.button_suggestion.event(ctx, event);
if self.multi_tap.is_timeout_event(event) { if self.multi_tap.timeout_event(event) {
self.on_timeout(ctx) self.on_timeout(ctx)
} else if let Some(ButtonMsg::Clicked) = self.button.event(ctx, event) { } else if let Some(ButtonMsg::Clicked) = self.button.event(ctx, event) {
self.on_input_click(ctx) self.on_input_click(ctx)

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 timeout_event(&mut self, event: Event) -> bool {
matches!((event, self.pending_timer()), (Event::Timer(t), Some(pt)) if pt == t) self.timer.expire(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

@ -321,7 +321,7 @@ impl Component for PassphraseKeyboard {
} }
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> { fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
if self.input.multi_tap.is_timeout_event(event) { if self.input.multi_tap.timeout_event(event) {
self.input.multi_tap.clear_pending_state(ctx); self.input.multi_tap.clear_pending_state(ctx);
return None; return 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, TimerToken, Component, Event, EventCtx, Label, Never, Pad, 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.expire(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

@ -106,7 +106,7 @@ impl Component for Slip39Input {
} }
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> { fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
if self.multi_tap.is_timeout_event(event) { if self.multi_tap.timeout_event(event) {
// Timeout occurred. Reset the pending key. // Timeout occurred. Reset the pending key.
self.multi_tap.clear_pending_state(ctx); self.multi_tap.clear_pending_state(ctx);
return Some(MnemonicInputMsg::TimedOut); return Some(MnemonicInputMsg::TimedOut);

View File

@ -11,7 +11,6 @@ use super::{super::theme, BinarySelection, ButtonContent, HoldToConfirm, TapToCo
/// - Tap to confirm /// - Tap to confirm
/// - Hold to confirm /// - Hold to confirm
/// - Yes/No selection /// - Yes/No selection
#[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

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

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 timer_event(&mut self, event: Event) -> bool {
self.long_pressed_timer == Some(token) self.long_pressed_timer.expire(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.timer_event(event) {
return Some(ButtonPos::Left); return Some(ButtonPos::Left);
} }
if self.middle_btn.is_timer_token(token) { if self.middle_btn.timer_event(event) {
return Some(ButtonPos::Middle); return Some(ButtonPos::Middle);
} }
if self.right_btn.is_timer_token(token) { if self.right_btn.timer_event(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.expire(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.expire(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,16 +779,12 @@ 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.expire(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() {
// Request new token and send the appropriate button trigger event
self.timer_token = Some(ctx.request_timer(self.get_auto_move_duration()));
self.auto_moved_screens += 1; self.auto_moved_screens += 1;
return self.moving_direction; return self.moving_direction;
} }
}
None None
} }
} }

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::{Alignment2D, Insets, Offset, Point, Rect}, geometry::{Alignment2D, Insets, Offset, Point, Rect},
shape, shape::{self, Renderer},
shape::Renderer,
}, },
}; };
@ -31,7 +30,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 +47,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 +316,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,13 +348,11 @@ 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.expire(event) => {
if self.long_timer == Some(token) {
self.long_timer = None;
if matches!(self.state, State::Pressed) { if matches!(self.state, State::Pressed) {
#[cfg(feature = "haptic")] #[cfg(feature = "haptic")]
if self.haptics { if self.haptics {
@ -365,7 +362,6 @@ impl Component for Button {
return Some(ButtonMsg::LongPressed); return Some(ButtonMsg::LongPressed);
} }
} }
}
_ => {} _ => {}
}; };
None None

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.expire(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

@ -93,7 +93,7 @@ impl Component for Bip39Input {
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> { fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
self.button_suggestion.event(ctx, event); self.button_suggestion.event(ctx, event);
if self.multi_tap.is_timeout_event(event) { if self.multi_tap.timeout_event(event) {
self.on_timeout(ctx) self.on_timeout(ctx)
} else if let Some(ButtonMsg::Clicked) = self.button.event(ctx, event) { } else if let Some(ButtonMsg::Clicked) = self.button.event(ctx, event) {
self.on_input_click(ctx) self.on_input_click(ctx)

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 {
@ -46,15 +55,10 @@ 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 timeout_event(&mut self, event: Event) -> bool {
matches!((event, self.pending_timer()), (Event::Timer(t), Some(pt)) if pt == t) self.pending.as_mut().map_or(false, |t| t.timer.expire(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 +99,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

@ -221,9 +221,15 @@ impl Component for PassphraseKeyboard {
} }
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> { fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
if self.input.inner().multi_tap.is_timeout_event(event) { let multitap_timeout = self.input.mutate(ctx, |ctx, i| {
self.input if i.multi_tap.timeout_event(event) {
.mutate(ctx, |ctx, i| i.multi_tap.clear_pending_state(ctx)); i.multi_tap.clear_pending_state(ctx);
true
} else {
false
}
});
if multitap_timeout {
return None; return None;
} }
if let Some(swipe) = self.page_swipe.event(ctx, event) { if let Some(swipe) = self.page_swipe.event(ctx, event) {

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.expire(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

@ -106,7 +106,7 @@ impl Component for Slip39Input {
} }
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> { fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
if self.multi_tap.is_timeout_event(event) { if self.multi_tap.timeout_event(event) {
// Timeout occurred. Reset the pending key. // Timeout occurred. Reset the pending key.
self.multi_tap.clear_pending_state(ctx); self.multi_tap.clear_pending_state(ctx);
return Some(MnemonicInputMsg::TimedOut); return Some(MnemonicInputMsg::TimedOut);

View File

@ -360,11 +360,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)
if task is None:
task = timer_task() task = timer_task()
self.timers[token] = task self.timers[token] = task
deadline = utime.ticks_add(utime.ticks_ms(), duration_ms) deadline = utime.ticks_add(utime.ticks_ms(), duration_ms)
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