From b4c283230e66148fc9f7b07d54ff333a5f3ccc4d Mon Sep 17 00:00:00 2001 From: grdddj Date: Wed, 2 Nov 2022 14:49:43 +0100 Subject: [PATCH] WIP - designs for smaller screen --- core/SConscript.firmware | 4 +- core/SConscript.unix | 4 +- core/assets/model_r/delete.png | Bin 0 -> 135 bytes core/assets/model_r/eye.png | Bin 0 -> 129 bytes core/assets/model_r/tick.png | Bin 0 -> 117 bytes core/embed/extmod/modtrezorui/display-unix.c | 2 +- core/embed/extmod/modtrezorui/display_defs.h | 10 +- .../{ => fonts}/font_unifont_bold_16.c | 2 +- .../{ => fonts}/font_unifont_bold_16.h | 3 + .../{ => fonts}/font_unifont_regular_16.c | 0 .../{ => fonts}/font_unifont_regular_16.h | 3 + core/embed/rust/src/ui/display/mod.rs | 16 +- core/embed/rust/src/ui/geometry.rs | 4 + .../rust/src/ui/model_tr/component/bip39.rs | 37 +- .../rust/src/ui/model_tr/component/button.rs | 12 +- .../model_tr/component/button_controller.rs | 16 +- .../rust/src/ui/model_tr/component/choice.rs | 183 ++++++-- .../src/ui/model_tr/component/choice_item.rs | 406 ++++++------------ .../rust/src/ui/model_tr/component/common.rs | 48 +-- .../rust/src/ui/model_tr/component/flow.rs | 10 +- .../src/ui/model_tr/component/flow_pages.rs | 16 +- .../component/flow_pages_poc_helpers.rs | 28 +- .../rust/src/ui/model_tr/component/frame.rs | 17 +- .../rust/src/ui/model_tr/component/mod.rs | 2 +- .../src/ui/model_tr/component/passphrase.rs | 37 +- .../rust/src/ui/model_tr/component/pin.rs | 59 ++- .../ui/model_tr/component/simple_choice.rs | 5 +- core/embed/rust/src/ui/model_tr/constant.rs | 2 +- core/embed/rust/src/ui/model_tr/layout.rs | 130 +++--- .../rust/src/ui/model_tr/res/delete.toif | Bin 0 -> 39 bytes core/embed/rust/src/ui/model_tr/res/eye.toif | Bin 0 -> 37 bytes core/embed/rust/src/ui/model_tr/res/tick.toif | Bin 0 -> 29 bytes core/embed/rust/src/ui/model_tr/theme.rs | 31 +- core/mocks/generated/trezorui2.pyi | 2 - core/src/apps/homescreen/__init__.py | 6 +- core/src/apps/homescreen/tr.py | 20 +- core/src/trezor/ui/layouts/tr/__init__.py | 66 +-- core/src/trezor/ui/layouts/tr/recovery.py | 16 +- core/src/trezor/ui/layouts/tr/reset.py | 23 +- core/tools/codegen/gen_font.py | 6 +- 40 files changed, 562 insertions(+), 664 deletions(-) create mode 100644 core/assets/model_r/delete.png create mode 100644 core/assets/model_r/eye.png create mode 100644 core/assets/model_r/tick.png rename core/embed/extmod/modtrezorui/{ => fonts}/font_unifont_bold_16.c (99%) rename core/embed/extmod/modtrezorui/{ => fonts}/font_unifont_bold_16.h (64%) rename core/embed/extmod/modtrezorui/{ => fonts}/font_unifont_regular_16.c (100%) rename core/embed/extmod/modtrezorui/{ => fonts}/font_unifont_regular_16.h (63%) create mode 100644 core/embed/rust/src/ui/model_tr/res/delete.toif create mode 100644 core/embed/rust/src/ui/model_tr/res/eye.toif create mode 100644 core/embed/rust/src/ui/model_tr/res/tick.toif diff --git a/core/SConscript.firmware b/core/SConscript.firmware index 9d6b79670..ef1e316f9 100644 --- a/core/SConscript.firmware +++ b/core/SConscript.firmware @@ -25,8 +25,8 @@ PYOPT = ARGUMENTS.get('PYOPT', '1') FROZEN = True if TREZOR_MODEL in ('1', 'R'): - FONT_NORMAL='Font_PixelOperator_Regular_8' - FONT_DEMIBOLD=None + FONT_NORMAL='Font_Unifont_Regular_16' + FONT_DEMIBOLD='Font_Unifont_Bold_16' FONT_BOLD='Font_PixelOperator_Bold_8' FONT_MONO='Font_PixelOperatorMono_Regular_8' if TREZOR_MODEL in ('T', ): diff --git a/core/SConscript.unix b/core/SConscript.unix index d096d0801..4e3c19985 100644 --- a/core/SConscript.unix +++ b/core/SConscript.unix @@ -26,8 +26,8 @@ FROZEN = ARGUMENTS.get('TREZOR_EMULATOR_FROZEN', 0) RASPI = os.getenv('TREZOR_EMULATOR_RASPI') == '1' if TREZOR_MODEL in ('1', 'R'): - FONT_NORMAL='Font_PixelOperator_Regular_8' - FONT_DEMIBOLD='Font_PixelOperator_Regular_8' + FONT_NORMAL='Font_Unifont_Regular_16' + FONT_DEMIBOLD='Font_Unifont_Bold_16' FONT_BOLD='Font_PixelOperator_Bold_8' FONT_MONO='Font_PixelOperatorMono_Regular_8' if TREZOR_MODEL in ('T', ): diff --git a/core/assets/model_r/delete.png b/core/assets/model_r/delete.png new file mode 100644 index 0000000000000000000000000000000000000000..2de55e4cde1bfa30e9e507c2331691dad206d290 GIT binary patch literal 135 zcmeAS@N?(olHy`uVBq!ia0vp^AT|dFkYIQ^(J%{0aTa()7Bet#3xhBt!>l;IL d3wysX(49r#3;TlWyMQ_vJYD@<);T3K0RS(sBNzYx literal 0 HcmV?d00001 diff --git a/core/assets/model_r/eye.png b/core/assets/model_r/eye.png new file mode 100644 index 0000000000000000000000000000000000000000..c3068447a00e5fb0d78bf620a5c532194add8a47 GIT binary patch literal 129 zcmeAS@N?(olHy`uVBq!ia0vp^JV4CG0VEhMwJO8{Db50q$YKTtZeb8+WSBKa0w~B{ z;_2(k{*0SZkXLNJHJboXNWs&^F@)oKa)JZf|G*jl16%$pPv+tI4+LDnOv1?w3>UBR VKVRvgaT=(W!PC{xWt~$(697`YAWZ-O literal 0 HcmV?d00001 diff --git a/core/assets/model_r/tick.png b/core/assets/model_r/tick.png new file mode 100644 index 0000000000000000000000000000000000000000..cec7f4402d3d81cd1ffc806da4cd504cd74054fb GIT binary patch literal 117 zcmeAS@N?(olHy`uVBq!ia0vp^96-zlA{cJxHK+qA&H|6fVg?3oVGw3ym^DWND9B#o z>Fdh=jGIxAL+!}aLqCB+BAzaeAsp9}6B-)+N4)r7@UPyalHq6p`_Jn;oPPl289ZJ6 KT-G@yGywo; i32 { display::backlight(-1) @@ -113,7 +113,8 @@ pub fn icon(center: Point, data: &[u8], fg_color: Color, bg_color: Color) { icon_rect(r, toif_data, fg_color, bg_color); } -/// Display icon at a specified Rectangle, expects already sliced data without header. +/// Display icon at a specified Rectangle, expects already sliced data without +/// header. pub fn icon_rect(r: Rect, toif_data: &[u8], fg_color: Color, bg_color: Color) { display::icon( r.x0, @@ -211,6 +212,7 @@ pub fn rect_outline_rounded(r: Rect, fg_color: Color, bg_color: Color, radius: u if radius == 1 { rect_fill_rounded(r, fg_color, bg_color, 1); rect_fill(inner_r, bg_color); + rect_fill_corners(inner_r, fg_color); } else if radius == 2 { rect_fill_rounded(r, fg_color, bg_color, 2); rect_fill_rounded(inner_r, bg_color, fg_color, 1); diff --git a/core/embed/rust/src/ui/geometry.rs b/core/embed/rust/src/ui/geometry.rs index 19cfdc8e7..ca5b07cbd 100644 --- a/core/embed/rust/src/ui/geometry.rs +++ b/core/embed/rust/src/ui/geometry.rs @@ -309,6 +309,10 @@ impl Rect { self.bottom_left().center(self.bottom_right()) } + pub const fn top_center(&self) -> Point { + self.top_left().center(self.top_right()) + } + pub const fn left_center(&self) -> Point { self.bottom_left().center(self.top_left()) } diff --git a/core/embed/rust/src/ui/model_tr/component/bip39.rs b/core/embed/rust/src/ui/model_tr/component/bip39.rs index cf05723b8..2193b6f9f 100644 --- a/core/embed/rust/src/ui/model_tr/component/bip39.rs +++ b/core/embed/rust/src/ui/model_tr/component/bip39.rs @@ -3,12 +3,13 @@ use crate::{ ui::{ component::{text::common::TextBox, Child, Component, ComponentExt, Event, EventCtx}, geometry::Rect, + util::char_to_string, }, }; use super::{ - choice_item::BigCharacterChoiceItem, ButtonDetails, ButtonLayout, ChangingTextLine, - ChoiceFactory, ChoiceItem, ChoicePage, ChoicePageMsg, TextChoiceItem, + ButtonDetails, ButtonLayout, ChangingTextLine, ChoiceFactory, ChoiceItem, ChoicePage, + ChoicePageMsg, }; use heapless::{String, Vec}; @@ -16,14 +17,14 @@ pub enum Bip39EntryMsg { ResultWord(String<15>), } -const CURRENT_LETTERS_ROW: i32 = 25; - const MAX_LENGTH: usize = 10; const MAX_CHOICE_LENGTH: usize = 26; /// Offer words when there will be fewer of them than this const OFFER_WORDS_THRESHOLD: usize = 10; +const PROMPT: &str = "_"; + struct ChoiceFactoryBIP39 { // TODO: replace these Vecs by iterators somehow? letter_choices: Option>, @@ -52,13 +53,14 @@ impl ChoiceFactoryBIP39 { fn get_word_item(&self, choice_index: u8) -> ChoiceItem { if let Some(word_choices) = &self.word_choices { let word = word_choices[choice_index as usize]; - let choice = TextChoiceItem::new(word, ButtonLayout::default_three_icons()); - let mut word_item = ChoiceItem::Text(choice); + let mut word_item = ChoiceItem::new(word, ButtonLayout::default_three_icons()); - // Adding BIN leftmost button and removing the rightmost one. + // Adding BIN leftmost button. if choice_index == 0 { word_item.set_left_btn(Some(ButtonDetails::bin_icon())); - } else if choice_index as usize == word_choices.len() - 1 { + } + // Removing the rightmost button. + if choice_index as usize == word_choices.len() - 1 { word_item.set_right_btn(None); } @@ -75,14 +77,17 @@ impl ChoiceFactoryBIP39 { // user-friendly) if let Some(letter_choices) = &self.letter_choices { let letter = letter_choices[choice_index as usize]; - let letter_choice = - BigCharacterChoiceItem::new(letter, ButtonLayout::default_three_icons()); - let mut letter_item = ChoiceItem::BigCharacter(letter_choice); + let mut letter_item = ChoiceItem::new( + char_to_string::<1>(letter), + ButtonLayout::default_three_icons(), + ); - // Adding BIN leftmost button and removing the rightmost one. + // Adding BIN leftmost button. if choice_index == 0 { letter_item.set_left_btn(Some(ButtonDetails::bin_icon())); - } else if choice_index as usize == letter_choices.len() - 1 { + } + // Removing the rightmost button. + if choice_index as usize == letter_choices.len() - 1 { letter_item.set_right_btn(None); } @@ -131,8 +136,8 @@ impl Bip39Entry { let choices = ChoiceFactoryBIP39::letters(letter_choices.clone()); Self { - choice_page: ChoicePage::new(choices), - chosen_letters: Child::new(ChangingTextLine::center_mono(String::new())), + choice_page: ChoicePage::new(choices).with_incomplete(), + chosen_letters: Child::new(ChangingTextLine::center_mono(String::from(PROMPT))), letter_choices, textbox: TextBox::empty(), offer_words: false, @@ -159,7 +164,7 @@ impl Bip39Entry { } fn update_chosen_letters(&mut self, ctx: &mut EventCtx) { - let text = build_string!({ MAX_LENGTH + 1 }, self.textbox.content(), "_"); + let text = build_string!({ MAX_LENGTH + 1 }, self.textbox.content(), PROMPT); self.chosen_letters.inner_mut().update_text(text); self.chosen_letters.request_complete_repaint(ctx); } diff --git a/core/embed/rust/src/ui/model_tr/component/button.rs b/core/embed/rust/src/ui/model_tr/component/button.rs index 7615bd46f..393ceafb5 100644 --- a/core/embed/rust/src/ui/model_tr/component/button.rs +++ b/core/embed/rust/src/ui/model_tr/component/button.rs @@ -233,10 +233,18 @@ where text_color, background_color, ); + display::rect_fill_corners(area_to_fill, theme::BG); } else if style.with_outline { - display::rect_outline_rounded(area, text_color, background_color, 2); + if background_color == theme::BG { + display::rect_outline_rounded(area, text_color, background_color, 2); + } else { + // With inverse colors having just radius of one, `rect_outline_rounded` + // is not suitable for inverse colors. + display::rect_fill(area, background_color); + display::rect_fill_corners(area, theme::BG); + } } else { - display::rect_fill(area, background_color) + display::rect_fill(area, background_color); } match &self.content { diff --git a/core/embed/rust/src/ui/model_tr/component/button_controller.rs b/core/embed/rust/src/ui/model_tr/component/button_controller.rs index eebbb59ef..5c6da78f5 100644 --- a/core/embed/rust/src/ui/model_tr/component/button_controller.rs +++ b/core/embed/rust/src/ui/model_tr/component/button_controller.rs @@ -23,6 +23,7 @@ enum ButtonState { } pub enum ButtonControllerMsg { + Pressed(ButtonPos), Triggered(ButtonPos), } @@ -275,16 +276,18 @@ impl> Component for ButtonController { let (new_state, event) = match self.state { ButtonState::Nothing => match button { ButtonEvent::ButtonPressed(which) => { - match which { + let event = match which { PhysicalButton::Left => { self.left_btn.hold_started(ctx); + Some(ButtonControllerMsg::Pressed(ButtonPos::Left)) } PhysicalButton::Right => { self.right_btn.hold_started(ctx); + Some(ButtonControllerMsg::Pressed(ButtonPos::Right)) } - _ => {} - } - (ButtonState::OneDown(which), None) + _ => None, + }; + (ButtonState::OneDown(which), event) } _ => (self.state, None), }, @@ -313,7 +316,10 @@ impl> Component for ButtonController { ButtonEvent::ButtonPressed(b) if b != which_down => { self.middle_hold_started(ctx); - (ButtonState::BothDown, None) + ( + ButtonState::BothDown, + Some(ButtonControllerMsg::Pressed(ButtonPos::Middle)), + ) } _ => (self.state, None), }, diff --git a/core/embed/rust/src/ui/model_tr/component/choice.rs b/core/embed/rust/src/ui/model_tr/component/choice.rs index 48626a426..312b1502a 100644 --- a/core/embed/rust/src/ui/model_tr/component/choice.rs +++ b/core/embed/rust/src/ui/model_tr/component/choice.rs @@ -3,7 +3,7 @@ use crate::ui::{ geometry::Rect, }; -use super::{theme, ButtonController, ButtonControllerMsg, ButtonPos, ChoiceItem, ChoiceItemAPI}; +use super::{theme, ButtonController, ButtonControllerMsg, ButtonPos, ChoiceItem}; pub enum ChoicePageMsg { Choice(u8), @@ -11,7 +11,8 @@ pub enum ChoicePageMsg { RightMost, } -const MIDDLE_ROW: i32 = 72; +const DEFAULT_ITEMS_DISTANCE: i16 = 10; +const DEFAULT_Y_BASELINE: i16 = 20; /// Interface for a specific component efficiently giving /// `ChoicePage` all the information it needs to render @@ -48,7 +49,18 @@ where pad: Pad, buttons: Child>, page_counter: u8, + /// How many pixels from top should we render the items. + y_baseline: i16, + /// How many pixels are between the items. + items_distance: i16, + /// Whether the choice page is "infinite" (carousel). is_carousel: bool, + /// Whether we should show items on left/right even when they cannot + /// be painted entirely (they would be cut off). + show_incomplete: bool, + /// Whether the middle selected item should be painted with + /// inverse colors - black on white. + inverse_selected_item: bool, } impl ChoicePage @@ -63,7 +75,11 @@ where pad: Pad::with_background(theme::BG), buttons: Child::new(ButtonController::new(initial_btn_layout)), page_counter: 0, + y_baseline: DEFAULT_Y_BASELINE, + items_distance: DEFAULT_ITEMS_DISTANCE, is_carousel: false, + show_incomplete: false, + inverse_selected_item: false, } } @@ -79,9 +95,30 @@ where self } + /// Show incomplete items, even when they cannot render in their entirety. + pub fn with_incomplete(mut self) -> Self { + self.show_incomplete = true; + self + } + + /// Adjust the horizontal baseline from the top of placement. + pub fn with_y_baseline(mut self, y_baseline: i16) -> Self { + self.y_baseline = y_baseline; + self + } + + /// Adjust the distance between the items. + pub fn with_items_distance(mut self, items_distance: i16) -> Self { + self.items_distance = items_distance; + self + } + /// Resetting the component, which enables reusing the same instance /// for multiple choice categories. /// + /// Used for example in passphrase, where there are multiple categories of + /// characters. + /// /// NOTE: from the client point of view, it would also be an option to /// always create a new instance with fresh setup, but I could not manage to /// properly clean up the previous instance - it would still be shown on @@ -97,8 +134,8 @@ where if reset_page_counter { self.page_counter = 0; } - self.update(ctx); self.is_carousel = is_carousel; + self.update(ctx); } /// Navigating to the chosen page index. @@ -107,24 +144,26 @@ where self.update(ctx); } - /// Display current, previous and next choice according to + /// Display current, previous and next choices according to /// the current ChoiceItem. fn paint_choices(&mut self) { - // Performing the appropriate `paint_XXX()` for the main choice - // and two adjacent choices when present - // In case of carousel mode, also showing the ones from other end. - self.show_current_choice(); + let available_area = self.pad.area.split_top(self.y_baseline).0; - if self.has_previous_choice() { - self.show_previous_choice(); - } else if self.is_carousel { - self.show_last_choice_on_left(); + // Drawing the current item in the middle. + self.show_current_choice(available_area); + + // Getting the remaining left and right areas. + let (left_area, _center_area, right_area) = + available_area.split_center(self.current_choice().width_center()); + + // Possibly drawing on the left side. + if self.has_previous_choice() || self.is_carousel { + self.show_left_choices(left_area); } - if self.has_next_choice() { - self.show_next_choice(); - } else if self.is_carousel { - self.show_first_choice_on_right(); + // Possibly drawing on the right side. + if self.has_next_choice() || self.is_carousel { + self.show_right_choices(right_area); } } @@ -140,62 +179,139 @@ where ctx.request_paint(); } + /// Index of the last page. fn last_page_index(&self) -> u8 { self.choices.count() as u8 - 1 } + /// Whether there is a previous choice (on the left). pub fn has_previous_choice(&self) -> bool { self.page_counter > 0 } + /// Whether there is a next choice (on the right). pub fn has_next_choice(&self) -> bool { self.page_counter < self.last_page_index() } + /// Gets choice at the current page index. fn current_choice(&self) -> ChoiceItem { self.get_choice(self.page_counter) } + /// Gets choice at the given page index. fn get_choice(&self, index: u8) -> ChoiceItem { self.choices.get(index) } - fn show_current_choice(&self) { - self.current_choice().paint_center(); + /// Display the current choice in the middle. + fn show_current_choice(&mut self, area: Rect) { + self.current_choice() + .paint_center(area, self.inverse_selected_item); + + // Color inversion is just one-time thing. + if self.inverse_selected_item { + self.inverse_selected_item = false; + } } - fn show_previous_choice(&self) { - self.get_choice(self.page_counter - 1).paint_left(); + /// Display all the choices fitting on the left side. + /// Going as far as possible. + fn show_left_choices(&self, area: Rect) { + let mut page_index = self.page_counter as i16 - 1; + let mut x_offset = 0; + loop { + // Breaking out of the loop if we exhausted left items + // and the carousel mode is not enabled. + if page_index < 0 { + if self.is_carousel { + // Moving to the last page. + page_index = self.last_page_index() as i16; + } else { + break; + } + } + + let current_choice = self.get_choice(page_index as u8); + let current_area = area.split_right(x_offset + self.items_distance).0; + + // When the item does not fit, we stop. + // Rendering the item anyway if the incomplete items are allowed. + if !current_choice.fits(current_area) { + if self.show_incomplete { + current_choice.paint_left(current_area); + } + break; + } + + // Rendering the item. + current_choice.paint_left(current_area); + + // Updating loop variables. + x_offset += current_choice.width_side() + self.items_distance; + page_index -= 1; + } } - fn show_next_choice(&self) { - self.get_choice(self.page_counter + 1).paint_right(); - } - - fn show_last_choice_on_left(&self) { - self.get_choice(self.last_page_index()).paint_left(); - } - - fn show_first_choice_on_right(&self) { - self.get_choice(0).paint_right(); + /// Display all the choices fitting on the right side. + /// Going as far as possible. + fn show_right_choices(&self, area: Rect) { + let mut page_index = self.page_counter + 1; + let mut x_offset = 3; // starts with a little offset to account for the middle highlight + loop { + // Breaking out of the loop if we exhausted right items + // and the carousel mode is not enabled. + if page_index > self.last_page_index() { + if self.is_carousel { + // Moving to the first page. + page_index = 0; + } else { + break; + } + } + + let current_choice = self.get_choice(page_index); + let current_area = area.split_left(x_offset + self.items_distance).1; + + // When the item does not fit, we stop. + // Rendering the item anyway if the incomplete items are allowed. + if !current_choice.fits(current_area) { + if self.show_incomplete { + current_choice.paint_right(current_area); + } + break; + } + + // Rendering the item. + current_choice.paint_right(current_area); + + // Updating loop variables. + x_offset += current_choice.width_side() + self.items_distance; + page_index += 1; + } } + /// Decrease the page counter to the previous page. fn decrease_page_counter(&mut self) { self.page_counter -= 1; } + /// Advance page counter to the next page. fn increase_page_counter(&mut self) { self.page_counter += 1; } + /// Set page to the first one. fn page_counter_to_zero(&mut self) { self.page_counter = 0; } + /// Set page to the last one. fn page_counter_to_max(&mut self) { self.page_counter = self.last_page_index(); } + /// Get current page counter. pub fn page_index(&self) -> u8 { self.page_counter } @@ -238,6 +354,7 @@ where fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { let button_event = self.buttons.event(ctx, event); + // Button was "triggered" - released. Doing the appropriate action. if let Some(ButtonControllerMsg::Triggered(pos)) = button_event { match pos { ButtonPos::Left => { @@ -277,6 +394,12 @@ where } } }; + // The middle button was "pressed", highlighting the current choice by color + // inversion. + if let Some(ButtonControllerMsg::Pressed(ButtonPos::Middle)) = button_event { + self.inverse_selected_item = true; + self.clear(ctx); + }; None } diff --git a/core/embed/rust/src/ui/model_tr/component/choice_item.rs b/core/embed/rust/src/ui/model_tr/component/choice_item.rs index c192d0236..511945dc4 100644 --- a/core/embed/rust/src/ui/model_tr/component/choice_item.rs +++ b/core/embed/rust/src/ui/model_tr/component/choice_item.rs @@ -1,338 +1,172 @@ -use crate::ui::{geometry::Point, display::Font, util::char_to_string}; +use crate::ui::{ + display::{rect_fill, rect_fill_corners, rect_outline_rounded, Font, Icon}, + geometry::{Offset, Rect}, + model_tr::theme, +}; use heapless::String; use super::{ - common::{display, display_center, display_right}, + common::{display, display_inverse, display_right}, ButtonDetails, ButtonLayout, }; -const MIDDLE_ROW: i16 = 61; -const LEFT_COL: i16 = 1; -const MIDDLE_COL: i16 = 64; -const RIGHT_COL: i16 = 127; - -/// Helper to unite the row height. -fn row_height() -> i16 { - // It never reaches the maximum height - Font::NORMAL.line_height() - 4 -} - -/// Component that can be used as a choice item. -/// Allows to have a choice of anything that can be painted on screen. -/// -/// Controls the painting of the current, previous and next item -/// through `paint_XXX()` methods. -/// Defines the behavior of all three buttons through `btn_XXX` attributes. -/// -/// Possible implementations: -/// - [x] `TextChoiceItem` - for regular text -/// - [x] `MultilineTextChoiceItem` - for multiline text -/// - [x] `BigCharacterChoiceItem` - for one big character -/// - [ ] `IconChoiceItem` - for showing icons -/// - [ ] `JustCenterChoice` - paint_left() and paint_right() show nothing -/// - [ ] `LongStringsChoice` - paint_left() and paint_right() show ellipsis -pub trait ChoiceItemAPI { - fn paint_center(&mut self); - fn paint_left(&mut self); - fn paint_right(&mut self); - fn btn_layout(&self) -> ButtonLayout<&'static str>; -} - -// TODO: consider having -// pub trait ChoiceItemOperations {} - -// TODO: consider storing all the text components as `T: AsRef` -// Tried, but it makes the code unnecessarily messy with all the -// definitions, which needs to be added to all the components using it. - -/// Storing all the possible implementations of `ChoiceItemAPI`. -/// Done like this as we want to use multiple different choice pages -/// at the same time in `ChoicePage` - for example Multiline and BigLetters +/// Simple string component used as a choice item. #[derive(Clone)] -pub enum ChoiceItem { - Text(TextChoiceItem), - MultilineText(MultilineTextChoiceItem), - BigCharacter(BigCharacterChoiceItem), +pub struct ChoiceItem { + text: String<50>, + icon: Option, + btn_layout: ButtonLayout<&'static str>, + font: Font, } impl ChoiceItem { - // TODO: can we somehow avoid the repetitions here? - pub fn set_left_btn(&mut self, btn_left: Option>) { - match self { - ChoiceItem::Text(item) => item.btn_layout.btn_left = btn_left, - ChoiceItem::MultilineText(item) => item.btn_layout.btn_left = btn_left, - ChoiceItem::BigCharacter(item) => item.btn_layout.btn_left = btn_left, - } - } - - pub fn set_middle_btn(&mut self, btn_middle: Option>) { - match self { - ChoiceItem::Text(item) => item.btn_layout.btn_middle = btn_middle, - ChoiceItem::MultilineText(item) => item.btn_layout.btn_middle = btn_middle, - ChoiceItem::BigCharacter(item) => item.btn_layout.btn_middle = btn_middle, - } - } - - pub fn set_right_btn(&mut self, btn_right: Option>) { - match self { - ChoiceItem::Text(item) => item.btn_layout.btn_right = btn_right, - ChoiceItem::MultilineText(item) => item.btn_layout.btn_right = btn_right, - ChoiceItem::BigCharacter(item) => item.btn_layout.btn_right = btn_right, - } - } - - pub fn set_text(&mut self, text: String<50>) { - match self { - ChoiceItem::Text(item) => item.text = text, - ChoiceItem::MultilineText(item) => item.text = text, - ChoiceItem::BigCharacter(_) => { - panic!("No text setting for BigCharacter") - } - } - } -} - -impl ChoiceItemAPI for ChoiceItem { - fn paint_center(&mut self) { - match self { - ChoiceItem::Text(item) => item.paint_center(), - ChoiceItem::MultilineText(item) => item.paint_center(), - ChoiceItem::BigCharacter(item) => item.paint_center(), - } - } - - fn paint_left(&mut self) { - match self { - ChoiceItem::Text(item) => item.paint_left(), - ChoiceItem::MultilineText(item) => item.paint_left(), - ChoiceItem::BigCharacter(item) => item.paint_left(), - } - } - - fn paint_right(&mut self) { - match self { - ChoiceItem::Text(item) => item.paint_right(), - ChoiceItem::MultilineText(item) => item.paint_right(), - ChoiceItem::BigCharacter(item) => item.paint_right(), - } - } - - fn btn_layout(&self) -> ButtonLayout<&'static str> { - match self { - ChoiceItem::Text(item) => item.btn_layout(), - ChoiceItem::MultilineText(item) => item.btn_layout(), - ChoiceItem::BigCharacter(item) => item.btn_layout(), - } - } -} - -/// Simple string component used as a choice item. -#[derive(Clone)] -pub struct TextChoiceItem { - pub text: String<50>, - pub btn_layout: ButtonLayout<&'static str>, -} - -impl TextChoiceItem { pub fn new(text: T, btn_layout: ButtonLayout<&'static str>) -> Self where T: AsRef, { Self { text: String::from(text.as_ref()), + icon: None, btn_layout, - } - } -} - -impl ChoiceItemAPI for TextChoiceItem { - fn paint_center(&mut self) { - // Displaying the center choice lower than the rest, - // to make it more clear this is the current choice - // (and also the left and right ones do not collide with it) - display_center( - Point::new(MIDDLE_COL, MIDDLE_ROW + row_height()), - &self.text, - Font::NORMAL, - ); - } - - fn paint_left(&mut self) { - display( - Point::new(LEFT_COL, MIDDLE_ROW), - &self.text, - Font::NORMAL, - ); - } - - fn paint_right(&mut self) { - display_right( - Point::new(RIGHT_COL, MIDDLE_ROW), - &self.text, - Font::NORMAL, - ); - } - - fn btn_layout(&self) -> ButtonLayout<&'static str> { - self.btn_layout.clone() - } -} - -/// Multiline string component used as a choice item. -/// -/// Lines are delimited by '\n' character, unless specified explicitly. -#[derive(Clone)] -pub struct MultilineTextChoiceItem { - // Arbitrary chosen. TODO: agree on this - pub text: String<50>, - delimiter: char, - pub btn_layout: ButtonLayout<&'static str>, -} - -impl MultilineTextChoiceItem { - pub fn new(text: String<50>, btn_layout: ButtonLayout<&'static str>) -> Self { - Self { - text, - delimiter: '\n', - btn_layout, + font: theme::FONT_CHOICE_ITEMS, } } - /// Allows for changing the line delimiter to arbitrary char. - pub fn use_delimiter(mut self, delimiter: char) -> Self { - self.delimiter = delimiter; + /// Allows to add the icon. + pub fn with_icon(mut self, icon: Icon) -> Self { + self.icon = Some(icon); self } -} -// TODO: Make all the text be centered vertically - account for amount of lines. -impl ChoiceItemAPI for MultilineTextChoiceItem { - fn paint_center(&mut self) { - // Displaying the center choice lower than the rest, - // to make it more clear this is the current choice - for (index, line) in self.text.split(self.delimiter).enumerate() { - let offset = MIDDLE_ROW + index as i16 * row_height() + row_height(); - display_center(Point::new(MIDDLE_COL, offset), &line, Font::NORMAL); + /// Allows to change the font. + pub fn with_font(mut self, font: Font) -> Self { + self.font = font; + self + } + + /// Getting the text width in pixels. + pub fn text_width(&self) -> i16 { + self.font.text_width(&self.text) + } + + /// Getting the overall width in pixels when displayed in center. + /// That means both the icon and text will be shown. + pub fn width_center(&self) -> i16 { + let icon_width = if let Some(icon) = self.icon { + icon.width() + 2 + } else { + 0 + }; + self.text_width() + icon_width + } + + /// Getting the non-central width in pixels. + /// It will show an icon if defined, otherwise the text, not both. + pub fn width_side(&self) -> i16 { + if let Some(icon) = self.icon { + icon.width() + } else { + self.text_width() } } - fn paint_left(&mut self) { - for (index, line) in self.text.split(self.delimiter).enumerate() { - let offset = MIDDLE_ROW + index as i16 * row_height(); - display(Point::new(LEFT_COL, offset), &line, Font::NORMAL); - } + /// Whether the whole item fits into the given rectangle. + pub fn fits(&self, rect: Rect) -> bool { + self.width_side() <= rect.width() } - fn paint_right(&mut self) { - for (index, line) in self.text.split(self.delimiter).enumerate() { - let offset = MIDDLE_ROW + index as i16 * row_height(); - display_right(Point::new(RIGHT_COL, offset), &line, Font::NORMAL); - } - } - - fn btn_layout(&self) -> ButtonLayout<&'static str> { - self.btn_layout.clone() - } -} - -/// Choice item displaying single characters in BIG font. -/// Middle choice is magnified 4 times, left and right 2 times. -#[derive(Clone)] -pub struct BigCharacterChoiceItem { - pub ch: char, - pub btn_layout: ButtonLayout<&'static str>, -} - -impl BigCharacterChoiceItem { - pub fn new(ch: char, btn_layout: ButtonLayout<&'static str>) -> Self { - Self { ch, btn_layout } - } - - /// Taking the first character from the `text`. - pub fn from_str(text: T, btn_layout: ButtonLayout<&'static str>) -> Self - where - T: AsRef, - { - Self { - ch: text.as_ref().chars().next().unwrap(), - btn_layout, - } - } - - fn _paint_char(&mut self, baseline: Point) { - display( - baseline, - &char_to_string::<1>(self.ch), - Font::NORMAL, + /// Draws highlight around this choice item. + /// Must be called before the item is drawn, otherwise it will + /// cover the item. + pub fn paint_rounded_highlight(&self, area: Rect, inverse: bool) { + let bound = 3; + let left_bottom = + area.bottom_center() + Offset::new(-self.width_center() / 2 - bound, bound + 1); + let outline_size = Offset::new( + self.width_center() + 2 * bound, + self.font.text_height() + 2 * bound - 3, // -3 because font is actually smaller ); + let outline = Rect::from_bottom_left_and_size(left_bottom, outline_size); + if inverse { + rect_fill(outline, theme::FG); + rect_fill_corners(outline, theme::BG); + } else { + rect_outline_rounded(outline, theme::FG, theme::BG, 1); + } } -} + /// Painting the item as the main choice in the middle. + /// Showing both the icon and text, if the icon is available. + pub fn paint_center(&self, area: Rect, inverse: bool) { + self.paint_rounded_highlight(area, inverse); -impl ChoiceItemAPI for BigCharacterChoiceItem { - fn paint_center(&mut self) { - self._paint_char(Point::new(MIDDLE_COL - 12, MIDDLE_ROW + 9)); + let mut baseline = area.bottom_center() + Offset::new(-self.width_center() / 2, 0); + if let Some(icon) = self.icon { + let fg_color = if inverse { theme::BG } else { theme::FG }; + let bg_color = if inverse { theme::FG } else { theme::BG }; + icon.draw_bottom_left(baseline, fg_color, bg_color); + baseline = baseline + Offset::new(icon.width() + 2, 0); + } + if inverse { + display_inverse(baseline, &self.text, self.font); + } else { + display(baseline, &self.text, self.font); + } } - fn paint_left(&mut self) { - self._paint_char(Point::new(LEFT_COL, MIDDLE_ROW)); + /// Painting the item as a choice on the left side from center. + /// Showing only the icon, if available, otherwise the text. + pub fn paint_left(&self, area: Rect) { + if let Some(icon) = self.icon { + icon.draw_bottom_right(area.bottom_right(), theme::FG, theme::BG); + } else { + display_right(area.bottom_right(), &self.text, self.font); + } } - fn paint_right(&mut self) { - self._paint_char(Point::new(RIGHT_COL - 12, MIDDLE_ROW)); + /// Painting the item as a choice on the right side from center. + /// Showing only the icon, if available, otherwise the text. + pub fn paint_right(&self, area: Rect) { + if let Some(icon) = self.icon { + icon.draw_bottom_left(area.bottom_left(), theme::FG, theme::BG); + } else { + display(area.bottom_left(), &self.text, self.font); + } } - fn btn_layout(&self) -> ButtonLayout<&'static str> { + /// Getting current button layout. + pub fn btn_layout(&self) -> ButtonLayout<&'static str> { self.btn_layout.clone() } + + /// Setting left button. + pub fn set_left_btn(&mut self, btn_left: Option>) { + self.btn_layout.btn_left = btn_left; + } + + /// Setting middle button. + pub fn set_middle_btn(&mut self, btn_middle: Option>) { + self.btn_layout.btn_middle = btn_middle; + } + + /// Setting right button. + pub fn set_right_btn(&mut self, btn_right: Option>) { + self.btn_layout.btn_right = btn_right; + } + + /// Changing the text. + pub fn set_text(&mut self, text: String<50>) { + self.text = text; + } } #[cfg(feature = "ui_debug")] impl crate::trace::Trace for ChoiceItem { fn trace(&self, t: &mut dyn crate::trace::Tracer) { t.open("ChoiceItem"); - match self { - ChoiceItem::Text(item) => item.trace(t), - ChoiceItem::MultilineText(item) => item.trace(t), - ChoiceItem::BigCharacter(item) => item.trace(t), - } - t.close(); - } -} - -#[cfg(feature = "ui_debug")] -impl crate::trace::Trace for TextChoiceItem { - fn trace(&self, t: &mut dyn crate::trace::Tracer) { - t.open("TextChoiceItem"); t.content_flag(); t.string(&self.text); t.content_flag(); t.close(); } } - -#[cfg(feature = "ui_debug")] -use crate::ui::util; - -#[cfg(feature = "ui_debug")] -impl crate::trace::Trace for MultilineTextChoiceItem { - fn trace(&self, t: &mut dyn crate::trace::Tracer) { - t.open("MultilineTextChoiceItem"); - t.content_flag(); - t.string(&self.text); - t.content_flag(); - t.field("delimiter", &(util::char_to_string::<1>(self.delimiter))); - t.close(); - } -} - -#[cfg(feature = "ui_debug")] -impl crate::trace::Trace for BigCharacterChoiceItem { - fn trace(&self, t: &mut dyn crate::trace::Tracer) { - t.open("BigCharacterChoiceItem"); - t.content_flag(); - t.string(&util::char_to_string::<1>(self.ch)); - t.content_flag(); - t.close(); - } -} diff --git a/core/embed/rust/src/ui/model_tr/component/common.rs b/core/embed/rust/src/ui/model_tr/component/common.rs index 83933b9ba..696a752eb 100644 --- a/core/embed/rust/src/ui/model_tr/component/common.rs +++ b/core/embed/rust/src/ui/model_tr/component/common.rs @@ -1,30 +1,22 @@ use crate::ui::{ display::{self, Font, Icon}, - geometry::{Offset, Point}, - model_tr::constant, + geometry::{Offset, Point, Rect}, }; use heapless::String; use super::theme; -/// Display header text. -pub fn display_header>(baseline: Point, text: T) { - // TODO: make this centered? - display::text( - baseline, - text.as_ref(), - theme::FONT_HEADER, - theme::FG, - theme::BG, - ); -} - -/// Display bold white text on black background +/// Display white text on black background pub fn display>(baseline: Point, text: &T, font: Font) { display::text(baseline, text.as_ref(), font, theme::FG, theme::BG); } +/// Display black text on white background +pub fn display_inverse>(baseline: Point, text: &T, font: Font) { + display::text(baseline, text.as_ref(), font, theme::BG, theme::FG); +} + /// Display white text on black background, /// centered around a baseline Point pub fn display_center>(baseline: Point, text: &T, font: Font) { @@ -76,23 +68,19 @@ pub fn display_secret_center_top>(secret: T, offset_from_top: i16) } } -/// Display title and possible subtitle together with a dotted line spanning -/// the entire width. +/// Display title/header centered at the top of the given area. /// Returning the painted height of the whole header. -pub fn paint_header>(top_left: Point, title: T, subtitle: Option) -> i16 { +pub fn paint_header_centered>(title: T, area: Rect) -> i16 { let text_heigth = theme::FONT_HEADER.text_height(); - let title_baseline = top_left + Offset::y(text_heigth); - display_header(title_baseline, title); - // Optionally painting the subtitle as well - // (and offsetting the dotted line in that case) - let mut dotted_line_offset = text_heigth + 2; - if let Some(subtitle) = subtitle { - dotted_line_offset += text_heigth; - display_header(title_baseline + Offset::y(text_heigth), subtitle); - } - let line_start = top_left + Offset::y(dotted_line_offset); - display::dotted_line_horizontal(line_start, constant::WIDTH, theme::FG, 2); - dotted_line_offset + let title_baseline = area.top_center() + Offset::y(text_heigth); + display::text_center( + title_baseline, + title.as_ref(), + theme::FONT_HEADER, + theme::FG, + theme::BG, + ); + text_heigth } /// Draws icon and text on the same line - icon on the left. diff --git a/core/embed/rust/src/ui/model_tr/component/flow.rs b/core/embed/rust/src/ui/model_tr/component/flow.rs index 014a15f02..ef2a959eb 100644 --- a/core/embed/rust/src/ui/model_tr/component/flow.rs +++ b/core/embed/rust/src/ui/model_tr/component/flow.rs @@ -2,7 +2,7 @@ use crate::{ micropython::buffer::StrBuffer, ui::{ component::{Child, Component, Event, EventCtx, Pad}, - geometry::{Point, Rect}, + geometry::Rect, }, }; @@ -62,8 +62,7 @@ where if get_new_page { self.current_page = self.pages.get(self.page_counter); } - let content_area = self.content_area; - self.current_page.place(content_area); + self.current_page.place(self.content_area); self.set_buttons(ctx); self.clear(ctx); } @@ -203,12 +202,13 @@ where fn paint(&mut self) { // TODO: might put horizontal scrollbar at the top right + // (not compatible with longer/centered titles) self.pad.paint(); - self.buttons.paint(); if let Some(title) = &self.common_title { - common::paint_header(Point::zero(), title, None); + common::paint_header_centered(title, self.content_area); } self.current_page.paint(); + self.buttons.paint(); } } diff --git a/core/embed/rust/src/ui/model_tr/component/flow_pages.rs b/core/embed/rust/src/ui/model_tr/component/flow_pages.rs index a39579a8b..814c8665e 100644 --- a/core/embed/rust/src/ui/model_tr/component/flow_pages.rs +++ b/core/embed/rust/src/ui/model_tr/component/flow_pages.rs @@ -1,11 +1,11 @@ use crate::{ - micropython::{buffer::StrBuffer}, + micropython::buffer::StrBuffer, ui::{ component::Paginate, display::{Font, Icon, IconAndName}, geometry::{Offset, Rect}, model_tr::theme, - util::ResultExt + util::ResultExt, }, }; @@ -68,9 +68,13 @@ pub struct Page { // For `layout.rs` impl Page { - pub fn new(btn_layout: ButtonLayout<&'static str>, btn_actions: ButtonActions) -> Self { + pub fn new( + btn_layout: ButtonLayout<&'static str>, + btn_actions: ButtonActions, + initial_text_font: Font, + ) -> Self { let style = TextStyle::new( - Font::NORMAL, + initial_text_font, theme::FG, theme::BG, theme::FG, @@ -200,6 +204,10 @@ impl Page { self.font(Font::NORMAL).text(text) } + pub fn text_mono(self, text: StrBuffer) -> Self { + self.font(Font::MONO).text(text) + } + pub fn text_bold(self, text: StrBuffer) -> Self { self.font(Font::BOLD).text(text) } diff --git a/core/embed/rust/src/ui/model_tr/component/flow_pages_poc_helpers.rs b/core/embed/rust/src/ui/model_tr/component/flow_pages_poc_helpers.rs index 20e52e15c..ef9b65bae 100644 --- a/core/embed/rust/src/ui/model_tr/component/flow_pages_poc_helpers.rs +++ b/core/embed/rust/src/ui/model_tr/component/flow_pages_poc_helpers.rs @@ -1,11 +1,6 @@ //! Mostly copy-pasted stuff from ui/component/text, //! but with small modifications. -//! It is really mostly changing Op::Text(&'a str) to Op::Text(String<100>), -//! having self.ops as Vec and changes revolving around it. -//! Even if some stuff could be reused now, I copy-pasted it anyway, as this -//! extension for Icons, Offsets, etc. should no longer live in -//! ui/component/text, and so they can be freely removed (as they are here as -//! well). +//! (support for more Ops like icon drawing or arbitrary offsets) use crate::{ micropython::buffer::StrBuffer, @@ -33,7 +28,7 @@ impl ToDisplay { } } -/// Operations that can be done on FormattedText. +/// Operations that can be done on the screen. #[derive(Clone)] pub enum Op { /// Render text with current color and font. @@ -151,26 +146,11 @@ impl TextLayout { } } - pub fn with_bounds(mut self, bounds: Rect) -> Self { - self.bounds = bounds; - self - } - /// Baseline `Point` where we are starting to draw the text. pub fn initial_cursor(&self) -> Point { self.bounds.top_left() + Offset::y(self.style.text_font.text_height() + self.padding_top) } - /// Trying to fit the content on the current screen. - pub fn fit_text(&self, text: &str) -> LayoutFit { - self.layout_text(text, &mut self.initial_cursor(), &mut TextNoOp) - } - - /// Draw as much text as possible on the current screen. - pub fn render_text(&self, text: &str) { - self.layout_text(text, &mut self.initial_cursor(), &mut TextRenderer); - } - /// Y coordinate of the bottom of the available space/bounds pub fn bottom_y(&self) -> i16 { (self.bounds.y1 - self.padding_bottom).max(self.bounds.y0) @@ -433,7 +413,6 @@ impl TextLayout { cursor.x += icon.width() as i16; LayoutFit::Fitting { - // TODO: how to handle this? It could collide with "skip_first_n_bytes" processed_chars: 1, height: 0, // it should just draw on one line } @@ -467,9 +446,6 @@ impl LayoutFit { } } -// TODO: LayoutSink could support even things like drawing icons -// or making custom x or y offsets from any position - /// Visitor for text segment operations. /// Defines responses for certain kind of events encountered /// when processing the content. diff --git a/core/embed/rust/src/ui/model_tr/component/frame.rs b/core/embed/rust/src/ui/model_tr/component/frame.rs index 0570dff62..33fa741aa 100644 --- a/core/embed/rust/src/ui/model_tr/component/frame.rs +++ b/core/embed/rust/src/ui/model_tr/component/frame.rs @@ -4,12 +4,10 @@ use crate::ui::{ geometry::{Insets, Rect}, }; -/// Component for holding another component and displaying -/// a title and optionally a subtitle describing that child component. +/// Component for holding another component and displaying a title. pub struct Frame { area: Rect, title: U, - subtitle: Option, content: Child, } @@ -18,10 +16,9 @@ where T: Component, U: AsRef, { - pub fn new(title: U, subtitle: Option, content: T) -> Self { + pub fn new(title: U, content: T) -> Self { Self { title, - subtitle, area: Rect::zero(), content: Child::new(content), } @@ -40,11 +37,10 @@ where type Msg = T::Msg; fn place(&mut self, bounds: Rect) -> Rect { - // Depending on whether there is subtitle or not - let title_space = if self.subtitle.is_some() { 12 } else { 4 }; + const TITLE_SPACE: i16 = 4; let (title_area, content_area) = bounds.split_top(theme::FONT_HEADER.line_height()); - let content_area = content_area.inset(Insets::top(title_space)); + let content_area = content_area.inset(Insets::top(TITLE_SPACE)); self.area = title_area; self.content.place(content_area); @@ -56,7 +52,7 @@ where } fn paint(&mut self) { - common::paint_header(self.area.top_left(), &self.title, self.subtitle.as_ref()); + common::paint_header_centered(&self.title, self.area); self.content.paint(); } } @@ -70,9 +66,6 @@ where fn trace(&self, t: &mut dyn crate::trace::Tracer) { t.open("Frame"); t.title(self.title.as_ref()); - if let Some(ref subtitle) = self.subtitle { - t.title(subtitle.as_ref()); - } t.field("content", &self.content); t.close(); } diff --git a/core/embed/rust/src/ui/model_tr/component/mod.rs b/core/embed/rust/src/ui/model_tr/component/mod.rs index 9e0a2b692..5c0b679dc 100644 --- a/core/embed/rust/src/ui/model_tr/component/mod.rs +++ b/core/embed/rust/src/ui/model_tr/component/mod.rs @@ -32,7 +32,7 @@ pub use confirm::{HoldToConfirm, HoldToConfirmMsg}; pub use button_controller::{ButtonController, ButtonControllerMsg}; pub use changing_text::ChangingTextLine; pub use choice::{ChoiceFactory, ChoicePage, ChoicePageMsg}; -pub use choice_item::{ChoiceItem, ChoiceItemAPI, MultilineTextChoiceItem, TextChoiceItem}; +pub use choice_item::ChoiceItem; pub use dialog::{Dialog, DialogMsg}; pub use flow::{Flow, FlowMsg}; pub use flow_pages::{FlowPages, Page}; diff --git a/core/embed/rust/src/ui/model_tr/component/passphrase.rs b/core/embed/rust/src/ui/model_tr/component/passphrase.rs index 4c2539292..991b58f47 100644 --- a/core/embed/rust/src/ui/model_tr/component/passphrase.rs +++ b/core/embed/rust/src/ui/model_tr/component/passphrase.rs @@ -2,13 +2,16 @@ use crate::{ time::Duration, ui::{ component::{text::common::TextBox, Child, Component, ComponentExt, Event, EventCtx}, + display::Icon, geometry::Rect, + model_tr::theme, + util::char_to_string, }, }; use super::{ - choice_item::BigCharacterChoiceItem, ButtonDetails, ButtonLayout, ChangingTextLine, - ChoiceFactory, ChoiceItem, ChoicePage, ChoicePageMsg, MultilineTextChoiceItem, TextChoiceItem, + ButtonDetails, ButtonLayout, ChangingTextLine, ChoiceFactory, ChoiceItem, ChoicePage, + ChoicePageMsg, }; use heapless::String; @@ -44,9 +47,9 @@ const SPECIAL_SYMBOLS: [char; 30] = [ '{', '}', ',', '\'', '`', ';', '"', '~', '$', '^', '=', ]; const MENU_LENGTH: usize = 6; -const DEL_INDEX: usize = MENU_LENGTH - 1; +const DELETE_INDEX: usize = MENU_LENGTH - 1; const SHOW_INDEX: usize = MENU_LENGTH - 2; -const MENU: [&str; MENU_LENGTH] = ["abc", "ABC", "123", "*#_", "SHOW PASS", "DEL LAST CHAR"]; +const MENU: [&str; MENU_LENGTH] = ["abc", "ABC", "123", "*#_", "SHOW", "DELETE"]; /// Get a character at a specified index for a specified category. fn get_char(current_category: &ChoiceCategory, index: u8) -> char { @@ -104,10 +107,10 @@ impl ChoiceFactoryPassphrase { /// MENU choices with accept and cancel hold-to-confirm side buttons. fn get_menu_item(&self, choice_index: u8) -> ChoiceItem { let choice = MENU[choice_index as usize]; - let item = - MultilineTextChoiceItem::new(String::from(choice), ButtonLayout::default_three_icons()) - .use_delimiter(' '); - let mut menu_item = ChoiceItem::MultilineText(item); + let mut menu_item = ChoiceItem::new( + String::<50>::from(choice), + ButtonLayout::default_three_icons(), + ); // Including accept button on the left and cancel on the very right // TODO: could have some icons instead of the shortcut text @@ -121,6 +124,13 @@ impl ChoiceFactoryPassphrase { )); } + // Including icons for some items. + if choice_index == DELETE_INDEX as u8 { + menu_item = menu_item.with_icon(Icon::new(theme::ICON_DELETE)); + } else if choice_index == SHOW_INDEX as u8 { + menu_item = menu_item.with_icon(Icon::new(theme::ICON_EYE)); + } + menu_item } @@ -128,13 +138,10 @@ impl ChoiceFactoryPassphrase { /// return back fn get_character_item(&self, choice_index: u8) -> ChoiceItem { if is_menu_choice(&self.current_category, choice_index) { - let menu_choice = - TextChoiceItem::new("MENU", ButtonLayout::three_icons_middle_text("RETURN")); - ChoiceItem::Text(menu_choice) + ChoiceItem::new("MENU", ButtonLayout::three_icons_middle_text("RETURN")) } else { let ch = get_char(&self.current_category, choice_index); - let char_choice = BigCharacterChoiceItem::new(ch, ButtonLayout::default_three_icons()); - ChoiceItem::BigCharacter(char_choice) + ChoiceItem::new(char_to_string::<1>(ch), ButtonLayout::default_three_icons()) } } } @@ -251,7 +258,7 @@ impl Component for PassphraseEntry { match msg { // Going to new category, applying some action or returning the result Some(ChoicePageMsg::Choice(page_counter)) => match page_counter as usize { - DEL_INDEX => { + DELETE_INDEX => { self.delete_last_digit(ctx); self.update_passphrase_dots(ctx); ctx.request_paint(); @@ -324,7 +331,7 @@ impl crate::trace::Trace for PassphraseEntry { let current_index = self.choice_page.page_index() as usize; match &self.current_category { ChoiceCategory::Menu => match current_index { - DEL_INDEX => ButtonAction::Action("Del last char").string(), + DELETE_INDEX => ButtonAction::Action("Del last char").string(), SHOW_INDEX => ButtonAction::Action("Show pass").string(), _ => ButtonAction::select_item(MENU[current_index]), }, diff --git a/core/embed/rust/src/ui/model_tr/component/pin.rs b/core/embed/rust/src/ui/model_tr/component/pin.rs index c9d63b34c..620be91ef 100644 --- a/core/embed/rust/src/ui/model_tr/component/pin.rs +++ b/core/embed/rust/src/ui/model_tr/component/pin.rs @@ -3,13 +3,15 @@ use crate::{ trezorhal::random, ui::{ component::{text::common::TextBox, Child, Component, ComponentExt, Event, EventCtx}, + display::Icon, geometry::Rect, + model_tr::theme, }, }; use super::{ - choice_item::BigCharacterChoiceItem, ButtonDetails, ButtonLayout, ChangingTextLine, - ChoiceFactory, ChoiceItem, ChoicePage, ChoicePageMsg, MultilineTextChoiceItem, + ButtonDetails, ButtonLayout, ChangingTextLine, ChoiceFactory, ChoiceItem, ChoicePage, + ChoicePageMsg, }; use heapless::String; @@ -22,16 +24,14 @@ const MAX_PIN_LENGTH: usize = 50; const MAX_VISIBLE_DOTS: usize = 18; const MAX_VISIBLE_DIGITS: usize = 18; -const CHOICE_LENGTH: usize = 14; -const EXIT_INDEX: usize = 0; -const DELETE_INDEX: usize = 1; -const SHOW_INDEX: usize = 2; -const PROMPT_INDEX: usize = 3; +const CHOICE_LENGTH: usize = 13; +const DELETE_INDEX: usize = 0; +const SHOW_INDEX: usize = 1; +const PROMPT_INDEX: usize = 2; const CHOICES: [&str; CHOICE_LENGTH] = [ - "EXIT", "DELETE", - "SHOW PIN", - "PLACEHOLDER FOR THE PROMPT", + "SHOW", + "ENTER PIN", "0", "1", "2", @@ -56,32 +56,23 @@ impl ChoiceFactoryPIN { impl ChoiceFactory for ChoiceFactoryPIN { fn get(&self, choice_index: u8) -> ChoiceItem { - let choice = CHOICES[choice_index as usize]; + let choice_str = CHOICES[choice_index as usize]; - // Depending on whether it is a digit (one character) or a text. - // Digits are BIG, the rest is multiline. - let mut choice_item = if choice.len() == 1 { - let item = - BigCharacterChoiceItem::from_str(choice, ButtonLayout::default_three_icons()); - ChoiceItem::BigCharacter(item) - } else { - let item = MultilineTextChoiceItem::new( - String::from(choice), - ButtonLayout::default_three_icons(), - ) - .use_delimiter(' '); - ChoiceItem::MultilineText(item) - }; + let mut choice_item = ChoiceItem::new(choice_str, ButtonLayout::default_three_icons()); // Action buttons have different middle button text - if [EXIT_INDEX, DELETE_INDEX, SHOW_INDEX, PROMPT_INDEX].contains(&(choice_index as usize)) { + if [DELETE_INDEX, SHOW_INDEX, PROMPT_INDEX].contains(&(choice_index as usize)) { let confirm_btn = ButtonDetails::armed_text("CONFIRM"); choice_item.set_middle_btn(Some(confirm_btn)); } - // Changing the prompt text for the wanted one - if choice_index == PROMPT_INDEX as u8 { - choice_item.set_text(String::from(self.prompt.as_ref())); + // Adding icons for appropriate items + if choice_index == DELETE_INDEX as u8 { + choice_item = choice_item.with_icon(Icon::new(theme::ICON_DELETE)); + } else if choice_index == SHOW_INDEX as u8 { + choice_item = choice_item.with_icon(Icon::new(theme::ICON_EYE)); + } else if choice_index == PROMPT_INDEX as u8 { + choice_item = choice_item.with_icon(Icon::new(theme::ICON_TICK)); } choice_item @@ -106,7 +97,7 @@ impl PinEntry { Self { choice_page: ChoicePage::new(choices) - .with_initial_page_counter(3) + .with_initial_page_counter(PROMPT_INDEX as u8) .with_carousel(), pin_dots: Child::new(ChangingTextLine::center_mono(String::new())), show_real_pin: false, @@ -177,7 +168,6 @@ impl Component for PinEntry { if let Some(ChoicePageMsg::Choice(page_counter)) = msg { // Performing action under specific index or appending new digit match page_counter as usize { - EXIT_INDEX => return Some(PinEntryMsg::Cancelled), DELETE_INDEX => { self.delete_last_digit(ctx); self.update_pin_dots(ctx); @@ -194,8 +184,10 @@ impl Component for PinEntry { self.append_new_digit(ctx, page_counter); self.update_pin_dots(ctx); // Choosing any random digit to be shown next - let new_page_counter = - random::uniform_between(4, (CHOICE_LENGTH - 1) as u32); + let new_page_counter = random::uniform_between( + PROMPT_INDEX as u32 + 1, + (CHOICE_LENGTH - 1) as u32, + ); self.choice_page .set_page_counter(ctx, new_page_counter as u8); ctx.request_paint(); @@ -224,7 +216,6 @@ impl crate::trace::Trace for PinEntry { ButtonPos::Middle => { let current_index = self.choice_page.page_index() as usize; match current_index { - EXIT_INDEX => ButtonAction::Cancel.string(), DELETE_INDEX => ButtonAction::Action("Delete last digit").string(), SHOW_INDEX => ButtonAction::Action("Show PIN").string(), PROMPT_INDEX => ButtonAction::Confirm.string(), diff --git a/core/embed/rust/src/ui/model_tr/component/simple_choice.rs b/core/embed/rust/src/ui/model_tr/component/simple_choice.rs index c0a4821f6..c5485c882 100644 --- a/core/embed/rust/src/ui/model_tr/component/simple_choice.rs +++ b/core/embed/rust/src/ui/model_tr/component/simple_choice.rs @@ -3,7 +3,7 @@ use crate::ui::{ geometry::Rect, }; -use super::{ButtonLayout, ChoiceFactory, ChoiceItem, ChoicePage, ChoicePageMsg, TextChoiceItem}; +use super::{ButtonLayout, ChoiceFactory, ChoiceItem, ChoicePage, ChoicePageMsg}; use heapless::{String, Vec}; #[cfg(feature = "ui_debug")] @@ -32,8 +32,7 @@ where { fn get(&self, choice_index: u8) -> ChoiceItem { let text = &self.choices[choice_index as usize]; - let text_item = TextChoiceItem::new(text, ButtonLayout::default_three_icons()); - let mut choice_item = ChoiceItem::Text(text_item); + let mut choice_item = ChoiceItem::new(text, ButtonLayout::default_three_icons()); // Disabling prev/next buttons for the first/last choice. if choice_index == 0 { diff --git a/core/embed/rust/src/ui/model_tr/constant.rs b/core/embed/rust/src/ui/model_tr/constant.rs index dc5f341e5..a51b69cca 100644 --- a/core/embed/rust/src/ui/model_tr/constant.rs +++ b/core/embed/rust/src/ui/model_tr/constant.rs @@ -1,7 +1,7 @@ use crate::ui::geometry::{Offset, Point, Rect}; pub const WIDTH: i16 = 128; -pub const HEIGHT: i16 = 128; +pub const HEIGHT: i16 = 64; pub const LINE_SPACE: i16 = 1; pub const FONT_BPP: i16 = 1; diff --git a/core/embed/rust/src/ui/model_tr/layout.rs b/core/embed/rust/src/ui/model_tr/layout.rs index 40ba08087..fc68d20a5 100644 --- a/core/embed/rust/src/ui/model_tr/layout.rs +++ b/core/embed/rust/src/ui/model_tr/layout.rs @@ -21,9 +21,11 @@ use crate::{ text::paragraphs::{Paragraph, Paragraphs}, FormattedText, }, + display::Font, layout::{ obj::{ComponentMsgObj, LayoutObj}, - result::{CANCELLED, CONFIRMED, INFO}, util::iter_into_vec, + result::{CANCELLED, CONFIRMED, INFO}, + util::iter_into_vec, }, }, }; @@ -160,9 +162,8 @@ extern "C" fn new_confirm_action(n_args: usize, args: *const Obj, kwargs: *mut M let obj = LayoutObj::new(Frame::new( title, - None, ButtonPage::new_str_buf( - FormattedText::new(theme::TEXT_NORMAL, theme::FORMATTED, format) + FormattedText::new(theme::TEXT_MONO, theme::FORMATTED, format) .with("action", action.unwrap_or_default()) .with("description", description.unwrap_or_default()), theme::BG, @@ -186,10 +187,9 @@ extern "C" fn new_confirm_text(n_args: usize, args: *const Obj, kwargs: *mut Map let obj = LayoutObj::new(Frame::new( title, - None, ButtonPage::new_str( Paragraphs::new([ - Paragraph::new(&theme::TEXT_NORMAL, description.unwrap_or_default()), + Paragraph::new(&theme::TEXT_MONO, description.unwrap_or_default()), Paragraph::new(&theme::TEXT_BOLD, data), ]), theme::BG, @@ -222,7 +222,7 @@ extern "C" fn confirm_output(n_args: usize, args: *const Obj, kwargs: *mut Map) Some(ButtonDetails::text("CONTINUE")), ); let btn_actions = ButtonActions::cancel_next(); - Page::<20>::new(btn_layout, btn_actions).icon_label_text( + Page::<20>::new(btn_layout, btn_actions, Font::NORMAL).icon_label_text( theme::ICON_USER, "Recipient".into(), address.clone(), @@ -239,7 +239,7 @@ extern "C" fn confirm_output(n_args: usize, args: *const Obj, kwargs: *mut Map) ), ); let btn_actions = ButtonActions::cancel_confirm(); - Page::<20>::new(btn_layout, btn_actions) + Page::<20>::new(btn_layout, btn_actions, Font::NORMAL) .icon_label_text( theme::ICON_USER, "Recipient".into(), @@ -281,7 +281,7 @@ extern "C" fn confirm_total(n_args: usize, args: *const Obj, kwargs: *mut Map) - ); let btn_actions = ButtonActions::cancel_confirm(); - let mut flow_page = Page::<25>::new(btn_layout, btn_actions) + let mut flow_page = Page::<25>::new(btn_layout, btn_actions, Font::NORMAL) .icon_label_text(theme::ICON_PARAM, total_label.clone(), total_amount.clone()) .newline() .icon_label_text(theme::ICON_PARAM, fee_label.clone(), fee_amount.clone()); @@ -305,6 +305,8 @@ extern "C" fn confirm_total(n_args: usize, args: *const Obj, kwargs: *mut Map) - extern "C" fn tutorial(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj { let block = |_args: &[Obj], _kwargs: &Map| { + const PAGE_COUNT: u8 = 7; + let get_page = |page_index| { // Lazy-loaded list of screens to show, with custom content, // buttons and actions triggered by these buttons. @@ -314,29 +316,19 @@ extern "C" fn tutorial(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj let screen = match page_index { // title, text, btn_layout, btn_actions 0 => ( - "Hello!", - "Welcome to Trezor.\n\n\nPress right to continue.", + "HELLO", + "Welcome to Trezor.\nPress right to continue.", ButtonLayout::cancel_and_arrow(), ButtonActions::last_next(), ), 1 => ( - "Basics", - "Use Trezor by clicking left & right.\nPress right to continue.", + "", + "Use Trezor by clicking left & right.\n\nContinue right.", ButtonLayout::left_right_arrows(), ButtonActions::prev_next(), ), 2 => ( - "Confirm", - "Press both left & right at the same time to confirm.", - ButtonLayout::new( - Some(ButtonDetails::left_arrow_icon()), - Some(ButtonDetails::armed_text("CONFIRM")), - None, - ), - ButtonActions::prev_next_with_middle(), - ), - 3 => ( - "Hold to confirm", + "HOLD TO CONFIRM", "Press & hold right to approve important operations.", ButtonLayout::new( Some(ButtonDetails::left_arrow_icon()), @@ -348,29 +340,27 @@ extern "C" fn tutorial(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj ), ButtonActions::prev_next(), ), - // TODO: merge these two scrolls into one, with using a scrollbar - 4 => ( - "Screen scroll", - "Press right to scroll down to read all content when text doesn't...", + 3 => ( + "SCREEN SCROLL", + "Press right to scroll down to read all content when text\ndoesn't fit on one screen. Press left to scroll up.", ButtonLayout::new( Some(ButtonDetails::left_arrow_icon()), None, - Some(ButtonDetails::down_arrow_icon_wide()), - ), + Some(ButtonDetails::text("GOT IT")), ), ButtonActions::prev_next(), ), + 4 => ( + "CONFIRM", + "Press both left & right at the same time to confirm.", + ButtonLayout::new( + Some(ButtonDetails::left_arrow_icon()), + Some(ButtonDetails::armed_text("CONFIRM")), + None, + ), + ButtonActions::prev_next_with_middle(), + ), 5 => ( - "Screen scroll", - "fit on one screen. Press left to scroll up.", - ButtonLayout::new( - Some(ButtonDetails::up_arrow_icon_wide()), - None, - Some(ButtonDetails::text("CONFIRM")), - ), - ButtonActions::prev_next(), - ), - 6 => ( - "Congrats!", + "CONGRATS!", "You're ready to use Trezor.", ButtonLayout::new( Some(ButtonDetails::text("AGAIN")), @@ -379,27 +369,30 @@ extern "C" fn tutorial(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj ), ButtonActions::beginning_confirm(), ), - 7 => ( - "Skip tutorial?", + 6 => ( + "SKIP TUTORIAL", "Sure you want to skip the tutorial?", ButtonLayout::new( Some(ButtonDetails::left_arrow_icon()), None, - Some(ButtonDetails::text("CONFIRM")), + Some(ButtonDetails::text("SKIP")), ), ButtonActions::beginning_cancel(), ), _ => unreachable!(), }; - Page::<10>::new(screen.2.clone(), screen.3.clone()) - .text_bold(screen.0.into()) - .newline() - .newline_half() - .text_normal(screen.1.into()) + let mut page = Page::<10>::new(screen.2.clone(), screen.3.clone(), Font::BOLD); + + // Add title if present + if !screen.0.is_empty() { + page = page.text_bold(screen.0.into()).newline().newline_half() + } + page = page.text_mono(screen.1.into()); + page }; - let pages = FlowPages::new(get_page, 8); + let pages = FlowPages::new(get_page, PAGE_COUNT); let obj = LayoutObj::new(Flow::new(pages).into_child())?; Ok(obj.into()) @@ -418,7 +411,7 @@ extern "C" fn pin_confirm_action(n_args: usize, args: *const Obj, kwargs: *mut M // as in the design. 0 => ( "PIN settings".into(), - "PIN should\ncontain at\nleast four\ndigits", + "PIN should contain at least 6 digits", ButtonLayout::cancel_and_text("GOT IT"), ButtonActions::cancel_next(), ), @@ -434,11 +427,11 @@ extern "C" fn pin_confirm_action(n_args: usize, args: *const Obj, kwargs: *mut M _ => unreachable!(), }; - Page::<10>::new(screen.2.clone(), screen.3.clone()) + Page::<10>::new(screen.2.clone(), screen.3.clone(), Font::BOLD) .text_bold(screen.0) .newline() .newline_half() - .text_normal(screen.1.into()) + .text_mono(screen.1.into()) }; let pages = FlowPages::new(get_page, 2); @@ -464,7 +457,7 @@ extern "C" fn request_pin(n_args: usize, args: *const Obj, kwargs: *mut Map) -> extern "C" fn show_share_words(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj { let block = |_args: &[Obj], kwargs: &Map| { let share_words_obj: Obj = kwargs.get(Qstr::MP_QSTR_share_words)?; - let title = "Recovery seed"; + let title = "RECOVERY SEED"; // Parsing the list of share words. // Assume there is always up to 24 words in the newly generated seed @@ -519,13 +512,8 @@ extern "C" fn show_share_words(n_args: usize, args: *const Obj, kwargs: *mut Map let obj = LayoutObj::new(Frame::new( title, - None, ButtonPage::new_str( - Paragraphs::new( - [ - Paragraph::new(&theme::TEXT_BOLD, text_to_show) - ] - ), + Paragraphs::new([Paragraph::new(&theme::TEXT_BOLD, text_to_show)]), theme::BG, ) .with_cancel_btn(cancel_btn) @@ -539,15 +527,10 @@ extern "C" fn show_share_words(n_args: usize, args: *const Obj, kwargs: *mut Map extern "C" fn select_word(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj { let block = |_args: &[Obj], kwargs: &Map| { let title: StrBuffer = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?; - let description: StrBuffer = kwargs.get(Qstr::MP_QSTR_description)?.try_into()?; let words_iterable: Obj = kwargs.get(Qstr::MP_QSTR_words)?; let words: Vec = iter_into_vec(words_iterable)?; - let obj = LayoutObj::new(Frame::new( - title, - Some(description), - SimpleChoice::new(words).into_child(), - ))?; + let obj = LayoutObj::new(Frame::new(title, SimpleChoice::new(words).into_child()))?; Ok(obj.into()) }; unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } @@ -556,15 +539,10 @@ extern "C" fn select_word(n_args: usize, args: *const Obj, kwargs: *mut Map) -> extern "C" fn request_word_count(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj { let block = |_args: &[Obj], kwargs: &Map| { let title: StrBuffer = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?; - let text: StrBuffer = kwargs.get(Qstr::MP_QSTR_text)?.try_into()?; let choices: Vec<&str, 5> = ["12", "18", "20", "24", "33"].into_iter().collect(); - let obj = LayoutObj::new(Frame::new( - title, - Some(text), - SimpleChoice::new(choices).into_child(), - ))?; + let obj = LayoutObj::new(Frame::new(title, SimpleChoice::new(choices).into_child()))?; Ok(obj.into()) }; unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } @@ -574,7 +552,7 @@ extern "C" fn request_word_bip39(n_args: usize, args: *const Obj, kwargs: *mut M let block = |_args: &[Obj], kwargs: &Map| { let prompt: StrBuffer = kwargs.get(Qstr::MP_QSTR_prompt)?.try_into()?; - let obj = LayoutObj::new(Frame::new(prompt, None, Bip39Entry::new().into_child()))?; + let obj = LayoutObj::new(Frame::new(prompt, Bip39Entry::new().into_child()))?; Ok(obj.into()) }; unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } @@ -585,11 +563,7 @@ extern "C" fn request_passphrase(n_args: usize, args: *const Obj, kwargs: *mut M let prompt: StrBuffer = kwargs.get(Qstr::MP_QSTR_prompt)?.try_into()?; let _max_len: u8 = kwargs.get(Qstr::MP_QSTR_max_len)?.try_into()?; - let obj = LayoutObj::new(Frame::new( - prompt, - None, - PassphraseEntry::new().into_child(), - ))?; + let obj = LayoutObj::new(Frame::new(prompt, PassphraseEntry::new().into_child()))?; Ok(obj.into()) }; unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } @@ -678,7 +652,6 @@ pub static mp_module_trezorui2: Module = obj_module! { /// def select_word( /// *, /// title: str, - /// description: str, /// words: Iterable[str], /// ) -> int: /// """Select mnemonic word from three possibilities - seed check after backup. The @@ -688,7 +661,6 @@ pub static mp_module_trezorui2: Module = obj_module! { /// def request_word_count( /// *, /// title: str, - /// text: str, /// ) -> str: # TODO: make it return int /// """Get word count for recovery.""" Qstr::MP_QSTR_request_word_count => obj_fn_kw!(0, request_word_count).as_obj(), diff --git a/core/embed/rust/src/ui/model_tr/res/delete.toif b/core/embed/rust/src/ui/model_tr/res/delete.toif new file mode 100644 index 0000000000000000000000000000000000000000..8b51bd5853bdda1606706d01e3ad65db284c5926 GIT binary patch literal 39 vcmWIX_jKoC;9!tuU|>l8@!`L{J+nPC^M7`>I)#KE45^JrB@S#-U|;|M)LaY5 literal 0 HcmV?d00001 diff --git a/core/embed/rust/src/ui/model_tr/res/eye.toif b/core/embed/rust/src/ui/model_tr/res/eye.toif new file mode 100644 index 0000000000000000000000000000000000000000..3d3fe799e0c26e57a5bdd3a169eac23a4f055030 GIT binary patch literal 37 scmWIX_jKoBU}KPEU|>j2U|_5h`1p{y<>!3H4m*QC|M^qu88{gj0I<^vj{pDw literal 0 HcmV?d00001 diff --git a/core/embed/rust/src/ui/model_tr/res/tick.toif b/core/embed/rust/src/ui/model_tr/res/tick.toif new file mode 100644 index 0000000000000000000000000000000000000000..aefc96fd43321d6cbccdfe1d15f5e0df03ee0c8f GIT binary patch literal 29 jcmWIX_jKoAU}F$uU|>j2NceF82>2KMX#BuDg`EKad=m;L literal 0 HcmV?d00001 diff --git a/core/embed/rust/src/ui/model_tr/theme.rs b/core/embed/rust/src/ui/model_tr/theme.rs index 8622f150a..87486ac46 100644 --- a/core/embed/rust/src/ui/model_tr/theme.rs +++ b/core/embed/rust/src/ui/model_tr/theme.rs @@ -9,7 +9,8 @@ pub const BG: Color = Color::black(); // Default background color. // Font constants. pub const FONT_BUTTON: Font = Font::MONO; -pub const FONT_HEADER: Font = Font::MONO; +pub const FONT_HEADER: Font = Font::BOLD; +pub const FONT_CHOICE_ITEMS: Font = Font::NORMAL; // Text constants. pub const TEXT_NORMAL: TextStyle = TextStyle::new(Font::NORMAL, FG, BG, FG, FG); @@ -25,17 +26,6 @@ pub const FORMATTED: FormattedFonts = FormattedFonts { }; // Icons with their names for debugging purposes -pub const ICON_SUCCESS: IconAndName = - IconAndName::new(include_res!("model_tr/res/success.toif"), "success"); -pub const ICON_FAIL: IconAndName = IconAndName::new(include_res!("model_tr/res/fail.toif"), "fail"); -pub const ICON_CANCEL_OUTLINE: IconAndName = IconAndName::new( - include_res!("model_tr/res/cancel_for_outline.toif"), - "cancel_outline", -); // 8*8 -pub const ICON_CANCEL: IconAndName = IconAndName::new( - include_res!("model_tr/res/cancel_no_outline.toif"), - "cancel", -); pub const ICON_ARM_LEFT: IconAndName = IconAndName::new(include_res!("model_tr/res/arm_left.toif"), "arm_left"); // 6*10 pub const ICON_ARM_RIGHT: IconAndName = @@ -48,12 +38,27 @@ pub const ICON_ARROW_UP: IconAndName = IconAndName::new(include_res!("model_tr/res/arrow_up.toif"), "arrow_up"); // 10*6 pub const ICON_ARROW_DOWN: IconAndName = IconAndName::new(include_res!("model_tr/res/arrow_down.toif"), "arrow_down"); // 10*6 -pub const ICON_BIN: IconAndName = IconAndName::new(include_res!("model_tr/res/bin.toif"), "bin"); // 10*10 pub const ICON_AMOUNT: IconAndName = IconAndName::new(include_res!("model_tr/res/amount.toif"), "amount"); // 10*10 +pub const ICON_BIN: IconAndName = IconAndName::new(include_res!("model_tr/res/bin.toif"), "bin"); // 10*10 +pub const ICON_CANCEL_OUTLINE: IconAndName = IconAndName::new( + include_res!("model_tr/res/cancel_for_outline.toif"), + "cancel_outline", +); // 8*8 +pub const ICON_CANCEL: IconAndName = IconAndName::new( + include_res!("model_tr/res/cancel_no_outline.toif"), + "cancel", +); // 8*8 +pub const ICON_DELETE: IconAndName = + IconAndName::new(include_res!("model_tr/res/delete.toif"), "delete"); // 12*8 +pub const ICON_EYE: IconAndName = IconAndName::new(include_res!("model_tr/res/eye.toif"), "eye"); // 12*6 +pub const ICON_FAIL: IconAndName = IconAndName::new(include_res!("model_tr/res/fail.toif"), "fail"); pub const ICON_LOCK: IconAndName = IconAndName::new(include_res!("model_tr/res/lock.toif"), "lock"); // 10*10 pub const ICON_PARAM: IconAndName = IconAndName::new(include_res!("model_tr/res/param.toif"), "param"); // 10*10 +pub const ICON_SUCCESS: IconAndName = + IconAndName::new(include_res!("model_tr/res/success.toif"), "success"); +pub const ICON_TICK: IconAndName = IconAndName::new(include_res!("model_tr/res/tick.toif"), "tick"); // 10*10 pub const ICON_USER: IconAndName = IconAndName::new(include_res!("model_tr/res/user.toif"), "user"); // 10*10 pub const ICON_WALLET: IconAndName = IconAndName::new(include_res!("model_tr/res/wallet.toif"), "wallet"); // 10*10 diff --git a/core/mocks/generated/trezorui2.pyi b/core/mocks/generated/trezorui2.pyi index e11cd8085..5f5112484 100644 --- a/core/mocks/generated/trezorui2.pyi +++ b/core/mocks/generated/trezorui2.pyi @@ -109,7 +109,6 @@ def show_share_words( def select_word( *, title: str, - description: str, words: Iterable[str], ) -> int: """Select mnemonic word from three possibilities - seed check after backup. The @@ -120,7 +119,6 @@ def select_word( def request_word_count( *, title: str, - text: str, ) -> str: # TODO: make it return int """Get word count for recovery.""" diff --git a/core/src/apps/homescreen/__init__.py b/core/src/apps/homescreen/__init__.py index 653202aa2..8a516286a 100644 --- a/core/src/apps/homescreen/__init__.py +++ b/core/src/apps/homescreen/__init__.py @@ -65,12 +65,10 @@ class HomescreenBase(ui.Layout): return storage.device.get_homescreen() or res.load( "apps/homescreen/res/bg.toif" ) - elif utils.MODEL in ("R",): + elif utils.MODEL in ("1", "R"): # TODO: make it possible to change - # TODO: make it a requirement of 128x64 px + # TODO: make it a requirement of XxX px # TODO: support it for ui.display.avatar, not only ui.display.icon - return res.load("trezor/res/model_r/homescreen.toif") # 128*64 px - elif utils.MODEL in ("1",): return res.load("trezor/res/homescreen_model_1.toif") # 64x36 px else: raise Exception("Unknown model") diff --git a/core/src/apps/homescreen/tr.py b/core/src/apps/homescreen/tr.py index 779bc4443..3090ab1c5 100644 --- a/core/src/apps/homescreen/tr.py +++ b/core/src/apps/homescreen/tr.py @@ -21,17 +21,17 @@ class Homescreen(HomescreenBase): # the icon more on the top. # Otherwise just showing the uppercase label in monospace. if not storage.device.is_initialized(): - ui.display.icon(0, 11, self.get_avatar(), ui.style.FG, ui.style.BG) + ui.display.icon(32, 5, self.get_avatar(), ui.style.FG, ui.style.BG) ui.display.text_center( - ui.WIDTH // 2, 98, "Go to", ui.BOLD, ui.FG, ui.BG + ui.WIDTH // 2, 52, "Go to", ui.BOLD, ui.FG, ui.BG ) ui.display.text_center( - ui.WIDTH // 2, 112, "trezor.io/start", ui.BOLD, ui.FG, ui.BG + ui.WIDTH // 2, 60, "trezor.io/start", ui.BOLD, ui.FG, ui.BG ) else: - ui.display.icon(0, 11, self.get_avatar(), ui.style.FG, ui.style.BG) + ui.display.icon(32, 11, self.get_avatar(), ui.style.FG, ui.style.BG) ui.display.text_center( - ui.WIDTH // 2, 112, self.label.upper(), ui.MONO, ui.FG, ui.BG + ui.WIDTH // 2, 60, self.label.upper(), ui.MONO, ui.FG, ui.BG ) @@ -57,20 +57,20 @@ class Lockscreen(HomescreenBase): ui.display.text_center( ui.WIDTH // 2, 9, self.label.upper(), ui.MONO, ui.FG, ui.BG ) - ui.display.icon(0, 11, self.get_avatar(), ui.style.FG, ui.style.BG) + ui.display.icon(32, 11, self.get_avatar(), ui.style.FG, ui.style.BG) # Lock icon placement depends on the lock_label text lock_icon = ui.res.load("trezor/res/model_r/lock.toif") if self.lock_label == "Not connected": - ui.display.icon(13, 90, lock_icon, ui.style.FG, ui.style.BG) + ui.display.icon(13, 45, lock_icon, ui.style.FG, ui.style.BG) else: - ui.display.icon(38, 90, lock_icon, ui.style.FG, ui.style.BG) + ui.display.icon(38, 45, lock_icon, ui.style.FG, ui.style.BG) ui.display.text_center( - ui.WIDTH // 2 + 10, 100, self.lock_label.upper(), ui.NORMAL, ui.FG, ui.BG + ui.WIDTH // 2 + 10, 52, self.lock_label.upper(), ui.NORMAL, ui.FG, ui.BG ) ui.display.text_center( - ui.WIDTH // 2, 115, self.tap_label.upper(), ui.MONO, ui.FG, ui.BG + ui.WIDTH // 2, 60, self.tap_label.upper(), ui.MONO, ui.FG, ui.BG ) def on_button_released(self, _x: int) -> None: diff --git a/core/src/trezor/ui/layouts/tr/__init__.py b/core/src/trezor/ui/layouts/tr/__init__.py index 56e294773..ad68af5b5 100644 --- a/core/src/trezor/ui/layouts/tr/__init__.py +++ b/core/src/trezor/ui/layouts/tr/__init__.py @@ -406,7 +406,7 @@ async def _placeholder_confirm( confirm_text( ctx=ctx, br_type=br_type, - title=title, + title=title.upper(), data=data, description=description, br_code=br_code, @@ -429,7 +429,7 @@ async def get_bool( ctx, RustLayout( trezorui2.confirm_action( - title=title, + title=title.upper(), action=data, description=description, verb=verb, @@ -501,7 +501,7 @@ async def confirm_action( ctx, RustLayout( trezorui2.confirm_action( - title=title, + title=title.upper(), action=action, description=description, verb=verb, @@ -565,7 +565,7 @@ async def confirm_reset_device( return await _placeholder_confirm( ctx=ctx, br_type="recover_device" if recovery else "setup_device", - title="Recovery mode" if recovery else "Create new wallet", + title="RECOVERY MODE" if recovery else "CREATE NEW WALLET", data="By continuing you agree to trezor.io/tos", description=prompt, br_code=ButtonRequestType.ProtectCall @@ -578,10 +578,10 @@ async def confirm_reset_device( async def confirm_backup(ctx: wire.GenericContext) -> bool: if await get_bool( ctx=ctx, - title="Success", - data="\nNew wallet created successfully!\n\n\nYou should back up your new wallet right now.", - verb="Back up", - verb_cancel="Skip", + title="SUCCESS", + data="New wallet created successfully!\nYou should back up your new wallet right now.", + verb="BACK UP", + verb_cancel="SKIP", br_type="backup_device", br_code=ButtonRequestType.ResetDevice, ): @@ -589,11 +589,11 @@ async def confirm_backup(ctx: wire.GenericContext) -> bool: confirmed = await get_bool( ctx=ctx, - title="Warning", + title="WARNING", data="Are you sure you want to skip the backup?\n\n", description="You can back up your Trezor once, at any time.", - verb="Back up", - verb_cancel="Skip", + verb="BACK UP", + verb_cancel="SKIP", br_type="backup_device", br_code=ButtonRequestType.ResetDevice, ) @@ -606,7 +606,7 @@ async def confirm_path_warning( return await _placeholder_confirm( ctx=ctx, br_type="path_warning", - title="Confirm path", + title="CONFIRM PATH", data=f"{path_type}\n{path} is unknown.\nAre you sure?", description="", br_code=ButtonRequestType.UnknownDerivationPath, @@ -619,7 +619,7 @@ async def show_xpub( return await _placeholder_confirm( ctx=ctx, br_type="show_xpub", - title=title, + title=title.upper(), data=xpub, description="", br_code=ButtonRequestType.PublicKey, @@ -648,7 +648,7 @@ async def show_address( return await _placeholder_confirm( ctx=ctx, br_type="show_address", - title=title, + title=title.upper(), data=text, description="", br_code=ButtonRequestType.Address, @@ -661,7 +661,7 @@ def show_pubkey( return confirm_blob( ctx, br_type="show_pubkey", - title=title, + title=title.upper(), data=pubkey, br_code=ButtonRequestType.PublicKey, ) @@ -684,7 +684,7 @@ async def _show_modal( ctx=ctx, br_type=br_type, br_code=br_code, - title=header, + title=header.upper(), action=subheader, description=content, verb=button_confirm, @@ -832,7 +832,7 @@ async def confirm_payment_request( return await _placeholder_confirm( ctx=ctx, br_type="confirm_payment_request", - title="Confirm sending", + title="CONFIRM SENDING", data=f"{amount} to\n{recipient_name}\n{memos_str}", description="", br_code=ButtonRequestType.ConfirmOutput, @@ -853,7 +853,7 @@ async def should_show_more( ) -> bool: return await get_bool( ctx=ctx, - title=title, + title=title.upper(), data=button_text, br_type=br_type, br_code=br_code, @@ -875,7 +875,7 @@ async def confirm_blob( await _placeholder_confirm( ctx=ctx, br_type=br_type, - title=title, + title=title.upper(), data=str(data), description=description, br_code=br_code, @@ -895,7 +895,7 @@ async def confirm_address( return confirm_blob( ctx=ctx, br_type=br_type, - title=title, + title=title.upper(), data=address, description=description, br_code=br_code, @@ -916,7 +916,7 @@ async def confirm_text( ctx, RustLayout( trezorui2.confirm_text( - title=title, + title=title.upper(), data=data, description=description, ) @@ -940,7 +940,7 @@ def confirm_amount( return _placeholder_confirm( ctx=ctx, br_type=br_type, - title=title, + title=title.upper(), data=amount, description=description, br_code=br_code, @@ -960,7 +960,7 @@ async def confirm_properties( await _placeholder_confirm( ctx=ctx, br_type=br_type, - title=title, + title=title.upper(), data="\n\n".join(f"{name or ''}\n{value or ''}" for name, value in props), description="", br_code=br_code, @@ -984,7 +984,7 @@ async def confirm_total( ctx, RustLayout( trezorui2.confirm_total_r( - title=title, + title=title.upper(), total_amount=total_amount, fee_amount=fee_amount, fee_rate_amount=fee_rate_amount, @@ -1004,7 +1004,7 @@ async def confirm_joint_total( await _placeholder_confirm( ctx=ctx, br_type="confirm_joint_total", - title="Joint transaction", + title="JOINT TRANSACTION", data=f"You are contributing:\n{spending_amount}\nto the total amount:\n{total_amount}", description="", br_code=ButtonRequestType.SignTx, @@ -1032,7 +1032,7 @@ async def confirm_metadata( await _placeholder_confirm( ctx=ctx, br_type=br_type, - title=title, + title=title.upper(), data=text, description="", br_code=br_code, @@ -1045,7 +1045,7 @@ async def confirm_replacement( await _placeholder_confirm( ctx=ctx, br_type="confirm_replacement", - title=description, + title=description.upper(), data=f"Confirm transaction ID:\n{txid}", description="", br_code=ButtonRequestType.SignTx, @@ -1069,7 +1069,7 @@ async def confirm_modify_output( await _placeholder_confirm( ctx=ctx, br_type="modify_output", - title="Modify amount", + title="MODIFY AMOUNT", data=text, description="", br_code=ButtonRequestType.ConfirmOutput, @@ -1099,7 +1099,7 @@ async def confirm_modify_fee( await _placeholder_confirm( ctx=ctx, br_type="modify_fee", - title="Modify fee", + title="MODIFY FEE", data=text, description="", br_code=ButtonRequestType.SignTx, @@ -1112,7 +1112,7 @@ async def confirm_coinjoin( await _placeholder_confirm( ctx=ctx, br_type="coinjoin_final", - title="Authorize CoinJoin", + title="AUTHORIZE COINJOIN", data=f"Maximum rounds: {max_rounds}\n\nMaximum mining fee:\n{max_fee_per_vbyte} sats/vbyte", description="", br_code=ButtonRequestType.Other, @@ -1131,7 +1131,7 @@ async def confirm_sign_identity( await _placeholder_confirm( ctx=ctx, br_type="confirm_sign_identity", - title=f"Sign {proto}", + title=f"Sign {proto}".upper(), data=text, description="", br_code=ButtonRequestType.Other, @@ -1151,7 +1151,7 @@ async def confirm_signverify( await _placeholder_confirm( ctx=ctx, br_type=br_type, - title=header, + title=header.upper(), data=f"Confirm address:\n{address}", description="", br_code=ButtonRequestType.Other, @@ -1160,7 +1160,7 @@ async def confirm_signverify( await _placeholder_confirm( ctx=ctx, br_type=br_type, - title=header, + title=header.upper(), data=f"Confirm message:\n{message}", description="", br_code=ButtonRequestType.Other, diff --git a/core/src/trezor/ui/layouts/tr/recovery.py b/core/src/trezor/ui/layouts/tr/recovery.py index e4f9b6ff1..37b5dbe22 100644 --- a/core/src/trezor/ui/layouts/tr/recovery.py +++ b/core/src/trezor/ui/layouts/tr/recovery.py @@ -14,19 +14,11 @@ if TYPE_CHECKING: async def request_word_count(ctx: wire.GenericContext, dry_run: bool) -> int: await button_request(ctx, "word_count", code=ButtonRequestType.MnemonicWordCount) - - if dry_run: - title = "Seed check" - else: - title = "Recovery mode" - text = "Number of words?" - count = await interact( ctx, RustLayout( trezorui2.request_word_count( - title=title, - text=text, + title="NUMBER OF WORDS", ) ), br_type="request_word_count", @@ -39,7 +31,7 @@ async def request_word_count(ctx: wire.GenericContext, dry_run: bool) -> int: async def request_word( ctx: wire.GenericContext, word_index: int, word_count: int, is_slip39: bool ) -> str: - prompt = f"Choose word {word_index + 1}/{word_count}" + prompt = f"WORD {word_index + 1} OF {word_count}" if is_slip39: raise NotImplementedError @@ -79,11 +71,13 @@ async def continue_recovery( text: str, subtext: str | None, info_func: Callable | None, + dry_run: bool, ) -> bool: return await get_bool( ctx=ctx, - title="Recovery", + title="START RECOVERY", data=f"{text}\n\n{subtext or ''}", + verb="START", br_type="recovery", br_code=ButtonRequestType.RecoveryHomepage, ) diff --git a/core/src/trezor/ui/layouts/tr/reset.py b/core/src/trezor/ui/layouts/tr/reset.py index 9dd1c8743..34bd7a6ed 100644 --- a/core/src/trezor/ui/layouts/tr/reset.py +++ b/core/src/trezor/ui/layouts/tr/reset.py @@ -39,19 +39,14 @@ async def select_word( count: int, group_index: int | None = None, ) -> str: + # TODO: it might not always be 3 words, it can happen it will be only two, + # but the probability is very small - 4 words containing two items two times + # (or one in "all all" seed) assert len(words) == 3 - if share_index is None: - title: str = "CHECK SEED" - elif group_index is None: - title = f"CHECK SHARE #{share_index + 1}" - else: - title = f"CHECK G{group_index + 1} - SHARE {share_index + 1}" - result = await ctx.wait( RustLayout( trezorui2.select_word( - title=title, - description=f"Select word {checked_index + 1}/{count}", + title=f"SELECT WORD {checked_index + 1}", words=(words[0].upper(), words[1].upper(), words[2].upper()), ) ) @@ -91,19 +86,11 @@ async def slip39_advanced_prompt_group_threshold( async def show_warning_backup(ctx: wire.GenericContext, slip39: bool) -> None: - if slip39: - description = ( - "Never make a digital copy of your shares and never upload them online." - ) - else: - description = ( - "Never make a digital copy of your seed and never upload it online." - ) await confirm_action( ctx, "backup_warning", "Caution", - description=description, + description="Never make a digital copy and never upload it online.", verb="I understand", verb_cancel=None, br_code=ButtonRequestType.ResetDevice, diff --git a/core/tools/codegen/gen_font.py b/core/tools/codegen/gen_font.py index 3dde8b2dd..c43c931f3 100755 --- a/core/tools/codegen/gen_font.py +++ b/core/tools/codegen/gen_font.py @@ -155,7 +155,6 @@ def process_face( ) nonprintable += " };\n" - yMin = bearingY - rows yMax = yMin + rows font_ymin = min(font_ymin, yMin) @@ -178,7 +177,10 @@ def process_face( f.write("#endif\n") f.write("#define Font_%s_%s_%d_HEIGHT %d\n" % (name, style, size, size)) - f.write("#define Font_%s_%s_%d_MAX_HEIGHT %d\n" % (name, style, size, font_ymax - font_ymin)) + f.write( + "#define Font_%s_%s_%d_MAX_HEIGHT %d\n" + % (name, style, size, font_ymax - font_ymin) + ) f.write("#define Font_%s_%s_%d_BASELINE %d\n" % (name, style, size, -font_ymin)) f.write( "extern const uint8_t* const Font_%s_%s_%d[%d + 1 - %d];\n"