parent
548651608c
commit
3c337723b2
@ -0,0 +1,30 @@
|
||||
use cstr_core::CStr;
|
||||
|
||||
extern "C" {
|
||||
// trezor-crypto/bip39.h
|
||||
fn mnemonic_complete_word(prefix: *const cty::c_char, len: cty::c_int) -> *const cty::c_char;
|
||||
fn mnemonic_word_completion_mask(prefix: *const cty::c_char, len: cty::c_int) -> u32;
|
||||
}
|
||||
|
||||
pub fn complete_word(prefix: &str) -> Option<&'static str> {
|
||||
if prefix.is_empty() {
|
||||
None
|
||||
} else {
|
||||
// SAFETY: `mnemonic_complete_word` shouldn't retain nor modify the passed byte
|
||||
// string, making the call safe.
|
||||
let word = unsafe { mnemonic_complete_word(prefix.as_ptr() as _, prefix.len() as _) };
|
||||
if word.is_null() {
|
||||
None
|
||||
} else {
|
||||
// SAFETY: On success, `mnemonic_complete_word` should return a 0-terminated
|
||||
// UTF-8 string with static lifetime.
|
||||
Some(unsafe { CStr::from_ptr(word).to_str().unwrap_unchecked() })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn word_completion_mask(prefix: &str) -> u32 {
|
||||
// SAFETY: `mnemonic_word_completion_mask` shouldn't retain nor modify the
|
||||
// passed byte string, making the call safe.
|
||||
unsafe { mnemonic_word_completion_mask(prefix.as_ptr() as _, prefix.len() as _) }
|
||||
}
|
@ -1,4 +1,6 @@
|
||||
pub mod bip39;
|
||||
pub mod common;
|
||||
#[cfg(feature = "ui")]
|
||||
pub mod display;
|
||||
pub mod random;
|
||||
pub mod slip39;
|
||||
|
@ -0,0 +1,36 @@
|
||||
use cstr_core::CStr;
|
||||
|
||||
mod ffi {
|
||||
extern "C" {
|
||||
// trezor-crypto/slip39.h
|
||||
pub fn slip39_word_completion_mask(prefix: u16) -> u16;
|
||||
pub fn button_sequence_to_word(sequence: u16) -> *const cty::c_char;
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculates which buttons still can be pressed after some already were.
|
||||
/// Returns a 9-bit bitmask, where each bit specifies which buttons
|
||||
/// can be further pressed (there are still words in this combination).
|
||||
/// LSB denotes first button.
|
||||
///
|
||||
/// Example: 110000110 - second, third, eighth and ninth button still can be
|
||||
/// pressed.
|
||||
pub fn word_completion_mask(prefix: u16) -> Option<u16> {
|
||||
if !(1..=9999).contains(&prefix) {
|
||||
None
|
||||
} else {
|
||||
Some(unsafe { ffi::slip39_word_completion_mask(prefix) })
|
||||
}
|
||||
}
|
||||
|
||||
/// Finds the first word that fits the given button prefix.
|
||||
pub fn button_sequence_to_word(prefix: u16) -> Option<&'static str> {
|
||||
let word = unsafe { ffi::button_sequence_to_word(prefix) };
|
||||
if word.is_null() {
|
||||
None
|
||||
} else {
|
||||
// SAFETY: On success, `button_sequence_to_word` should return a 0-terminated
|
||||
// UTF-8 string with static lifetime.
|
||||
Some(unsafe { CStr::from_ptr(word).to_str().unwrap_unchecked() })
|
||||
}
|
||||
}
|
@ -0,0 +1,89 @@
|
||||
use crate::ui::{
|
||||
component::{Component, ComponentExt, Event, EventCtx, Pad},
|
||||
display::Color,
|
||||
geometry::Rect,
|
||||
};
|
||||
|
||||
pub struct Maybe<T> {
|
||||
inner: T,
|
||||
pad: Pad,
|
||||
visible: bool,
|
||||
}
|
||||
|
||||
impl<T> Maybe<T> {
|
||||
pub fn new(pad: Pad, inner: T, visible: bool) -> Self {
|
||||
Self {
|
||||
inner,
|
||||
visible,
|
||||
pad,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn visible(area: Rect, clear: Color, inner: T) -> Self {
|
||||
Self::new(Pad::with_background(area, clear), inner, true)
|
||||
}
|
||||
|
||||
pub fn hidden(area: Rect, clear: Color, inner: T) -> Self {
|
||||
Self::new(Pad::with_background(area, clear), inner, false)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Maybe<T>
|
||||
where
|
||||
T: Component,
|
||||
{
|
||||
pub fn show_if(&mut self, ctx: &mut EventCtx, show: bool) {
|
||||
if self.visible != show {
|
||||
self.visible = show;
|
||||
|
||||
// Invalidate the pad, so either we prepare a fresh canvas for the content, or
|
||||
// paint over it.
|
||||
self.pad.clear();
|
||||
if show {
|
||||
// Make sure the whole inner tree is painted.
|
||||
self.inner.request_complete_repaint(ctx);
|
||||
} else {
|
||||
// Just make sure out `paint` method is called, to clear the pad.
|
||||
ctx.request_paint();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn show(&mut self, ctx: &mut EventCtx) {
|
||||
self.show_if(ctx, true)
|
||||
}
|
||||
|
||||
pub fn hide(&mut self, ctx: &mut EventCtx) {
|
||||
self.show_if(ctx, false)
|
||||
}
|
||||
|
||||
pub fn inner(&self) -> &T {
|
||||
&self.inner
|
||||
}
|
||||
|
||||
pub fn inner_mut(&mut self) -> &mut T {
|
||||
&mut self.inner
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Component for Maybe<T>
|
||||
where
|
||||
T: Component,
|
||||
{
|
||||
type Msg = T::Msg;
|
||||
|
||||
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||
if self.visible {
|
||||
self.inner.event(ctx, event)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn paint(&mut self) {
|
||||
self.pad.paint();
|
||||
if self.visible {
|
||||
self.inner.paint();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,214 @@
|
||||
use crate::{
|
||||
trezorhal::bip39,
|
||||
ui::{
|
||||
component::{Component, Event, EventCtx},
|
||||
display,
|
||||
geometry::{Offset, Rect},
|
||||
model_tt::{
|
||||
component::{
|
||||
keyboard::{
|
||||
common::{MultiTapKeyboard, TextBox},
|
||||
mnemonic::{MnemonicInput, MnemonicInputMsg, MNEMONIC_KEY_COUNT},
|
||||
},
|
||||
Button, ButtonContent, ButtonMsg,
|
||||
},
|
||||
theme,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const MAX_LENGTH: usize = 8;
|
||||
|
||||
pub struct Bip39Input {
|
||||
button: Button<&'static [u8]>,
|
||||
textbox: TextBox<MAX_LENGTH>,
|
||||
multi_tap: MultiTapKeyboard,
|
||||
suggested_word: Option<&'static str>,
|
||||
}
|
||||
|
||||
impl MnemonicInput for Bip39Input {
|
||||
fn new(area: Rect) -> Self {
|
||||
Self {
|
||||
button: Button::empty(area),
|
||||
textbox: TextBox::empty(),
|
||||
multi_tap: MultiTapKeyboard::new(),
|
||||
suggested_word: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the key set. Keys are further specified as indices into this
|
||||
/// array.
|
||||
fn keys() -> [&'static str; MNEMONIC_KEY_COUNT] {
|
||||
["abc", "def", "ghi", "jkl", "mno", "pqr", "stu", "vwx", "yz"]
|
||||
}
|
||||
|
||||
/// Returns `true` if given key index can continue towards a valid mnemonic
|
||||
/// word, `false` otherwise.
|
||||
fn can_key_press_lead_to_a_valid_word(&self, key: usize) -> bool {
|
||||
// Currently pending key is always enabled.
|
||||
let key_is_pending = self.multi_tap.pending_key() == Some(key);
|
||||
// Keys that contain letters from the completion mask are enabled as well.
|
||||
let key_matches_mask =
|
||||
bip39::word_completion_mask(self.textbox.content()) & Self::key_mask(key) != 0;
|
||||
key_is_pending || key_matches_mask
|
||||
}
|
||||
|
||||
/// Key button was clicked. If this button is pending, let's cycle the
|
||||
/// pending character in textbox. If not, let's just append the first
|
||||
/// character.
|
||||
fn on_key_click(&mut self, ctx: &mut EventCtx, key: usize) {
|
||||
let edit = self.multi_tap.click_key(ctx, key, Self::keys()[key]);
|
||||
self.textbox.apply(ctx, edit);
|
||||
self.complete_word_from_dictionary(ctx);
|
||||
}
|
||||
|
||||
/// Backspace button was clicked, let's delete the last character of input
|
||||
/// and clear the pending marker.
|
||||
fn on_backspace_click(&mut self, ctx: &mut EventCtx) {
|
||||
self.multi_tap.clear_pending_state(ctx);
|
||||
self.textbox.delete_last(ctx);
|
||||
self.complete_word_from_dictionary(ctx);
|
||||
}
|
||||
|
||||
fn is_empty(&self) -> bool {
|
||||
self.textbox.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for Bip39Input {
|
||||
type Msg = MnemonicInputMsg;
|
||||
|
||||
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||
if self.multi_tap.is_timeout_event(event) {
|
||||
self.on_timeout(ctx)
|
||||
} else if let Some(ButtonMsg::Clicked) = self.button.event(ctx, event) {
|
||||
self.on_input_click(ctx)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn paint(&mut self) {
|
||||
let area = self.button.area();
|
||||
let style = self.button.style();
|
||||
|
||||
// First, paint the button background.
|
||||
self.button.paint_background(&style);
|
||||
|
||||
// Paint the entered content (the prefix of the suggested word).
|
||||
let text = self.textbox.content().as_bytes();
|
||||
let width = style.font.text_width(text);
|
||||
// Content starts in the left-center point, offset by 16px to the right and 8px
|
||||
// to the bottom.
|
||||
let text_baseline = area.top_left().center(area.bottom_left()) + Offset::new(16, 8);
|
||||
display::text(
|
||||
text_baseline,
|
||||
text,
|
||||
style.font,
|
||||
style.text_color,
|
||||
style.button_color,
|
||||
);
|
||||
|
||||
// Paint the rest of the suggested dictionary word.
|
||||
if let Some(word) = self.suggested_word.and_then(|w| w.get(text.len()..)) {
|
||||
let word_baseline = text_baseline + Offset::new(width, 0);
|
||||
display::text(
|
||||
word_baseline,
|
||||
word.as_bytes(),
|
||||
style.font,
|
||||
theme::GREY_LIGHT,
|
||||
style.button_color,
|
||||
);
|
||||
}
|
||||
|
||||
// Paint the pending marker.
|
||||
if self.multi_tap.pending_key().is_some() {
|
||||
// Measure the width of the last character of input.
|
||||
if let Some(last) = text.last().copied() {
|
||||
let last_width = style.font.text_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));
|
||||
display::rect_fill(marker_rect, style.text_color);
|
||||
}
|
||||
}
|
||||
|
||||
// Paint the icon.
|
||||
if let ButtonContent::Icon(icon) = self.button.content() {
|
||||
// Icon is painted in the right-center point, of expected size 16x16 pixels, and
|
||||
// 16px from the right edge.
|
||||
let icon_center = area.top_right().center(area.bottom_right()) - Offset::new(16 + 8, 0);
|
||||
display::icon(icon_center, icon, style.text_color, style.button_color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Bip39Input {
|
||||
/// Compute a bitmask of all letters contained in given key text. Lowest bit
|
||||
/// is 'a', second lowest 'b', etc.
|
||||
fn key_mask(key: usize) -> u32 {
|
||||
let mut mask = 0;
|
||||
for ch in Self::keys()[key].as_bytes() {
|
||||
// We assume the key text is lower-case alphabetic ASCII, making the subtraction
|
||||
// and the shift panic-free.
|
||||
mask |= 1 << (ch - b'a');
|
||||
}
|
||||
mask
|
||||
}
|
||||
|
||||
/// Input button was clicked. If the content matches the suggested word,
|
||||
/// let's confirm it, otherwise just auto-complete.
|
||||
fn on_input_click(&mut self, ctx: &mut EventCtx) -> Option<MnemonicInputMsg> {
|
||||
match self.suggested_word {
|
||||
Some(word) if word == self.textbox.content() => {
|
||||
self.textbox.clear(ctx);
|
||||
Some(MnemonicInputMsg::Confirmed)
|
||||
}
|
||||
Some(word) => {
|
||||
self.textbox.replace(ctx, word);
|
||||
Some(MnemonicInputMsg::Completed)
|
||||
}
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Timeout occurred. If we can auto-complete current input, let's just
|
||||
/// reset the pending marker. If not, input is invalid, let's backspace the
|
||||
/// last character.
|
||||
fn on_timeout(&mut self, ctx: &mut EventCtx) -> Option<MnemonicInputMsg> {
|
||||
self.multi_tap.clear_pending_state(ctx);
|
||||
if self.suggested_word.is_none() {
|
||||
self.textbox.delete_last(ctx);
|
||||
self.complete_word_from_dictionary(ctx);
|
||||
}
|
||||
Some(MnemonicInputMsg::TimedOut)
|
||||
}
|
||||
|
||||
fn complete_word_from_dictionary(&mut self, ctx: &mut EventCtx) {
|
||||
self.suggested_word = bip39::complete_word(self.textbox.content());
|
||||
|
||||
// Change the style of the button depending on the completed word.
|
||||
if let Some(word) = self.suggested_word {
|
||||
if self.textbox.content() == word {
|
||||
// Confirm button.
|
||||
self.button.enable(ctx);
|
||||
self.button.set_stylesheet(ctx, theme::button_confirm());
|
||||
self.button
|
||||
.set_content(ctx, ButtonContent::Icon(theme::ICON_CONFIRM));
|
||||
} else {
|
||||
// Auto-complete button.
|
||||
self.button.enable(ctx);
|
||||
self.button.set_stylesheet(ctx, theme::button_default());
|
||||
self.button
|
||||
.set_content(ctx, ButtonContent::Icon(theme::ICON_CLICK));
|
||||
}
|
||||
} else {
|
||||
// Disabled button.
|
||||
self.button.disable(ctx);
|
||||
self.button.set_stylesheet(ctx, theme::button_default());
|
||||
self.button.set_content(ctx, ButtonContent::Text(b""));
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,214 @@
|
||||
use heapless::String;
|
||||
|
||||
use crate::util::ResultExt;
|
||||
use crate::{
|
||||
time::Duration,
|
||||
ui::component::{Event, EventCtx, TimerToken},
|
||||
};
|
||||
|
||||
/// 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>,
|
||||
}
|
||||
|
||||
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,
|
||||
/// Timer for clearing the pending state.
|
||||
timer: TimerToken,
|
||||
}
|
||||
|
||||
impl MultiTapKeyboard {
|
||||
/// Create a new, empty, multi-tap state.
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
timeout: Duration::from_secs(1),
|
||||
pending: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// 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)
|
||||
}
|
||||
|
||||
/// Return the token for the currently pending timer.
|
||||
pub fn pending_timer(&self) -> Option<TimerToken> {
|
||||
self.pending.as_ref().map(|p| p.timer)
|
||||
}
|
||||
|
||||
/// Returns `true` if `event` is an `Event::Timer` for the currently pending
|
||||
/// timer.
|
||||
pub fn is_timeout_event(&self, event: Event) -> bool {
|
||||
matches!((event, self.pending_timer()), (Event::Timer(t), Some(pt)) if pt == t)
|
||||
}
|
||||
|
||||
/// 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) {
|
||||
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 {
|
||||
// To simplify things, we assume the key text is ASCII-only.
|
||||
let ascii_text = key_text.as_bytes();
|
||||
|
||||
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 deadline.
|
||||
//
|
||||
// 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 ascii_text.len() > 1 {
|
||||
Some(Pending {
|
||||
key,
|
||||
press,
|
||||
timer: ctx.request_timer(self.timeout),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let ch = ascii_text[press % ascii_text.len()] as char;
|
||||
if is_pending {
|
||||
TextEdit::ReplaceLast(ch)
|
||||
} else {
|
||||
TextEdit::Append(ch)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Reified editing operations of `TextBox`.
|
||||
///
|
||||
/// Note: This does not contain all supported editing operations, only the ones
|
||||
/// we currently use.
|
||||
pub enum TextEdit {
|
||||
ReplaceLast(char),
|
||||
Append(char),
|
||||
}
|
||||
|
||||
/// Wraps a character buffer of maximum length `L` and provides text editing
|
||||
/// operations over it. Text ops usually take a `EventCtx` to request a paint
|
||||
/// pass in case of any state modification.
|
||||
pub struct TextBox<const L: usize> {
|
||||
text: String<L>,
|
||||
}
|
||||
|
||||
impl<const L: usize> TextBox<L> {
|
||||
/// Create a new `TextBox` with content `text`.
|
||||
pub fn new(text: String<L>) -> Self {
|
||||
Self { text }
|
||||
}
|
||||
|
||||
/// Create an empty `TextBox`.
|
||||
pub fn empty() -> Self {
|
||||
Self::new(String::new())
|
||||
}
|
||||
|
||||
pub fn content(&self) -> &str {
|
||||
&self.text
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.text.is_empty()
|
||||
}
|
||||
|
||||
/// Delete the last character of content, if any.
|
||||
pub fn delete_last(&mut self, ctx: &mut EventCtx) {
|
||||
let changed = self.text.pop().is_some();
|
||||
if changed {
|
||||
ctx.request_paint();
|
||||
}
|
||||
}
|
||||
|
||||
/// Replaces the last character of the content with `ch`. If the content is
|
||||
/// empty, `ch` is appended.
|
||||
pub fn replace_last(&mut self, ctx: &mut EventCtx, ch: char) {
|
||||
let previous = self.text.pop();
|
||||
self.text
|
||||
.push(ch)
|
||||
.assert_if_debugging_ui("TextBox has zero capacity");
|
||||
let changed = previous != Some(ch);
|
||||
if changed {
|
||||
ctx.request_paint();
|
||||
}
|
||||
}
|
||||
|
||||
/// Append `ch` at the end of the content.
|
||||
pub fn append(&mut self, ctx: &mut EventCtx, ch: char) {
|
||||
self.text.push(ch).assert_if_debugging_ui("TextBox is full");
|
||||
ctx.request_paint();
|
||||
}
|
||||
|
||||
/// Replace the textbox content with `text`.
|
||||
pub fn replace(&mut self, ctx: &mut EventCtx, text: &str) {
|
||||
if self.text != text {
|
||||
self.text.clear();
|
||||
self.text
|
||||
.push_str(text)
|
||||
.assert_if_debugging_ui("TextBox is full");
|
||||
ctx.request_paint();
|
||||
}
|
||||
}
|
||||
|
||||
/// Clear the textbox content.
|
||||
pub fn clear(&mut self, ctx: &mut EventCtx) {
|
||||
self.replace(ctx, "");
|
||||
}
|
||||
|
||||
/// Apply a editing operation to the text buffer.
|
||||
pub fn apply(&mut self, ctx: &mut EventCtx, edit: TextEdit) {
|
||||
match edit {
|
||||
TextEdit::ReplaceLast(char) => self.replace_last(ctx, char),
|
||||
TextEdit::Append(char) => self.append(ctx, char),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Analogue to `[T]::enumerate().map(...)`.
|
||||
pub fn array_map_enumerate<T, U, const L: usize>(
|
||||
array: [T; L],
|
||||
mut func: impl FnMut(usize, T) -> U,
|
||||
) -> [U; L] {
|
||||
let mut i = 0;
|
||||
array.map(|t| {
|
||||
let u = func(i, t);
|
||||
i += 1;
|
||||
u
|
||||
})
|
||||
}
|
@ -0,0 +1,160 @@
|
||||
use crate::ui::{
|
||||
component::{Child, Component, Event, EventCtx, Label, Maybe},
|
||||
geometry::{Grid, Rect},
|
||||
model_tt::{
|
||||
component::{keyboard::common::array_map_enumerate, Button, ButtonMsg},
|
||||
theme,
|
||||
},
|
||||
};
|
||||
|
||||
pub const MNEMONIC_KEY_COUNT: usize = 9;
|
||||
|
||||
pub enum MnemonicKeyboardMsg {
|
||||
Confirmed,
|
||||
}
|
||||
|
||||
pub struct MnemonicKeyboard<T> {
|
||||
/// Initial prompt, displayed on empty input.
|
||||
prompt: Child<Maybe<Label<&'static [u8]>>>,
|
||||
/// Backspace button.
|
||||
back: Child<Maybe<Button<&'static [u8]>>>,
|
||||
/// Input area, acting as the auto-complete and confirm button.
|
||||
input: Child<Maybe<T>>,
|
||||
/// Key buttons.
|
||||
keys: [Child<Button<&'static [u8]>>; MNEMONIC_KEY_COUNT],
|
||||
}
|
||||
|
||||
impl<T> MnemonicKeyboard<T>
|
||||
where
|
||||
T: MnemonicInput,
|
||||
{
|
||||
pub fn new(area: Rect, prompt: &'static [u8]) -> Self {
|
||||
let grid = Grid::new(area, 3, 4);
|
||||
let back_area = grid.row_col(0, 0);
|
||||
let input_area = grid.row_col(0, 1).union(grid.row_col(0, 3));
|
||||
let prompt_area = grid.row_col(0, 0).union(grid.row_col(0, 3));
|
||||
let prompt_origin = prompt_area.top_left();
|
||||
|
||||
let input = T::new(input_area);
|
||||
let keys = T::keys();
|
||||
|
||||
Self {
|
||||
prompt: Child::new(Maybe::visible(
|
||||
prompt_area,
|
||||
theme::BG,
|
||||
Label::left_aligned(prompt_origin, prompt, theme::label_default()),
|
||||
)),
|
||||
back: Child::new(Maybe::hidden(
|
||||
back_area,
|
||||
theme::BG,
|
||||
Button::with_icon(back_area, theme::ICON_BACK).styled(theme::button_clear()),
|
||||
)),
|
||||
input: Child::new(Maybe::hidden(input_area, theme::BG, input)),
|
||||
keys: Self::key_buttons(keys, &grid, grid.cols), // Start in the second row.
|
||||
}
|
||||
}
|
||||
|
||||
fn key_buttons(
|
||||
keys: [&'static str; MNEMONIC_KEY_COUNT],
|
||||
grid: &Grid,
|
||||
offset: usize,
|
||||
) -> [Child<Button<&'static [u8]>>; MNEMONIC_KEY_COUNT] {
|
||||
array_map_enumerate(keys, |index, text| {
|
||||
Child::new(Button::with_text(
|
||||
grid.cell(offset + index),
|
||||
text.as_bytes(),
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
fn on_input_change(&mut self, ctx: &mut EventCtx) {
|
||||
self.toggle_key_buttons(ctx);
|
||||
self.toggle_prompt_or_input(ctx);
|
||||
}
|
||||
|
||||
/// Either enable or disable the key buttons, depending on the dictionary
|
||||
/// completion mask and the pending key.
|
||||
fn toggle_key_buttons(&mut self, ctx: &mut EventCtx) {
|
||||
for (key, btn) in self.keys.iter_mut().enumerate() {
|
||||
let enabled = self
|
||||
.input
|
||||
.inner()
|
||||
.inner()
|
||||
.can_key_press_lead_to_a_valid_word(key);
|
||||
btn.mutate(ctx, |ctx, b| b.enabled(ctx, enabled));
|
||||
}
|
||||
}
|
||||
|
||||
/// After edit operations, we need to either show or hide the prompt, the
|
||||
/// input, and the back button.
|
||||
fn toggle_prompt_or_input(&mut self, ctx: &mut EventCtx) {
|
||||
let prompt_visible = self.input.inner().inner().is_empty();
|
||||
self.prompt
|
||||
.mutate(ctx, |ctx, p| p.show_if(ctx, prompt_visible));
|
||||
self.input
|
||||
.mutate(ctx, |ctx, i| i.show_if(ctx, !prompt_visible));
|
||||
self.back
|
||||
.mutate(ctx, |ctx, b| b.show_if(ctx, !prompt_visible));
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Component for MnemonicKeyboard<T>
|
||||
where
|
||||
T: MnemonicInput,
|
||||
{
|
||||
type Msg = MnemonicKeyboardMsg;
|
||||
|
||||
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||
match self.input.event(ctx, event) {
|
||||
Some(MnemonicInputMsg::Confirmed) => {
|
||||
// Confirmed, bubble up.
|
||||
return Some(MnemonicKeyboardMsg::Confirmed);
|
||||
}
|
||||
Some(_) => {
|
||||
// Either a timeout or a completion.
|
||||
self.on_input_change(ctx);
|
||||
return None;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
if let Some(ButtonMsg::Clicked) = self.back.event(ctx, event) {
|
||||
self.input
|
||||
.mutate(ctx, |ctx, i| i.inner_mut().on_backspace_click(ctx));
|
||||
self.on_input_change(ctx);
|
||||
return None;
|
||||
}
|
||||
for (key, btn) in self.keys.iter_mut().enumerate() {
|
||||
if let Some(ButtonMsg::Clicked) = btn.event(ctx, event) {
|
||||
self.input
|
||||
.mutate(ctx, |ctx, i| i.inner_mut().on_key_click(ctx, key));
|
||||
self.on_input_change(ctx);
|
||||
return None;
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn paint(&mut self) {
|
||||
self.prompt.paint();
|
||||
self.input.paint();
|
||||
self.back.paint();
|
||||
for btn in &mut self.keys {
|
||||
btn.paint();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait MnemonicInput: Component<Msg = MnemonicInputMsg> {
|
||||
fn new(area: Rect) -> Self;
|
||||
fn keys() -> [&'static str; MNEMONIC_KEY_COUNT];
|
||||
fn can_key_press_lead_to_a_valid_word(&self, key: usize) -> bool;
|
||||
fn on_key_click(&mut self, ctx: &mut EventCtx, key: usize);
|
||||
fn on_backspace_click(&mut self, ctx: &mut EventCtx);
|
||||
fn is_empty(&self) -> bool;
|
||||
}
|
||||
|
||||
pub enum MnemonicInputMsg {
|
||||
Confirmed,
|
||||
Completed,
|
||||
TimedOut,
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
pub mod bip39;
|
||||
pub mod mnemonic;
|
||||
pub mod passphrase;
|
||||
pub mod pin;
|
||||
pub mod slip39;
|
||||
|
||||
mod common;
|
@ -0,0 +1,242 @@
|
||||
use core::iter;
|
||||
|
||||
use heapless::String;
|
||||
|
||||
use crate::{
|
||||
trezorhal::slip39,
|
||||
ui::{
|
||||
component::{Component, Event, EventCtx},
|
||||
display,
|
||||
geometry::{Offset, Rect},
|
||||
model_tt::{
|
||||
component::{
|
||||
keyboard::{
|
||||
common::{MultiTapKeyboard, TextBox, TextEdit},
|
||||
mnemonic::{MnemonicInput, MnemonicInputMsg, MNEMONIC_KEY_COUNT},
|
||||
},
|
||||
Button, ButtonContent, ButtonMsg,
|
||||
},
|
||||
theme,
|
||||
},
|
||||
},
|
||||
util::ResultExt,
|
||||
};
|
||||
|
||||
const MAX_LENGTH: usize = 8;
|
||||
|
||||
pub struct Slip39Input {
|
||||
button: Button<&'static [u8]>,
|
||||
textbox: TextBox<MAX_LENGTH>,
|
||||
multi_tap: MultiTapKeyboard,
|
||||
final_word: Option<&'static str>,
|
||||
input_mask: Slip39Mask,
|
||||
}
|
||||
|
||||
impl MnemonicInput for Slip39Input {
|
||||
fn new(area: Rect) -> Self {
|
||||
Self {
|
||||
button: Button::empty(area),
|
||||
textbox: TextBox::empty(),
|
||||
multi_tap: MultiTapKeyboard::new(),
|
||||
final_word: None,
|
||||
input_mask: Slip39Mask::full(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the key set. Keys are further specified as indices into this
|
||||
/// array.
|
||||
fn keys() -> [&'static str; MNEMONIC_KEY_COUNT] {
|
||||
["ab", "cd", "ef", "ghij", "klm", "nopq", "rs", "tuv", "xyz"]
|
||||
}
|
||||
|
||||
/// Returns `true` if given key index can continue towards a valid mnemonic
|
||||
/// word, `false` otherwise.
|
||||
fn can_key_press_lead_to_a_valid_word(&self, key: usize) -> bool {
|
||||
if self.input_mask.is_final() {
|
||||
false
|
||||
} else {
|
||||
// Currently pending key is always enabled.
|
||||
// Keys that mach the completion mask are enabled as well.
|
||||
self.multi_tap.pending_key() == Some(key) || self.input_mask.contains_key(key)
|
||||
}
|
||||
}
|
||||
|
||||
/// Key button was clicked. If this button is pending, let's cycle the
|
||||
/// pending character in textbox. If not, let's just append the first
|
||||
/// character.
|
||||
fn on_key_click(&mut self, ctx: &mut EventCtx, key: usize) {
|
||||
let edit = self.multi_tap.click_key(ctx, key, Self::keys()[key]);
|
||||
if let TextEdit::Append(_) = edit {
|
||||
// This key press wasn't just a pending key rotation, so let's push the key
|
||||
// digit to the buffer.
|
||||
self.textbox.append(ctx, Self::key_digit(key));
|
||||
} else {
|
||||
// Ignore the pending char rotation. We use the pending key to paint
|
||||
// the last character, but the mnemonic word computation depends
|
||||
// only on the pressed key, not on the specific character inside it.
|
||||
}
|
||||
self.complete_word_from_dictionary(ctx);
|
||||
}
|
||||
|
||||
/// Backspace button was clicked, let's delete the last character of input
|
||||
/// and clear the pending marker.
|
||||
fn on_backspace_click(&mut self, ctx: &mut EventCtx) {
|
||||
self.multi_tap.clear_pending_state(ctx);
|
||||
self.textbox.delete_last(ctx);
|
||||
self.complete_word_from_dictionary(ctx);
|
||||
}
|
||||
|
||||
fn is_empty(&self) -> bool {
|
||||
self.textbox.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for Slip39Input {
|
||||
type Msg = MnemonicInputMsg;
|
||||
|
||||
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||
if self.multi_tap.is_timeout_event(event) {
|
||||
// Timeout occurred. Reset the pending key.
|
||||
self.multi_tap.clear_pending_state(ctx);
|
||||
return Some(MnemonicInputMsg::TimedOut);
|
||||
}
|
||||
if let Some(ButtonMsg::Clicked) = self.button.event(ctx, event) {
|
||||
// Input button was clicked. If the whole word is totally identified, let's
|
||||
// confirm it, otherwise don't do anything.
|
||||
if self.input_mask.is_final() {
|
||||
return Some(MnemonicInputMsg::Confirmed);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn paint(&mut self) {
|
||||
let area = self.button.area();
|
||||
let style = self.button.style();
|
||||
|
||||
// First, paint the button background.
|
||||
self.button.paint_background(&style);
|
||||
|
||||
// Content starts in the left-center point, offset by 16px to the right and 8px
|
||||
// to the bottom.
|
||||
let text_baseline = area.top_left().center(area.bottom_left()) + Offset::new(16, 8);
|
||||
|
||||
// To simplify things, we always copy the printed string here, even if it
|
||||
// wouldn't be strictly necessary.
|
||||
let mut text: String<MAX_LENGTH> = String::new();
|
||||
|
||||
if let Some(word) = self.final_word {
|
||||
// We're done with input, paint the full word.
|
||||
text.push_str(word)
|
||||
.assert_if_debugging_ui("Text buffer is too small");
|
||||
} else {
|
||||
// Paint an asterisk for each letter of input.
|
||||
for ch in iter::repeat('*').take(self.textbox.content().len()) {
|
||||
text.push(ch)
|
||||
.assert_if_debugging_ui("Text buffer is too small");
|
||||
}
|
||||
// If we're in the pending state, paint the pending character at the end.
|
||||
if let (Some(key), Some(press)) =
|
||||
(self.multi_tap.pending_key(), self.multi_tap.pending_press())
|
||||
{
|
||||
let ascii_text = Self::keys()[key].as_bytes();
|
||||
let ch = ascii_text[press % ascii_text.len()] as char;
|
||||
text.pop();
|
||||
text.push(ch)
|
||||
.assert_if_debugging_ui("Text buffer is too small");
|
||||
}
|
||||
}
|
||||
display::text(
|
||||
text_baseline,
|
||||
text.as_bytes(),
|
||||
style.font,
|
||||
style.text_color,
|
||||
style.button_color,
|
||||
);
|
||||
let width = style.font.text_width(text.as_bytes());
|
||||
|
||||
// Paint the pending marker.
|
||||
if self.multi_tap.pending_key().is_some() && self.final_word.is_none() {
|
||||
// Measure the width of the last character of input.
|
||||
if let Some(last) = text.as_bytes().last().copied() {
|
||||
let last_width = style.font.text_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));
|
||||
display::rect_fill(marker_rect, style.text_color);
|
||||
}
|
||||
}
|
||||
|
||||
// Paint the icon.
|
||||
if let ButtonContent::Icon(icon) = self.button.content() {
|
||||
// Icon is painted in the right-center point, of expected size 16x16 pixels, and
|
||||
// 16px from the right edge.
|
||||
let icon_center = area.top_right().center(area.bottom_right()) - Offset::new(16 + 8, 0);
|
||||
display::icon(icon_center, icon, style.text_color, style.button_color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Slip39Input {
|
||||
/// Convert a key index into the key digit. This is what we push into the
|
||||
/// input buffer.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// Self::key_digit(0) == '1';
|
||||
/// Self::key_digit(1) == '2';
|
||||
/// ```
|
||||
fn key_digit(key: usize) -> char {
|
||||
let index = key + 1;
|
||||
char::from_digit(index as u32, 10).unwrap()
|
||||
}
|
||||
|
||||
fn complete_word_from_dictionary(&mut self, ctx: &mut EventCtx) {
|
||||
let sequence = self.input_sequence();
|
||||
self.final_word = sequence.and_then(slip39::button_sequence_to_word);
|
||||
self.input_mask = sequence
|
||||
.and_then(slip39::word_completion_mask)
|
||||
.map(Slip39Mask)
|
||||
.unwrap_or_else(Slip39Mask::full);
|
||||
|
||||
// Change the style of the button depending on the input.
|
||||
if self.final_word.is_some() {
|
||||
// Confirm button.
|
||||
self.button.enable(ctx);
|
||||
self.button.set_stylesheet(ctx, theme::button_confirm());
|
||||
self.button
|
||||
.set_content(ctx, ButtonContent::Icon(theme::ICON_CONFIRM));
|
||||
} else {
|
||||
// Disabled button.
|
||||
self.button.disable(ctx);
|
||||
self.button.set_stylesheet(ctx, theme::button_default());
|
||||
self.button.set_content(ctx, ButtonContent::Text(b""));
|
||||
}
|
||||
}
|
||||
|
||||
fn input_sequence(&self) -> Option<u16> {
|
||||
self.textbox.content().parse().ok()
|
||||
}
|
||||
}
|
||||
|
||||
struct Slip39Mask(u16);
|
||||
|
||||
impl Slip39Mask {
|
||||
/// Return a mask with all keys allowed.
|
||||
fn full() -> Self {
|
||||
Self(0x1FF) // All buttons are allowed. 9-bit bitmap all set to 1.
|
||||
}
|
||||
|
||||
/// Returns `true` if `key` can lead to a valid SLIP39 word with this mask.
|
||||
fn contains_key(&self, key: usize) -> bool {
|
||||
self.0 & (1 << key) != 0
|
||||
}
|
||||
|
||||
/// Returns `true` if mask has exactly one bit set to 1, or is equal to 0.
|
||||
fn is_final(&self) -> bool {
|
||||
self.0.count_ones() <= 1
|
||||
}
|
||||
}
|
Binary file not shown.
Binary file not shown.
Loading…
Reference in new issue