diff --git a/core/embed/rust/src/ui/display/mod.rs b/core/embed/rust/src/ui/display/mod.rs index ea916c7a94..73f1034f04 100644 --- a/core/embed/rust/src/ui/display/mod.rs +++ b/core/embed/rust/src/ui/display/mod.rs @@ -980,8 +980,12 @@ pub fn text_multiline( let mut line_text: String<50> = String::new(); 'characters: loop { if let Some(character) = text.chars().nth(characters_drawn) { - unwrap!(line_text.push(character)); characters_drawn += 1; + if character == '\n' { + // The line is forced to end. + break 'characters; + } + unwrap!(line_text.push(character)); } else { // No more characters to draw. break 'characters; @@ -994,11 +998,11 @@ pub fn text_multiline( } } text_left(baseline, &line_text, font, fg_color, bg_color); + taken_from_top += line_height; if characters_drawn == characters_overall { // No more lines to draw. break 'lines; } - taken_from_top += line_height; } // Some of the area was unused and is free to draw some further text. Some(area.split_top(taken_from_top).1) diff --git a/core/embed/rust/src/ui/model_tr/component/share_words.rs b/core/embed/rust/src/ui/model_tr/component/share_words.rs index b14a3cae75..0c577db60b 100644 --- a/core/embed/rust/src/ui/model_tr/component/share_words.rs +++ b/core/embed/rust/src/ui/model_tr/component/share_words.rs @@ -2,31 +2,28 @@ use crate::{ micropython::buffer::StrBuffer, ui::{ component::{Component, Event, EventCtx, Never, Paginate}, - display::Font, + display::{text_multiline, Font}, geometry::{Offset, Rect}, + model_tr::theme, }, }; use heapless::{String, Vec}; -use super::common::display_inverse; +use super::common::display; const WORDS_PER_PAGE: usize = 3; -const EXTRA_LINE_HEIGHT: i16 = 3; +const EXTRA_LINE_HEIGHT: i16 = 2; const NUMBER_X_OFFSET: i16 = 5; const NUMBER_WORD_OFFSET: i16 = 20; const NUMBER_FONT: Font = Font::DEMIBOLD; const WORD_FONT: Font = Font::NORMAL; /// Showing the given share words. -/// -/// Displays them in inverse colors - black text on white background. -/// It is that because of the OLED side attack - lot of white noise makes -/// the attack much harder. pub struct ShareWords { area: Rect, share_words: Vec, - word_index: usize, + page_index: usize, } impl ShareWords { @@ -34,7 +31,79 @@ impl ShareWords { Self { area: Rect::zero(), share_words, - word_index: 0, + page_index: 0, + } + } + + fn word_index(&self) -> usize { + (self.page_index - 1) * WORDS_PER_PAGE + } + + fn is_entry_page(&self) -> bool { + self.page_index == 0 + } + + fn is_final_page(&self) -> bool { + self.page_index == self.total_page_count() - 1 + } + + fn total_page_count(&self) -> usize { + let word_screens = if self.share_words.len() % WORDS_PER_PAGE == 0 { + self.share_words.len() / WORDS_PER_PAGE + } else { + self.share_words.len() / WORDS_PER_PAGE + 1 + }; + // One page before the words, one after it + 1 + word_screens + 1 + } + + /// Display the first page with user information. + fn render_entry_page(&self) { + // TODO: will it be always 12, or do we need to check the length? + // It would need creating a String out of it, which is not ideal. + let free_area = text_multiline( + self.area, + "Write all 12\nwords in order on\nrecovery seed card.", + Font::BOLD, + theme::FG, + theme::BG, + ); + if let Some(free_area) = free_area { + // Creating a small vertical distance + text_multiline( + free_area.split_top(3).1, + "Do NOT make\ndigital copies!", + Font::MONO, + theme::FG, + theme::BG, + ); + } + } + + /// Display the final page with user confirmation. + fn render_final_page(&self) { + // Moving vertically down to avoid collision with the scrollbar + // and to look better. + text_multiline( + self.area.split_top(12).1, + "I wrote down all\n12 words in order.", + Font::MONO, + theme::FG, + theme::BG, + ); + } + + /// Display current set of recovery words. + fn render_words(&self) { + let mut y_offset = 0; + // Showing the word index and the words itself + for i in 0..WORDS_PER_PAGE { + y_offset += NUMBER_FONT.line_height() + EXTRA_LINE_HEIGHT; + let index = self.word_index() + i; + let word = self.share_words[index].clone(); + let baseline = self.area.top_left() + Offset::new(NUMBER_X_OFFSET, y_offset); + display(baseline, &inttostr!(index as u8 + 1), NUMBER_FONT); + display(baseline + Offset::x(NUMBER_WORD_OFFSET), &word, WORD_FONT); } } } @@ -52,30 +121,24 @@ impl Component for ShareWords { } fn paint(&mut self) { - let mut y_offset = 0; - // Showing the word index and the words itself - for i in 0..WORDS_PER_PAGE { - y_offset += NUMBER_FONT.line_height() + EXTRA_LINE_HEIGHT; - let index = self.word_index + i; - let word = self.share_words[index].clone(); - let baseline = self.area.top_left() + Offset::new(NUMBER_X_OFFSET, y_offset); - display_inverse(baseline, &inttostr!(index as u8 + 1), NUMBER_FONT); - display_inverse(baseline + Offset::x(NUMBER_WORD_OFFSET), &word, WORD_FONT); + if self.is_entry_page() { + self.render_entry_page(); + } else if self.is_final_page() { + self.render_final_page(); + } else { + self.render_words(); } } } impl Paginate for ShareWords { fn page_count(&mut self) -> usize { - if self.share_words.len() % WORDS_PER_PAGE == 0 { - self.share_words.len() / WORDS_PER_PAGE - } else { - self.share_words.len() / WORDS_PER_PAGE + 1 - } + // Not defining the logic here, as we do not want it to be `&mut`. + self.total_page_count() } fn change_page(&mut self, active_page: usize) { - self.word_index = active_page * WORDS_PER_PAGE; + self.page_index = active_page; } } @@ -84,11 +147,17 @@ impl crate::trace::Trace for ShareWords { fn trace(&self, t: &mut dyn crate::trace::Tracer) { t.open("ShareWords"); t.content_flag(); - for i in 0..WORDS_PER_PAGE { - let index = self.word_index + i; - let word = self.share_words[index].clone(); - let content = build_string!(20, inttostr!(index as u8 + 1), " ", &word, "\n"); - t.string(&content); + if self.is_entry_page() { + t.string("entry page"); + } else if self.is_final_page() { + t.string("final page"); + } else { + for i in 0..WORDS_PER_PAGE { + let index = self.word_index() + i; + let word = self.share_words[index].clone(); + let content = build_string!(20, inttostr!(index as u8 + 1), " ", &word, "\n"); + t.string(&content); + } } t.content_flag(); t.close(); diff --git a/core/embed/rust/src/ui/model_tr/layout.rs b/core/embed/rust/src/ui/model_tr/layout.rs index fc607837bb..43382e3610 100644 --- a/core/embed/rust/src/ui/model_tr/layout.rs +++ b/core/embed/rust/src/ui/model_tr/layout.rs @@ -524,13 +524,11 @@ extern "C" fn show_share_words(n_args: usize, args: *const Obj, kwargs: *mut Map let share_words: Vec = iter_into_vec(share_words_obj)?; let confirm_btn = - Some(ButtonDetails::text("CONFIRM").with_duration(Duration::from_secs(1))); + Some(ButtonDetails::text("HOLD TO CONFIRM").with_duration(Duration::from_secs(1))); let obj = LayoutObj::new( - ButtonPage::new_str(ShareWords::new(share_words), theme::FG) - .with_cancel_btn(None) - .with_confirm_btn(confirm_btn) - .with_scrollbar(false), + ButtonPage::new_str(ShareWords::new(share_words), theme::BG) + .with_confirm_btn(confirm_btn), )?; Ok(obj.into()) }; diff --git a/core/src/trezor/ui/layouts/tr/reset.py b/core/src/trezor/ui/layouts/tr/reset.py index b52063cc92..219283bf04 100644 --- a/core/src/trezor/ui/layouts/tr/reset.py +++ b/core/src/trezor/ui/layouts/tr/reset.py @@ -5,7 +5,7 @@ from trezor.enums import ButtonRequestType import trezorui2 from ..common import interact -from . import RustLayout, _placeholder_confirm, confirm_action, get_bool +from . import RustLayout, confirm_action, get_bool if TYPE_CHECKING: from trezor import wire @@ -19,15 +19,6 @@ async def show_share_words( share_index: int | None = None, group_index: int | None = None, ) -> None: - await _placeholder_confirm( - ctx=ctx, - br_type="backup_words", - title="SHARE WORDS", - description="Write all words in order on recovery seed card.", - data="Do NOT make digital copies.", - br_code=ButtonRequestType.ResetDevice, - ) - # Showing words, asking for write down confirmation and preparing for check # until user accepts everything. while True: @@ -42,18 +33,6 @@ async def show_share_words( br_code=ButtonRequestType.ResetDevice, ) - wrote_down = await get_bool( - ctx=ctx, - title="SHARE WORDS", - data=f"I wrote down all {len(share_words)} words in order.", - verb="I WROTE DOWN", - hold=True, - br_type="backup_words", - br_code=ButtonRequestType.ResetDevice, - ) - if not wrote_down: - continue - ready_to_check = await get_bool( ctx=ctx, title="CHECK PHRASE",