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;
|
const HALF_SCREEN_BUTTON_WIDTH: i16 = constant::WIDTH / 2 - 1;
|
||||||
|
|
||||||
#[derive(Copy, Clone)]
|
#[derive(Copy, Clone, Eq, PartialEq)]
|
||||||
pub enum ButtonPos {
|
pub enum ButtonPos {
|
||||||
Left,
|
Left,
|
||||||
Middle,
|
Middle,
|
||||||
|
@ -3,8 +3,9 @@ use super::{
|
|||||||
};
|
};
|
||||||
use crate::{
|
use crate::{
|
||||||
strutil::StringType,
|
strutil::StringType,
|
||||||
|
time::Duration,
|
||||||
ui::{
|
ui::{
|
||||||
component::{base::Event, Component, EventCtx, Pad},
|
component::{base::Event, Component, EventCtx, Pad, TimerToken},
|
||||||
event::{ButtonEvent, PhysicalButton},
|
event::{ButtonEvent, PhysicalButton},
|
||||||
geometry::Rect,
|
geometry::Rect,
|
||||||
},
|
},
|
||||||
@ -195,6 +196,9 @@ where
|
|||||||
///
|
///
|
||||||
/// Only "final" button events are returned in `ButtonControllerMsg::Triggered`,
|
/// Only "final" button events are returned in `ButtonControllerMsg::Triggered`,
|
||||||
/// based upon the buttons being long-press or not.
|
/// 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>
|
pub struct ButtonController<T>
|
||||||
where
|
where
|
||||||
T: StringType,
|
T: StringType,
|
||||||
@ -204,10 +208,12 @@ where
|
|||||||
middle_btn: ButtonContainer<T>,
|
middle_btn: ButtonContainer<T>,
|
||||||
right_btn: ButtonContainer<T>,
|
right_btn: ButtonContainer<T>,
|
||||||
state: ButtonState,
|
state: ButtonState,
|
||||||
// Button area is needed so the buttons
|
/// Button area is needed so the buttons
|
||||||
// can be "re-placed" after their text is changed
|
/// can be "re-placed" after their text is changed
|
||||||
// Will be set in `place`
|
/// Will be set in `place`
|
||||||
button_area: Rect,
|
button_area: Rect,
|
||||||
|
/// Handling optional ignoring of buttons after pressing the other button.
|
||||||
|
ignore_btn_delay: Option<IgnoreButtonDelay>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> ButtonController<T>
|
impl<T> ButtonController<T>
|
||||||
@ -222,9 +228,17 @@ where
|
|||||||
right_btn: ButtonContainer::new(ButtonPos::Right, btn_layout.btn_right),
|
right_btn: ButtonContainer::new(ButtonPos::Right, btn_layout.btn_right),
|
||||||
state: ButtonState::Nothing,
|
state: ButtonState::Nothing,
|
||||||
button_area: Rect::zero(),
|
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.
|
/// Updating all the three buttons to the wanted states.
|
||||||
pub fn set(&mut self, btn_layout: ButtonLayout<T>) {
|
pub fn set(&mut self, btn_layout: ButtonLayout<T>) {
|
||||||
self.pad.clear();
|
self.pad.clear();
|
||||||
@ -240,6 +254,14 @@ where
|
|||||||
self.right_btn.set_pressed(ctx, right);
|
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.
|
/// Handle middle button hold-to-confirm start.
|
||||||
/// We need to cancel possible holds in both other buttons.
|
/// We need to cancel possible holds in both other buttons.
|
||||||
fn middle_hold_started(&mut self, ctx: &mut EventCtx) {
|
fn middle_hold_started(&mut self, ctx: &mut EventCtx) {
|
||||||
@ -291,22 +313,29 @@ where
|
|||||||
// _ _
|
// _ _
|
||||||
ButtonState::Nothing => match button_event {
|
ButtonState::Nothing => match button_event {
|
||||||
// ▼ * | * ▼
|
// ▼ * | * ▼
|
||||||
ButtonEvent::ButtonPressed(which) => (
|
ButtonEvent::ButtonPressed(which) => {
|
||||||
// ↓ _ | _ ↓
|
// ↓ _ | _ ↓
|
||||||
ButtonState::OneDown(which),
|
// Initial button press will set the timer for second button,
|
||||||
match which {
|
// and after some delay, it will become un-clickable
|
||||||
// ▼ *
|
if let Some(ignore_btn_delay) = &mut self.ignore_btn_delay {
|
||||||
PhysicalButton::Left => {
|
ignore_btn_delay.handle_button_press(ctx, which);
|
||||||
self.left_btn.hold_started(ctx);
|
}
|
||||||
Some(ButtonControllerMsg::Pressed(ButtonPos::Left))
|
(
|
||||||
}
|
ButtonState::OneDown(which),
|
||||||
// * ▼
|
match which {
|
||||||
PhysicalButton::Right => {
|
// ▼ *
|
||||||
self.right_btn.hold_started(ctx);
|
PhysicalButton::Left => {
|
||||||
Some(ButtonControllerMsg::Pressed(ButtonPos::Right))
|
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),
|
_ => (self.state, None),
|
||||||
},
|
},
|
||||||
// ↓ _ | _ ↓
|
// ↓ _ | _ ↓
|
||||||
@ -314,18 +343,32 @@ where
|
|||||||
// ▲ * | * ▲
|
// ▲ * | * ▲
|
||||||
ButtonEvent::ButtonReleased(b) if b == which_down => match which_down {
|
ButtonEvent::ButtonReleased(b) if b == which_down => match which_down {
|
||||||
// ▲ *
|
// ▲ *
|
||||||
|
// Trigger the button and make the second one clickable in all cases
|
||||||
PhysicalButton::Left => {
|
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))
|
(ButtonState::Nothing, self.left_btn.maybe_trigger(ctx))
|
||||||
}
|
}
|
||||||
// * ▲
|
// * ▲
|
||||||
PhysicalButton::Right => {
|
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))
|
(ButtonState::Nothing, self.right_btn.maybe_trigger(ctx))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// * ▼ | ▼ *
|
// * ▼ | ▼ *
|
||||||
ButtonEvent::ButtonPressed(b) if b != which_down => {
|
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);
|
self.middle_hold_started(ctx);
|
||||||
(
|
(
|
||||||
// ↓ ↓
|
// ↓ ↓
|
||||||
@ -356,6 +399,11 @@ where
|
|||||||
// ▲ * | * ▲
|
// ▲ * | * ▲
|
||||||
ButtonEvent::ButtonReleased(b) if b != which_up => {
|
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))
|
(ButtonState::Nothing, self.middle_btn.maybe_trigger(ctx))
|
||||||
}
|
}
|
||||||
_ => (self.state, None),
|
_ => (self.state, None),
|
||||||
@ -394,8 +442,13 @@ where
|
|||||||
self.state = new_state;
|
self.state = new_state;
|
||||||
event
|
event
|
||||||
}
|
}
|
||||||
// HoldToConfirm expiration
|
// Timer - handle clickable properties and HoldToConfirm expiration
|
||||||
Event::Timer(_) => self.handle_htc_expiration(ctx, event),
|
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,
|
_ => 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
|
// DEBUG-ONLY SECTION BELOW
|
||||||
|
|
||||||
#[cfg(feature = "ui_debug")]
|
#[cfg(feature = "ui_debug")]
|
||||||
@ -443,3 +669,10 @@ impl<T: StringType> crate::trace::Trace for ButtonController<T> {
|
|||||||
t.child("right_btn", &self.right_btn);
|
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,
|
page_counter: usize,
|
||||||
return_confirmed_index: bool,
|
return_confirmed_index: bool,
|
||||||
show_scrollbar: 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>
|
impl<F, T> Flow<F, T>
|
||||||
@ -55,6 +58,7 @@ where
|
|||||||
page_counter: 0,
|
page_counter: 0,
|
||||||
return_confirmed_index: false,
|
return_confirmed_index: false,
|
||||||
show_scrollbar: true,
|
show_scrollbar: true,
|
||||||
|
ignore_second_button_ms: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -77,6 +81,12 @@ where
|
|||||||
self
|
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> {
|
pub fn confirmed_index(&self) -> Option<usize> {
|
||||||
self.return_confirmed_index.then_some(self.page_counter)
|
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.
|
// We finally found how long is the first page, and can set its button layout.
|
||||||
self.current_page.place(content_area);
|
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.pad.place(title_content_area);
|
||||||
self.buttons.place(button_area);
|
self.buttons.place(button_area);
|
||||||
|
@ -3,10 +3,13 @@ use crate::{
|
|||||||
ui::{
|
ui::{
|
||||||
component::{Child, Component, Event, EventCtx, Pad},
|
component::{Child, Component, Event, EventCtx, Pad},
|
||||||
geometry::{Insets, Offset, Rect},
|
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;
|
const DEFAULT_ITEMS_DISTANCE: i16 = 10;
|
||||||
|
|
||||||
@ -79,6 +82,13 @@ where
|
|||||||
/// Whether the middle selected item should be painted with
|
/// Whether the middle selected item should be painted with
|
||||||
/// inverse colors - black on white.
|
/// inverse colors - black on white.
|
||||||
inverse_selected_item: bool,
|
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>
|
impl<F, T, A> ChoicePage<F, T, A>
|
||||||
@ -89,16 +99,30 @@ where
|
|||||||
pub fn new(choices: F) -> Self {
|
pub fn new(choices: F) -> Self {
|
||||||
let initial_btn_layout = choices.get(0).0.btn_layout();
|
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 {
|
Self {
|
||||||
choices,
|
choices,
|
||||||
pad: Pad::with_background(theme::BG),
|
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,
|
page_counter: 0,
|
||||||
items_distance: DEFAULT_ITEMS_DISTANCE,
|
items_distance: DEFAULT_ITEMS_DISTANCE,
|
||||||
is_carousel: false,
|
is_carousel: false,
|
||||||
show_incomplete: false,
|
show_incomplete: false,
|
||||||
show_only_one_item: false,
|
show_only_one_item: false,
|
||||||
inverse_selected_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 {
|
pub fn with_initial_page_counter(mut self, page_counter: usize) -> Self {
|
||||||
self.page_counter = page_counter;
|
self.page_counter = page_counter;
|
||||||
let initial_btn_layout = self.get_current_choice().0.btn_layout();
|
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
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -156,9 +183,33 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Navigating to the chosen page index.
|
/// Navigating to the chosen page index.
|
||||||
pub fn set_page_counter(&mut self, ctx: &mut EventCtx, page_counter: usize) {
|
pub fn set_page_counter(
|
||||||
self.page_counter = page_counter;
|
&mut self,
|
||||||
self.update(ctx);
|
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
|
/// Display current, previous and next choices according to
|
||||||
@ -356,14 +407,66 @@ where
|
|||||||
/// whether they are long-pressed, and painting them.
|
/// whether they are long-pressed, and painting them.
|
||||||
fn set_buttons(&mut self, ctx: &mut EventCtx) {
|
fn set_buttons(&mut self, ctx: &mut EventCtx) {
|
||||||
let btn_layout = self.get_current_choice().0.btn_layout();
|
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);
|
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 {
|
pub fn choice_factory(&self) -> &F {
|
||||||
&self.choices
|
&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>
|
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> {
|
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);
|
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 {
|
if let Some(ButtonControllerMsg::Triggered(pos)) = button_event {
|
||||||
match pos {
|
match pos {
|
||||||
ButtonPos::Left => {
|
ButtonPos::Left => {
|
||||||
if self.has_previous_choice() {
|
// Clicked BACK. Decrease the page counter.
|
||||||
// Clicked BACK. Decrease the page counter.
|
// In case of carousel going to the right end.
|
||||||
self.decrease_page_counter();
|
self.move_left(ctx);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
ButtonPos::Right => {
|
ButtonPos::Right => {
|
||||||
if self.has_next_choice() {
|
// Clicked NEXT. Increase the page counter.
|
||||||
// Clicked NEXT. Increase the page counter.
|
// In case of carousel going to the left end.
|
||||||
self.increase_page_counter();
|
self.move_right(ctx);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
ButtonPos::Middle => {
|
ButtonPos::Middle => {
|
||||||
// Clicked SELECT. Send current choice index
|
// 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.
|
// inversion.
|
||||||
if let Some(ButtonControllerMsg::Pressed(ButtonPos::Middle)) = button_event {
|
if let Some(ButtonControllerMsg::Pressed(ButtonPos::Middle)) = button_event {
|
||||||
self.inverse_selected_item = true;
|
self.inverse_selected_item = true;
|
||||||
|
@ -323,8 +323,11 @@ where
|
|||||||
|
|
||||||
/// Randomly choose an index in the current category
|
/// Randomly choose an index in the current category
|
||||||
fn randomize_category_position(&mut self, ctx: &mut EventCtx) {
|
fn randomize_category_position(&mut self, ctx: &mut EventCtx) {
|
||||||
self.choice_page
|
self.choice_page.set_page_counter(
|
||||||
.set_page_counter(ctx, random_category_position(&self.current_category));
|
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> {
|
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
|
// Same with showing last digit
|
||||||
if self.show_real_pin {
|
if !matches!(event, Event::Timer(_)) {
|
||||||
self.show_real_pin = false;
|
if self.show_real_pin {
|
||||||
self.update(ctx)
|
self.show_real_pin = false;
|
||||||
}
|
self.update(ctx)
|
||||||
if self.show_last_digit {
|
}
|
||||||
self.show_last_digit = false;
|
if self.show_last_digit {
|
||||||
self.update(ctx)
|
self.show_last_digit = false;
|
||||||
|
self.update(ctx)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Any button event will show the "real" prompt
|
// Any button event will show the "real" prompt
|
||||||
@ -254,7 +256,7 @@ where
|
|||||||
self.textbox.append(ctx, ch);
|
self.textbox.append(ctx, ch);
|
||||||
// Choosing random digit to be shown next
|
// Choosing random digit to be shown next
|
||||||
self.choice_page
|
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.show_last_digit = true;
|
||||||
self.update(ctx);
|
self.update(ctx);
|
||||||
None
|
None
|
||||||
|
@ -13,7 +13,7 @@ pub use button::{
|
|||||||
Button, ButtonAction, ButtonActions, ButtonContent, ButtonDetails, ButtonLayout, ButtonPos,
|
Button, ButtonAction, ButtonActions, ButtonContent, ButtonDetails, ButtonLayout, ButtonPos,
|
||||||
ButtonStyle, ButtonStyleSheet,
|
ButtonStyle, ButtonStyleSheet,
|
||||||
};
|
};
|
||||||
pub use button_controller::{ButtonController, ButtonControllerMsg};
|
pub use button_controller::{AutomaticMover, ButtonController, ButtonControllerMsg};
|
||||||
pub use common_messages::CancelConfirmMsg;
|
pub use common_messages::CancelConfirmMsg;
|
||||||
pub use error::ErrorScreen;
|
pub use error::ErrorScreen;
|
||||||
pub use hold_to_confirm::{HoldToConfirm, HoldToConfirmMsg};
|
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)
|
Rect::from_top_left_and_size(Point::zero(), SIZE)
|
||||||
}
|
}
|
||||||
pub const SCREEN: Rect = screen();
|
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 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())
|
Ok(obj.into())
|
||||||
};
|
};
|
||||||
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
|
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
|
||||||
|
Loading…
Reference in New Issue
Block a user