From 9bff86142da5c6b1be030f88096647d96ac35ea0 Mon Sep 17 00:00:00 2001 From: obrusvit Date: Sat, 9 Nov 2024 18:15:05 +0100 Subject: [PATCH] refactor(core): move and cleanup show_share_words - model_t version was moved from using plain Paragraph to a dedicated component `ShareWords` so that it's consistent with other models. This allowed to move formatting to Rust and allowed the trait function to have `words` parameter of type `Vec` - model_r ShareWords::render slightly refactored to be consistent with the new model_t version - mercury uses a unique version. The reason is that mercury SwipeFlow contains also the initial screen with instructions and prompt screen at the end. --- core/embed/rust/librust_qstr.h | 5 +- core/embed/rust/src/ui/api/firmware_upy.rs | 70 +++++++++++ .../ui/model_mercury/flow/show_share_words.rs | 8 +- .../embed/rust/src/ui/model_mercury/layout.rs | 45 ------- .../src/ui/model_mercury/ui_features_fw.rs | 32 +++++ .../src/ui/model_tr/component/share_words.rs | 39 +++--- core/embed/rust/src/ui/model_tr/layout.rs | 26 ---- .../rust/src/ui/model_tr/ui_features_fw.rs | 30 ++++- .../rust/src/ui/model_tt/component/mod.rs | 4 + .../src/ui/model_tt/component/share_words.rs | 115 ++++++++++++++++++ core/embed/rust/src/ui/model_tt/layout.rs | 31 ----- .../rust/src/ui/model_tt/ui_features_fw.rs | 28 ++++- core/embed/rust/src/ui/ui_features_fw.rs | 15 +++ core/mocks/generated/trezorui2.pyi | 31 ----- core/mocks/generated/trezorui_api.pyi | 22 ++++ core/src/trezor/ui/layouts/mercury/reset.py | 22 ++-- core/src/trezor/ui/layouts/tr/reset.py | 6 +- core/src/trezor/ui/layouts/tt/reset.py | 31 +---- 18 files changed, 356 insertions(+), 204 deletions(-) create mode 100644 core/embed/rust/src/ui/model_tt/component/share_words.rs diff --git a/core/embed/rust/librust_qstr.h b/core/embed/rust/librust_qstr.h index 1e3497ccc1..901e37fa92 100644 --- a/core/embed/rust/librust_qstr.h +++ b/core/embed/rust/librust_qstr.h @@ -254,7 +254,6 @@ static void _librust_qstrs(void) { MP_QSTR_flow_confirm_summary; MP_QSTR_flow_get_address; MP_QSTR_flow_prompt_backup; - MP_QSTR_flow_show_share_words; MP_QSTR_flow_warning_hi_prio; MP_QSTR_get_language; MP_QSTR_get_transition_out; @@ -291,6 +290,7 @@ static void _librust_qstrs(void) { MP_QSTR_inputs__return; MP_QSTR_inputs__show; MP_QSTR_inputs__space; + MP_QSTR_instructions; MP_QSTR_instructions__continue_holding; MP_QSTR_instructions__continue_in_app; MP_QSTR_instructions__enter_next_share; @@ -641,7 +641,6 @@ static void _librust_qstrs(void) { MP_QSTR_set_brightness; MP_QSTR_setting__adjust; MP_QSTR_setting__apply; - MP_QSTR_share_words; MP_QSTR_share_words__words_in_order; MP_QSTR_share_words__wrote_down_all; MP_QSTR_show_address_details; @@ -658,6 +657,7 @@ static void _librust_qstrs(void) { MP_QSTR_show_progress_coinjoin; MP_QSTR_show_remaining_shares; MP_QSTR_show_share_words; + MP_QSTR_show_share_words_mercury; MP_QSTR_show_simple; MP_QSTR_show_success; MP_QSTR_show_wait_text; @@ -683,7 +683,6 @@ static void _librust_qstrs(void) { MP_QSTR_summary_title; MP_QSTR_text; MP_QSTR_text_confirm; - MP_QSTR_text_info; MP_QSTR_text_mono; MP_QSTR_time_ms; MP_QSTR_timer; diff --git a/core/embed/rust/src/ui/api/firmware_upy.rs b/core/embed/rust/src/ui/api/firmware_upy.rs index 091e5e09d5..3f7cfb153a 100644 --- a/core/embed/rust/src/ui/api/firmware_upy.rs +++ b/core/embed/rust/src/ui/api/firmware_upy.rs @@ -2,6 +2,7 @@ use crate::{ io::BinaryData, micropython::{ gc::Gc, + iter::IterBuf, list::List, macros::{obj_fn_1, obj_fn_kw, obj_module}, map::Map, @@ -25,6 +26,7 @@ use crate::{ ui_features_fw::UIFeaturesFirmware, }, }; +use heapless::Vec; /// Dummy implementation so that we can use `Empty` in a return type of unimplemented trait /// function @@ -478,6 +480,54 @@ extern "C" fn new_show_remaining_shares(n_args: usize, args: *const Obj, kwargs: unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } } +extern "C" fn new_show_share_words(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj { + let block = move |_args: &[Obj], kwargs: &Map| { + let words: Obj = kwargs.get(Qstr::MP_QSTR_words)?; + let title: Option = kwargs + .get(Qstr::MP_QSTR_title) + .and_then(Obj::try_into_option) + .unwrap_or(None); + + let words: Vec = util::iter_into_vec(words)?; + + let layout = ModelUI::show_share_words(words, title)?; + Ok(LayoutObj::new_root(layout)?.into()) + }; + unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } +} + +extern "C" fn new_show_share_words_mercury( + n_args: usize, + args: *const Obj, + kwargs: *mut Map, +) -> Obj { + let block = move |_args: &[Obj], kwargs: &Map| { + let words: Obj = kwargs.get(Qstr::MP_QSTR_words)?; + let subtitle: Option = kwargs + .get(Qstr::MP_QSTR_subtitle) + .and_then(Obj::try_into_option) + .unwrap_or(None); + let instructions: Obj = kwargs.get(Qstr::MP_QSTR_instructions)?; + let text_footer: Option = kwargs + .get(Qstr::MP_QSTR_description) + .and_then(Obj::try_into_option) + .unwrap_or(None); + let text_confirm: TString = kwargs.get(Qstr::MP_QSTR_text_confirm)?.try_into()?; + + let words: Vec = util::iter_into_vec(words)?; + + let layout = ModelUI::show_share_words_mercury( + words, + subtitle, + instructions, + text_footer, + text_confirm, + )?; + Ok(LayoutObj::new_root(layout)?.into()) + }; + unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } +} + extern "C" fn new_show_simple(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj { let block = move |_args: &[Obj], kwargs: &Map| { let text: TString = kwargs.get(Qstr::MP_QSTR_text)?.try_into()?; @@ -923,6 +973,26 @@ pub static mp_module_trezorui_api: Module = obj_module! { /// """Shows SLIP39 state after info button is pressed on `confirm_recovery`.""" Qstr::MP_QSTR_show_remaining_shares => obj_fn_kw!(0, new_show_remaining_shares).as_obj(), + /// def show_share_words( + /// *, + /// words: Iterable[str], + /// title: str | None = None, + /// ) -> LayoutObj[UiResult]: + /// """Show mnemonic for backup.""" + Qstr::MP_QSTR_show_share_words => obj_fn_kw!(0, new_show_share_words).as_obj(), + + /// def show_share_words_mercury( + /// *, + /// words: Iterable[str], + /// subtitle: str | None, + /// instructions: Iterable[str], + /// text_footer: str | None, + /// text_confirm: str, + /// ) -> LayoutObj[UiResult]: + /// """Show mnemonic for wallet backup preceded by an instruction screen and followed by a + /// confirmation screen.""" + Qstr::MP_QSTR_show_share_words_mercury => obj_fn_kw!(0, new_show_share_words_mercury).as_obj(), + /// def show_simple( /// *, /// text: str, diff --git a/core/embed/rust/src/ui/model_mercury/flow/show_share_words.rs b/core/embed/rust/src/ui/model_mercury/flow/show_share_words.rs index 76203de9c6..8b8f3979e7 100644 --- a/core/embed/rust/src/ui/model_mercury/flow/show_share_words.rs +++ b/core/embed/rust/src/ui/model_mercury/flow/show_share_words.rs @@ -78,15 +78,15 @@ fn footer_updating_func( } pub fn new_show_share_words( - title: TString<'static>, - subtitle: TString<'static>, share_words_vec: Vec, 33>, - description: Option>, + subtitle: TString<'static>, instructions_paragraphs: ParagraphVecShort<'static>, + text_footer: Option>, text_confirm: TString<'static>, ) -> Result { let nwords = share_words_vec.len(); let paragraphs_spacing = 8; + let title = TR::reset__recovery_wallet_backup_title.into(); let content_instruction = Frame::left_aligned( title, @@ -97,7 +97,7 @@ pub fn new_show_share_words( ), ) .with_subtitle(TR::words__instructions.into()) - .with_footer(TR::instructions__swipe_up.into(), description) + .with_footer(TR::instructions__swipe_up.into(), text_footer) .with_swipe(Direction::Up, SwipeSettings::default()) .map(|msg| matches!(msg, FrameMsg::Content(_)).then_some(FlowMsg::Confirmed)) .one_button_request(ButtonRequestCode::ResetDevice.with_name("share_words")) diff --git a/core/embed/rust/src/ui/model_mercury/layout.rs b/core/embed/rust/src/ui/model_mercury/layout.rs index 880d61debe..4f4ba0251d 100644 --- a/core/embed/rust/src/ui/model_mercury/layout.rs +++ b/core/embed/rust/src/ui/model_mercury/layout.rs @@ -658,38 +658,6 @@ extern "C" fn new_confirm_total(n_args: usize, args: *const Obj, kwargs: *mut Ma unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } } -extern "C" fn new_show_share_words(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj { - let block = move |_args: &[Obj], kwargs: &Map| { - let title: TString = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?; - let subtitle: TString = kwargs.get(Qstr::MP_QSTR_subtitle)?.try_into()?; - let share_words_obj: Obj = kwargs.get(Qstr::MP_QSTR_words)?; - let share_words_vec: Vec = util::iter_into_vec(share_words_obj)?; - let description: Option = kwargs - .get(Qstr::MP_QSTR_description)? - .try_into_option()? - .and_then(|desc: TString| if desc.is_empty() { None } else { Some(desc) }); - let text_info: Obj = kwargs.get(Qstr::MP_QSTR_text_info)?; - let text_confirm: TString = kwargs.get(Qstr::MP_QSTR_text_confirm)?.try_into()?; - - let mut instructions_paragraphs = ParagraphVecShort::new(); - for item in IterBuf::new().try_iterate(text_info)? { - let text: TString = item.try_into()?; - instructions_paragraphs.add(Paragraph::new(&theme::TEXT_MAIN_GREY_LIGHT, text)); - } - - let flow = flow::show_share_words::new_show_share_words( - title, - subtitle, - share_words_vec, - description, - instructions_paragraphs, - text_confirm, - )?; - Ok(LayoutObj::new_root(flow)?.into()) - }; - unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } -} - extern "C" fn new_confirm_with_info(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj { let block = move |_args: &[Obj], kwargs: &Map| { let title: TString = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?; @@ -932,19 +900,6 @@ pub static mp_module_trezorui2: Module = obj_module! { /// """Prompt a user to create backup with an option to skip.""" Qstr::MP_QSTR_flow_prompt_backup => obj_fn_0!(new_prompt_backup).as_obj(), - /// def flow_show_share_words( - /// *, - /// title: str, - /// subtitle: str, - /// words: Iterable[str], - /// description: str, - /// text_info: Iterable[str], - /// text_confirm: str, - /// ) -> LayoutObj[UiResult]: - /// """Show wallet backup words preceded by an instruction screen and followed by - /// confirmation.""" - Qstr::MP_QSTR_flow_show_share_words => obj_fn_kw!(0, new_show_share_words).as_obj(), - /// def flow_get_address( /// *, /// address: str | bytes, diff --git a/core/embed/rust/src/ui/model_mercury/ui_features_fw.rs b/core/embed/rust/src/ui/model_mercury/ui_features_fw.rs index 60582e67b8..c15f5c5c75 100644 --- a/core/embed/rust/src/ui/model_mercury/ui_features_fw.rs +++ b/core/embed/rust/src/ui/model_mercury/ui_features_fw.rs @@ -540,6 +540,38 @@ impl UIFeaturesFirmware for ModelMercuryFeatures { Ok(obj) } + fn show_share_words( + words: heapless::Vec, 33>, + _title: Option>, + ) -> Result { + Err::, Error>(Error::ValueError( + c"use flow_share_words instead", + )) + } + + fn show_share_words_mercury( + words: heapless::Vec, 33>, + subtitle: Option>, + instructions: crate::micropython::obj::Obj, + text_footer: Option>, + text_confirm: TString<'static>, + ) -> Result { + let mut instructions_paragraphs = ParagraphVecShort::new(); + for item in crate::micropython::iter::IterBuf::new().try_iterate(instructions)? { + let text: TString = item.try_into()?; + instructions_paragraphs.add(Paragraph::new(&theme::TEXT_MAIN_GREY_LIGHT, text)); + } + + let flow = flow::show_share_words::new_show_share_words( + words, + subtitle.unwrap_or(TString::empty()), + instructions_paragraphs, + text_footer, + text_confirm, + )?; + Ok(flow) + } + fn show_remaining_shares( pages_iterable: crate::micropython::obj::Obj, // TODO: replace Obj ) -> Result { 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 4a603db85b..ca419b0189 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 @@ -92,23 +92,24 @@ impl<'a> ShareWords<'a> { fn render_words<'s>(&'s self, target: &mut impl Renderer<'s>) { let mut y_offset = 0; // Showing the word index and the words itself - for i in 0..WORDS_PER_PAGE { + for (word_idx, word) in self + .share_words + .iter() + .enumerate() + .skip(self.page_index * WORDS_PER_PAGE) + .take(WORDS_PER_PAGE) + { + let ordinal = word_idx + 1; y_offset += NUMBER_FONT.line_height() + EXTRA_LINE_HEIGHT; - let index = self.word_index() + i; - if index >= self.share_words.len() { - break; - } - let word = &self.share_words[index]; - let baseline = self.area.top_left() + Offset::y(y_offset); - let ordinal = uformat!("{}.", index + 1); + let base = self.area.top_left() + Offset::y(y_offset); - shape::Text::new(baseline + Offset::x(NUMBER_X_OFFSET), &ordinal) + let ordinal_txt = uformat!("{}.", ordinal); + shape::Text::new(base + Offset::x(NUMBER_X_OFFSET), &ordinal_txt) .with_font(NUMBER_FONT) .with_fg(theme::FG) .render(target); - word.map(|w| { - shape::Text::new(baseline + Offset::x(WORD_X_OFFSET), w) + shape::Text::new(base + Offset::x(WORD_X_OFFSET), w) .with_font(WORD_FONT) .with_fg(theme::FG) .render(target); @@ -171,13 +172,15 @@ impl<'a> crate::trace::Trace for ShareWords<'a> { self.get_final_text() } else { let mut content = ShortString::new(); - for i in 0..WORDS_PER_PAGE { - let index = self.word_index() + i; - if index >= self.share_words.len() { - break; - } - self.share_words[index] - .map(|word| unwrap!(uwrite!(content, "{}. {}\n", index + 1, word))); + for (word_idx, word) in self + .share_words + .iter() + .enumerate() + .skip(self.page_index * WORDS_PER_PAGE) + .take(WORDS_PER_PAGE) + { + let ordinal = word_idx + 1; + word.map(|w| unwrap!(uwrite!(content, "{}. {}\n", ordinal, w))); } content }; diff --git a/core/embed/rust/src/ui/model_tr/layout.rs b/core/embed/rust/src/ui/model_tr/layout.rs index c0dfb29047..16b66c11fa 100644 --- a/core/embed/rust/src/ui/model_tr/layout.rs +++ b/core/embed/rust/src/ui/model_tr/layout.rs @@ -839,25 +839,6 @@ extern "C" fn new_confirm_more(n_args: usize, args: *const Obj, kwargs: *mut Map unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } } -extern "C" fn new_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 share_words: Vec = util::iter_into_vec(share_words_obj)?; - - let cancel_btn = Some(ButtonDetails::up_arrow_icon()); - let confirm_btn = - Some(ButtonDetails::text(TR::buttons__hold_to_confirm.into()).with_default_duration()); - - let obj = LayoutObj::new( - ButtonPage::new(ShareWords::new(share_words), theme::BG) - .with_cancel_btn(cancel_btn) - .with_confirm_btn(confirm_btn), - )?; - Ok(obj.into()) - }; - unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } -} - #[no_mangle] pub static mp_module_trezorui2: Module = obj_module! { /// from trezor import utils @@ -1016,11 +997,4 @@ pub static mp_module_trezorui2: Module = obj_module! { /// """Confirm long content with the possibility to go back from any page. /// Meant to be used with confirm_with_info.""" Qstr::MP_QSTR_confirm_more => obj_fn_kw!(0, new_confirm_more).as_obj(), - - /// def show_share_words( - /// *, - /// share_words: Iterable[str], - /// ) -> LayoutObj[UiResult]: - /// """Shows a backup seed.""" - Qstr::MP_QSTR_show_share_words => obj_fn_kw!(0, new_show_share_words).as_obj(), }; diff --git a/core/embed/rust/src/ui/model_tr/ui_features_fw.rs b/core/embed/rust/src/ui/model_tr/ui_features_fw.rs index 8b477d3f3a..284eff5a53 100644 --- a/core/embed/rust/src/ui/model_tr/ui_features_fw.rs +++ b/core/embed/rust/src/ui/model_tr/ui_features_fw.rs @@ -36,7 +36,7 @@ use super::{ component::{ ButtonDetails, ButtonPage, CoinJoinProgress, ConfirmHomescreen, Flow, FlowPages, Frame, Homescreen, Lockscreen, NumberInput, PassphraseEntry, PinEntry, Progress, ScrollableFrame, - SimpleChoice, WordlistEntry, WordlistType, + ShareWords, SimpleChoice, WordlistEntry, WordlistType, }, theme, ModelTRFeatures, }; @@ -603,6 +603,34 @@ impl UIFeaturesFirmware for ModelTRFeatures { Ok(obj) } + fn show_share_words( + words: heapless::Vec, 33>, + _title: Option>, + ) -> Result { + let cancel_btn = Some(ButtonDetails::up_arrow_icon()); + let confirm_btn = + Some(ButtonDetails::text(TR::buttons__hold_to_confirm.into()).with_default_duration()); + + let layout = RootComponent::new( + ButtonPage::new(ShareWords::new(words), theme::BG) + .with_cancel_btn(cancel_btn) + .with_confirm_btn(confirm_btn), + ); + Ok(layout) + } + + fn show_share_words_mercury( + _words: heapless::Vec, 33>, + _subtitle: Option>, + _instructions: crate::micropython::obj::Obj, + _text_footer: Option>, + _text_confirm: TString<'static>, + ) -> Result { + Err::, Error>(Error::ValueError( + c"use show_share_words", + )) + } + fn show_remaining_shares( pages_iterable: crate::micropython::obj::Obj, // TODO: replace Obj ) -> Result { diff --git a/core/embed/rust/src/ui/model_tt/component/mod.rs b/core/embed/rust/src/ui/model_tt/component/mod.rs index e3475d6679..738694fc48 100644 --- a/core/embed/rust/src/ui/model_tt/component/mod.rs +++ b/core/embed/rust/src/ui/model_tt/component/mod.rs @@ -23,6 +23,8 @@ mod progress; mod result; mod scroll; mod set_brightness; +#[cfg(feature = "translations")] +mod share_words; mod simple_page; mod swipe; mod welcome_screen; @@ -59,6 +61,8 @@ pub use progress::Progress; pub use result::{ResultFooter, ResultScreen, ResultStyle}; pub use scroll::ScrollBar; pub use set_brightness::SetBrightnessDialog; +#[cfg(feature = "translations")] +pub use share_words::ShareWords; pub use simple_page::SimplePage; pub use swipe::{Swipe, SwipeDirection}; pub use welcome_screen::WelcomeScreen; diff --git a/core/embed/rust/src/ui/model_tt/component/share_words.rs b/core/embed/rust/src/ui/model_tt/component/share_words.rs new file mode 100644 index 0000000000..88e1af3630 --- /dev/null +++ b/core/embed/rust/src/ui/model_tt/component/share_words.rs @@ -0,0 +1,115 @@ +use crate::{ + strutil::TString, + ui::{ + component::{Component, Event, EventCtx, Never, Paginate}, + display::Font, + geometry::{Offset, Rect}, + model_tt::theme, + shape::{self, Renderer}, + }, +}; +use heapless::Vec; +use ufmt::uwrite; + +const WORDS_PER_PAGE: usize = 4; +const TOP_PADDING_OFFSET: i16 = 13; +const WORD_FONT: Font = Font::MONO; +const MAX_WORDS: usize = 33; // super-shamir has 33 words, all other have less + +/// Showing the given share words. +pub struct ShareWords<'a> { + area: Rect, + share_words: Vec, MAX_WORDS>, + page_index: usize, +} + +impl<'a> ShareWords<'a> { + pub fn new(share_words: Vec, MAX_WORDS>) -> Self { + Self { + area: Rect::zero(), + share_words, + page_index: 0, + } + } + + fn total_page_count(&self) -> usize { + (self.share_words.len() + WORDS_PER_PAGE - 1) / WORDS_PER_PAGE + } +} + +impl<'a> Component for ShareWords<'a> { + type Msg = Never; + + fn place(&mut self, bounds: Rect) -> Rect { + self.area = bounds; + bounds + } + + fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { + None + } + + fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { + let line_height = WORD_FONT.line_height(); + let ordinal_largest_on_this_page = + (WORDS_PER_PAGE * (self.page_index + 1)).min(self.share_words.len()); + let is_largest_double_digit = ordinal_largest_on_this_page >= 10; + let mut y_offset = self.area.top_left().y + TOP_PADDING_OFFSET; + + for (word_idx, word) in self + .share_words + .iter() + .enumerate() + .skip(self.page_index * WORDS_PER_PAGE) + .take(WORDS_PER_PAGE) + { + let ordinal = word_idx + 1; + let base = self.area.top_left() + Offset::y(y_offset); + word.map(|w| { + let double_digit = ordinal >= 10; + let text_fmt = if double_digit || !is_largest_double_digit { + uformat!("{}. {}", ordinal, w) + } else { + uformat!(" {}. {}", ordinal, w) + }; + shape::Text::new(base, &text_fmt) + .with_font(WORD_FONT) + .with_fg(theme::FG) + .render(target); + }); + y_offset += line_height; + } + } +} + +impl<'a> Paginate for ShareWords<'a> { + fn page_count(&mut self) -> usize { + // 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.page_index = active_page; + } +} + +// DEBUG-ONLY SECTION BELOW + +#[cfg(feature = "ui_debug")] +impl<'a> crate::trace::Trace for ShareWords<'a> { + fn trace(&self, t: &mut dyn crate::trace::Tracer) { + t.component("ShareWords"); + let mut content = heapless::String::<64>::new(); + for (word_idx, word) in self + .share_words + .iter() + .enumerate() + .skip(self.page_index * WORDS_PER_PAGE) + .take(WORDS_PER_PAGE) + { + let ordinal = word_idx + 1; + word.map(|w| unwrap!(uwrite!(content, "{}. {}\n", ordinal, w))); + } + t.string("screen_content", content.as_str().into()); + } +} diff --git a/core/embed/rust/src/ui/model_tt/layout.rs b/core/embed/rust/src/ui/model_tt/layout.rs index 0c57550c7e..206cb2198b 100644 --- a/core/embed/rust/src/ui/model_tt/layout.rs +++ b/core/embed/rust/src/ui/model_tt/layout.rs @@ -767,29 +767,6 @@ extern "C" fn new_confirm_more(n_args: usize, args: *const Obj, kwargs: *mut Map unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } } -extern "C" fn new_show_share_words(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj { - let block = move |_args: &[Obj], kwargs: &Map| { - let title: TString = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?; - let pages: Obj = kwargs.get(Qstr::MP_QSTR_pages)?; - - let mut paragraphs = ParagraphVecLong::new(); - for page in IterBuf::new().try_iterate(pages)? { - let text: TString = page.try_into()?; - paragraphs.add(Paragraph::new(&theme::TEXT_MONO, text).break_after()); - } - - let obj = LayoutObj::new(Frame::left_aligned( - theme::label_title(), - title, - ButtonPage::new(paragraphs.into_paragraphs(), theme::BG) - .with_hold()? - .without_cancel(), - ))?; - Ok(obj.into()) - }; - unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } -} - #[no_mangle] pub static mp_module_trezorui2: Module = obj_module! { /// from trezor import utils @@ -919,14 +896,6 @@ pub static mp_module_trezorui2: Module = obj_module! { /// """Confirm long content with the possibility to go back from any page. /// Meant to be used with confirm_with_info.""" Qstr::MP_QSTR_confirm_more => obj_fn_kw!(0, new_confirm_more).as_obj(), - - /// def show_share_words( - /// *, - /// title: str, - /// pages: Iterable[str], - /// ) -> LayoutObj[UiResult]: - /// """Show mnemonic for backup. Expects the words pre-divided into individual pages.""" - Qstr::MP_QSTR_show_share_words => obj_fn_kw!(0, new_show_share_words).as_obj(), }; #[cfg(test)] diff --git a/core/embed/rust/src/ui/model_tt/ui_features_fw.rs b/core/embed/rust/src/ui/model_tt/ui_features_fw.rs index 32664a49c0..a707048b3d 100644 --- a/core/embed/rust/src/ui/model_tt/ui_features_fw.rs +++ b/core/embed/rust/src/ui/model_tt/ui_features_fw.rs @@ -29,7 +29,7 @@ use super::{ check_homescreen_format, Bip39Input, Button, ButtonMsg, ButtonPage, ButtonStyleSheet, CancelConfirmMsg, CoinJoinProgress, Dialog, FidoConfirm, Frame, Homescreen, IconDialog, Lockscreen, MnemonicKeyboard, NumberInputDialog, PassphraseKeyboard, PinKeyboard, Progress, - SelectWordCount, SetBrightnessDialog, Slip39Input, + SelectWordCount, SetBrightnessDialog, ShareWords, Slip39Input, }, theme, ModelTTFeatures, }; @@ -635,6 +635,32 @@ impl UIFeaturesFirmware for ModelTTFeatures { Ok(obj) } + fn show_share_words( + words: heapless::Vec, 33>, + title: Option>, + ) -> Result { + let layout = RootComponent::new(Frame::left_aligned( + theme::label_title(), + title.unwrap_or(TString::empty()), + ButtonPage::new(ShareWords::new(words), theme::BG) + .with_hold()? + .without_cancel(), + )); + Ok(layout) + } + + fn show_share_words_mercury( + _words: heapless::Vec, 33>, + _subtitle: Option>, + _instructions: crate::micropython::obj::Obj, + _text_footer: Option>, + _text_confirm: TString<'static>, + ) -> Result { + Err::, Error>(Error::ValueError( + c"use show_share_words", + )) + } + fn show_remaining_shares( pages_iterable: crate::micropython::obj::Obj, // TODO: replace Obj ) -> Result { diff --git a/core/embed/rust/src/ui/ui_features_fw.rs b/core/embed/rust/src/ui/ui_features_fw.rs index ac2b7e307b..0eb9e4eeab 100644 --- a/core/embed/rust/src/ui/ui_features_fw.rs +++ b/core/embed/rust/src/ui/ui_features_fw.rs @@ -4,6 +4,7 @@ use crate::{ micropython::{gc::Gc, list::List, obj::Obj}, strutil::TString, }; +use heapless::Vec; use super::layout::{ obj::{LayoutMaybeTrace, LayoutObj}, @@ -175,6 +176,20 @@ pub trait UIFeaturesFirmware { pages_iterable: Obj, // TODO: replace Obj ) -> Result; + fn show_share_words( + words: Vec, 33>, + title: Option>, + ) -> Result; + + // TODO: merge with `show_share_words` instead of having specific version for mercury + fn show_share_words_mercury( + words: Vec, 33>, + subtitle: Option>, + instructions: Obj, // TODO: replace Obj + text_footer: Option>, // footer description at instruction screen + text_confirm: TString<'static>, + ) -> Result; + fn show_simple( text: TString<'static>, title: Option>, diff --git a/core/mocks/generated/trezorui2.pyi b/core/mocks/generated/trezorui2.pyi index 3a2b9fa8f3..7c221b8f9d 100644 --- a/core/mocks/generated/trezorui2.pyi +++ b/core/mocks/generated/trezorui2.pyi @@ -136,20 +136,6 @@ def flow_prompt_backup() -> LayoutObj[UiResult]: """Prompt a user to create backup with an option to skip.""" -# rust/src/ui/model_mercury/layout.rs -def flow_show_share_words( - *, - title: str, - subtitle: str, - words: Iterable[str], - description: str, - text_info: Iterable[str], - text_confirm: str, -) -> LayoutObj[UiResult]: - """Show wallet backup words preceded by an instruction screen and followed by - confirmation.""" - - # rust/src/ui/model_mercury/layout.rs def flow_get_address( *, @@ -386,14 +372,6 @@ def confirm_more( ) -> object: """Confirm long content with the possibility to go back from any page. Meant to be used with confirm_with_info.""" - - -# rust/src/ui/model_tr/layout.rs -def show_share_words( - *, - share_words: Iterable[str], -) -> LayoutObj[UiResult]: - """Shows a backup seed.""" from trezor import utils from trezorui_api import * @@ -530,12 +508,3 @@ def confirm_more( ) -> LayoutObj[UiResult]: """Confirm long content with the possibility to go back from any page. Meant to be used with confirm_with_info.""" - - -# rust/src/ui/model_tt/layout.rs -def show_share_words( - *, - title: str, - pages: Iterable[str], -) -> LayoutObj[UiResult]: - """Show mnemonic for backup. Expects the words pre-divided into individual pages.""" diff --git a/core/mocks/generated/trezorui_api.pyi b/core/mocks/generated/trezorui_api.pyi index e80aec2e8d..9080a21632 100644 --- a/core/mocks/generated/trezorui_api.pyi +++ b/core/mocks/generated/trezorui_api.pyi @@ -361,6 +361,28 @@ def show_remaining_shares( """Shows SLIP39 state after info button is pressed on `confirm_recovery`.""" +# rust/src/ui/api/firmware_upy.rs +def show_share_words( + *, + words: Iterable[str], + title: str | None = None, +) -> LayoutObj[UiResult]: + """Show mnemonic for backup.""" + + +# rust/src/ui/api/firmware_upy.rs +def show_share_words_mercury( + *, + words: Iterable[str], + subtitle: str | None, + instructions: Iterable[str], + text_footer: str | None, + text_confirm: str, +) -> LayoutObj[UiResult]: + """Show mnemonic for wallet backup preceded by an instruction screen and followed by a + confirmation screen.""" + + # rust/src/ui/api/firmware_upy.rs def show_simple( *, diff --git a/core/src/trezor/ui/layouts/mercury/reset.py b/core/src/trezor/ui/layouts/mercury/reset.py index 1c95b04030..27b31bc399 100644 --- a/core/src/trezor/ui/layouts/mercury/reset.py +++ b/core/src/trezor/ui/layouts/mercury/reset.py @@ -1,6 +1,5 @@ from typing import Awaitable, Callable, Sequence -import trezorui2 import trezorui_api from trezor import TR, ui from trezor.enums import ButtonRequestType @@ -17,9 +16,8 @@ def show_share_words( share_index: int | None = None, group_index: int | None = None, ) -> Awaitable[None]: - title = TR.reset__recovery_wallet_backup_title if share_index is None: - subtitle = "" + subtitle = None elif group_index is None: subtitle = TR.reset__recovery_share_title_template.format(share_index + 1) else: @@ -27,24 +25,24 @@ def show_share_words( group_index + 1, share_index + 1 ) words_count = len(share_words) - description = "" - text_info = [TR.reset__write_down_words_template.format(words_count)] + description = None + instructions = [TR.reset__write_down_words_template.format(words_count)] if words_count == 20 and share_index is None: # 1-of-1 SLIP39: inform the user about repeated words - text_info.append(TR.reset__words_may_repeat) + instructions.append(TR.reset__words_may_repeat) if share_index == 0: # regular SLIP39, 1st share description = TR.instructions__shares_start_with_1 - text_info.append(TR.reset__repeat_for_all_shares) + instructions.append(TR.reset__repeat_for_all_shares) + assert len(instructions) < 3 text_confirm = TR.reset__words_written_down_template.format(words_count) return raise_if_not_confirmed( - trezorui2.flow_show_share_words( - title=title, - subtitle=subtitle, + trezorui_api.show_share_words_mercury( words=share_words, - description=description, - text_info=text_info, + subtitle=subtitle, + instructions=instructions, + text_footer=description, text_confirm=text_confirm, ), None, diff --git a/core/src/trezor/ui/layouts/tr/reset.py b/core/src/trezor/ui/layouts/tr/reset.py index 1c0197df32..1dd8d684bd 100644 --- a/core/src/trezor/ui/layouts/tr/reset.py +++ b/core/src/trezor/ui/layouts/tr/reset.py @@ -1,6 +1,5 @@ from typing import TYPE_CHECKING -import trezorui2 from trezor import TR from trezor.enums import ButtonRequestType import trezorui_api @@ -50,8 +49,9 @@ async def show_share_words( ) result = await interact( - trezorui2.show_share_words( # type: ignore [Arguments missing for parameters] - share_words=share_words, # type: ignore [No parameter named "share_words"] + trezorui_api.show_share_words( + words=share_words, + title=None, ), br_name, br_code, diff --git a/core/src/trezor/ui/layouts/tt/reset.py b/core/src/trezor/ui/layouts/tt/reset.py index 2a580b2e56..ec4e1675a0 100644 --- a/core/src/trezor/ui/layouts/tt/reset.py +++ b/core/src/trezor/ui/layouts/tt/reset.py @@ -1,6 +1,5 @@ from typing import Awaitable, Callable, Sequence -import trezorui2 import trezorui_api from trezor import TR from trezor.enums import ButtonRequestType @@ -10,30 +9,6 @@ from ..common import interact, raise_if_not_confirmed CONFIRMED = trezorui_api.CONFIRMED # global_import_cache -def _split_share_into_pages(share_words: Sequence[str], per_page: int = 4) -> list[str]: - pages: list[str] = [] - current = "" - fill = 2 - - for i, word in enumerate(share_words): - if i % per_page == 0: - if i != 0: - pages.append(current) - current = "" - - # Align numbers to the right. - lastnum = i + per_page + 1 - fill = 1 if lastnum < 10 else 2 - else: - current += "\n" - current += f"{i + 1:>{fill}}. {word}" - - if current: - pages.append(current) - - return pages - - def show_share_words( share_words: Sequence[str], share_index: int | None = None, @@ -48,12 +23,10 @@ def show_share_words( group_index + 1, share_index + 1 ) - pages = _split_share_into_pages(share_words) - return raise_if_not_confirmed( - trezorui2.show_share_words( + trezorui_api.show_share_words( + words=share_words, title=title, - pages=pages, ), "backup_words", ButtonRequestType.ResetDevice,