mirror of
https://github.com/trezor/trezor-firmware.git
synced 2024-11-22 23:48:12 +00:00
feat(core/ui): T3T1 recovery keyboards
[no changelog]
This commit is contained in:
parent
3f0ab537af
commit
e5e8e27abc
@ -2,16 +2,16 @@ use crate::{
|
|||||||
trezorhal::bip39,
|
trezorhal::bip39,
|
||||||
ui::{
|
ui::{
|
||||||
component::{text::common::TextBox, Component, Event, EventCtx},
|
component::{text::common::TextBox, Component, Event, EventCtx},
|
||||||
display,
|
geometry::{Alignment, Alignment2D, Offset, Point, Rect},
|
||||||
geometry::{Alignment2D, Offset, Rect},
|
|
||||||
model_mercury::{
|
model_mercury::{
|
||||||
component::{
|
component::{
|
||||||
keyboard::{
|
keyboard::{
|
||||||
common::{paint_pending_marker, render_pending_marker, MultiTapKeyboard},
|
common::{render_pending_marker, render_pill_shape, MultiTapKeyboard},
|
||||||
mnemonic::{MnemonicInput, MnemonicInputMsg, MNEMONIC_KEY_COUNT},
|
mnemonic::{MnemonicInput, MnemonicInputMsg, MNEMONIC_KEY_COUNT},
|
||||||
},
|
},
|
||||||
Button, ButtonContent, ButtonMsg,
|
Button, ButtonMsg,
|
||||||
},
|
},
|
||||||
|
constant::WIDTH,
|
||||||
theme,
|
theme,
|
||||||
},
|
},
|
||||||
shape,
|
shape,
|
||||||
@ -104,100 +104,66 @@ impl Component for Bip39Input {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn paint(&mut self) {
|
fn paint(&mut self) {
|
||||||
let area = self.button.area();
|
todo!("remove when ui-t3t1 done");
|
||||||
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();
|
|
||||||
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_left(
|
|
||||||
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);
|
|
||||||
let style = self.button_suggestion.style();
|
|
||||||
display::text_left(
|
|
||||||
word_baseline,
|
|
||||||
word,
|
|
||||||
style.font,
|
|
||||||
style.text_color,
|
|
||||||
style.button_color,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Paint the pending marker.
|
|
||||||
if self.multi_tap.pending_key().is_some() {
|
|
||||||
paint_pending_marker(text_baseline, text, style.font, 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);
|
|
||||||
icon.draw(
|
|
||||||
icon_center,
|
|
||||||
Alignment2D::CENTER,
|
|
||||||
style.text_color,
|
|
||||||
style.button_color,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||||
let area = self.button.area();
|
let area = self.button.area();
|
||||||
let style = self.button.style();
|
let style = self.button.style();
|
||||||
|
|
||||||
// First, paint the button background.
|
|
||||||
self.button.render_background(target, style);
|
|
||||||
|
|
||||||
// Paint the entered content (the prefix of the suggested word).
|
// Paint the entered content (the prefix of the suggested word).
|
||||||
let text = self.textbox.content();
|
let text = self.textbox.content();
|
||||||
let width = style.font.text_width(text);
|
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.
|
// User input together with suggestion is centered vertically in the input area
|
||||||
let text_baseline = area.top_left().center(area.bottom_left()) + Offset::new(16, 8);
|
// and centered horizontally on the screen
|
||||||
shape::Text::new(text_baseline, text)
|
let text_base_y = area.left_center().y + style.font.allcase_text_height() / 2;
|
||||||
|
let text_base_x = if let Some(word) = self.suggested_word {
|
||||||
|
style.font.horz_center(0, WIDTH, word)
|
||||||
|
} else {
|
||||||
|
style.font.horz_center(0, WIDTH, text)
|
||||||
|
};
|
||||||
|
let text_base = Point::new(text_base_x, text_base_y);
|
||||||
|
|
||||||
|
// Render pill-shaped button
|
||||||
|
if let Some(word) = self.suggested_word {
|
||||||
|
let choice_unambiguous = self.is_choice_unambiguous();
|
||||||
|
render_pill_shape(
|
||||||
|
target,
|
||||||
|
text_base,
|
||||||
|
word,
|
||||||
|
style,
|
||||||
|
if choice_unambiguous { Some(area) } else { None },
|
||||||
|
);
|
||||||
|
if choice_unambiguous {
|
||||||
|
// Icon is painted in the right-center point, 10px from the right edge.
|
||||||
|
let icon_right_center = area.right_center() - Offset::x(10);
|
||||||
|
shape::ToifImage::new(icon_right_center, theme::ICON_SIMPLE_CHECKMARK24.toif)
|
||||||
|
.with_align(Alignment2D::CENTER_RIGHT)
|
||||||
|
.with_fg(style.icon_color)
|
||||||
|
.render(target);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Render text input + suggested completion
|
||||||
|
shape::Text::new(text_base, text)
|
||||||
.with_font(style.font)
|
.with_font(style.font)
|
||||||
.with_fg(style.text_color)
|
.with_fg(style.text_color)
|
||||||
|
.with_align(Alignment::Start)
|
||||||
.render(target);
|
.render(target);
|
||||||
|
|
||||||
// Paint the rest of the suggested dictionary word.
|
|
||||||
if let Some(word) = self.suggested_word.and_then(|w| w.get(text.len()..)) {
|
if let Some(word) = self.suggested_word.and_then(|w| w.get(text.len()..)) {
|
||||||
let word_baseline = text_baseline + Offset::new(width, 0);
|
let word_baseline = text_base + Offset::x(width);
|
||||||
let style = self.button_suggestion.style();
|
let style = self.button_suggestion.style();
|
||||||
shape::Text::new(word_baseline, word)
|
shape::Text::new(word_baseline, word)
|
||||||
.with_font(style.font)
|
.with_font(style.font)
|
||||||
.with_fg(style.text_color)
|
.with_fg(style.text_color)
|
||||||
|
.with_align(Alignment::Start)
|
||||||
.render(target);
|
.render(target);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Paint the pending marker.
|
// Paint the pending marker.
|
||||||
if self.multi_tap.pending_key().is_some() {
|
if self.multi_tap.pending_key().is_some() {
|
||||||
render_pending_marker(target, text_baseline, text, style.font, style.text_color);
|
render_pending_marker(target, text_base, text, style.font, 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);
|
|
||||||
shape::ToifImage::new(icon_center, icon.toif)
|
|
||||||
.with_align(Alignment2D::CENTER)
|
|
||||||
.with_fg(style.icon_color)
|
|
||||||
.render(target);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -227,8 +193,7 @@ impl Bip39Input {
|
|||||||
|
|
||||||
// Styling the input to reflect already filled word
|
// Styling the input to reflect already filled word
|
||||||
Self {
|
Self {
|
||||||
button: Button::with_icon(theme::ICON_CONFIRM_INPUT)
|
button: Button::empty().styled(theme::button_recovery_confirm()),
|
||||||
.styled(theme::button_pin_confirm()),
|
|
||||||
textbox: TextBox::new(unwrap!(String::try_from(word))),
|
textbox: TextBox::new(unwrap!(String::try_from(word))),
|
||||||
multi_tap: MultiTapKeyboard::new(),
|
multi_tap: MultiTapKeyboard::new(),
|
||||||
options_num: bip39::options_num(word),
|
options_num: bip39::options_num(word),
|
||||||
@ -249,13 +214,18 @@ impl Bip39Input {
|
|||||||
mask
|
mask
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_choice_unambiguous(&self) -> bool {
|
||||||
|
if let (Some(word), Some(_num)) = (self.suggested_word, self.options_num) {
|
||||||
|
return word.eq(self.textbox.content());
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
/// Input button was clicked. If the content matches the suggested word,
|
/// Input button was clicked. If the content matches the suggested word,
|
||||||
/// let's confirm it, otherwise just auto-complete.
|
/// let's confirm it, otherwise just auto-complete.
|
||||||
fn on_input_click(&mut self, ctx: &mut EventCtx) -> Option<MnemonicInputMsg> {
|
fn on_input_click(&mut self, ctx: &mut EventCtx) -> Option<MnemonicInputMsg> {
|
||||||
if let (Some(word), Some(num)) = (self.suggested_word, self.options_num) {
|
if let (Some(word), Some(_num)) = (self.suggested_word, self.options_num) {
|
||||||
return if num == 1 && word.starts_with(self.textbox.content())
|
return if word.eq(self.textbox.content()) {
|
||||||
|| num > 1 && word.eq(self.textbox.content())
|
|
||||||
{
|
|
||||||
// Confirm button.
|
// Confirm button.
|
||||||
self.textbox.clear(ctx);
|
self.textbox.clear(ctx);
|
||||||
Some(MnemonicInputMsg::Confirmed)
|
Some(MnemonicInputMsg::Confirmed)
|
||||||
@ -286,24 +256,19 @@ impl Bip39Input {
|
|||||||
self.suggested_word = bip39::complete_word(self.textbox.content());
|
self.suggested_word = bip39::complete_word(self.textbox.content());
|
||||||
|
|
||||||
// Change the style of the button depending on the completed word.
|
// Change the style of the button depending on the completed word.
|
||||||
if let (Some(word), Some(num)) = (self.suggested_word, self.options_num) {
|
if let (Some(word), Some(_num)) = (self.suggested_word, self.options_num) {
|
||||||
if num == 1 && word.starts_with(self.textbox.content())
|
if word.eq(self.textbox.content()) {
|
||||||
|| num > 1 && word.eq(self.textbox.content())
|
|
||||||
{
|
|
||||||
// Confirm button.
|
// Confirm button.
|
||||||
self.button.enable(ctx);
|
self.button.enable(ctx);
|
||||||
self.button.set_stylesheet(ctx, theme::button_pin_confirm());
|
|
||||||
self.button
|
self.button
|
||||||
.set_content(ctx, ButtonContent::Icon(theme::ICON_CONFIRM_INPUT));
|
.set_stylesheet(ctx, theme::button_recovery_confirm());
|
||||||
self.button_suggestion
|
self.button_suggestion
|
||||||
.set_stylesheet(ctx, theme::button_suggestion_confirm());
|
.set_stylesheet(ctx, theme::button_suggestion_confirm());
|
||||||
} else {
|
} else {
|
||||||
// Auto-complete button.
|
// Auto-complete button.
|
||||||
self.button.enable(ctx);
|
self.button.enable(ctx);
|
||||||
self.button
|
self.button
|
||||||
.set_stylesheet(ctx, theme::button_bip39_autocomplete());
|
.set_stylesheet(ctx, theme::button_recovery_autocomplete());
|
||||||
self.button
|
|
||||||
.set_content(ctx, ButtonContent::Icon(theme::ICON_AUTOFILL));
|
|
||||||
self.button_suggestion
|
self.button_suggestion
|
||||||
.set_stylesheet(ctx, theme::button_suggestion_autocomplete());
|
.set_stylesheet(ctx, theme::button_suggestion_autocomplete());
|
||||||
}
|
}
|
||||||
@ -311,7 +276,6 @@ impl Bip39Input {
|
|||||||
// Disabled button.
|
// Disabled button.
|
||||||
self.button.disable(ctx);
|
self.button.disable(ctx);
|
||||||
self.button.set_stylesheet(ctx, theme::button_keyboard());
|
self.button.set_stylesheet(ctx, theme::button_keyboard());
|
||||||
self.button.set_content(ctx, ButtonContent::Text("".into()));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,12 +3,14 @@ use crate::{
|
|||||||
ui::{
|
ui::{
|
||||||
component::{text::common::TextEdit, Event, EventCtx, TimerToken},
|
component::{text::common::TextEdit, Event, EventCtx, TimerToken},
|
||||||
display::{self, Color, Font},
|
display::{self, Color, Font},
|
||||||
geometry::{Offset, Point, Rect},
|
geometry::{Alignment2D, Offset, Point, Rect},
|
||||||
shape,
|
shape,
|
||||||
shape::Renderer,
|
shape::Renderer,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use super::super::ButtonStyle;
|
||||||
|
|
||||||
/// Contains state commonly used in implementations multi-tap keyboards.
|
/// Contains state commonly used in implementations multi-tap keyboards.
|
||||||
pub struct MultiTapKeyboard {
|
pub struct MultiTapKeyboard {
|
||||||
/// Configured timeout after which we cancel currently pending key.
|
/// Configured timeout after which we cancel currently pending key.
|
||||||
@ -150,3 +152,41 @@ pub fn render_pending_marker<'s>(
|
|||||||
shape::Bar::new(marker_rect).with_bg(color).render(target);
|
shape::Bar::new(marker_rect).with_bg(color).render(target);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Create a pill-shaped button around a text.
|
||||||
|
pub fn render_pill_shape<'s>(
|
||||||
|
target: &mut impl Renderer<'s>,
|
||||||
|
base_point: Point,
|
||||||
|
text: &str,
|
||||||
|
style: &ButtonStyle,
|
||||||
|
expand_area: Option<Rect>,
|
||||||
|
) {
|
||||||
|
let pill_radius = 18;
|
||||||
|
let pill_bearing_x = 17;
|
||||||
|
let pill_bearing_y = 12;
|
||||||
|
let pill_height = 2 * pill_radius + 4; // adding 4px looks better, if the height is just 2*r it does not look like a
|
||||||
|
// perfect half-circle but there is a visible narrowing of the pill shape
|
||||||
|
let pill_width = style.font.text_width(text) + 2 * pill_bearing_x;
|
||||||
|
|
||||||
|
let pill_baseline = base_point + Offset::new(-pill_bearing_x, pill_bearing_y);
|
||||||
|
let mut pill_area = Rect::snap(
|
||||||
|
pill_baseline,
|
||||||
|
Offset::new(pill_width, pill_height),
|
||||||
|
Alignment2D::BOTTOM_LEFT,
|
||||||
|
);
|
||||||
|
if let Some(area) = expand_area {
|
||||||
|
// "dummy" rectangle to use in the `union` call
|
||||||
|
let expander = Rect::snap(
|
||||||
|
area.bottom_right(),
|
||||||
|
Offset::uniform(1),
|
||||||
|
Alignment2D::BOTTOM_RIGHT,
|
||||||
|
);
|
||||||
|
pill_area = pill_area.union(expander);
|
||||||
|
}
|
||||||
|
shape::Bar::new(pill_area)
|
||||||
|
.with_bg(style.background_color)
|
||||||
|
.with_fg(style.button_color)
|
||||||
|
.with_radius(pill_radius)
|
||||||
|
.with_thickness(2)
|
||||||
|
.render(target);
|
||||||
|
}
|
||||||
|
@ -2,7 +2,7 @@ use crate::{
|
|||||||
strutil::TString,
|
strutil::TString,
|
||||||
ui::{
|
ui::{
|
||||||
component::{maybe::paint_overlapping, Child, Component, Event, EventCtx, Label, Maybe},
|
component::{maybe::paint_overlapping, Child, Component, Event, EventCtx, Label, Maybe},
|
||||||
geometry::{Alignment, Grid, Rect},
|
geometry::{Alignment, Grid, Insets, Rect},
|
||||||
model_mercury::{
|
model_mercury::{
|
||||||
component::{Button, ButtonMsg, Swipe, SwipeDirection},
|
component::{Button, ButtonMsg, Swipe, SwipeDirection},
|
||||||
theme,
|
theme,
|
||||||
@ -12,6 +12,7 @@ use crate::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
pub const MNEMONIC_KEY_COUNT: usize = 9;
|
pub const MNEMONIC_KEY_COUNT: usize = 9;
|
||||||
|
const BACK_BUTTON_RIGHT_EXPAND: i16 = 24;
|
||||||
|
|
||||||
pub enum MnemonicKeyboardMsg {
|
pub enum MnemonicKeyboardMsg {
|
||||||
Confirmed,
|
Confirmed,
|
||||||
@ -21,7 +22,9 @@ pub enum MnemonicKeyboardMsg {
|
|||||||
pub struct MnemonicKeyboard<T> {
|
pub struct MnemonicKeyboard<T> {
|
||||||
/// Initial prompt, displayed on empty input.
|
/// Initial prompt, displayed on empty input.
|
||||||
prompt: Child<Maybe<Label<'static>>>,
|
prompt: Child<Maybe<Label<'static>>>,
|
||||||
/// Backspace button.
|
/// Delete a char button.
|
||||||
|
erase: Child<Maybe<Button>>,
|
||||||
|
/// Go to previous word button
|
||||||
back: Child<Maybe<Button>>,
|
back: Child<Maybe<Button>>,
|
||||||
/// Input area, acting as the auto-complete and confirm button.
|
/// Input area, acting as the auto-complete and confirm button.
|
||||||
input: Child<Maybe<T>>,
|
input: Child<Maybe<T>>,
|
||||||
@ -40,19 +43,24 @@ where
|
|||||||
pub fn new(input: T, prompt: TString<'static>, can_go_back: bool) -> Self {
|
pub fn new(input: T, prompt: TString<'static>, can_go_back: bool) -> Self {
|
||||||
// Input might be already pre-filled
|
// Input might be already pre-filled
|
||||||
let prompt_visible = input.is_empty();
|
let prompt_visible = input.is_empty();
|
||||||
|
let erase_btn = Button::with_icon(theme::ICON_DELETE)
|
||||||
|
.styled(theme::button_default())
|
||||||
|
.with_expanded_touch_area(Insets::right(BACK_BUTTON_RIGHT_EXPAND))
|
||||||
|
.with_long_press(theme::ERASE_HOLD_DURATION);
|
||||||
|
let back_btn = Button::with_icon(theme::ICON_CHEVRON_LEFT)
|
||||||
|
.styled(theme::button_default())
|
||||||
|
.with_expanded_touch_area(Insets::right(BACK_BUTTON_RIGHT_EXPAND));
|
||||||
Self {
|
Self {
|
||||||
prompt: Child::new(Maybe::new(
|
prompt: Child::new(Maybe::new(
|
||||||
theme::BG,
|
theme::BG,
|
||||||
Label::centered(prompt, theme::TEXT_MAIN_GREY_LIGHT),
|
Label::centered(prompt, theme::TEXT_MAIN_GREY_LIGHT).vertically_centered(),
|
||||||
prompt_visible,
|
prompt_visible,
|
||||||
)),
|
)),
|
||||||
|
erase: Child::new(Maybe::new(theme::BG, erase_btn, !prompt_visible)),
|
||||||
back: Child::new(Maybe::new(
|
back: Child::new(Maybe::new(
|
||||||
theme::BG,
|
theme::BG,
|
||||||
Button::with_icon(theme::ICON_DELETE)
|
back_btn,
|
||||||
.styled(theme::button_default())
|
prompt_visible && can_go_back,
|
||||||
.with_long_press(theme::ERASE_HOLD_DURATION),
|
|
||||||
!prompt_visible,
|
|
||||||
)),
|
)),
|
||||||
input: Child::new(Maybe::new(theme::BG, input, !prompt_visible)),
|
input: Child::new(Maybe::new(theme::BG, input, !prompt_visible)),
|
||||||
keys: T::keys()
|
keys: T::keys()
|
||||||
@ -86,15 +94,18 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// After edit operations, we need to either show or hide the prompt, the
|
/// After edit operations, we need to either show or hide the prompt, the
|
||||||
/// input, and the back button.
|
/// input, the erase button and the back button.
|
||||||
fn toggle_prompt_or_input(&mut self, ctx: &mut EventCtx) {
|
fn toggle_prompt_or_input(&mut self, ctx: &mut EventCtx) {
|
||||||
let prompt_visible = self.input.inner().inner().is_empty();
|
let prompt_visible = self.input.inner().inner().is_empty();
|
||||||
self.prompt
|
self.prompt
|
||||||
.mutate(ctx, |ctx, p| p.show_if(ctx, prompt_visible));
|
.mutate(ctx, |ctx, p| p.show_if(ctx, prompt_visible));
|
||||||
self.input
|
self.input
|
||||||
.mutate(ctx, |ctx, i| i.show_if(ctx, !prompt_visible));
|
.mutate(ctx, |ctx, i| i.show_if(ctx, !prompt_visible));
|
||||||
self.back
|
self.erase
|
||||||
.mutate(ctx, |ctx, b| b.show_if(ctx, !prompt_visible));
|
.mutate(ctx, |ctx, b| b.show_if(ctx, !prompt_visible));
|
||||||
|
self.back.mutate(ctx, |ctx, b| {
|
||||||
|
b.show_if(ctx, prompt_visible && self.can_go_back)
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn mnemonic(&self) -> Option<&'static str> {
|
pub fn mnemonic(&self) -> Option<&'static str> {
|
||||||
@ -109,22 +120,25 @@ where
|
|||||||
type Msg = MnemonicKeyboardMsg;
|
type Msg = MnemonicKeyboardMsg;
|
||||||
|
|
||||||
fn place(&mut self, bounds: Rect) -> Rect {
|
fn place(&mut self, bounds: Rect) -> Rect {
|
||||||
let height_input_area: i16 = 30;
|
let height_input_area: i16 = 38;
|
||||||
let space_top: i16 = 8;
|
let padding_top: i16 = 6;
|
||||||
|
let back_btn_area_width: i16 = 32;
|
||||||
let (remaining, keyboard_area) =
|
let (remaining, keyboard_area) =
|
||||||
bounds.split_bottom(3 * theme::BUTTON_HEIGHT + 2 * theme::KEYBOARD_SPACING);
|
bounds.split_bottom(3 * theme::MNEMONIC_BUTTON_HEIGHT + 2 * theme::KEYBOARD_SPACING);
|
||||||
let prompt_area = remaining
|
let prompt_area = remaining
|
||||||
.split_top(space_top)
|
.split_top(padding_top)
|
||||||
.1
|
.1
|
||||||
.split_top(height_input_area)
|
.split_top(height_input_area)
|
||||||
.0;
|
.0;
|
||||||
assert!(prompt_area.height() == height_input_area);
|
assert!(prompt_area.height() == height_input_area);
|
||||||
|
|
||||||
let (back_btn_area, input_area) = prompt_area.split_left(30);
|
let (back_btn_area, input_area) = prompt_area.split_left(back_btn_area_width);
|
||||||
|
let input_area = input_area.inset(Insets::left(BACK_BUTTON_RIGHT_EXPAND));
|
||||||
let keyboard_grid = Grid::new(keyboard_area, 3, 3).with_spacing(theme::KEYBOARD_SPACING);
|
let keyboard_grid = Grid::new(keyboard_area, 3, 3).with_spacing(theme::KEYBOARD_SPACING);
|
||||||
|
|
||||||
self.swipe.place(bounds);
|
self.swipe.place(bounds);
|
||||||
self.prompt.place(prompt_area);
|
self.prompt.place(prompt_area);
|
||||||
|
self.erase.place(back_btn_area);
|
||||||
self.back.place(back_btn_area);
|
self.back.place(back_btn_area);
|
||||||
self.input.place(input_area);
|
self.input.place(input_area);
|
||||||
|
|
||||||
@ -135,8 +149,11 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||||
// Swipe will cause going back to the previous word when allowed.
|
// Back button or swipe will cause going back to the previous word when allowed.
|
||||||
if self.can_go_back {
|
if self.can_go_back {
|
||||||
|
if let Some(ButtonMsg::Clicked) = self.back.event(ctx, event) {
|
||||||
|
return Some(MnemonicKeyboardMsg::Previous);
|
||||||
|
}
|
||||||
if let Some(SwipeDirection::Right) = self.swipe.event(ctx, event) {
|
if let Some(SwipeDirection::Right) = self.swipe.event(ctx, event) {
|
||||||
return Some(MnemonicKeyboardMsg::Previous);
|
return Some(MnemonicKeyboardMsg::Previous);
|
||||||
}
|
}
|
||||||
@ -155,7 +172,7 @@ where
|
|||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
match self.back.event(ctx, event) {
|
match self.erase.event(ctx, event) {
|
||||||
Some(ButtonMsg::Clicked) => {
|
Some(ButtonMsg::Clicked) => {
|
||||||
self.input
|
self.input
|
||||||
.mutate(ctx, |ctx, i| i.inner_mut().on_backspace_click(ctx));
|
.mutate(ctx, |ctx, i| i.inner_mut().on_backspace_click(ctx));
|
||||||
@ -182,7 +199,7 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn paint(&mut self) {
|
fn paint(&mut self) {
|
||||||
paint_overlapping(&mut [&mut self.prompt, &mut self.input, &mut self.back]);
|
paint_overlapping(&mut [&mut self.prompt, &mut self.input, &mut self.erase]);
|
||||||
for btn in &mut self.keys {
|
for btn in &mut self.keys {
|
||||||
btn.paint();
|
btn.paint();
|
||||||
}
|
}
|
||||||
@ -191,9 +208,12 @@ where
|
|||||||
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||||
if self.input.inner().inner().is_empty() {
|
if self.input.inner().inner().is_empty() {
|
||||||
self.prompt.render(target);
|
self.prompt.render(target);
|
||||||
|
if self.can_go_back {
|
||||||
|
self.back.render(target);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
self.input.render(target);
|
self.input.render(target);
|
||||||
self.back.render(target);
|
self.erase.render(target);
|
||||||
}
|
}
|
||||||
|
|
||||||
for btn in &self.keys {
|
for btn in &self.keys {
|
||||||
@ -205,6 +225,7 @@ where
|
|||||||
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
|
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
|
||||||
self.prompt.bounds(sink);
|
self.prompt.bounds(sink);
|
||||||
self.input.bounds(sink);
|
self.input.bounds(sink);
|
||||||
|
self.erase.bounds(sink);
|
||||||
self.back.bounds(sink);
|
self.back.bounds(sink);
|
||||||
for btn in &self.keys {
|
for btn in &self.keys {
|
||||||
btn.bounds(sink)
|
btn.bounds(sink)
|
||||||
|
@ -9,12 +9,12 @@ use crate::{
|
|||||||
text::common::{TextBox, TextEdit},
|
text::common::{TextBox, TextEdit},
|
||||||
Component, Event, EventCtx,
|
Component, Event, EventCtx,
|
||||||
},
|
},
|
||||||
display,
|
constant::WIDTH,
|
||||||
geometry::{Alignment2D, Offset, Rect},
|
geometry::{Alignment, Alignment2D, Offset, Point, Rect},
|
||||||
model_mercury::{
|
model_mercury::{
|
||||||
component::{
|
component::{
|
||||||
keyboard::{
|
keyboard::{
|
||||||
common::{paint_pending_marker, render_pending_marker, MultiTapKeyboard},
|
common::{render_pending_marker, render_pill_shape, MultiTapKeyboard},
|
||||||
mnemonic::{MnemonicInput, MnemonicInputMsg, MNEMONIC_KEY_COUNT},
|
mnemonic::{MnemonicInput, MnemonicInputMsg, MNEMONIC_KEY_COUNT},
|
||||||
},
|
},
|
||||||
Button, ButtonContent, ButtonMsg,
|
Button, ButtonContent, ButtonMsg,
|
||||||
@ -124,88 +124,35 @@ impl Component for Slip39Input {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn paint(&mut self) {
|
fn paint(&mut self) {
|
||||||
let area = self.button.area();
|
todo!("remove when ui-t3t1 done")
|
||||||
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())
|
|
||||||
{
|
|
||||||
assert!(!Self::keys()[key].is_empty());
|
|
||||||
// Now we can be sure that the looped iterator will return a value.
|
|
||||||
let ch = unwrap!(Self::keys()[key].chars().cycle().nth(press));
|
|
||||||
text.pop();
|
|
||||||
text.push(ch)
|
|
||||||
.assert_if_debugging_ui("Text buffer is too small");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
display::text_left(
|
|
||||||
text_baseline,
|
|
||||||
text.as_str(),
|
|
||||||
style.font,
|
|
||||||
style.text_color,
|
|
||||||
style.button_color,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Paint the pending marker.
|
|
||||||
if self.multi_tap.pending_key().is_some() && self.final_word.is_none() {
|
|
||||||
paint_pending_marker(text_baseline, text.as_str(), style.font, 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);
|
|
||||||
icon.draw(
|
|
||||||
icon_center,
|
|
||||||
Alignment2D::CENTER,
|
|
||||||
style.icon_color,
|
|
||||||
style.button_color,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||||
let area = self.button.area();
|
let area = self.button.area();
|
||||||
let style = self.button.style();
|
let style = self.button.style();
|
||||||
|
|
||||||
// First, paint the button background.
|
// Content is center-aligned
|
||||||
self.button.render_background(target, style);
|
let text_base_y = area.left_center().y + style.font.allcase_text_height() / 2;
|
||||||
|
let text_center = Point::new(WIDTH / 2, text_base_y);
|
||||||
// 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
|
// To simplify things, we always copy the printed string here, even if it
|
||||||
// wouldn't be strictly necessary.
|
// wouldn't be strictly necessary.
|
||||||
let mut text: String<MAX_LENGTH> = String::new();
|
let mut text: String<MAX_LENGTH> = String::new();
|
||||||
|
|
||||||
if let Some(word) = self.final_word {
|
if let Some(word) = self.final_word {
|
||||||
// We're done with input, paint the full word.
|
// We're done with input, paint the full word.
|
||||||
text.push_str(word)
|
text.push_str(word)
|
||||||
.assert_if_debugging_ui("Text buffer is too small");
|
.assert_if_debugging_ui("Text buffer is too small");
|
||||||
|
let pill_base = Point::new(
|
||||||
|
style.font.horz_center(0, WIDTH, text.as_str()),
|
||||||
|
text_center.y,
|
||||||
|
);
|
||||||
|
render_pill_shape(target, pill_base, text.as_str(), style, Some(area));
|
||||||
|
// Icon is painted in the right-center point, 10px from the right edge.
|
||||||
|
let icon_right_center = area.right_center() - Offset::x(10);
|
||||||
|
shape::ToifImage::new(icon_right_center, theme::ICON_SIMPLE_CHECKMARK24.toif)
|
||||||
|
.with_align(Alignment2D::CENTER_RIGHT)
|
||||||
|
.with_fg(style.icon_color)
|
||||||
|
.render(target);
|
||||||
} else {
|
} else {
|
||||||
// Paint an asterisk for each letter of input.
|
// Paint an asterisk for each letter of input.
|
||||||
for ch in iter::repeat('*').take(self.textbox.content().len()) {
|
for ch in iter::repeat('*').take(self.textbox.content().len()) {
|
||||||
@ -224,32 +171,26 @@ impl Component for Slip39Input {
|
|||||||
.assert_if_debugging_ui("Text buffer is too small");
|
.assert_if_debugging_ui("Text buffer is too small");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
shape::Text::new(text_baseline, text.as_str())
|
shape::Text::new(text_center, text.as_str())
|
||||||
.with_font(style.font)
|
.with_font(style.font)
|
||||||
.with_fg(style.text_color)
|
.with_fg(style.text_color)
|
||||||
|
.with_align(Alignment::Center)
|
||||||
.render(target);
|
.render(target);
|
||||||
|
|
||||||
// Paint the pending marker.
|
// Paint the pending marker.
|
||||||
|
let text_base = Point::new(
|
||||||
|
style.font.horz_center(0, WIDTH, text.as_str()),
|
||||||
|
text_center.y,
|
||||||
|
);
|
||||||
if self.multi_tap.pending_key().is_some() && self.final_word.is_none() {
|
if self.multi_tap.pending_key().is_some() && self.final_word.is_none() {
|
||||||
render_pending_marker(
|
render_pending_marker(
|
||||||
target,
|
target,
|
||||||
text_baseline,
|
text_base,
|
||||||
text.as_str(),
|
text.as_str(),
|
||||||
style.font,
|
style.font,
|
||||||
style.text_color,
|
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);
|
|
||||||
shape::ToifImage::new(icon_center, icon.toif)
|
|
||||||
.with_align(Alignment2D::CENTER)
|
|
||||||
.with_fg(style.icon_color)
|
|
||||||
.render(target);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "ui_bounds")]
|
#[cfg(feature = "ui_bounds")]
|
||||||
@ -262,7 +203,7 @@ impl Slip39Input {
|
|||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
// Button has the same style the whole time
|
// Button has the same style the whole time
|
||||||
button: Button::empty().styled(theme::button_pin_confirm()),
|
button: Button::empty().styled(theme::button_recovery_confirm()),
|
||||||
textbox: TextBox::empty(),
|
textbox: TextBox::empty(),
|
||||||
multi_tap: MultiTapKeyboard::new(),
|
multi_tap: MultiTapKeyboard::new(),
|
||||||
final_word: None,
|
final_word: None,
|
||||||
@ -280,7 +221,7 @@ impl Slip39Input {
|
|||||||
|
|
||||||
Self {
|
Self {
|
||||||
// Button has the same style the whole time
|
// Button has the same style the whole time
|
||||||
button: Button::empty().styled(theme::button_pin_confirm()),
|
button: Button::empty().styled(theme::button_recovery_confirm()),
|
||||||
textbox: TextBox::new(buff),
|
textbox: TextBox::new(buff),
|
||||||
multi_tap: MultiTapKeyboard::new(),
|
multi_tap: MultiTapKeyboard::new(),
|
||||||
final_word,
|
final_word,
|
||||||
@ -356,7 +297,7 @@ impl Slip39Input {
|
|||||||
// Confirm button.
|
// Confirm button.
|
||||||
self.button.enable(ctx);
|
self.button.enable(ctx);
|
||||||
self.button
|
self.button
|
||||||
.set_content(ctx, ButtonContent::Icon(theme::ICON_CONFIRM_INPUT));
|
.set_content(ctx, ButtonContent::Icon(theme::ICON_SIMPLE_CHECKMARK24));
|
||||||
} else {
|
} else {
|
||||||
// Disabled button.
|
// Disabled button.
|
||||||
self.button.disable(ctx);
|
self.button.disable(ctx);
|
||||||
|
@ -75,6 +75,7 @@ include_icon!(ICON_PAGE_UP, "model_mercury/res/page_up20.toif");
|
|||||||
|
|
||||||
// 24x24
|
// 24x24
|
||||||
include_icon!(ICON_CANCEL, "model_mercury/res/cancel24.toif");
|
include_icon!(ICON_CANCEL, "model_mercury/res/cancel24.toif");
|
||||||
|
include_icon!(ICON_CHEVRON_LEFT, "model_mercury/res/chevron_left24.toif");
|
||||||
include_icon!(ICON_CHEVRON_RIGHT, "model_mercury/res/chevron_right24.toif");
|
include_icon!(ICON_CHEVRON_RIGHT, "model_mercury/res/chevron_right24.toif");
|
||||||
include_icon!(ICON_DOWNLOAD, "model_mercury/res/download24.toif");
|
include_icon!(ICON_DOWNLOAD, "model_mercury/res/download24.toif");
|
||||||
include_icon!(ICON_KEY, "model_mercury/res/key20.toif");
|
include_icon!(ICON_KEY, "model_mercury/res/key20.toif");
|
||||||
@ -576,27 +577,28 @@ pub const fn button_passphrase_next() -> ButtonStyleSheet {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const fn button_bip39_autocomplete() -> ButtonStyleSheet {
|
pub const fn button_recovery_confirm() -> ButtonStyleSheet {
|
||||||
ButtonStyleSheet {
|
ButtonStyleSheet {
|
||||||
normal: &ButtonStyle {
|
normal: &ButtonStyle {
|
||||||
font: Font::MONO,
|
font: Font::DEMIBOLD,
|
||||||
text_color: FG,
|
text_color: GREEN_LIME,
|
||||||
button_color: BG,
|
button_color: GREEN_LIGHT,
|
||||||
icon_color: GREY_LIGHT,
|
icon_color: GREEN_LIME,
|
||||||
background_color: BG,
|
background_color: GREEN_DARK,
|
||||||
},
|
},
|
||||||
active: &ButtonStyle {
|
active: &ButtonStyle {
|
||||||
font: Font::MONO,
|
font: Font::DEMIBOLD,
|
||||||
text_color: FG,
|
text_color: GREEN_DARK,
|
||||||
button_color: BG,
|
button_color: GREEN_LIGHT,
|
||||||
icon_color: GREY_LIGHT,
|
icon_color: GREEN_DARK,
|
||||||
background_color: BG,
|
background_color: GREEN_LIGHT,
|
||||||
},
|
},
|
||||||
|
// used in SLIP-39 recovery for "*"
|
||||||
disabled: &ButtonStyle {
|
disabled: &ButtonStyle {
|
||||||
font: Font::MONO,
|
font: Font::DEMIBOLD,
|
||||||
text_color: FG,
|
text_color: GREY_LIGHT,
|
||||||
button_color: BG,
|
button_color: BG,
|
||||||
icon_color: GREY_LIGHT,
|
icon_color: BG,
|
||||||
background_color: BG,
|
background_color: BG,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -605,24 +607,52 @@ pub const fn button_bip39_autocomplete() -> ButtonStyleSheet {
|
|||||||
pub const fn button_suggestion_confirm() -> ButtonStyleSheet {
|
pub const fn button_suggestion_confirm() -> ButtonStyleSheet {
|
||||||
ButtonStyleSheet {
|
ButtonStyleSheet {
|
||||||
normal: &ButtonStyle {
|
normal: &ButtonStyle {
|
||||||
font: Font::MONO,
|
font: Font::DEMIBOLD,
|
||||||
|
text_color: GREY_LIGHT, // difference
|
||||||
|
button_color: GREEN_LIGHT,
|
||||||
|
icon_color: GREEN_LIME,
|
||||||
|
background_color: GREEN_DARK,
|
||||||
|
},
|
||||||
|
active: &ButtonStyle {
|
||||||
|
font: Font::DEMIBOLD,
|
||||||
|
text_color: GREEN_LIME,
|
||||||
|
button_color: GREEN_LIGHT,
|
||||||
|
icon_color: GREEN_DARK,
|
||||||
|
background_color: GREEN_LIGHT,
|
||||||
|
},
|
||||||
|
// not used
|
||||||
|
disabled: &ButtonStyle {
|
||||||
|
font: Font::DEMIBOLD,
|
||||||
|
text_color: BG,
|
||||||
|
button_color: BG,
|
||||||
|
icon_color: BG,
|
||||||
|
background_color: BG,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn button_recovery_autocomplete() -> ButtonStyleSheet {
|
||||||
|
ButtonStyleSheet {
|
||||||
|
normal: &ButtonStyle {
|
||||||
|
font: Font::DEMIBOLD,
|
||||||
text_color: GREY_LIGHT,
|
text_color: GREY_LIGHT,
|
||||||
button_color: GREEN,
|
button_color: GREY_EXTRA_DARK,
|
||||||
icon_color: GREY_LIGHT,
|
icon_color: GREY_LIGHT,
|
||||||
background_color: BG,
|
background_color: BG,
|
||||||
},
|
},
|
||||||
active: &ButtonStyle {
|
active: &ButtonStyle {
|
||||||
font: Font::MONO,
|
font: Font::DEMIBOLD,
|
||||||
text_color: FG,
|
text_color: BG,
|
||||||
button_color: GREEN_DARK,
|
button_color: FG,
|
||||||
icon_color: GREY_LIGHT,
|
icon_color: BG,
|
||||||
background_color: BG,
|
background_color: FG,
|
||||||
},
|
},
|
||||||
|
// not used
|
||||||
disabled: &ButtonStyle {
|
disabled: &ButtonStyle {
|
||||||
font: Font::MONO,
|
font: Font::DEMIBOLD,
|
||||||
text_color: GREY_LIGHT,
|
text_color: BG,
|
||||||
button_color: GREY_DARK,
|
button_color: BG,
|
||||||
icon_color: GREY_LIGHT,
|
icon_color: BG,
|
||||||
background_color: BG,
|
background_color: BG,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -631,24 +661,25 @@ pub const fn button_suggestion_confirm() -> ButtonStyleSheet {
|
|||||||
pub const fn button_suggestion_autocomplete() -> ButtonStyleSheet {
|
pub const fn button_suggestion_autocomplete() -> ButtonStyleSheet {
|
||||||
ButtonStyleSheet {
|
ButtonStyleSheet {
|
||||||
normal: &ButtonStyle {
|
normal: &ButtonStyle {
|
||||||
font: Font::MONO,
|
font: Font::DEMIBOLD,
|
||||||
text_color: GREY_LIGHT,
|
text_color: GREY,
|
||||||
button_color: GREY_DARK, // same as PIN buttons
|
button_color: BG,
|
||||||
icon_color: GREY_LIGHT,
|
icon_color: BG,
|
||||||
background_color: BG,
|
background_color: BG,
|
||||||
},
|
},
|
||||||
active: &ButtonStyle {
|
active: &ButtonStyle {
|
||||||
font: Font::MONO,
|
font: Font::MONO,
|
||||||
text_color: FG,
|
text_color: BG,
|
||||||
button_color: GREEN_DARK,
|
button_color: FG,
|
||||||
icon_color: GREY_LIGHT,
|
icon_color: BG,
|
||||||
background_color: BG,
|
background_color: FG,
|
||||||
},
|
},
|
||||||
|
// not used
|
||||||
disabled: &ButtonStyle {
|
disabled: &ButtonStyle {
|
||||||
font: Font::MONO,
|
font: Font::MONO,
|
||||||
text_color: GREY_LIGHT,
|
text_color: BG,
|
||||||
button_color: BG,
|
button_color: BG,
|
||||||
icon_color: GREY_LIGHT,
|
icon_color: BG,
|
||||||
background_color: BG,
|
background_color: BG,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -34,7 +34,7 @@ async def request_word_count(dry_run: bool) -> int:
|
|||||||
async def request_word(
|
async def request_word(
|
||||||
word_index: int, word_count: int, is_slip39: bool, prefill_word: str = ""
|
word_index: int, word_count: int, is_slip39: bool, prefill_word: str = ""
|
||||||
) -> str:
|
) -> str:
|
||||||
prompt = TR.recovery__type_word_x_of_y_template.format(word_index + 1, word_count)
|
prompt = TR.recovery__word_x_of_y_template.format(word_index + 1, word_count)
|
||||||
can_go_back = word_index > 0
|
can_go_back = word_index > 0
|
||||||
if is_slip39:
|
if is_slip39:
|
||||||
keyboard = RustLayout(
|
keyboard = RustLayout(
|
||||||
|
Loading…
Reference in New Issue
Block a user