mirror of
https://github.com/trezor/trezor-firmware.git
synced 2025-02-20 11:32:04 +00:00
chore(core): show the last passphrase character for a while
This commit is contained in:
parent
997c27adb3
commit
02b2bf99e3
1
core/.changelog.d/3959.added
Normal file
1
core/.changelog.d/3959.added
Normal file
@ -0,0 +1 @@
|
|||||||
|
Show last typed passphrase character for short period of time
|
@ -1,12 +1,14 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
strutil::{ShortString, TString},
|
strutil::{ShortString, TString},
|
||||||
|
time::Duration,
|
||||||
translations::TR,
|
translations::TR,
|
||||||
ui::{
|
ui::{
|
||||||
component::{
|
component::{
|
||||||
base::ComponentExt, swipe_detect::SwipeConfig, text::common::TextBox, Component, Event,
|
base::ComponentExt, swipe_detect::SwipeConfig, text::common::TextBox, Component, Event,
|
||||||
EventCtx, Label, Maybe, Never, Swipe,
|
EventCtx, Label, Maybe, Never, Swipe, Timer,
|
||||||
},
|
},
|
||||||
display,
|
display,
|
||||||
|
event::TouchEvent,
|
||||||
geometry::{Alignment, Direction, Grid, Insets, Offset, Rect},
|
geometry::{Alignment, Direction, Grid, Insets, Offset, Rect},
|
||||||
model_mercury::{
|
model_mercury::{
|
||||||
component::{
|
component::{
|
||||||
@ -22,7 +24,7 @@ use crate::{
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
use core::cell::Cell;
|
use core::{cell::Cell, mem};
|
||||||
use num_traits::ToPrimitive;
|
use num_traits::ToPrimitive;
|
||||||
|
|
||||||
pub enum PassphraseKeyboardMsg {
|
pub enum PassphraseKeyboardMsg {
|
||||||
@ -30,6 +32,14 @@ pub enum PassphraseKeyboardMsg {
|
|||||||
Cancelled,
|
Cancelled,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq, Debug, Copy, Clone)]
|
||||||
|
#[cfg_attr(feature = "ui_debug", derive(ufmt::derive::uDebug))]
|
||||||
|
enum DisplayStyle {
|
||||||
|
Dots,
|
||||||
|
Chars,
|
||||||
|
LastChar,
|
||||||
|
}
|
||||||
|
|
||||||
/// Enum keeping track of which keyboard is shown and which comes next. Keep the
|
/// Enum keeping track of which keyboard is shown and which comes next. Keep the
|
||||||
/// number of values and the constant PAGE_COUNT in synch.
|
/// number of values and the constant PAGE_COUNT in synch.
|
||||||
#[repr(u32)]
|
#[repr(u32)]
|
||||||
@ -105,6 +115,8 @@ const MAX_LENGTH: usize = 50;
|
|||||||
const CONFIRM_BTN_INSETS: Insets = Insets::new(5, 0, 5, 0);
|
const CONFIRM_BTN_INSETS: Insets = Insets::new(5, 0, 5, 0);
|
||||||
const CONFIRM_EMPTY_BTN_MARGIN_RIGHT: i16 = 7;
|
const CONFIRM_EMPTY_BTN_MARGIN_RIGHT: i16 = 7;
|
||||||
const CONFIRM_EMPTY_BTN_INSETS: Insets = Insets::new(5, CONFIRM_EMPTY_BTN_MARGIN_RIGHT, 5, 0);
|
const CONFIRM_EMPTY_BTN_INSETS: Insets = Insets::new(5, CONFIRM_EMPTY_BTN_MARGIN_RIGHT, 5, 0);
|
||||||
|
const INPUT_INSETS: Insets = Insets::new(10, 2, 10, 4);
|
||||||
|
const LAST_DIGIT_TIMEOUT_S: u32 = 1;
|
||||||
|
|
||||||
impl PassphraseKeyboard {
|
impl PassphraseKeyboard {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
@ -272,7 +284,6 @@ impl Component for PassphraseKeyboard {
|
|||||||
fn place(&mut self, bounds: Rect) -> Rect {
|
fn place(&mut self, bounds: Rect) -> Rect {
|
||||||
const CONFIRM_BTN_WIDTH: i16 = 78;
|
const CONFIRM_BTN_WIDTH: i16 = 78;
|
||||||
const CONFIRM_EMPTY_BTN_WIDTH: i16 = 32;
|
const CONFIRM_EMPTY_BTN_WIDTH: i16 = 32;
|
||||||
const INPUT_INSETS: Insets = Insets::new(10, 2, 10, 4);
|
|
||||||
|
|
||||||
let bounds = bounds.inset(theme::borders());
|
let bounds = bounds.inset(theme::borders());
|
||||||
let (top_area, keypad_area) =
|
let (top_area, keypad_area) =
|
||||||
@ -284,7 +295,7 @@ impl Component for PassphraseKeyboard {
|
|||||||
.1;
|
.1;
|
||||||
|
|
||||||
let top_area = top_area.inset(INPUT_INSETS);
|
let top_area = top_area.inset(INPUT_INSETS);
|
||||||
let input_area = input_area.inset(INPUT_INSETS);
|
// let input_area = input_area.inset(INPUT_INSETS);
|
||||||
let confirm_btn_area = confirm_btn_area.inset(CONFIRM_BTN_INSETS);
|
let confirm_btn_area = confirm_btn_area.inset(CONFIRM_BTN_INSETS);
|
||||||
let confirm_empty_btn_area = confirm_empty_btn_area.inset(CONFIRM_EMPTY_BTN_INSETS);
|
let confirm_empty_btn_area = confirm_empty_btn_area.inset(CONFIRM_EMPTY_BTN_INSETS);
|
||||||
|
|
||||||
@ -323,8 +334,14 @@ impl Component for PassphraseKeyboard {
|
|||||||
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||||
if self.input.multi_tap.timeout_event(event) {
|
if self.input.multi_tap.timeout_event(event) {
|
||||||
self.input.multi_tap.clear_pending_state(ctx);
|
self.input.multi_tap.clear_pending_state(ctx);
|
||||||
|
self.input
|
||||||
|
.last_char_timer
|
||||||
|
.start(ctx, Duration::from_secs(LAST_DIGIT_TIMEOUT_S));
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.input.event(ctx, event);
|
||||||
|
|
||||||
if let Some(swipe) = self.page_swipe.event(ctx, event) {
|
if let Some(swipe) = self.page_swipe.event(ctx, event) {
|
||||||
// We have detected a horizontal swipe. Change the keyboard page.
|
// We have detected a horizontal swipe. Change the keyboard page.
|
||||||
self.on_page_change(ctx, swipe);
|
self.on_page_change(ctx, swipe);
|
||||||
@ -383,6 +400,17 @@ impl Component for PassphraseKeyboard {
|
|||||||
let edit = text.map(|c| self.input.multi_tap.click_key(ctx, key, c));
|
let edit = text.map(|c| self.input.multi_tap.click_key(ctx, key, c));
|
||||||
self.input.textbox.apply(ctx, edit);
|
self.input.textbox.apply(ctx, edit);
|
||||||
self.after_edit(ctx);
|
self.after_edit(ctx);
|
||||||
|
if text.len() == 1 {
|
||||||
|
// If the key has just one character, it is immediately applied and the last
|
||||||
|
// digit timer should be started
|
||||||
|
self.input
|
||||||
|
.last_char_timer
|
||||||
|
.start(ctx, Duration::from_secs(LAST_DIGIT_TIMEOUT_S));
|
||||||
|
} else {
|
||||||
|
// multi tap timer is runnig, the last digit timer should be stopped
|
||||||
|
self.input.last_char_timer.stop();
|
||||||
|
}
|
||||||
|
self.input.display_style = DisplayStyle::LastChar;
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -416,14 +444,46 @@ struct Input {
|
|||||||
area: Rect,
|
area: Rect,
|
||||||
textbox: TextBox,
|
textbox: TextBox,
|
||||||
multi_tap: MultiTapKeyboard,
|
multi_tap: MultiTapKeyboard,
|
||||||
|
display_style: DisplayStyle,
|
||||||
|
last_char_timer: Timer,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Input {
|
impl Input {
|
||||||
|
const TWITCH: i16 = 4;
|
||||||
|
|
||||||
fn new() -> Self {
|
fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
area: Rect::zero(),
|
area: Rect::zero(),
|
||||||
textbox: TextBox::empty(MAX_LENGTH),
|
textbox: TextBox::empty(MAX_LENGTH),
|
||||||
multi_tap: MultiTapKeyboard::new(),
|
multi_tap: MultiTapKeyboard::new(),
|
||||||
|
display_style: DisplayStyle::LastChar,
|
||||||
|
last_char_timer: Timer::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_display_style(&self, passphrase: &ShortString) -> ShortString {
|
||||||
|
if passphrase.len() == 0 {
|
||||||
|
ShortString::new()
|
||||||
|
} else {
|
||||||
|
match self.display_style {
|
||||||
|
DisplayStyle::Dots => {
|
||||||
|
let mut dots = ShortString::new();
|
||||||
|
for _ in 0..(passphrase.len()) {
|
||||||
|
dots.push('*').unwrap();
|
||||||
|
}
|
||||||
|
dots
|
||||||
|
}
|
||||||
|
DisplayStyle::Chars => passphrase.clone(),
|
||||||
|
DisplayStyle::LastChar => {
|
||||||
|
let mut dots = ShortString::new();
|
||||||
|
for _ in 0..(passphrase.len() - 1) {
|
||||||
|
dots.push('*').unwrap();
|
||||||
|
}
|
||||||
|
// This should not fail because the passphrase is not empty.
|
||||||
|
dots.push(passphrase.chars().last().unwrap()).unwrap();
|
||||||
|
dots
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -436,26 +496,55 @@ impl Component for Input {
|
|||||||
self.area
|
self.area
|
||||||
}
|
}
|
||||||
|
|
||||||
fn event(&mut self, _ctx: &mut EventCtx, _event: Event) -> Option<Self::Msg> {
|
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||||
|
match event {
|
||||||
|
Event::Timer(_) if self.last_char_timer.expire(event) => {
|
||||||
|
self.display_style = DisplayStyle::Dots;
|
||||||
|
ctx.request_paint();
|
||||||
|
}
|
||||||
|
Event::Touch(TouchEvent::TouchStart(pos)) if self.area.contains(pos) => {
|
||||||
|
self.display_style = DisplayStyle::Chars;
|
||||||
|
ctx.request_paint();
|
||||||
|
}
|
||||||
|
Event::Touch(TouchEvent::TouchEnd(_)) => {
|
||||||
|
if mem::replace(&mut self.display_style, DisplayStyle::Dots) == DisplayStyle::Chars
|
||||||
|
{
|
||||||
|
ctx.request_paint();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||||
let style = theme::label_keyboard();
|
let style = theme::label_keyboard_mono();
|
||||||
|
let text_area = self.area.inset(INPUT_INSETS);
|
||||||
|
|
||||||
let text_baseline = self.area.top_left() + Offset::y(style.text_font.text_height())
|
let mut text_baseline = text_area.top_left() + Offset::y(style.text_font.text_height())
|
||||||
- Offset::y(style.text_font.text_baseline());
|
- Offset::y(style.text_font.text_baseline());
|
||||||
|
|
||||||
let text = self.textbox.content();
|
let text = self.textbox.content();
|
||||||
|
|
||||||
shape::Bar::new(self.area).with_bg(theme::BG).render(target);
|
shape::Bar::new(text_area).with_bg(theme::BG).render(target);
|
||||||
|
|
||||||
// Find out how much text can fit into the textbox.
|
// Find out how much text can fit into the textbox.
|
||||||
// Accounting for the pending marker, which draws itself one pixel longer than
|
// Accounting for the pending marker, which draws itself one pixel longer than
|
||||||
// the last character
|
// the last character
|
||||||
let available_area_width = self.area.width() - 1;
|
let available_area_width = text_area.width() - 1;
|
||||||
let text_to_display =
|
let truncated =
|
||||||
long_line_content_with_ellipsis(text, "...", style.text_font, available_area_width);
|
long_line_content_with_ellipsis(text, "", style.text_font, available_area_width);
|
||||||
|
|
||||||
|
// Jiggle hidden passphrase when overflowed.
|
||||||
|
if text.len() > truncated.len()
|
||||||
|
&& text.len() % 2 == 0
|
||||||
|
&& (self.display_style == DisplayStyle::Dots
|
||||||
|
|| self.display_style == DisplayStyle::LastChar)
|
||||||
|
{
|
||||||
|
text_baseline.x += Self::TWITCH;
|
||||||
|
}
|
||||||
|
|
||||||
|
let text_to_display = self.apply_display_style(&truncated);
|
||||||
|
|
||||||
shape::Text::new(text_baseline, &text_to_display)
|
shape::Text::new(text_baseline, &text_to_display)
|
||||||
.with_font(style.text_font)
|
.with_font(style.text_font)
|
||||||
|
@ -136,6 +136,10 @@ pub const fn label_keyboard() -> TextStyle {
|
|||||||
TextStyle::new(Font::DEMIBOLD, GREY_EXTRA_LIGHT, BG, GREY_LIGHT, GREY_LIGHT)
|
TextStyle::new(Font::DEMIBOLD, GREY_EXTRA_LIGHT, BG, GREY_LIGHT, GREY_LIGHT)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub const fn label_keyboard_mono() -> TextStyle {
|
||||||
|
TextStyle::new(Font::MONO, GREY_EXTRA_LIGHT, BG, GREY_LIGHT, GREY_LIGHT)
|
||||||
|
}
|
||||||
|
|
||||||
pub const fn label_keyboard_prompt() -> TextStyle {
|
pub const fn label_keyboard_prompt() -> TextStyle {
|
||||||
TextStyle::new(Font::DEMIBOLD, GREY_LIGHT, BG, GREY_LIGHT, GREY_LIGHT)
|
TextStyle::new(Font::DEMIBOLD, GREY_LIGHT, BG, GREY_LIGHT, GREY_LIGHT)
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,12 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
strutil::{ShortString, TString},
|
strutil::{ShortString, TString},
|
||||||
|
time::Duration,
|
||||||
translations::TR,
|
translations::TR,
|
||||||
trezorhal::random,
|
trezorhal::random,
|
||||||
ui::{
|
ui::{
|
||||||
component::{text::common::TextBox, Child, Component, ComponentExt, Event, EventCtx},
|
component::{
|
||||||
|
text::common::TextBox, Child, Component, ComponentExt, Event, EventCtx, Timer,
|
||||||
|
},
|
||||||
display::Icon,
|
display::Icon,
|
||||||
geometry::Rect,
|
geometry::Rect,
|
||||||
shape::Renderer,
|
shape::Renderer,
|
||||||
@ -43,6 +46,8 @@ const DIGITS_INDEX: usize = 5;
|
|||||||
const SPECIAL_INDEX: usize = 6;
|
const SPECIAL_INDEX: usize = 6;
|
||||||
const SPACE_INDEX: usize = 7;
|
const SPACE_INDEX: usize = 7;
|
||||||
|
|
||||||
|
const LAST_DIGIT_TIMEOUT_S: u32 = 1;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
struct MenuItem {
|
struct MenuItem {
|
||||||
text: TString<'static>,
|
text: TString<'static>,
|
||||||
@ -273,6 +278,7 @@ pub struct PassphraseEntry {
|
|||||||
show_last_digit: bool,
|
show_last_digit: bool,
|
||||||
textbox: TextBox,
|
textbox: TextBox,
|
||||||
current_category: ChoiceCategory,
|
current_category: ChoiceCategory,
|
||||||
|
last_char_timer: Timer,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PassphraseEntry {
|
impl PassphraseEntry {
|
||||||
@ -286,6 +292,7 @@ impl PassphraseEntry {
|
|||||||
show_last_digit: false,
|
show_last_digit: false,
|
||||||
textbox: TextBox::empty(MAX_PASSPHRASE_LENGTH),
|
textbox: TextBox::empty(MAX_PASSPHRASE_LENGTH),
|
||||||
current_category: ChoiceCategory::Menu,
|
current_category: ChoiceCategory::Menu,
|
||||||
|
last_char_timer: Timer::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -383,17 +390,22 @@ impl Component for PassphraseEntry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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 non-timer event when showing real passphrase should hide it
|
match event {
|
||||||
// Same with showing last digit
|
// Timeout for showing the last digit.
|
||||||
if !matches!(event, Event::Timer(_)) {
|
Event::Timer(_) if self.last_char_timer.expire(event) => {
|
||||||
if self.show_plain_passphrase {
|
if self.show_last_digit {
|
||||||
|
self.show_last_digit = false;
|
||||||
|
self.update_passphrase_dots(ctx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Other timers are ignored.
|
||||||
|
Event::Timer(_) => {}
|
||||||
|
// Any non-timer event when showing plain passphrase should hide it
|
||||||
|
_ if self.show_plain_passphrase => {
|
||||||
self.show_plain_passphrase = false;
|
self.show_plain_passphrase = false;
|
||||||
self.update_passphrase_dots(ctx);
|
self.update_passphrase_dots(ctx);
|
||||||
}
|
}
|
||||||
if self.show_last_digit {
|
_ => {}
|
||||||
self.show_last_digit = false;
|
|
||||||
self.update_passphrase_dots(ctx);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some((action, long_press)) = self.choice_page.event(ctx, event) {
|
if let Some((action, long_press)) = self.choice_page.event(ctx, event) {
|
||||||
@ -437,6 +449,8 @@ impl Component for PassphraseEntry {
|
|||||||
PassphraseAction::Character(ch) if !self.is_full() => {
|
PassphraseAction::Character(ch) if !self.is_full() => {
|
||||||
self.append_char(ctx, ch);
|
self.append_char(ctx, ch);
|
||||||
self.show_last_digit = true;
|
self.show_last_digit = true;
|
||||||
|
self.last_char_timer
|
||||||
|
.start(ctx, Duration::from_secs(LAST_DIGIT_TIMEOUT_S));
|
||||||
self.update_passphrase_dots(ctx);
|
self.update_passphrase_dots(ctx);
|
||||||
self.randomize_category_position(ctx);
|
self.randomize_category_position(ctx);
|
||||||
ctx.request_paint();
|
ctx.request_paint();
|
||||||
|
@ -1,10 +1,13 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
strutil::TString,
|
strutil::{ShortString, TString},
|
||||||
|
time::Duration,
|
||||||
ui::{
|
ui::{
|
||||||
component::{
|
component::{
|
||||||
base::ComponentExt, text::common::TextBox, Child, Component, Event, EventCtx, Never,
|
base::ComponentExt, text::common::TextBox, Child, Component, Event, EventCtx, Never,
|
||||||
|
Timer,
|
||||||
},
|
},
|
||||||
display,
|
display,
|
||||||
|
event::TouchEvent,
|
||||||
geometry::{Grid, Offset, Rect},
|
geometry::{Grid, Offset, Rect},
|
||||||
model_tt::component::{
|
model_tt::component::{
|
||||||
button::{Button, ButtonContent, ButtonMsg},
|
button::{Button, ButtonContent, ButtonMsg},
|
||||||
@ -12,13 +15,11 @@ use crate::{
|
|||||||
swipe::{Swipe, SwipeDirection},
|
swipe::{Swipe, SwipeDirection},
|
||||||
theme, ScrollBar,
|
theme, ScrollBar,
|
||||||
},
|
},
|
||||||
shape,
|
shape::{self, Renderer},
|
||||||
shape::Renderer,
|
|
||||||
util::long_line_content_with_ellipsis,
|
util::long_line_content_with_ellipsis,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
use core::{cell::Cell, mem};
|
||||||
use core::cell::Cell;
|
|
||||||
|
|
||||||
#[cfg_attr(feature = "debug", derive(ufmt::derive::uDebug))]
|
#[cfg_attr(feature = "debug", derive(ufmt::derive::uDebug))]
|
||||||
pub enum PassphraseKeyboardMsg {
|
pub enum PassphraseKeyboardMsg {
|
||||||
@ -26,6 +27,14 @@ pub enum PassphraseKeyboardMsg {
|
|||||||
Cancelled,
|
Cancelled,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq, Debug, Copy, Clone)]
|
||||||
|
#[cfg_attr(feature = "ui_debug", derive(ufmt::derive::uDebug))]
|
||||||
|
enum DisplayStyle {
|
||||||
|
Dots,
|
||||||
|
Chars,
|
||||||
|
LastChar,
|
||||||
|
}
|
||||||
|
|
||||||
pub struct PassphraseKeyboard {
|
pub struct PassphraseKeyboard {
|
||||||
page_swipe: Swipe,
|
page_swipe: Swipe,
|
||||||
input: Child<Input>,
|
input: Child<Input>,
|
||||||
@ -50,6 +59,8 @@ const KEYBOARD: [[&str; KEY_COUNT]; PAGE_COUNT] = [
|
|||||||
const MAX_LENGTH: usize = 50;
|
const MAX_LENGTH: usize = 50;
|
||||||
const INPUT_AREA_HEIGHT: i16 = ScrollBar::DOT_SIZE + 9;
|
const INPUT_AREA_HEIGHT: i16 = ScrollBar::DOT_SIZE + 9;
|
||||||
|
|
||||||
|
const LAST_DIGIT_TIMEOUT_S: u32 = 1;
|
||||||
|
|
||||||
impl PassphraseKeyboard {
|
impl PassphraseKeyboard {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
@ -189,7 +200,7 @@ impl Component for PassphraseKeyboard {
|
|||||||
let (input_area, key_grid_area) =
|
let (input_area, key_grid_area) =
|
||||||
bounds.split_bottom(4 * theme::PIN_BUTTON_HEIGHT + 3 * theme::BUTTON_SPACING);
|
bounds.split_bottom(4 * theme::PIN_BUTTON_HEIGHT + 3 * theme::BUTTON_SPACING);
|
||||||
|
|
||||||
let (input_area, scroll_area) = input_area.split_bottom(INPUT_AREA_HEIGHT);
|
let (_, scroll_area) = input_area.split_bottom(INPUT_AREA_HEIGHT);
|
||||||
let (scroll_area, _) = scroll_area.split_top(ScrollBar::DOT_SIZE);
|
let (scroll_area, _) = scroll_area.split_top(ScrollBar::DOT_SIZE);
|
||||||
|
|
||||||
let key_grid = Grid::new(key_grid_area, 4, 3).with_spacing(theme::BUTTON_SPACING);
|
let key_grid = Grid::new(key_grid_area, 4, 3).with_spacing(theme::BUTTON_SPACING);
|
||||||
@ -231,8 +242,14 @@ impl Component for PassphraseKeyboard {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
if multitap_timeout {
|
if multitap_timeout {
|
||||||
|
// the character has been added, show it for a bit and then hide it
|
||||||
|
self.input
|
||||||
|
.mutate(ctx, |ctx, t| t.start_timer(ctx, LAST_DIGIT_TIMEOUT_S));
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.input.event(ctx, event);
|
||||||
|
|
||||||
if let Some(swipe) = self.page_swipe.event(ctx, event) {
|
if let Some(swipe) = self.page_swipe.event(ctx, event) {
|
||||||
// We have detected a horizontal swipe. Change the keyboard page.
|
// We have detected a horizontal swipe. Change the keyboard page.
|
||||||
self.on_page_swipe(ctx, swipe);
|
self.on_page_swipe(ctx, swipe);
|
||||||
@ -288,6 +305,17 @@ impl Component for PassphraseKeyboard {
|
|||||||
i.textbox.apply(ctx, edit);
|
i.textbox.apply(ctx, edit);
|
||||||
});
|
});
|
||||||
self.after_edit(ctx);
|
self.after_edit(ctx);
|
||||||
|
if text.len() == 1 {
|
||||||
|
// If the key has just one character, it is immediately applied and the last
|
||||||
|
// digit timer should be started
|
||||||
|
self.input
|
||||||
|
.mutate(ctx, |ctx, t| t.start_timer(ctx, LAST_DIGIT_TIMEOUT_S));
|
||||||
|
} else {
|
||||||
|
// multi tap timer is runnig, the last digit timer should be stopped
|
||||||
|
self.input.mutate(ctx, |_ctx, t| t.stop_timer());
|
||||||
|
}
|
||||||
|
self.input
|
||||||
|
.mutate(ctx, |_ctx, t| t.set_display_style(DisplayStyle::LastChar));
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -313,14 +341,59 @@ struct Input {
|
|||||||
area: Rect,
|
area: Rect,
|
||||||
textbox: TextBox,
|
textbox: TextBox,
|
||||||
multi_tap: MultiTapKeyboard,
|
multi_tap: MultiTapKeyboard,
|
||||||
|
display_style: DisplayStyle,
|
||||||
|
last_char_timer: Timer,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Input {
|
impl Input {
|
||||||
|
const TWITCH: i16 = 4;
|
||||||
|
|
||||||
fn new() -> Self {
|
fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
area: Rect::zero(),
|
area: Rect::zero(),
|
||||||
textbox: TextBox::empty(MAX_LENGTH),
|
textbox: TextBox::empty(MAX_LENGTH),
|
||||||
multi_tap: MultiTapKeyboard::new(),
|
multi_tap: MultiTapKeyboard::new(),
|
||||||
|
display_style: DisplayStyle::LastChar,
|
||||||
|
last_char_timer: Timer::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_display_style(&mut self, display_style: DisplayStyle) {
|
||||||
|
self.display_style = display_style;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn start_timer(&mut self, ctx: &mut EventCtx, timeout_ms: u32) {
|
||||||
|
self.last_char_timer
|
||||||
|
.start(ctx, Duration::from_secs(timeout_ms));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn stop_timer(&mut self) {
|
||||||
|
self.last_char_timer.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_display_style(&self, passphrase: &ShortString) -> ShortString {
|
||||||
|
if passphrase.len() == 0 {
|
||||||
|
ShortString::new()
|
||||||
|
} else {
|
||||||
|
match self.display_style {
|
||||||
|
DisplayStyle::Dots => {
|
||||||
|
let mut dots = ShortString::new();
|
||||||
|
for _ in 0..(passphrase.len()) {
|
||||||
|
dots.push('*').unwrap();
|
||||||
|
}
|
||||||
|
dots
|
||||||
|
}
|
||||||
|
DisplayStyle::Chars => passphrase.clone(),
|
||||||
|
DisplayStyle::LastChar => {
|
||||||
|
let mut dots = ShortString::new();
|
||||||
|
for _ in 0..(passphrase.len() - 1) {
|
||||||
|
dots.push('*').unwrap();
|
||||||
|
}
|
||||||
|
// This should not fail because the passphrase is not empty.
|
||||||
|
dots.push(passphrase.chars().last().unwrap()).unwrap();
|
||||||
|
dots
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -333,14 +406,31 @@ impl Component for Input {
|
|||||||
self.area
|
self.area
|
||||||
}
|
}
|
||||||
|
|
||||||
fn event(&mut self, _ctx: &mut EventCtx, _event: Event) -> Option<Self::Msg> {
|
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||||
|
match event {
|
||||||
|
Event::Timer(_) if self.last_char_timer.expire(event) => {
|
||||||
|
self.display_style = DisplayStyle::Dots;
|
||||||
|
ctx.request_paint();
|
||||||
|
}
|
||||||
|
Event::Touch(TouchEvent::TouchStart(pos)) if self.area.contains(pos) => {
|
||||||
|
self.display_style = DisplayStyle::Chars;
|
||||||
|
ctx.request_paint();
|
||||||
|
}
|
||||||
|
Event::Touch(TouchEvent::TouchEnd(_)) => {
|
||||||
|
if mem::replace(&mut self.display_style, DisplayStyle::Dots) == DisplayStyle::Chars
|
||||||
|
{
|
||||||
|
ctx.request_paint();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||||
let style = theme::label_keyboard();
|
let style = theme::label_keyboard_mono();
|
||||||
|
|
||||||
let text_baseline = self.area.top_left() + Offset::y(style.text_font.text_height())
|
let mut text_baseline = self.area.top_left() + Offset::y(style.text_font.text_height())
|
||||||
- Offset::y(style.text_font.text_baseline());
|
- Offset::y(style.text_font.text_baseline());
|
||||||
|
|
||||||
let text = self.textbox.content();
|
let text = self.textbox.content();
|
||||||
@ -351,8 +441,19 @@ impl Component for Input {
|
|||||||
// Accounting for the pending marker, which draws itself one pixel longer than
|
// Accounting for the pending marker, which draws itself one pixel longer than
|
||||||
// the last character
|
// the last character
|
||||||
let available_area_width = self.area.width() - 1;
|
let available_area_width = self.area.width() - 1;
|
||||||
let text_to_display =
|
let truncated =
|
||||||
long_line_content_with_ellipsis(text, "...", style.text_font, available_area_width);
|
long_line_content_with_ellipsis(text, "", style.text_font, available_area_width);
|
||||||
|
|
||||||
|
// Jiggle hidden passphrase when overflowed.
|
||||||
|
if text.len() > truncated.len()
|
||||||
|
&& text.len() % 2 == 0
|
||||||
|
&& (self.display_style == DisplayStyle::Dots
|
||||||
|
|| self.display_style == DisplayStyle::LastChar)
|
||||||
|
{
|
||||||
|
text_baseline.x += Self::TWITCH;
|
||||||
|
}
|
||||||
|
|
||||||
|
let text_to_display = self.apply_display_style(&truncated);
|
||||||
|
|
||||||
shape::Text::new(text_baseline, &text_to_display)
|
shape::Text::new(text_baseline, &text_to_display)
|
||||||
.with_font(style.text_font)
|
.with_font(style.text_font)
|
||||||
|
@ -120,6 +120,10 @@ pub const fn label_keyboard() -> TextStyle {
|
|||||||
TextStyle::new(Font::DEMIBOLD, OFF_WHITE, BG, GREY_LIGHT, GREY_LIGHT)
|
TextStyle::new(Font::DEMIBOLD, OFF_WHITE, BG, GREY_LIGHT, GREY_LIGHT)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub const fn label_keyboard_mono() -> TextStyle {
|
||||||
|
TextStyle::new(Font::MONO, OFF_WHITE, BG, GREY_LIGHT, GREY_LIGHT)
|
||||||
|
}
|
||||||
|
|
||||||
pub const fn label_keyboard_prompt() -> TextStyle {
|
pub const fn label_keyboard_prompt() -> TextStyle {
|
||||||
TextStyle::new(Font::DEMIBOLD, GREY_LIGHT, BG, GREY_LIGHT, GREY_LIGHT)
|
TextStyle::new(Font::DEMIBOLD, GREY_LIGHT, BG, GREY_LIGHT, GREY_LIGHT)
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user