mirror of
https://github.com/trezor/trezor-firmware.git
synced 2025-02-28 15:22:14 +00:00
feat(eckhart): Add common keyboard functionality
This commit is contained in:
parent
a1a52713ee
commit
84dc09ffcc
@ -0,0 +1,168 @@
|
|||||||
|
use crate::{
|
||||||
|
time::Duration,
|
||||||
|
ui::{
|
||||||
|
component::{text::common::TextEdit, Event, EventCtx, Timer},
|
||||||
|
display::{Color, Font},
|
||||||
|
geometry::{Insets, Offset, Point, Rect},
|
||||||
|
shape::{Bar, Renderer},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::super::theme;
|
||||||
|
|
||||||
|
/// Contains state commonly used in implementations multi-tap keyboards.
|
||||||
|
pub struct MultiTapKeyboard {
|
||||||
|
/// Configured timeout after which we cancel currently pending key.
|
||||||
|
timeout: Duration,
|
||||||
|
/// The currently pending state.
|
||||||
|
pending: Option<Pending>,
|
||||||
|
/// Timer for clearing the pending state.
|
||||||
|
timer: Timer,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Pending {
|
||||||
|
/// Index of the pending key.
|
||||||
|
key: usize,
|
||||||
|
/// Index of the key press (how many times the `key` was pressed, minus
|
||||||
|
/// one).
|
||||||
|
press: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MultiTapKeyboard {
|
||||||
|
/// Create a new, empty, multi-tap state.
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
timeout: Duration::from_secs(1),
|
||||||
|
pending: None,
|
||||||
|
timer: Timer::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return the index of the currently pending key, if any.
|
||||||
|
pub fn pending_key(&self) -> Option<usize> {
|
||||||
|
self.pending.as_ref().map(|p| p.key)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return the index of the pending key press.
|
||||||
|
pub fn pending_press(&self) -> Option<usize> {
|
||||||
|
self.pending.as_ref().map(|p| p.press)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns `true` if `event` is an `Event::Timer` for the currently pending
|
||||||
|
/// timer.
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Register a click to a key. `MultiTapKeyboard` itself does not have any
|
||||||
|
/// concept of the key set, so both the key index and the key content is
|
||||||
|
/// taken here. Returns a text editing operation the caller should apply on
|
||||||
|
/// the output buffer. Takes `EventCtx` to request a timeout for cancelling
|
||||||
|
/// the pending state. Caller is required to handle the timer event and
|
||||||
|
/// call `Self::clear_pending_state` when the timer hits.
|
||||||
|
pub fn click_key(&mut self, ctx: &mut EventCtx, key: usize, key_text: &str) -> TextEdit {
|
||||||
|
let (is_pending, press) = match &self.pending {
|
||||||
|
Some(pending) if pending.key == key => {
|
||||||
|
// This key is pending. Cycle the last inserted character through the
|
||||||
|
// key content.
|
||||||
|
(true, pending.press.wrapping_add(1))
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
// This key is not pending. Append the first character in the key.
|
||||||
|
(false, 0)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// If the key has more then one character, we need to set it as pending, so we
|
||||||
|
// can cycle through on the repeated clicks. We also request a timer so we can
|
||||||
|
// reset the pending state after a timeout.
|
||||||
|
//
|
||||||
|
// Note: It might seem that we should make sure to `request_paint` in case we
|
||||||
|
// progress into a pending state (to display the pending marker), but such
|
||||||
|
// 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 {
|
||||||
|
self.timer.start(ctx, self.timeout);
|
||||||
|
Some(Pending { key, press })
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(!key_text.is_empty());
|
||||||
|
// Now we can be sure that a looped iterator will return a value
|
||||||
|
let ch = unwrap!(key_text.chars().cycle().nth(press));
|
||||||
|
if is_pending {
|
||||||
|
TextEdit::ReplaceLast(ch)
|
||||||
|
} else {
|
||||||
|
TextEdit::Append(ch)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a visible "underscoring" of the last letter of a text.
|
||||||
|
pub fn render_pending_marker<'s>(
|
||||||
|
target: &mut impl Renderer<'s>,
|
||||||
|
text_baseline: Point,
|
||||||
|
text: &str,
|
||||||
|
font: Font,
|
||||||
|
color: Color,
|
||||||
|
) {
|
||||||
|
// Measure the width of the last character of input.
|
||||||
|
if let Some(last) = text.chars().last() {
|
||||||
|
let width = font.text_width(text);
|
||||||
|
let last_width = font.char_width(last);
|
||||||
|
// Draw the marker 2px under the start of the baseline of the last character.
|
||||||
|
let marker_origin = text_baseline + Offset::new(width - last_width, 2);
|
||||||
|
// Draw the marker 1px longer than the last character, and 3px thick.
|
||||||
|
let marker_rect =
|
||||||
|
Rect::from_top_left_and_size(marker_origin, Offset::new(last_width + 1, 3));
|
||||||
|
Bar::new(marker_rect).with_bg(color).render(target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `DisplayStyle` isused to determine whether the text is fully hidden, fully
|
||||||
|
/// shown, or partially visible.
|
||||||
|
#[derive(PartialEq, Debug, Copy, Clone)]
|
||||||
|
#[cfg_attr(feature = "ui_debug", derive(ufmt::derive::uDebug))]
|
||||||
|
pub(crate) enum DisplayStyle {
|
||||||
|
Hidden,
|
||||||
|
Shown,
|
||||||
|
LastOnly,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The number and colors of fading icons to display.
|
||||||
|
pub const FADING_ICON_COUNT: usize = 4;
|
||||||
|
pub const FADING_ICON_COLORS: [Color; FADING_ICON_COUNT] = [
|
||||||
|
theme::GREY_SUPER_DARK,
|
||||||
|
theme::GREY_EXTRA_DARK,
|
||||||
|
theme::GREY_DARK,
|
||||||
|
theme::GREY,
|
||||||
|
];
|
||||||
|
|
||||||
|
/// Visible area of the keypad. The touchable area is smaller
|
||||||
|
pub const KEYPAD_VISIBLE_HEIGHT: i16 = 440;
|
||||||
|
/// Area of the input field that is touchable
|
||||||
|
pub const INPUT_TOUCH_HEIGHT: i16 = 96;
|
||||||
|
|
||||||
|
const TEXTBOX_HEIGHT: i16 = 72;
|
||||||
|
const INPUT_SIDE_PADDING: i16 = 24;
|
||||||
|
const INPUT_TOP_PADDING: i16 = 16;
|
||||||
|
|
||||||
|
pub const KEYBOARD_INPUT_RADIUS: i16 = 12;
|
||||||
|
pub const KEYBOARD_INPUT_INSETS: Insets = Insets::new(
|
||||||
|
INPUT_TOP_PADDING,
|
||||||
|
INPUT_SIDE_PADDING,
|
||||||
|
INPUT_TOUCH_HEIGHT - INPUT_TOP_PADDING - TEXTBOX_HEIGHT,
|
||||||
|
INPUT_SIDE_PADDING,
|
||||||
|
);
|
@ -0,0 +1 @@
|
|||||||
|
mod common;
|
@ -5,6 +5,7 @@ mod error;
|
|||||||
mod header;
|
mod header;
|
||||||
mod hint;
|
mod hint;
|
||||||
mod hold_to_confirm;
|
mod hold_to_confirm;
|
||||||
|
mod keyboard;
|
||||||
mod result;
|
mod result;
|
||||||
mod select_word_screen;
|
mod select_word_screen;
|
||||||
mod share_words;
|
mod share_words;
|
||||||
|
Loading…
Reference in New Issue
Block a user