chore(core/rust): Add mnemonic keyboards

pull/2133/head
Jan Pochyla 2 years ago committed by matejcik
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();
}
}
}

@ -4,6 +4,7 @@ pub mod base;
pub mod empty;
pub mod label;
pub mod map;
pub mod maybe;
pub mod pad;
pub mod paginated;
pub mod text;
@ -13,6 +14,7 @@ pub use base::{Child, Component, ComponentExt, Event, EventCtx, Never, TimerToke
pub use empty::Empty;
pub use label::{Label, LabelStyle};
pub use map::Map;
pub use maybe::Maybe;
pub use pad::Pad;
pub use paginated::{PageMsg, Paginate};
pub use text::{

@ -188,6 +188,15 @@ impl Rect {
point.x >= self.x0 && point.x < self.x1 && point.y >= self.y0 && point.y < self.y1
}
pub fn union(&self, other: Self) -> Self {
Self {
x0: self.x0.min(other.x0),
y0: self.y0.min(other.y0),
x1: self.x1.max(other.x1),
y1: self.y1.max(other.y1),
}
}
pub fn inset(&self, insets: Insets) -> Self {
Self {
x0: self.x0 + insets.left,

@ -1,10 +1,11 @@
use super::{event::TouchEvent, theme};
use crate::ui::{
component::{Component, Event, EventCtx, Map},
display::{self, Color, Font},
geometry::{Grid, Insets, Offset, Rect},
};
use super::{event::TouchEvent, theme};
pub enum ButtonMsg {
Pressed,
Released,
@ -43,6 +44,10 @@ impl<T> Button<T> {
Self::new(area, ButtonContent::Icon(image))
}
pub fn empty(area: Rect) -> Self {
Self::new(area, ButtonContent::Empty)
}
pub fn styled(mut self, styles: ButtonStyleSheet) -> Self {
self.styles = styles;
self
@ -75,11 +80,28 @@ impl<T> Button<T> {
matches!(self.state, State::Disabled)
}
pub fn set_content(&mut self, ctx: &mut EventCtx, content: ButtonContent<T>)
where
T: PartialEq,
{
if self.content != content {
self.content = content;
ctx.request_paint();
}
}
pub fn content(&self) -> &ButtonContent<T> {
&self.content
}
fn style(&self) -> &ButtonStyle {
pub fn set_stylesheet(&mut self, ctx: &mut EventCtx, styles: ButtonStyleSheet) {
if self.styles != styles {
self.styles = styles;
ctx.request_paint();
}
}
pub fn style(&self) -> &ButtonStyle {
match self.state {
State::Initial | State::Released => self.styles.normal,
State::Pressed => self.styles.active,
@ -87,12 +109,75 @@ impl<T> Button<T> {
}
}
pub fn area(&self) -> Rect {
self.area
}
fn set(&mut self, ctx: &mut EventCtx, state: State) {
if self.state != state {
self.state = state;
ctx.request_paint();
}
}
pub fn paint_background(&self, style: &ButtonStyle) {
if style.border_width > 0 {
// Paint the border and a smaller background on top of it.
display::rect_fill_rounded(
self.area,
style.border_color,
style.background_color,
style.border_radius,
);
display::rect_fill_rounded(
self.area.inset(Insets::uniform(style.border_width)),
style.button_color,
style.border_color,
style.border_radius,
);
} else {
// We do not need to draw an explicit border in this case, just a
// bigger background.
display::rect_fill_rounded(
self.area,
style.button_color,
style.background_color,
style.border_radius,
);
}
}
pub fn paint_content(&self, style: &ButtonStyle)
where
T: AsRef<[u8]>,
{
match &self.content {
ButtonContent::Empty => {}
ButtonContent::Text(text) => {
let text = text.as_ref();
let width = style.font.text_width(text);
let height = style.font.text_height();
let start_of_baseline = self.area.center()
+ Offset::new(-width / 2, height / 2)
+ Offset::y(Self::BASELINE_OFFSET);
display::text(
start_of_baseline,
text,
style.font,
style.text_color,
style.button_color,
);
}
ButtonContent::Icon(icon) => {
display::icon(
self.area.center(),
icon,
style.text_color,
style.button_color,
);
}
}
}
}
impl<T> Component for Button<T>
@ -157,61 +242,8 @@ where
fn paint(&mut self) {
let style = self.style();
if style.border_width > 0 {
// Paint the border and a smaller background on top of it.
display::rect_fill_rounded(
self.area,
style.border_color,
style.background_color,
style.border_radius,
);
display::rect_fill_rounded(
self.area.inset(Insets::uniform(style.border_width)),
style.button_color,
style.border_color,
style.border_radius,
);
} else {
// We do not need to draw an explicit border in this case, just a
// bigger background.
display::rect_fill_rounded(
self.area,
style.button_color,
style.background_color,
style.border_radius,
);
}
match &self.content {
ButtonContent::Text(text) => {
let text = text.as_ref();
let width = style.font.text_width(text);
let height = style.font.text_height();
let start_of_baseline = self.area.center()
+ Offset::new(-width / 2, height / 2)
+ Offset::y(Self::BASELINE_OFFSET);
display::text(
start_of_baseline,
text,
style.font,
style.text_color,
style.button_color,
);
}
ButtonContent::Icon(icon) => {
display::icon(
self.area.center(),
icon,
style.text_color,
style.button_color,
);
}
}
}
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
sink(self.area)
self.paint_background(&style);
self.paint_content(&style);
}
}
@ -223,6 +255,7 @@ where
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.open("Button");
match &self.content {
ButtonContent::Empty => {}
ButtonContent::Text(text) => t.field("text", text),
ButtonContent::Icon(_) => t.symbol("icon"),
}
@ -238,17 +271,21 @@ enum State {
Disabled,
}
#[derive(PartialEq, Eq)]
pub enum ButtonContent<T> {
Empty,
Text(T),
Icon(&'static [u8]),
}
#[derive(PartialEq, Eq)]
pub struct ButtonStyleSheet {
pub normal: &'static ButtonStyle,
pub active: &'static ButtonStyle,
pub disabled: &'static ButtonStyle,
}
#[derive(PartialEq, Eq)]
pub struct ButtonStyle {
pub font: Font,
pub text_color: Color,

@ -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;

@ -6,15 +6,14 @@ use crate::{
component::{base::ComponentExt, Child, Component, Event, EventCtx, Never, TimerToken},
display,
geometry::{Grid, Rect},
model_tt::component::{
button::{Button, ButtonContent, ButtonMsg::Clicked},
swipe::{Swipe, SwipeDirection},
theme,
},
},
};
use super::{
button::{Button, ButtonContent, ButtonMsg::Clicked},
swipe::{Swipe, SwipeDirection},
theme,
};
pub enum PassphraseKeyboardMsg {
Confirmed,
Cancelled,

@ -10,15 +10,14 @@ use crate::{
},
display,
geometry::{Grid, Offset, Point, Rect},
model_tt::component::{
button::{Button, ButtonContent, ButtonMsg::Clicked},
theme,
},
},
};
use super::{
button::{Button, ButtonContent, ButtonMsg::Clicked},
theme,
};
pub enum PinDialogMsg {
pub enum PinKeyboardMsg {
Confirmed,
Cancelled,
}
@ -26,7 +25,7 @@ pub enum PinDialogMsg {
const MAX_LENGTH: usize = 9;
const DIGIT_COUNT: usize = 10; // 0..10
pub struct PinDialog {
pub struct PinKeyboard {
digits: Vec<u8, MAX_LENGTH>,
major_prompt: Label<&'static [u8]>,
minor_prompt: Label<&'static [u8]>,
@ -37,7 +36,7 @@ pub struct PinDialog {
digit_btns: [Child<Button<&'static str>>; DIGIT_COUNT],
}
impl PinDialog {
impl PinKeyboard {
pub fn new(area: Rect, major_prompt: &'static [u8], minor_prompt: &'static [u8]) -> Self {
let digits = Vec::new();
@ -141,15 +140,15 @@ impl PinDialog {
}
}
impl Component for PinDialog {
type Msg = PinDialogMsg;
impl Component for PinKeyboard {
type Msg = PinKeyboardMsg;
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
if let Some(Clicked) = self.confirm_btn.event(ctx, event) {
return Some(PinDialogMsg::Confirmed);
return Some(PinKeyboardMsg::Confirmed);
}
if let Some(Clicked) = self.cancel_btn.event(ctx, event) {
return Some(PinDialogMsg::Cancelled);
return Some(PinKeyboardMsg::Cancelled);
}
if let Some(Clicked) = self.reset_btn.event(ctx, event) {
self.digits.clear();

@ -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
}
}

@ -2,10 +2,9 @@ mod button;
mod confirm;
mod dialog;
mod frame;
mod keyboard;
mod loader;
mod page;
mod passphrase;
mod pin;
mod swipe;
pub use button::{Button, ButtonContent, ButtonMsg, ButtonStyle, ButtonStyleSheet};

@ -41,6 +41,8 @@ pub const ICON_SIZE: i32 = 16;
pub const ICON_CANCEL: &[u8] = include_res!("model_tt/res/cancel.toif");
pub const ICON_CONFIRM: &[u8] = include_res!("model_tt/res/confirm.toif");
pub const ICON_SPACE: &[u8] = include_res!("model_tt/res/space.toif");
pub const ICON_BACK: &[u8] = include_res!("model_tt/res/left.toif");
pub const ICON_CLICK: &[u8] = include_res!("model_tt/res/click.toif");
pub fn label_default() -> LabelStyle {
LabelStyle {

@ -81,3 +81,16 @@ pub unsafe fn try_with_args_and_kwargs_inline(
};
unsafe { try_or_raise(block) }
}
pub trait ResultExt {
fn assert_if_debugging_ui(self, message: &str);
}
impl<T, E> ResultExt for Result<T, E> {
fn assert_if_debugging_ui(self, #[allow(unused)] message: &str) {
#[cfg(feature = "ui_debug")]
if self.is_err() {
panic!("{}", message);
}
}
}

Loading…
Cancel
Save