1
0
mirror of https://github.com/trezor/trezor-firmware.git synced 2024-11-15 12:08:59 +00:00

refactor(core/ui): reusable timers

This commit is contained in:
matejcik 2024-04-10 11:42:03 +02:00 committed by matejcik
parent d385667b4a
commit 9e911605da
29 changed files with 280 additions and 271 deletions

View File

@ -402,6 +402,11 @@ pub struct TimerToken(u32);
impl TimerToken {
/// Value of an invalid (or missing) token.
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 {
Self(raw)
@ -410,11 +415,77 @@ impl TimerToken {
pub const fn into_raw(self) -> u32 {
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 {
timers: Vec<(TimerToken, Duration), { Self::MAX_TIMERS }>,
next_token: u32,
place_requested: bool,
paint_requested: bool,
anim_frame_scheduled: bool,
@ -433,17 +504,12 @@ impl EventCtx {
/// How long into the future we should schedule the animation frame timer.
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.
const MAX_TIMERS: usize = 4;
pub fn new() -> Self {
Self {
timers: Vec::new(),
next_token: Self::STARTING_TIMER_TOKEN,
place_requested: false,
paint_requested: false,
anim_frame_scheduled: false,
@ -476,13 +542,6 @@ impl EventCtx {
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.
pub fn request_anim_frame(&mut self) {
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) {
self.root_repaint_requested = true;
}
@ -551,10 +614,7 @@ impl EventCtx {
#[cfg(feature = "ui_debug")]
assert!(self.button_request.is_none());
// replace self with a new instance, keeping only the fields we care about
*self = Self {
next_token: self.next_token,
..Self::new()
}
*self = Self::new();
}
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) {
self.transition_out = Some(attach_type);
}

View File

@ -3,7 +3,7 @@ use crate::{
time::{Duration, Instant},
ui::{
animation::Animation,
component::{Component, Event, EventCtx, Never, TimerToken},
component::{Component, Event, EventCtx, Never, Timer},
display::{self, Color, Font},
geometry::{Offset, Rect},
shape::{self, Renderer},
@ -24,7 +24,7 @@ enum State {
pub struct Marquee {
area: Rect,
pause_token: Option<TimerToken>,
pause_timer: Timer,
min_offset: i16,
max_offset: i16,
state: State,
@ -40,7 +40,7 @@ impl Marquee {
pub fn new(text: TString<'static>, font: Font, fg: Color, bg: Color) -> Self {
Self {
area: Rect::zero(),
pause_token: None,
pause_timer: Timer::new(),
min_offset: 0,
max_offset: 0,
state: State::Initial,
@ -154,53 +154,50 @@ impl Component for Marquee {
let now = Instant::now();
if let Event::Timer(token) = event {
if self.pause_token == Some(token) {
match self.state {
State::PauseLeft => {
let 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);
}
_ => {}
if self.pause_timer.expire(event) {
match self.state {
State::PauseLeft => {
let 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);
}
_ => {}
}
// 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.
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();
}
if token == EventCtx::ANIM_FRAME_TIMER {
if self.is_animating() {
// 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();
}
match self.state {
State::Right(_) => {
if self.is_at_right(now) {
self.pause_token = Some(ctx.request_timer(self.pause));
self.state = State::PauseRight;
}
match self.state {
State::Right(_) => {
if self.is_at_right(now) {
self.pause_timer.start(ctx, 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
}

View File

@ -27,7 +27,7 @@ pub mod text;
pub mod timeout;
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 button_request::{ButtonRequestExt, SendButtonRequest};
#[cfg(all(feature = "jpeg", feature = "ui_image_buffer", feature = "micropython"))]

View File

@ -1,23 +1,22 @@
use crate::{
time::Duration,
ui::{
component::{Component, Event, EventCtx, TimerToken},
component::{Component, Event, EventCtx, Timer},
geometry::Rect,
shape::Renderer,
},
};
#[derive(Clone)]
pub struct Timeout {
time_ms: u32,
timer: Option<TimerToken>,
timer: Timer,
}
impl Timeout {
pub fn new(time_ms: u32) -> Self {
Self {
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> {
match event {
// Set up timer.
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,
if matches!(event, Event::Attach(_)) {
self.timer.start(ctx, Duration::from_millis(self.time_ms));
}
self.timer.expire(event).then_some(())
}
fn paint(&mut self) {}

View File

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

View File

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

View File

@ -4,12 +4,11 @@ use crate::{
strutil::TString,
time::Duration,
ui::{
component::{Component, Event, EventCtx, TimerToken},
component::{Component, Event, EventCtx, Timer},
display::{self, toif::Icon, Color, Font},
event::TouchEvent,
geometry::{Alignment, Alignment2D, Insets, Offset, Point, Rect},
shape,
shape::Renderer,
shape::{self, Renderer},
util::split_two_lines,
},
};
@ -23,7 +22,6 @@ pub enum ButtonMsg {
LongPressed,
}
#[derive(Clone)]
pub struct Button {
area: Rect,
touch_expand: Option<Insets>,
@ -33,7 +31,7 @@ pub struct Button {
radius: Option<u8>,
state: State,
long_press: Option<Duration>,
long_timer: Option<TimerToken>,
long_timer: Timer,
haptic: bool,
}
@ -53,7 +51,7 @@ impl Button {
radius: None,
state: State::Initial,
long_press: None,
long_timer: None,
long_timer: Timer::new(),
haptic: true,
}
}
@ -312,7 +310,7 @@ impl Component for Button {
}
self.set(ctx, State::Pressed);
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);
}
@ -344,13 +342,13 @@ impl Component for Button {
State::Pressed => {
// Touch finished outside our area.
self.set(ctx, State::Initial);
self.long_timer = None;
self.long_timer.stop();
return Some(ButtonMsg::Released);
}
_ => {
// Touch finished outside our area.
self.set(ctx, State::Initial);
self.long_timer = None;
self.long_timer.stop();
}
}
}
@ -363,28 +361,25 @@ impl Component for Button {
State::Pressed => {
// Touch aborted
self.set(ctx, State::Initial);
self.long_timer = None;
self.long_timer.stop();
return Some(ButtonMsg::Released);
}
_ => {
// Irrelevant touch abort
self.set(ctx, State::Initial);
self.long_timer = None;
self.long_timer.stop();
}
}
}
Event::Timer(token) => {
if self.long_timer == Some(token) {
self.long_timer = None;
if matches!(self.state, State::Pressed) {
#[cfg(feature = "haptic")]
if self.haptic {
play(HapticEffect::ButtonPress);
}
self.set(ctx, State::Initial);
return Some(ButtonMsg::LongPressed);
Event::Timer(_) if self.long_timer.expire(event) => {
if matches!(self.state, State::Pressed) {
#[cfg(feature = "haptic")]
if self.haptic {
play(HapticEffect::ButtonPress);
}
self.set(ctx, State::Initial);
return Some(ButtonMsg::LongPressed);
}
}
_ => {}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -321,7 +321,7 @@ impl Component for PassphraseKeyboard {
}
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);
return None;
}

View File

@ -8,7 +8,7 @@ use crate::{
component::{
base::{AttachType, ComponentExt},
text::TextStyle,
Component, Event, EventCtx, Label, Never, Pad, TimerToken,
Component, Event, EventCtx, Label, Never, Pad, Timer,
},
display::Font,
event::TouchEvent,
@ -256,7 +256,7 @@ pub struct PinKeyboard<'a> {
cancel_btn: Button,
confirm_btn: Button,
digit_btns: [(Button, usize); DIGIT_COUNT],
warning_timer: Option<TimerToken>,
warning_timer: Timer,
attach_animation: AttachAnimation,
close_animation: CloseAnimation,
close_confirm: bool,
@ -295,7 +295,7 @@ impl<'a> PinKeyboard<'a> {
.styled(theme::button_pin_confirm())
.initially_enabled(false),
digit_btns: Self::generate_digit_buttons(),
warning_timer: None,
warning_timer: Timer::new(),
attach_animation: AttachAnimation::default(),
close_animation: CloseAnimation::default(),
close_confirm: false,
@ -417,10 +417,10 @@ impl Component for PinKeyboard<'_> {
match event {
// Set up timer to switch off warning prompt.
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.
Event::Timer(token) if Some(token) == self.warning_timer => {
Event::Timer(_) if self.warning_timer.expire(event) => {
self.major_warning = None;
self.minor_prompt.request_complete_repaint(ctx);
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> {
if self.multi_tap.is_timeout_event(event) {
if self.multi_tap.timeout_event(event) {
// Timeout occurred. Reset the pending key.
self.multi_tap.clear_pending_state(ctx);
return Some(MnemonicInputMsg::TimedOut);

View File

@ -11,7 +11,6 @@ use super::{super::theme, BinarySelection, ButtonContent, HoldToConfirm, TapToCo
/// - Tap to confirm
/// - Hold to confirm
/// - Yes/No selection
#[derive(Clone)]
pub enum PromptScreen {
Tap(TapToConfirm),
Hold(HoldToConfirm),

View File

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

View File

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

View File

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

View File

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

View File

@ -5,13 +5,12 @@ use crate::{
time::Duration,
ui::{
component::{
Component, ComponentExt, Event, EventCtx, FixedHeightBar, MsgMap, Split, TimerToken,
Component, ComponentExt, Event, EventCtx, FixedHeightBar, MsgMap, Split, Timer,
},
display::{self, toif::Icon, Color, Font},
event::TouchEvent,
geometry::{Alignment2D, Insets, Offset, Point, Rect},
shape,
shape::Renderer,
shape::{self, Renderer},
},
};
@ -31,7 +30,7 @@ pub struct Button {
styles: ButtonStyleSheet,
state: State,
long_press: Option<Duration>,
long_timer: Option<TimerToken>,
long_timer: Timer,
haptics: bool,
}
@ -48,7 +47,7 @@ impl Button {
styles: theme::button_default(),
state: State::Initial,
long_press: None,
long_timer: None,
long_timer: Timer::new(),
haptics: true,
}
}
@ -317,7 +316,7 @@ impl Component for Button {
}
self.set(ctx, State::Pressed);
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);
}
@ -349,21 +348,18 @@ impl Component for Button {
_ => {
// Touch finished outside our area.
self.set(ctx, State::Initial);
self.long_timer = None;
self.long_timer.stop();
}
}
}
Event::Timer(token) => {
if self.long_timer == Some(token) {
self.long_timer = None;
if matches!(self.state, State::Pressed) {
#[cfg(feature = "haptic")]
if self.haptics {
haptic::play(HapticEffect::ButtonPress);
}
self.set(ctx, State::Initial);
return Some(ButtonMsg::LongPressed);
Event::Timer(_) if self.long_timer.expire(event) => {
if matches!(self.state, State::Pressed) {
#[cfg(feature = "haptic")]
if self.haptics {
haptic::play(HapticEffect::ButtonPress);
}
self.set(ctx, State::Initial);
return Some(ButtonMsg::LongPressed);
}
}
_ => {}

View File

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

View File

@ -1,7 +1,7 @@
use crate::{
time::Duration,
ui::{
component::{text::common::TextEdit, Event, EventCtx, TimerToken},
component::{text::common::TextEdit, Event, EventCtx, Timer},
display::{self, Color, Font},
geometry::{Offset, Point, Rect},
shape,
@ -24,7 +24,16 @@ struct Pending {
/// one).
press: usize,
/// 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 {
@ -46,15 +55,10 @@ impl MultiTapKeyboard {
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
/// timer.
pub fn is_timeout_event(&self, event: Event) -> bool {
matches!((event, self.pending_timer()), (Event::Timer(t), Some(pt)) if pt == t)
pub fn timeout_event(&mut self, event: Event) -> bool {
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
@ -95,11 +99,7 @@ impl MultiTapKeyboard {
// transition only happens as a result of an append op, so the painting should
// be requested by handling the `TextEdit`.
self.pending = if key_text.len() > 1 {
Some(Pending {
key,
press,
timer: ctx.request_timer(self.timeout),
})
Some(Pending::start(ctx, key, press, self.timeout))
} else {
None
};

View File

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

View File

@ -7,7 +7,7 @@ use crate::{
ui::{
component::{
base::ComponentExt, text::TextStyle, Child, Component, Event, EventCtx, Label, Maybe,
Never, Pad, TimerToken,
Never, Pad, Timer,
},
display::{self, Font},
event::TouchEvent,
@ -54,7 +54,7 @@ pub struct PinKeyboard<'a> {
cancel_btn: Child<Maybe<Button>>,
confirm_btn: Child<Button>,
digit_btns: [Child<Button>; DIGIT_COUNT],
warning_timer: Option<TimerToken>,
warning_timer: Timer,
}
impl<'a> PinKeyboard<'a> {
@ -99,7 +99,7 @@ impl<'a> PinKeyboard<'a> {
.initially_enabled(false)
.into_child(),
digit_btns: Self::generate_digit_buttons(),
warning_timer: None,
warning_timer: Timer::new(),
}
}
@ -201,10 +201,10 @@ impl Component for PinKeyboard<'_> {
match event {
// Set up timer to switch off warning prompt.
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.
Event::Timer(token) if Some(token) == self.warning_timer => {
Event::Timer(_) if self.warning_timer.expire(event) => {
self.major_warning = None;
self.textbox_pad.clear();
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> {
if self.multi_tap.is_timeout_event(event) {
if self.multi_tap.timeout_event(event) {
// Timeout occurred. Reset the pending key.
self.multi_tap.clear_pending_state(ctx);
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
return
assert token not in self.timers
task = timer_task()
self.timers[token] = task
task = self.timers.get(token)
if task is None:
task = timer_task()
self.timers[token] = task
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:
"""Process a message coming out of the Rust layout. Set is as a result and shut