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,
|
||||
ui::{
|
||||
component::{text::common::TextBox, Component, Event, EventCtx},
|
||||
display,
|
||||
geometry::{Alignment2D, Offset, Rect},
|
||||
geometry::{Alignment, Alignment2D, Offset, Point, Rect},
|
||||
model_mercury::{
|
||||
component::{
|
||||
keyboard::{
|
||||
common::{paint_pending_marker, render_pending_marker, MultiTapKeyboard},
|
||||
common::{render_pending_marker, render_pill_shape, MultiTapKeyboard},
|
||||
mnemonic::{MnemonicInput, MnemonicInputMsg, MNEMONIC_KEY_COUNT},
|
||||
},
|
||||
Button, ButtonContent, ButtonMsg,
|
||||
Button, ButtonMsg,
|
||||
},
|
||||
constant::WIDTH,
|
||||
theme,
|
||||
},
|
||||
shape,
|
||||
@ -104,100 +104,66 @@ impl Component for Bip39Input {
|
||||
}
|
||||
|
||||
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();
|
||||
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,
|
||||
);
|
||||
}
|
||||
todo!("remove when ui-t3t1 done");
|
||||
}
|
||||
|
||||
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||
let area = self.button.area();
|
||||
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).
|
||||
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);
|
||||
shape::Text::new(text_baseline, text)
|
||||
|
||||
// User input together with suggestion is centered vertically in the input area
|
||||
// and centered horizontally on the screen
|
||||
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_fg(style.text_color)
|
||||
.with_align(Alignment::Start)
|
||||
.render(target);
|
||||
|
||||
// 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 word_baseline = text_base + Offset::x(width);
|
||||
let style = self.button_suggestion.style();
|
||||
shape::Text::new(word_baseline, word)
|
||||
.with_font(style.font)
|
||||
.with_fg(style.text_color)
|
||||
.with_align(Alignment::Start)
|
||||
.render(target);
|
||||
}
|
||||
|
||||
// Paint the pending marker.
|
||||
if self.multi_tap.pending_key().is_some() {
|
||||
render_pending_marker(target, 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);
|
||||
shape::ToifImage::new(icon_center, icon.toif)
|
||||
.with_align(Alignment2D::CENTER)
|
||||
.with_fg(style.icon_color)
|
||||
.render(target);
|
||||
render_pending_marker(target, text_base, text, style.font, style.text_color);
|
||||
}
|
||||
}
|
||||
|
||||
@ -227,8 +193,7 @@ impl Bip39Input {
|
||||
|
||||
// Styling the input to reflect already filled word
|
||||
Self {
|
||||
button: Button::with_icon(theme::ICON_CONFIRM_INPUT)
|
||||
.styled(theme::button_pin_confirm()),
|
||||
button: Button::empty().styled(theme::button_recovery_confirm()),
|
||||
textbox: TextBox::new(unwrap!(String::try_from(word))),
|
||||
multi_tap: MultiTapKeyboard::new(),
|
||||
options_num: bip39::options_num(word),
|
||||
@ -249,13 +214,18 @@ impl Bip39Input {
|
||||
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,
|
||||
/// let's confirm it, otherwise just auto-complete.
|
||||
fn on_input_click(&mut self, ctx: &mut EventCtx) -> Option<MnemonicInputMsg> {
|
||||
if let (Some(word), Some(num)) = (self.suggested_word, self.options_num) {
|
||||
return if num == 1 && word.starts_with(self.textbox.content())
|
||||
|| num > 1 && word.eq(self.textbox.content())
|
||||
{
|
||||
if let (Some(word), Some(_num)) = (self.suggested_word, self.options_num) {
|
||||
return if word.eq(self.textbox.content()) {
|
||||
// Confirm button.
|
||||
self.textbox.clear(ctx);
|
||||
Some(MnemonicInputMsg::Confirmed)
|
||||
@ -286,24 +256,19 @@ impl Bip39Input {
|
||||
self.suggested_word = bip39::complete_word(self.textbox.content());
|
||||
|
||||
// Change the style of the button depending on the completed word.
|
||||
if let (Some(word), Some(num)) = (self.suggested_word, self.options_num) {
|
||||
if num == 1 && word.starts_with(self.textbox.content())
|
||||
|| num > 1 && word.eq(self.textbox.content())
|
||||
{
|
||||
if let (Some(word), Some(_num)) = (self.suggested_word, self.options_num) {
|
||||
if word.eq(self.textbox.content()) {
|
||||
// Confirm button.
|
||||
self.button.enable(ctx);
|
||||
self.button.set_stylesheet(ctx, theme::button_pin_confirm());
|
||||
self.button
|
||||
.set_content(ctx, ButtonContent::Icon(theme::ICON_CONFIRM_INPUT));
|
||||
.set_stylesheet(ctx, theme::button_recovery_confirm());
|
||||
self.button_suggestion
|
||||
.set_stylesheet(ctx, theme::button_suggestion_confirm());
|
||||
} else {
|
||||
// Auto-complete button.
|
||||
self.button.enable(ctx);
|
||||
self.button
|
||||
.set_stylesheet(ctx, theme::button_bip39_autocomplete());
|
||||
self.button
|
||||
.set_content(ctx, ButtonContent::Icon(theme::ICON_AUTOFILL));
|
||||
.set_stylesheet(ctx, theme::button_recovery_autocomplete());
|
||||
self.button_suggestion
|
||||
.set_stylesheet(ctx, theme::button_suggestion_autocomplete());
|
||||
}
|
||||
@ -311,7 +276,6 @@ impl Bip39Input {
|
||||
// Disabled button.
|
||||
self.button.disable(ctx);
|
||||
self.button.set_stylesheet(ctx, theme::button_keyboard());
|
||||
self.button.set_content(ctx, ButtonContent::Text("".into()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,12 +3,14 @@ use crate::{
|
||||
ui::{
|
||||
component::{text::common::TextEdit, Event, EventCtx, TimerToken},
|
||||
display::{self, Color, Font},
|
||||
geometry::{Offset, Point, Rect},
|
||||
geometry::{Alignment2D, Offset, Point, Rect},
|
||||
shape,
|
||||
shape::Renderer,
|
||||
},
|
||||
};
|
||||
|
||||
use super::super::ButtonStyle;
|
||||
|
||||
/// Contains state commonly used in implementations multi-tap keyboards.
|
||||
pub struct MultiTapKeyboard {
|
||||
/// 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);
|
||||
}
|
||||
}
|
||||
|
||||
/// 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,
|
||||
ui::{
|
||||
component::{maybe::paint_overlapping, Child, Component, Event, EventCtx, Label, Maybe},
|
||||
geometry::{Alignment, Grid, Rect},
|
||||
geometry::{Alignment, Grid, Insets, Rect},
|
||||
model_mercury::{
|
||||
component::{Button, ButtonMsg, Swipe, SwipeDirection},
|
||||
theme,
|
||||
@ -12,6 +12,7 @@ use crate::{
|
||||
};
|
||||
|
||||
pub const MNEMONIC_KEY_COUNT: usize = 9;
|
||||
const BACK_BUTTON_RIGHT_EXPAND: i16 = 24;
|
||||
|
||||
pub enum MnemonicKeyboardMsg {
|
||||
Confirmed,
|
||||
@ -21,7 +22,9 @@ pub enum MnemonicKeyboardMsg {
|
||||
pub struct MnemonicKeyboard<T> {
|
||||
/// Initial prompt, displayed on empty input.
|
||||
prompt: Child<Maybe<Label<'static>>>,
|
||||
/// Backspace button.
|
||||
/// Delete a char button.
|
||||
erase: Child<Maybe<Button>>,
|
||||
/// Go to previous word button
|
||||
back: Child<Maybe<Button>>,
|
||||
/// Input area, acting as the auto-complete and confirm button.
|
||||
input: Child<Maybe<T>>,
|
||||
@ -40,19 +43,24 @@ where
|
||||
pub fn new(input: T, prompt: TString<'static>, can_go_back: bool) -> Self {
|
||||
// Input might be already pre-filled
|
||||
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 {
|
||||
prompt: Child::new(Maybe::new(
|
||||
theme::BG,
|
||||
Label::centered(prompt, theme::TEXT_MAIN_GREY_LIGHT),
|
||||
Label::centered(prompt, theme::TEXT_MAIN_GREY_LIGHT).vertically_centered(),
|
||||
prompt_visible,
|
||||
)),
|
||||
erase: Child::new(Maybe::new(theme::BG, erase_btn, !prompt_visible)),
|
||||
back: Child::new(Maybe::new(
|
||||
theme::BG,
|
||||
Button::with_icon(theme::ICON_DELETE)
|
||||
.styled(theme::button_default())
|
||||
.with_long_press(theme::ERASE_HOLD_DURATION),
|
||||
!prompt_visible,
|
||||
back_btn,
|
||||
prompt_visible && can_go_back,
|
||||
)),
|
||||
input: Child::new(Maybe::new(theme::BG, input, !prompt_visible)),
|
||||
keys: T::keys()
|
||||
@ -86,15 +94,18 @@ where
|
||||
}
|
||||
|
||||
/// 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) {
|
||||
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
|
||||
self.erase
|
||||
.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> {
|
||||
@ -109,22 +120,25 @@ where
|
||||
type Msg = MnemonicKeyboardMsg;
|
||||
|
||||
fn place(&mut self, bounds: Rect) -> Rect {
|
||||
let height_input_area: i16 = 30;
|
||||
let space_top: i16 = 8;
|
||||
let height_input_area: i16 = 38;
|
||||
let padding_top: i16 = 6;
|
||||
let back_btn_area_width: i16 = 32;
|
||||
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
|
||||
.split_top(space_top)
|
||||
.split_top(padding_top)
|
||||
.1
|
||||
.split_top(height_input_area)
|
||||
.0;
|
||||
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);
|
||||
|
||||
self.swipe.place(bounds);
|
||||
self.prompt.place(prompt_area);
|
||||
self.erase.place(back_btn_area);
|
||||
self.back.place(back_btn_area);
|
||||
self.input.place(input_area);
|
||||
|
||||
@ -135,8 +149,11 @@ where
|
||||
}
|
||||
|
||||
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 let Some(ButtonMsg::Clicked) = self.back.event(ctx, event) {
|
||||
return Some(MnemonicKeyboardMsg::Previous);
|
||||
}
|
||||
if let Some(SwipeDirection::Right) = self.swipe.event(ctx, event) {
|
||||
return Some(MnemonicKeyboardMsg::Previous);
|
||||
}
|
||||
@ -155,7 +172,7 @@ where
|
||||
_ => {}
|
||||
}
|
||||
|
||||
match self.back.event(ctx, event) {
|
||||
match self.erase.event(ctx, event) {
|
||||
Some(ButtonMsg::Clicked) => {
|
||||
self.input
|
||||
.mutate(ctx, |ctx, i| i.inner_mut().on_backspace_click(ctx));
|
||||
@ -182,7 +199,7 @@ where
|
||||
}
|
||||
|
||||
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 {
|
||||
btn.paint();
|
||||
}
|
||||
@ -191,9 +208,12 @@ where
|
||||
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||
if self.input.inner().inner().is_empty() {
|
||||
self.prompt.render(target);
|
||||
if self.can_go_back {
|
||||
self.back.render(target);
|
||||
}
|
||||
} else {
|
||||
self.input.render(target);
|
||||
self.back.render(target);
|
||||
self.erase.render(target);
|
||||
}
|
||||
|
||||
for btn in &self.keys {
|
||||
@ -205,6 +225,7 @@ where
|
||||
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
|
||||
self.prompt.bounds(sink);
|
||||
self.input.bounds(sink);
|
||||
self.erase.bounds(sink);
|
||||
self.back.bounds(sink);
|
||||
for btn in &self.keys {
|
||||
btn.bounds(sink)
|
||||
|
@ -9,12 +9,12 @@ use crate::{
|
||||
text::common::{TextBox, TextEdit},
|
||||
Component, Event, EventCtx,
|
||||
},
|
||||
display,
|
||||
geometry::{Alignment2D, Offset, Rect},
|
||||
constant::WIDTH,
|
||||
geometry::{Alignment, Alignment2D, Offset, Point, Rect},
|
||||
model_mercury::{
|
||||
component::{
|
||||
keyboard::{
|
||||
common::{paint_pending_marker, render_pending_marker, MultiTapKeyboard},
|
||||
common::{render_pending_marker, render_pill_shape, MultiTapKeyboard},
|
||||
mnemonic::{MnemonicInput, MnemonicInputMsg, MNEMONIC_KEY_COUNT},
|
||||
},
|
||||
Button, ButtonContent, ButtonMsg,
|
||||
@ -124,88 +124,35 @@ impl Component for Slip39Input {
|
||||
}
|
||||
|
||||
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())
|
||||
{
|
||||
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,
|
||||
);
|
||||
}
|
||||
todo!("remove when ui-t3t1 done")
|
||||
}
|
||||
|
||||
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||
let area = self.button.area();
|
||||
let style = self.button.style();
|
||||
|
||||
// First, paint the button background.
|
||||
self.button.render_background(target, 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);
|
||||
// Content is center-aligned
|
||||
let text_base_y = area.left_center().y + style.font.allcase_text_height() / 2;
|
||||
let text_center = Point::new(WIDTH / 2, text_base_y);
|
||||
|
||||
// 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");
|
||||
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 {
|
||||
// Paint an asterisk for each letter of input.
|
||||
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");
|
||||
}
|
||||
}
|
||||
shape::Text::new(text_baseline, text.as_str())
|
||||
shape::Text::new(text_center, text.as_str())
|
||||
.with_font(style.font)
|
||||
.with_fg(style.text_color)
|
||||
.with_align(Alignment::Center)
|
||||
.render(target);
|
||||
|
||||
// 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() {
|
||||
render_pending_marker(
|
||||
target,
|
||||
text_baseline,
|
||||
text_base,
|
||||
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);
|
||||
shape::ToifImage::new(icon_center, icon.toif)
|
||||
.with_align(Alignment2D::CENTER)
|
||||
.with_fg(style.icon_color)
|
||||
.render(target);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ui_bounds")]
|
||||
@ -262,7 +203,7 @@ impl Slip39Input {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
// 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(),
|
||||
multi_tap: MultiTapKeyboard::new(),
|
||||
final_word: None,
|
||||
@ -280,7 +221,7 @@ impl Slip39Input {
|
||||
|
||||
Self {
|
||||
// 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),
|
||||
multi_tap: MultiTapKeyboard::new(),
|
||||
final_word,
|
||||
@ -356,7 +297,7 @@ impl Slip39Input {
|
||||
// Confirm button.
|
||||
self.button.enable(ctx);
|
||||
self.button
|
||||
.set_content(ctx, ButtonContent::Icon(theme::ICON_CONFIRM_INPUT));
|
||||
.set_content(ctx, ButtonContent::Icon(theme::ICON_SIMPLE_CHECKMARK24));
|
||||
} else {
|
||||
// Disabled button.
|
||||
self.button.disable(ctx);
|
||||
|
@ -75,6 +75,7 @@ include_icon!(ICON_PAGE_UP, "model_mercury/res/page_up20.toif");
|
||||
|
||||
// 24x24
|
||||
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_DOWNLOAD, "model_mercury/res/download24.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 {
|
||||
normal: &ButtonStyle {
|
||||
font: Font::MONO,
|
||||
text_color: FG,
|
||||
button_color: BG,
|
||||
icon_color: GREY_LIGHT,
|
||||
background_color: BG,
|
||||
font: Font::DEMIBOLD,
|
||||
text_color: GREEN_LIME,
|
||||
button_color: GREEN_LIGHT,
|
||||
icon_color: GREEN_LIME,
|
||||
background_color: GREEN_DARK,
|
||||
},
|
||||
active: &ButtonStyle {
|
||||
font: Font::MONO,
|
||||
text_color: FG,
|
||||
button_color: BG,
|
||||
icon_color: GREY_LIGHT,
|
||||
background_color: BG,
|
||||
font: Font::DEMIBOLD,
|
||||
text_color: GREEN_DARK,
|
||||
button_color: GREEN_LIGHT,
|
||||
icon_color: GREEN_DARK,
|
||||
background_color: GREEN_LIGHT,
|
||||
},
|
||||
// used in SLIP-39 recovery for "*"
|
||||
disabled: &ButtonStyle {
|
||||
font: Font::MONO,
|
||||
text_color: FG,
|
||||
font: Font::DEMIBOLD,
|
||||
text_color: GREY_LIGHT,
|
||||
button_color: BG,
|
||||
icon_color: GREY_LIGHT,
|
||||
icon_color: BG,
|
||||
background_color: BG,
|
||||
},
|
||||
}
|
||||
@ -605,24 +607,52 @@ pub const fn button_bip39_autocomplete() -> ButtonStyleSheet {
|
||||
pub const fn button_suggestion_confirm() -> ButtonStyleSheet {
|
||||
ButtonStyleSheet {
|
||||
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,
|
||||
button_color: GREEN,
|
||||
button_color: GREY_EXTRA_DARK,
|
||||
icon_color: GREY_LIGHT,
|
||||
background_color: BG,
|
||||
},
|
||||
active: &ButtonStyle {
|
||||
font: Font::MONO,
|
||||
text_color: FG,
|
||||
button_color: GREEN_DARK,
|
||||
icon_color: GREY_LIGHT,
|
||||
background_color: BG,
|
||||
font: Font::DEMIBOLD,
|
||||
text_color: BG,
|
||||
button_color: FG,
|
||||
icon_color: BG,
|
||||
background_color: FG,
|
||||
},
|
||||
// not used
|
||||
disabled: &ButtonStyle {
|
||||
font: Font::MONO,
|
||||
text_color: GREY_LIGHT,
|
||||
button_color: GREY_DARK,
|
||||
icon_color: GREY_LIGHT,
|
||||
font: Font::DEMIBOLD,
|
||||
text_color: BG,
|
||||
button_color: BG,
|
||||
icon_color: BG,
|
||||
background_color: BG,
|
||||
},
|
||||
}
|
||||
@ -631,24 +661,25 @@ pub const fn button_suggestion_confirm() -> ButtonStyleSheet {
|
||||
pub const fn button_suggestion_autocomplete() -> ButtonStyleSheet {
|
||||
ButtonStyleSheet {
|
||||
normal: &ButtonStyle {
|
||||
font: Font::MONO,
|
||||
text_color: GREY_LIGHT,
|
||||
button_color: GREY_DARK, // same as PIN buttons
|
||||
icon_color: GREY_LIGHT,
|
||||
font: Font::DEMIBOLD,
|
||||
text_color: GREY,
|
||||
button_color: BG,
|
||||
icon_color: BG,
|
||||
background_color: BG,
|
||||
},
|
||||
active: &ButtonStyle {
|
||||
font: Font::MONO,
|
||||
text_color: FG,
|
||||
button_color: GREEN_DARK,
|
||||
icon_color: GREY_LIGHT,
|
||||
background_color: BG,
|
||||
text_color: BG,
|
||||
button_color: FG,
|
||||
icon_color: BG,
|
||||
background_color: FG,
|
||||
},
|
||||
// not used
|
||||
disabled: &ButtonStyle {
|
||||
font: Font::MONO,
|
||||
text_color: GREY_LIGHT,
|
||||
text_color: BG,
|
||||
button_color: BG,
|
||||
icon_color: GREY_LIGHT,
|
||||
icon_color: BG,
|
||||
background_color: BG,
|
||||
},
|
||||
}
|
||||
|
@ -34,7 +34,7 @@ async def request_word_count(dry_run: bool) -> int:
|
||||
async def request_word(
|
||||
word_index: int, word_count: int, is_slip39: bool, prefill_word: 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
|
||||
if is_slip39:
|
||||
keyboard = RustLayout(
|
||||
|
Loading…
Reference in New Issue
Block a user