1
0
mirror of https://github.com/trezor/trezor-firmware.git synced 2024-11-27 01:48:17 +00:00

feat(core/ui): T3T1 recovery keyboards

[no changelog]
This commit is contained in:
obrusvit 2024-05-20 21:46:53 +02:00 committed by Martin Milata
parent 3f0ab537af
commit e5e8e27abc
6 changed files with 232 additions and 235 deletions

View File

@ -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()));
} }
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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