mirror of
https://github.com/trezor/trezor-firmware.git
synced 2025-01-03 03:50:58 +00:00
feat(core/rust): add ChoicePage animation and hold-to-move functionality
[no changelog]
This commit is contained in:
parent
272fb4842a
commit
568c5f135b
@ -13,7 +13,7 @@ use super::{loader::DEFAULT_DURATION_MS, theme};
|
||||
|
||||
const HALF_SCREEN_BUTTON_WIDTH: i16 = constant::WIDTH / 2 - 1;
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
#[derive(Copy, Clone, Eq, PartialEq)]
|
||||
pub enum ButtonPos {
|
||||
Left,
|
||||
Middle,
|
||||
|
@ -3,8 +3,9 @@ use super::{
|
||||
};
|
||||
use crate::{
|
||||
strutil::StringType,
|
||||
time::Duration,
|
||||
ui::{
|
||||
component::{base::Event, Component, EventCtx, Pad},
|
||||
component::{base::Event, Component, EventCtx, Pad, TimerToken},
|
||||
event::{ButtonEvent, PhysicalButton},
|
||||
geometry::Rect,
|
||||
},
|
||||
@ -195,6 +196,9 @@ where
|
||||
///
|
||||
/// Only "final" button events are returned in `ButtonControllerMsg::Triggered`,
|
||||
/// based upon the buttons being long-press or not.
|
||||
///
|
||||
/// There is optional complexity with IgnoreButtonDelay, which gets executed
|
||||
/// only in cases where clients request it.
|
||||
pub struct ButtonController<T>
|
||||
where
|
||||
T: StringType,
|
||||
@ -204,10 +208,12 @@ where
|
||||
middle_btn: ButtonContainer<T>,
|
||||
right_btn: ButtonContainer<T>,
|
||||
state: ButtonState,
|
||||
// Button area is needed so the buttons
|
||||
// can be "re-placed" after their text is changed
|
||||
// Will be set in `place`
|
||||
/// Button area is needed so the buttons
|
||||
/// can be "re-placed" after their text is changed
|
||||
/// Will be set in `place`
|
||||
button_area: Rect,
|
||||
/// Handling optional ignoring of buttons after pressing the other button.
|
||||
ignore_btn_delay: Option<IgnoreButtonDelay>,
|
||||
}
|
||||
|
||||
impl<T> ButtonController<T>
|
||||
@ -222,9 +228,17 @@ where
|
||||
right_btn: ButtonContainer::new(ButtonPos::Right, btn_layout.btn_right),
|
||||
state: ButtonState::Nothing,
|
||||
button_area: Rect::zero(),
|
||||
ignore_btn_delay: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set up the logic to ignore a button after some time from pressing
|
||||
/// the other button.
|
||||
pub fn with_ignore_btn_delay(mut self, delay_ms: u32) -> Self {
|
||||
self.ignore_btn_delay = Some(IgnoreButtonDelay::new(delay_ms));
|
||||
self
|
||||
}
|
||||
|
||||
/// Updating all the three buttons to the wanted states.
|
||||
pub fn set(&mut self, btn_layout: ButtonLayout<T>) {
|
||||
self.pad.clear();
|
||||
@ -240,6 +254,14 @@ where
|
||||
self.right_btn.set_pressed(ctx, right);
|
||||
}
|
||||
|
||||
pub fn highlight_button(&mut self, ctx: &mut EventCtx, pos: ButtonPos) {
|
||||
match pos {
|
||||
ButtonPos::Left => self.left_btn.set_pressed(ctx, true),
|
||||
ButtonPos::Middle => self.middle_btn.set_pressed(ctx, true),
|
||||
ButtonPos::Right => self.right_btn.set_pressed(ctx, true),
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle middle button hold-to-confirm start.
|
||||
/// We need to cancel possible holds in both other buttons.
|
||||
fn middle_hold_started(&mut self, ctx: &mut EventCtx) {
|
||||
@ -291,22 +313,29 @@ where
|
||||
// _ _
|
||||
ButtonState::Nothing => match button_event {
|
||||
// ▼ * | * ▼
|
||||
ButtonEvent::ButtonPressed(which) => (
|
||||
ButtonEvent::ButtonPressed(which) => {
|
||||
// ↓ _ | _ ↓
|
||||
ButtonState::OneDown(which),
|
||||
match which {
|
||||
// ▼ *
|
||||
PhysicalButton::Left => {
|
||||
self.left_btn.hold_started(ctx);
|
||||
Some(ButtonControllerMsg::Pressed(ButtonPos::Left))
|
||||
}
|
||||
// * ▼
|
||||
PhysicalButton::Right => {
|
||||
self.right_btn.hold_started(ctx);
|
||||
Some(ButtonControllerMsg::Pressed(ButtonPos::Right))
|
||||
}
|
||||
},
|
||||
),
|
||||
// Initial button press will set the timer for second button,
|
||||
// and after some delay, it will become un-clickable
|
||||
if let Some(ignore_btn_delay) = &mut self.ignore_btn_delay {
|
||||
ignore_btn_delay.handle_button_press(ctx, which);
|
||||
}
|
||||
(
|
||||
ButtonState::OneDown(which),
|
||||
match which {
|
||||
// ▼ *
|
||||
PhysicalButton::Left => {
|
||||
self.left_btn.hold_started(ctx);
|
||||
Some(ButtonControllerMsg::Pressed(ButtonPos::Left))
|
||||
}
|
||||
// * ▼
|
||||
PhysicalButton::Right => {
|
||||
self.right_btn.hold_started(ctx);
|
||||
Some(ButtonControllerMsg::Pressed(ButtonPos::Right))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
_ => (self.state, None),
|
||||
},
|
||||
// ↓ _ | _ ↓
|
||||
@ -314,18 +343,32 @@ where
|
||||
// ▲ * | * ▲
|
||||
ButtonEvent::ButtonReleased(b) if b == which_down => match which_down {
|
||||
// ▲ *
|
||||
// Trigger the button and make the second one clickable in all cases
|
||||
PhysicalButton::Left => {
|
||||
if let Some(ignore_btn_delay) = &mut self.ignore_btn_delay {
|
||||
ignore_btn_delay.make_button_clickable(ButtonPos::Right);
|
||||
}
|
||||
// _ _
|
||||
(ButtonState::Nothing, self.left_btn.maybe_trigger(ctx))
|
||||
}
|
||||
// * ▲
|
||||
PhysicalButton::Right => {
|
||||
if let Some(ignore_btn_delay) = &mut self.ignore_btn_delay {
|
||||
ignore_btn_delay.make_button_clickable(ButtonPos::Left);
|
||||
}
|
||||
// _ _
|
||||
(ButtonState::Nothing, self.right_btn.maybe_trigger(ctx))
|
||||
}
|
||||
},
|
||||
// * ▼ | ▼ *
|
||||
ButtonEvent::ButtonPressed(b) if b != which_down => {
|
||||
// Buttons may be non-clickable (caused by long-holding the other
|
||||
// button)
|
||||
if let Some(ignore_btn_delay) = &self.ignore_btn_delay {
|
||||
if ignore_btn_delay.ignore_button(b) {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
self.middle_hold_started(ctx);
|
||||
(
|
||||
// ↓ ↓
|
||||
@ -356,6 +399,11 @@ where
|
||||
// ▲ * | * ▲
|
||||
ButtonEvent::ButtonReleased(b) if b != which_up => {
|
||||
// _ _
|
||||
// Both button needs to be clickable now
|
||||
if let Some(ignore_btn_delay) = &mut self.ignore_btn_delay {
|
||||
ignore_btn_delay.make_button_clickable(ButtonPos::Left);
|
||||
ignore_btn_delay.make_button_clickable(ButtonPos::Right);
|
||||
}
|
||||
(ButtonState::Nothing, self.middle_btn.maybe_trigger(ctx))
|
||||
}
|
||||
_ => (self.state, None),
|
||||
@ -394,8 +442,13 @@ where
|
||||
self.state = new_state;
|
||||
event
|
||||
}
|
||||
// HoldToConfirm expiration
|
||||
Event::Timer(_) => self.handle_htc_expiration(ctx, event),
|
||||
// Timer - handle clickable properties and HoldToConfirm expiration
|
||||
Event::Timer(token) => {
|
||||
if let Some(ignore_btn_delay) = &mut self.ignore_btn_delay {
|
||||
ignore_btn_delay.handle_timer_token(token);
|
||||
}
|
||||
self.handle_htc_expiration(ctx, event)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@ -421,6 +474,179 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
/// When one button is pressed, the other one becomes un-clickable after some
|
||||
/// small time period. This is to prevent accidental clicks when user is holding
|
||||
/// the button to automatically move through items.
|
||||
struct IgnoreButtonDelay {
|
||||
/// How big is the delay after the button is inactive
|
||||
delay: Duration,
|
||||
/// Whether left button is currently clickable
|
||||
left_clickable: bool,
|
||||
/// Whether right button is currently clickable
|
||||
right_clickable: bool,
|
||||
/// Timer for setting the left_clickable
|
||||
left_clickable_timer: Option<TimerToken>,
|
||||
/// Timer for setting the right_clickable
|
||||
right_clickable_timer: Option<TimerToken>,
|
||||
}
|
||||
|
||||
impl IgnoreButtonDelay {
|
||||
pub fn new(delay_ms: u32) -> Self {
|
||||
Self {
|
||||
delay: Duration::from_millis(delay_ms),
|
||||
left_clickable: true,
|
||||
right_clickable: true,
|
||||
left_clickable_timer: None,
|
||||
right_clickable_timer: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn make_button_clickable(&mut self, pos: ButtonPos) {
|
||||
match pos {
|
||||
ButtonPos::Left => {
|
||||
self.left_clickable = true;
|
||||
self.left_clickable_timer = None;
|
||||
}
|
||||
ButtonPos::Right => {
|
||||
self.right_clickable = true;
|
||||
self.right_clickable_timer = None;
|
||||
}
|
||||
ButtonPos::Middle => {}
|
||||
}
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
if matches!(button, PhysicalButton::Right) {
|
||||
self.left_clickable_timer = Some(ctx.request_timer(self.delay));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ignore_button(&self, button: PhysicalButton) -> bool {
|
||||
if matches!(button, PhysicalButton::Left) && !self.left_clickable {
|
||||
return true;
|
||||
}
|
||||
if matches!(button, PhysicalButton::Right) && !self.right_clickable {
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
pub fn handle_timer_token(&mut self, token: TimerToken) {
|
||||
if self.left_clickable_timer == Some(token) {
|
||||
self.left_clickable = false;
|
||||
self.left_clickable_timer = None;
|
||||
}
|
||||
if self.right_clickable_timer == Some(token) {
|
||||
self.right_clickable = false;
|
||||
self.right_clickable_timer = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Component allowing for automatically moving through items (e.g. Choice
|
||||
/// items).
|
||||
///
|
||||
/// Users are in full control of starting/stopping the movement.
|
||||
///
|
||||
/// Can be started e.g. by holding left/right button.
|
||||
pub struct AutomaticMover {
|
||||
/// For requesting timer events repeatedly
|
||||
timer_token: Option<TimerToken>,
|
||||
/// Which direction should we go (which button is down)
|
||||
moving_direction: Option<ButtonPos>,
|
||||
/// How many screens were moved automatically
|
||||
auto_moved_screens: usize,
|
||||
/// Function to get duration of each movement according to the already moved
|
||||
/// steps
|
||||
duration_func: fn(usize) -> u32,
|
||||
}
|
||||
|
||||
impl AutomaticMover {
|
||||
pub fn new() -> Self {
|
||||
fn default_duration_func(steps: usize) -> u32 {
|
||||
match steps {
|
||||
x if x < 3 => 200,
|
||||
x if x < 10 => 150,
|
||||
_ => 100,
|
||||
}
|
||||
}
|
||||
|
||||
Self {
|
||||
timer_token: None,
|
||||
moving_direction: None,
|
||||
auto_moved_screens: 0,
|
||||
duration_func: default_duration_func,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_duration_func(mut self, duration_func: fn(usize) -> u32) -> Self {
|
||||
self.duration_func = duration_func;
|
||||
self
|
||||
}
|
||||
|
||||
/// Determines how long to wait between automatic movements.
|
||||
/// Moves quicker with increasing number of screens moved.
|
||||
/// Can be forced to be always the same (e.g. for animation purposes).
|
||||
fn get_auto_move_duration(&self) -> Duration {
|
||||
// Calculating duration from function
|
||||
let ms_duration = (self.duration_func)(self.auto_moved_screens);
|
||||
Duration::from_millis(ms_duration)
|
||||
}
|
||||
|
||||
/// In which direction we are moving, if any
|
||||
pub fn moving_direction(&self) -> Option<ButtonPos> {
|
||||
self.moving_direction
|
||||
}
|
||||
|
||||
// Whether we are currently moving.
|
||||
pub fn is_moving(&self) -> bool {
|
||||
self.moving_direction.is_some()
|
||||
}
|
||||
|
||||
/// Whether we have done at least one automatic movement.
|
||||
pub fn was_moving(&self) -> bool {
|
||||
self.auto_moved_screens > 0
|
||||
}
|
||||
|
||||
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()));
|
||||
}
|
||||
|
||||
pub fn stop_moving(&mut self) {
|
||||
self.moving_direction = None;
|
||||
self.timer_token = None;
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for AutomaticMover {
|
||||
type Msg = ButtonPos;
|
||||
|
||||
fn place(&mut self, bounds: Rect) -> Rect {
|
||||
bounds
|
||||
}
|
||||
|
||||
fn paint(&mut self) {}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
// DEBUG-ONLY SECTION BELOW
|
||||
|
||||
#[cfg(feature = "ui_debug")]
|
||||
@ -443,3 +669,10 @@ impl<T: StringType> crate::trace::Trace for ButtonController<T> {
|
||||
t.child("right_btn", &self.right_btn);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ui_debug")]
|
||||
impl crate::trace::Trace for AutomaticMover {
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
t.component("AutomaticMover");
|
||||
}
|
||||
}
|
||||
|
@ -30,6 +30,9 @@ where
|
||||
page_counter: usize,
|
||||
return_confirmed_index: bool,
|
||||
show_scrollbar: bool,
|
||||
/// Possibly enforcing the second button to be ignored after some time after
|
||||
/// pressing the first button
|
||||
ignore_second_button_ms: Option<u32>,
|
||||
}
|
||||
|
||||
impl<F, T> Flow<F, T>
|
||||
@ -55,6 +58,7 @@ where
|
||||
page_counter: 0,
|
||||
return_confirmed_index: false,
|
||||
show_scrollbar: true,
|
||||
ignore_second_button_ms: None,
|
||||
}
|
||||
}
|
||||
|
||||
@ -77,6 +81,12 @@ where
|
||||
self
|
||||
}
|
||||
|
||||
/// Ignoring the second button duration
|
||||
pub fn with_ignore_second_button_ms(mut self, ignore_second_button_ms: u32) -> Self {
|
||||
self.ignore_second_button_ms = Some(ignore_second_button_ms);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn confirmed_index(&self) -> Option<usize> {
|
||||
self.return_confirmed_index.then_some(self.page_counter)
|
||||
}
|
||||
@ -225,7 +235,14 @@ where
|
||||
|
||||
// We finally found how long is the first page, and can set its button layout.
|
||||
self.current_page.place(content_area);
|
||||
self.buttons = Child::new(ButtonController::new(self.current_page.btn_layout()));
|
||||
if let Some(ignore_ms) = self.ignore_second_button_ms {
|
||||
self.buttons = Child::new(
|
||||
ButtonController::new(self.current_page.btn_layout())
|
||||
.with_ignore_btn_delay(ignore_ms),
|
||||
);
|
||||
} else {
|
||||
self.buttons = Child::new(ButtonController::new(self.current_page.btn_layout()));
|
||||
}
|
||||
|
||||
self.pad.place(title_content_area);
|
||||
self.buttons.place(button_area);
|
||||
|
@ -3,10 +3,13 @@ use crate::{
|
||||
ui::{
|
||||
component::{Child, Component, Event, EventCtx, Pad},
|
||||
geometry::{Insets, Offset, Rect},
|
||||
util::animation_disabled,
|
||||
},
|
||||
};
|
||||
|
||||
use super::super::{theme, ButtonController, ButtonControllerMsg, ButtonLayout, ButtonPos};
|
||||
use super::super::{
|
||||
constant, theme, AutomaticMover, ButtonController, ButtonControllerMsg, ButtonLayout, ButtonPos,
|
||||
};
|
||||
|
||||
const DEFAULT_ITEMS_DISTANCE: i16 = 10;
|
||||
|
||||
@ -79,6 +82,13 @@ where
|
||||
/// Whether the middle selected item should be painted with
|
||||
/// inverse colors - black on white.
|
||||
inverse_selected_item: bool,
|
||||
/// For moving through the items when holding left/right button
|
||||
holding_mover: AutomaticMover,
|
||||
/// For doing quick animations when changing the page counter.
|
||||
animation_mover: AutomaticMover,
|
||||
/// How many animated steps we should still do (positive for right, negative
|
||||
/// for left).
|
||||
animated_steps_to_do: i16,
|
||||
}
|
||||
|
||||
impl<F, T, A> ChoicePage<F, T, A>
|
||||
@ -89,16 +99,30 @@ where
|
||||
pub fn new(choices: F) -> Self {
|
||||
let initial_btn_layout = choices.get(0).0.btn_layout();
|
||||
|
||||
/// First move happens immediately, then in 35 ms intervals
|
||||
fn animation_duration_func(steps: usize) -> u32 {
|
||||
match steps {
|
||||
0 => 0,
|
||||
_ => 35,
|
||||
}
|
||||
}
|
||||
|
||||
Self {
|
||||
choices,
|
||||
pad: Pad::with_background(theme::BG),
|
||||
buttons: Child::new(ButtonController::new(initial_btn_layout)),
|
||||
buttons: Child::new(
|
||||
ButtonController::new(initial_btn_layout)
|
||||
.with_ignore_btn_delay(constant::IGNORE_OTHER_BTN_MS),
|
||||
),
|
||||
page_counter: 0,
|
||||
items_distance: DEFAULT_ITEMS_DISTANCE,
|
||||
is_carousel: false,
|
||||
show_incomplete: false,
|
||||
show_only_one_item: false,
|
||||
inverse_selected_item: false,
|
||||
holding_mover: AutomaticMover::new(),
|
||||
animation_mover: AutomaticMover::new().with_duration_func(animation_duration_func),
|
||||
animated_steps_to_do: 0,
|
||||
}
|
||||
}
|
||||
|
||||
@ -107,7 +131,10 @@ where
|
||||
pub fn with_initial_page_counter(mut self, page_counter: usize) -> Self {
|
||||
self.page_counter = page_counter;
|
||||
let initial_btn_layout = self.get_current_choice().0.btn_layout();
|
||||
self.buttons = Child::new(ButtonController::new(initial_btn_layout));
|
||||
self.buttons = Child::new(
|
||||
ButtonController::new(initial_btn_layout)
|
||||
.with_ignore_btn_delay(constant::IGNORE_OTHER_BTN_MS),
|
||||
);
|
||||
self
|
||||
}
|
||||
|
||||
@ -156,9 +183,33 @@ where
|
||||
}
|
||||
|
||||
/// Navigating to the chosen page index.
|
||||
pub fn set_page_counter(&mut self, ctx: &mut EventCtx, page_counter: usize) {
|
||||
self.page_counter = page_counter;
|
||||
self.update(ctx);
|
||||
pub fn set_page_counter(
|
||||
&mut self,
|
||||
ctx: &mut EventCtx,
|
||||
page_counter: usize,
|
||||
do_animation: bool,
|
||||
) {
|
||||
// Either moving with animation or just jumping to the final position directly.
|
||||
if do_animation && !animation_disabled() {
|
||||
let diff = page_counter as i16 - self.page_counter as i16;
|
||||
// When there would be a small number of animation frames (3 or less),
|
||||
// animating in the opposite direction to make the animation longer.
|
||||
self.animated_steps_to_do = match diff {
|
||||
-3..=0 => diff + self.choices.count() as i16,
|
||||
1..=3 => diff - self.choices.count() as i16,
|
||||
_ => diff,
|
||||
};
|
||||
// Starting the movement immediately - either left or right.
|
||||
let pos = if self.animated_steps_to_do > 0 {
|
||||
ButtonPos::Right
|
||||
} else {
|
||||
ButtonPos::Left
|
||||
};
|
||||
self.animation_mover.start_moving(ctx, pos);
|
||||
} else {
|
||||
self.page_counter = page_counter;
|
||||
self.update(ctx);
|
||||
}
|
||||
}
|
||||
|
||||
/// Display current, previous and next choices according to
|
||||
@ -356,14 +407,66 @@ where
|
||||
/// whether they are long-pressed, and painting them.
|
||||
fn set_buttons(&mut self, ctx: &mut EventCtx) {
|
||||
let btn_layout = self.get_current_choice().0.btn_layout();
|
||||
self.buttons.mutate(ctx, |_ctx, buttons| {
|
||||
self.buttons.mutate(ctx, |ctx, buttons| {
|
||||
buttons.set(btn_layout);
|
||||
// When user holds one of the buttons, highlighting it.
|
||||
if let Some(btn_down) = self.holding_mover.moving_direction() {
|
||||
buttons.highlight_button(ctx, btn_down);
|
||||
}
|
||||
ctx.request_paint();
|
||||
});
|
||||
}
|
||||
|
||||
pub fn choice_factory(&self) -> &F {
|
||||
&self.choices
|
||||
}
|
||||
|
||||
/// Go to the choice visually on the left.
|
||||
fn move_left(&mut self, ctx: &mut EventCtx) {
|
||||
if self.has_previous_choice() {
|
||||
self.decrease_page_counter();
|
||||
self.update(ctx);
|
||||
} else if self.is_carousel {
|
||||
self.page_counter_to_max();
|
||||
self.update(ctx);
|
||||
}
|
||||
}
|
||||
|
||||
/// Go to the choice visually on the right.
|
||||
fn move_right(&mut self, ctx: &mut EventCtx) {
|
||||
if self.has_next_choice() {
|
||||
self.increase_page_counter();
|
||||
self.update(ctx);
|
||||
} else if self.is_carousel {
|
||||
self.page_counter_to_zero();
|
||||
self.update(ctx);
|
||||
}
|
||||
}
|
||||
|
||||
/// Possibly doing an animation movement with the choice - either left or
|
||||
/// right.
|
||||
fn animation_event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<ButtonPos> {
|
||||
if animation_disabled() {
|
||||
return None;
|
||||
}
|
||||
// Stopping the movement if it is moving and there are no steps left
|
||||
if self.animated_steps_to_do == 0 {
|
||||
if self.animation_mover.is_moving() {
|
||||
self.animation_mover.stop_moving();
|
||||
}
|
||||
return None;
|
||||
}
|
||||
let animation_result = self.animation_mover.event(ctx, event);
|
||||
// When about to animate, decreasing the number of steps to do.
|
||||
if animation_result.is_some() {
|
||||
if self.animated_steps_to_do > 0 {
|
||||
self.animated_steps_to_do -= 1;
|
||||
} else {
|
||||
self.animated_steps_to_do += 1;
|
||||
}
|
||||
}
|
||||
animation_result
|
||||
}
|
||||
}
|
||||
|
||||
impl<F, T, A> Component for ChoicePage<F, T, A>
|
||||
@ -381,32 +484,67 @@ where
|
||||
}
|
||||
|
||||
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||
// Possible animation movement when setting (randomizing) the page counter.
|
||||
if let Some(animation_direction) = self.animation_event(ctx, event) {
|
||||
match animation_direction {
|
||||
ButtonPos::Left => self.move_left(ctx),
|
||||
ButtonPos::Right => self.move_right(ctx),
|
||||
_ => {}
|
||||
}
|
||||
return None;
|
||||
}
|
||||
|
||||
// When animation is running, ignoring all user events
|
||||
if self.animation_mover.is_moving() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Possible automatic movement when user is holding left or right button.
|
||||
if let Some(auto_move_direction) = self.holding_mover.event(ctx, event) {
|
||||
match auto_move_direction {
|
||||
ButtonPos::Left => self.move_left(ctx),
|
||||
ButtonPos::Right => self.move_right(ctx),
|
||||
_ => {}
|
||||
}
|
||||
return None;
|
||||
}
|
||||
|
||||
let button_event = self.buttons.event(ctx, event);
|
||||
|
||||
// Button was "triggered" - released. Doing the appropriate action.
|
||||
// Possibly stopping or starting the automatic mover.
|
||||
if let Some(moving_direction) = self.holding_mover.moving_direction() {
|
||||
// Stopping the automatic movement when the released button is the same as the
|
||||
// direction we were moving, or when the pressed button is the
|
||||
// opposite one (user does middle-click).
|
||||
if matches!(button_event, Some(ButtonControllerMsg::Triggered(pos)) if pos == moving_direction)
|
||||
|| matches!(button_event, Some(ButtonControllerMsg::Pressed(pos)) if pos != moving_direction)
|
||||
{
|
||||
self.holding_mover.stop_moving();
|
||||
// Ignoring the event when it already did some automatic movements. (Otherwise
|
||||
// it would do one more movement.)
|
||||
if self.holding_mover.was_moving() {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
} else if let Some(ButtonControllerMsg::Pressed(pos)) = button_event {
|
||||
// Starting the movement when left/right button is pressed.
|
||||
if matches!(pos, ButtonPos::Left | ButtonPos::Right) {
|
||||
self.holding_mover.start_moving(ctx, pos);
|
||||
}
|
||||
}
|
||||
|
||||
// There was a legitimate button event - doing some action
|
||||
if let Some(ButtonControllerMsg::Triggered(pos)) = button_event {
|
||||
match pos {
|
||||
ButtonPos::Left => {
|
||||
if self.has_previous_choice() {
|
||||
// Clicked BACK. Decrease the page counter.
|
||||
self.decrease_page_counter();
|
||||
self.update(ctx);
|
||||
} else if self.is_carousel {
|
||||
// In case of carousel going to the right end.
|
||||
self.page_counter_to_max();
|
||||
self.update(ctx);
|
||||
}
|
||||
// Clicked BACK. Decrease the page counter.
|
||||
// In case of carousel going to the right end.
|
||||
self.move_left(ctx);
|
||||
}
|
||||
ButtonPos::Right => {
|
||||
if self.has_next_choice() {
|
||||
// Clicked NEXT. Increase the page counter.
|
||||
self.increase_page_counter();
|
||||
self.update(ctx);
|
||||
} else if self.is_carousel {
|
||||
// In case of carousel going to the left end.
|
||||
self.page_counter_to_zero();
|
||||
self.update(ctx);
|
||||
}
|
||||
// Clicked NEXT. Increase the page counter.
|
||||
// In case of carousel going to the left end.
|
||||
self.move_right(ctx);
|
||||
}
|
||||
ButtonPos::Middle => {
|
||||
// Clicked SELECT. Send current choice index
|
||||
@ -415,7 +553,7 @@ where
|
||||
}
|
||||
}
|
||||
};
|
||||
// The middle button was "pressed", highlighting the current choice by color
|
||||
// The middle button was pressed, highlighting the current choice by color
|
||||
// inversion.
|
||||
if let Some(ButtonControllerMsg::Pressed(ButtonPos::Middle)) = button_event {
|
||||
self.inverse_selected_item = true;
|
||||
|
@ -323,8 +323,11 @@ where
|
||||
|
||||
/// Randomly choose an index in the current category
|
||||
fn randomize_category_position(&mut self, ctx: &mut EventCtx) {
|
||||
self.choice_page
|
||||
.set_page_counter(ctx, random_category_position(&self.current_category));
|
||||
self.choice_page.set_page_counter(
|
||||
ctx,
|
||||
random_category_position(&self.current_category),
|
||||
true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -219,15 +219,17 @@ where
|
||||
}
|
||||
|
||||
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||
// Any event when showing real PIN should hide it
|
||||
// Any non-timer event when showing real PIN should hide it
|
||||
// Same with showing last digit
|
||||
if self.show_real_pin {
|
||||
self.show_real_pin = false;
|
||||
self.update(ctx)
|
||||
}
|
||||
if self.show_last_digit {
|
||||
self.show_last_digit = false;
|
||||
self.update(ctx)
|
||||
if !matches!(event, Event::Timer(_)) {
|
||||
if self.show_real_pin {
|
||||
self.show_real_pin = false;
|
||||
self.update(ctx)
|
||||
}
|
||||
if self.show_last_digit {
|
||||
self.show_last_digit = false;
|
||||
self.update(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
// Any button event will show the "real" prompt
|
||||
@ -254,7 +256,7 @@ where
|
||||
self.textbox.append(ctx, ch);
|
||||
// Choosing random digit to be shown next
|
||||
self.choice_page
|
||||
.set_page_counter(ctx, get_random_digit_position());
|
||||
.set_page_counter(ctx, get_random_digit_position(), true);
|
||||
self.show_last_digit = true;
|
||||
self.update(ctx);
|
||||
None
|
||||
|
@ -13,7 +13,7 @@ pub use button::{
|
||||
Button, ButtonAction, ButtonActions, ButtonContent, ButtonDetails, ButtonLayout, ButtonPos,
|
||||
ButtonStyle, ButtonStyleSheet,
|
||||
};
|
||||
pub use button_controller::{ButtonController, ButtonControllerMsg};
|
||||
pub use button_controller::{AutomaticMover, ButtonController, ButtonControllerMsg};
|
||||
pub use common_messages::CancelConfirmMsg;
|
||||
pub use error::ErrorScreen;
|
||||
pub use hold_to_confirm::{HoldToConfirm, HoldToConfirmMsg};
|
||||
|
@ -22,3 +22,5 @@ pub const fn screen() -> Rect {
|
||||
Rect::from_top_left_and_size(Point::zero(), SIZE)
|
||||
}
|
||||
pub const SCREEN: Rect = screen();
|
||||
|
||||
pub const IGNORE_OTHER_BTN_MS: u32 = 200;
|
||||
|
@ -859,7 +859,14 @@ extern "C" fn tutorial(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj
|
||||
|
||||
let pages = FlowPages::new(get_page, PAGE_COUNT);
|
||||
|
||||
let obj = LayoutObj::new(Flow::new(pages).with_scrollbar(false))?;
|
||||
// Setting the ignore-second-button to mimic all the Choice pages, to teach user
|
||||
// that they should really press both buttons at the same time to achieve
|
||||
// middle-click.
|
||||
let obj = LayoutObj::new(
|
||||
Flow::new(pages)
|
||||
.with_scrollbar(false)
|
||||
.with_ignore_second_button_ms(constant::IGNORE_OTHER_BTN_MS),
|
||||
)?;
|
||||
Ok(obj.into())
|
||||
};
|
||||
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
|
||||
|
Loading…
Reference in New Issue
Block a user