diff --git a/core/embed/rust/librust_qstr.h b/core/embed/rust/librust_qstr.h index 6daa251c2..83721d376 100644 --- a/core/embed/rust/librust_qstr.h +++ b/core/embed/rust/librust_qstr.h @@ -259,7 +259,6 @@ static void _librust_qstrs(void) { MP_QSTR_haptic_feedback__enable; MP_QSTR_haptic_feedback__subtitle; MP_QSTR_haptic_feedback__title; - MP_QSTR_highlight_repeated; MP_QSTR_hold; MP_QSTR_hold_danger; MP_QSTR_homescreen__click_to_connect; diff --git a/core/embed/rust/src/ui/model_mercury/component/choose_credential.rs b/core/embed/rust/src/ui/model_mercury/component/choose_credential.rs deleted file mode 100644 index 43cc6b4bf..000000000 --- a/core/embed/rust/src/ui/model_mercury/component/choose_credential.rs +++ /dev/null @@ -1,101 +0,0 @@ -use crate::{ - strutil::TString, - translations::TR, - ui::{ - component::{swipe_detect::SwipeSettings, Component, SwipeDirection}, - flow::{Swipable, SwipePage}, - }, -}; - -use super::{ - Frame, FrameMsg, InternallySwipable as _, PagedVerticalMenu, SwipeContent, - VerticalMenuChoiceMsg, -}; - -#[derive(Copy, Clone, PartialEq, Eq)] -pub enum ConfirmFido { - Intro, - ChooseCredential, - Details, - Tap, - Menu, -} - -/// Wrapper that updates `Footer` content whenever page is changed. -pub struct ChooseCredential TString<'static>>( - Frame>>>, -); - -impl TString<'static>> ChooseCredential { - pub fn new(label_fn: F, num_accounts: usize) -> Self { - let content_choose_credential = Frame::left_aligned( - TR::fido__title_select_credential.into(), - SwipeContent::new(SwipePage::vertical(PagedVerticalMenu::new( - num_accounts, - label_fn, - ))), - ) - .with_subtitle(TR::fido__title_for_authentication.into()) - .with_menu_button() - .with_footer_page_hint( - TR::fido__more_credentials.into(), - TR::buttons__go_back.into(), - TR::instructions__swipe_up.into(), - TR::instructions__swipe_down.into(), - ) - .with_swipe(SwipeDirection::Down, SwipeSettings::default()) - .with_swipe(SwipeDirection::Right, SwipeSettings::immediate()) - .with_vertical_pages(); - - Self(content_choose_credential) - } -} - -impl TString<'static>> Component for ChooseCredential { - type Msg = FrameMsg; - - fn place(&mut self, bounds: crate::ui::geometry::Rect) -> crate::ui::geometry::Rect { - self.0.place(bounds) - } - - fn event( - &mut self, - ctx: &mut crate::ui::component::EventCtx, - event: crate::ui::component::Event, - ) -> Option { - let msg = self.0.event(ctx, event); - let current_page = self.0.inner().inner().inner().current_page(); - - self.0.update_footer_counter( - ctx, - current_page, - Some(self.0.inner().inner().inner().num_pages()), - ); - msg - } - - fn paint(&mut self) { - self.0.paint() - } - - fn render<'s>(&'s self, target: &mut impl crate::ui::shape::Renderer<'s>) { - self.0.render(target) - } -} - -impl TString<'static>> Swipable for ChooseCredential { - fn get_swipe_config(&self) -> crate::ui::component::swipe_detect::SwipeConfig { - self.0.get_swipe_config() - } - - fn get_internal_page_count(&self) -> usize { - self.0.get_internal_page_count() - } -} - -#[cfg(feature = "ui_debug")] -impl TString<'static>> crate::trace::Trace for ChooseCredential { - fn trace(&self, t: &mut dyn crate::trace::Tracer) { - self.0.trace(t) - } -} diff --git a/core/embed/rust/src/ui/model_mercury/component/frame.rs b/core/embed/rust/src/ui/model_mercury/component/frame.rs index 410c04b94..3f7fa384a 100644 --- a/core/embed/rust/src/ui/model_mercury/component/frame.rs +++ b/core/embed/rust/src/ui/model_mercury/component/frame.rs @@ -85,7 +85,9 @@ pub struct Frame { bounds: Rect, content: T, header: Header, + header_update_fn: Option, footer: Option>, + footer_update_fn: Option, swipe: SwipeConfig, internal_page_cnt: usize, horizontal_swipe: HorizontalSwipe, @@ -106,7 +108,9 @@ where border: theme::borders(), content, header: Header::new(alignment, title), + header_update_fn: None, footer: None, + footer_update_fn: None, swipe: SwipeConfig::new(), internal_page_cnt: 1, horizontal_swipe: HorizontalSwipe::new(), @@ -217,6 +221,16 @@ where self } + pub fn register_header_update_fn(mut self, f: fn(&T, &mut EventCtx, &mut Header)) -> Self { + self.header_update_fn = Some(f); + self + } + + pub fn register_footer_update_fn(mut self, f: fn(&T, &mut EventCtx, &mut Footer)) -> Self { + self.footer_update_fn = Some(f); + self + } + pub fn with_danger(self) -> Self { self.button_styled(theme::button_danger()) .title_styled(theme::label_title_danger()) @@ -230,15 +244,6 @@ where self.header.update_title(ctx, new_title); } - pub fn update_subtitle( - &mut self, - ctx: &mut EventCtx, - new_subtitle: TString<'static>, - new_style: Option, - ) { - self.header.update_subtitle(ctx, new_subtitle, new_style); - } - pub fn update_content(&mut self, ctx: &mut EventCtx, update_fn: F) -> R where F: Fn(&mut EventCtx, &mut T) -> R, @@ -248,17 +253,6 @@ where res } - pub fn update_footer_counter( - &mut self, - ctx: &mut EventCtx, - current: usize, - max: Option, - ) { - if let Some(footer) = &mut self.footer { - footer.update_page_counter(ctx, current, max); - } - } - #[inline(never)] pub fn with_swipe(mut self, dir: SwipeDirection, settings: SwipeSettings) -> Self { self.footer = self.footer.map(|f| f.with_swipe(dir)); @@ -316,6 +310,16 @@ where return msg; } + if let Some(header_update_fn) = self.header_update_fn { + header_update_fn(&self.content, ctx, &mut self.header); + } + + if let Some(footer_update_fn) = self.footer_update_fn { + if let Some(footer) = &mut self.footer { + footer_update_fn(&self.content, ctx, footer); + } + } + None } diff --git a/core/embed/rust/src/ui/model_mercury/component/mod.rs b/core/embed/rust/src/ui/model_mercury/component/mod.rs index 48f3a3513..113ab70d7 100644 --- a/core/embed/rust/src/ui/model_mercury/component/mod.rs +++ b/core/embed/rust/src/ui/model_mercury/component/mod.rs @@ -4,8 +4,6 @@ mod address_details; mod binary_selection; pub mod bl_confirm; mod button; -#[cfg(feature = "universal_fw")] -mod choose_credential; #[cfg(feature = "translations")] mod coinjoin_progress; mod fido; @@ -48,8 +46,6 @@ pub use address_details::AddressDetails; #[cfg(feature = "ui_overlay")] pub use binary_selection::{BinarySelection, BinarySelectionMsg}; pub use button::{Button, ButtonContent, ButtonMsg, ButtonStyle, ButtonStyleSheet, IconText}; -#[cfg(feature = "universal_fw")] -pub use choose_credential::ChooseCredential; #[cfg(feature = "translations")] pub use coinjoin_progress::CoinJoinProgress; pub use error::ErrorScreen; diff --git a/core/embed/rust/src/ui/model_mercury/component/share_words.rs b/core/embed/rust/src/ui/model_mercury/component/share_words.rs index 46562c79b..bb8dac64d 100644 --- a/core/embed/rust/src/ui/model_mercury/component/share_words.rs +++ b/core/embed/rust/src/ui/model_mercury/component/share_words.rs @@ -1,18 +1,14 @@ -use super::{theme, InternallySwipableContent}; +use super::theme; use crate::{ strutil::TString, translations::TR, ui::{ component::{ - base::AttachType, - swipe_detect::{SwipeConfig, SwipeSettings}, - Component, Event, EventCtx, Never, SwipeDirection, + base::AttachType, text::TextStyle, Component, Event, EventCtx, Never, SwipeDirection, }, event::SwipeEvent, geometry::{Alignment, Alignment2D, Insets, Offset, Rect}, - model_mercury::component::{ - swipe_content::SwipeAttachAnimation, Frame, FrameMsg, InternallySwipable, - }, + model_mercury::component::{swipe_content::SwipeAttachAnimation, InternallySwipable}, shape::{self, Renderer}, }, }; @@ -25,43 +21,45 @@ type IndexVec = Vec; /// Component showing mnemonic/share words during backup procedure. Model T3T1 /// contains one word per screen. A user is instructed to swipe up/down to see /// next/previous word. -/// This is a wrapper around a Frame so that the subtitle and Footer of the -/// Frame can be updated based on the index of the word shown. Actual share -/// words are rendered within `ShareWordsInner` component, pub struct ShareWords<'a> { + share_words: Vec, MAX_WORDS>, subtitle: TString<'static>, - frame: Frame>>, - repeated_indices: Option, + page_index: i16, + next_index: i16, + /// Area reserved for a shown word from mnemonic/share + area_word: Rect, + progress: i16, + attach_animation: SwipeAttachAnimation, + wait_for_attach: bool, + repeated_indices: IndexVec, } impl<'a> ShareWords<'a> { - pub fn new( - title: TString<'static>, - subtitle: TString<'static>, - share_words: Vec, MAX_WORDS>, - highlight_repeated: bool, - ) -> Self { - let repeated_indices = if highlight_repeated { - Some(Self::find_repeated(share_words.as_slice())) - } else { - None - }; - let n_words = share_words.len(); + const AREA_WORD_HEIGHT: i16 = 91; + + pub fn new(share_words: Vec, MAX_WORDS>, subtitle: TString<'static>) -> Self { + let repeated_indices = Self::find_repeated(share_words.as_slice()); Self { + share_words, subtitle, - frame: Frame::left_aligned( - title, - InternallySwipableContent::new(ShareWordsInner::new(share_words)), - ) - .with_swipe(SwipeDirection::Up, SwipeSettings::default()) - .with_swipe(SwipeDirection::Down, SwipeSettings::default()) - .with_vertical_pages() - .with_subtitle(subtitle) - .with_footer_counter(TR::instructions__swipe_up.into(), n_words as u8), + page_index: 0, + next_index: 0, + area_word: Rect::zero(), + progress: 0, + attach_animation: SwipeAttachAnimation::new(), + wait_for_attach: false, repeated_indices, } } + fn is_first_page(&self) -> bool { + self.page_index == 0 + } + + fn is_final_page(&self) -> bool { + self.page_index == self.share_words.len() as i16 - 1 + } + fn find_repeated(share_words: &[TString]) -> IndexVec { let mut repeated_indices = IndexVec::new(); for i in (0..share_words.len()).rev() { @@ -73,93 +71,16 @@ impl<'a> ShareWords<'a> { repeated_indices.reverse(); repeated_indices } -} -impl<'a> Component for ShareWords<'a> { - type Msg = FrameMsg; - - fn place(&mut self, bounds: Rect) -> Rect { - self.frame.place(bounds); - bounds - } - - fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { - let page_index = self.frame.inner().inner().page_index; - if let Some(repeated_indices) = &self.repeated_indices { - if repeated_indices.contains(&(page_index as u8)) { - let updated_subtitle = TString::from_translation(TR::reset__the_word_is_repeated); - self.frame - .update_subtitle(ctx, updated_subtitle, Some(theme::TEXT_SUB_GREEN_LIME)); - } else { - self.frame - .update_subtitle(ctx, self.subtitle, Some(theme::TEXT_SUB_GREY)); - } + pub fn subtitle(&self) -> (TString<'static>, &'static TextStyle) { + if self.repeated_indices.contains(&(self.page_index as u8)) { + return ( + TString::from_translation(TR::reset__the_word_is_repeated), + &theme::TEXT_SUB_GREEN_LIME, + ); } - self.frame - .update_footer_counter(ctx, page_index as usize, None); - self.frame.event(ctx, event) - } - fn paint(&mut self) { - // TODO: remove when ui-t3t1 done - } - - fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { - self.frame.render(target); - } -} - -#[cfg(feature = "micropython")] -impl<'a> crate::ui::flow::Swipable for ShareWords<'a> { - fn get_swipe_config(&self) -> SwipeConfig { - self.frame.get_swipe_config() - } - - fn get_internal_page_count(&self) -> usize { - self.frame.get_internal_page_count() - } -} - -#[cfg(feature = "ui_debug")] -impl<'a> crate::trace::Trace for ShareWords<'a> { - fn trace(&self, t: &mut dyn crate::trace::Tracer) { - t.component("ShareWords"); - t.child("inner", &self.frame); - } -} - -struct ShareWordsInner<'a> { - share_words: Vec, MAX_WORDS>, - page_index: i16, - next_index: i16, - /// Area reserved for a shown word from mnemonic/share - area_word: Rect, - progress: i16, - attach_animation: SwipeAttachAnimation, - wait_for_attach: bool, -} - -impl<'a> ShareWordsInner<'a> { - const AREA_WORD_HEIGHT: i16 = 91; - - fn new(share_words: Vec, MAX_WORDS>) -> Self { - Self { - share_words, - page_index: 0, - next_index: 0, - area_word: Rect::zero(), - progress: 0, - attach_animation: SwipeAttachAnimation::new(), - wait_for_attach: false, - } - } - - fn is_first_page(&self) -> bool { - self.page_index == 0 - } - - fn is_final_page(&self) -> bool { - self.page_index == self.share_words.len() as i16 - 1 + (self.subtitle, &theme::TEXT_SUB_GREY) } fn render_word<'s>(&self, word_index: i16, target: &mut impl Renderer<'s>, area: Rect) { @@ -200,7 +121,7 @@ impl<'a> ShareWordsInner<'a> { } } -impl<'a> Component for ShareWordsInner<'a> { +impl<'a> Component for ShareWords<'a> { type Msg = Never; fn place(&mut self, bounds: Rect) -> Rect { @@ -210,7 +131,7 @@ impl<'a> Component for ShareWordsInner<'a> { self.area_word = Rect::snap( used_area.center(), - Offset::new(used_area.width(), ShareWordsInner::AREA_WORD_HEIGHT), + Offset::new(used_area.width(), ShareWords::AREA_WORD_HEIGHT), Alignment2D::CENTER, ); @@ -311,7 +232,7 @@ impl<'a> Component for ShareWordsInner<'a> { let offset = self .attach_animation - .get_offset(t, ShareWordsInner::AREA_WORD_HEIGHT); + .get_offset(t, ShareWords::AREA_WORD_HEIGHT); target.in_clip(self.area_word, &|target| { target.with_origin(offset, &|target| { @@ -324,7 +245,7 @@ impl<'a> Component for ShareWordsInner<'a> { } } -impl InternallySwipable for ShareWordsInner<'_> { +impl InternallySwipable for ShareWords<'_> { fn current_page(&self) -> usize { self.page_index as usize } @@ -335,7 +256,7 @@ impl InternallySwipable for ShareWordsInner<'_> { } #[cfg(feature = "ui_debug")] -impl<'a> crate::trace::Trace for ShareWordsInner<'a> { +impl<'a> crate::trace::Trace for ShareWords<'a> { fn trace(&self, t: &mut dyn crate::trace::Tracer) { t.component("ShareWordsInner"); let word = &self.share_words[self.page_index as usize]; diff --git a/core/embed/rust/src/ui/model_mercury/flow/confirm_fido.rs b/core/embed/rust/src/ui/model_mercury/flow/confirm_fido.rs index 3961c4114..306563b5e 100644 --- a/core/embed/rust/src/ui/model_mercury/flow/confirm_fido.rs +++ b/core/embed/rust/src/ui/model_mercury/flow/confirm_fido.rs @@ -7,11 +7,11 @@ use crate::{ component::{ swipe_detect::SwipeSettings, text::paragraphs::{Paragraph, Paragraphs}, - ComponentExt, SwipeDirection, + ComponentExt, EventCtx, SwipeDirection, }, flow::{ base::{DecisionBuilder as _, StateChange}, - FlowMsg, FlowState, SwipeFlow, + FlowMsg, FlowState, SwipeFlow, SwipePage, }, layout::obj::LayoutObj, }, @@ -19,8 +19,8 @@ use crate::{ use super::super::{ component::{ - ChooseCredential, FidoCredential, Frame, FrameMsg, PromptMsg, PromptScreen, SwipeContent, - VerticalMenu, VerticalMenuChoiceMsg, + FidoCredential, Footer, Frame, FrameMsg, InternallySwipable, PagedVerticalMenu, PromptMsg, + PromptScreen, SwipeContent, VerticalMenu, VerticalMenuChoiceMsg, }, theme, }; @@ -85,6 +85,16 @@ pub extern "C" fn new_confirm_fido(n_args: usize, args: *const Obj, kwargs: *mut unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, ConfirmFido::new_obj) } } +fn footer_update_fn( + content: &SwipeContent TString<'static>>>>, + ctx: &mut EventCtx, + footer: &mut Footer, +) { + let current_page = content.inner().inner().current_page(); + let total_pages = content.inner().inner().num_pages(); + footer.update_page_counter(ctx, current_page, Some(total_pages)); +} + impl ConfirmFido { const EXTRA_PADDING: i16 = 6; @@ -123,11 +133,30 @@ impl ConfirmFido { .try_into() .unwrap_or_else(|_| TString::from_str("-")) }; - let content_choose_credential = - ChooseCredential::new(label_fn, num_accounts).map(|msg| match msg { - FrameMsg::Button(_) => Some(FlowMsg::Info), - FrameMsg::Content(VerticalMenuChoiceMsg::Selected(i)) => Some(FlowMsg::Choice(i)), - }); + + let content_choose_credential = Frame::left_aligned( + TR::fido__title_select_credential.into(), + SwipeContent::new(SwipePage::vertical(PagedVerticalMenu::new( + num_accounts, + label_fn, + ))), + ) + .with_subtitle(TR::fido__title_for_authentication.into()) + .with_menu_button() + .with_footer_page_hint( + TR::fido__more_credentials.into(), + TR::buttons__go_back.into(), + TR::instructions__swipe_up.into(), + TR::instructions__swipe_down.into(), + ) + .register_footer_update_fn(footer_update_fn) + .with_swipe(SwipeDirection::Down, SwipeSettings::default()) + .with_swipe(SwipeDirection::Right, SwipeSettings::immediate()) + .with_vertical_pages() + .map(|msg| match msg { + FrameMsg::Button(_) => Some(FlowMsg::Info), + FrameMsg::Content(VerticalMenuChoiceMsg::Selected(i)) => Some(FlowMsg::Choice(i)), + }); let get_account = move || { let current = CRED_SELECTED.load(Ordering::Relaxed); 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 06fd8e78a..99b51833c 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 @@ -8,20 +8,20 @@ use crate::{ component::{ swipe_detect::SwipeSettings, text::paragraphs::{Paragraph, ParagraphSource, ParagraphVecShort, Paragraphs, VecExt}, - ButtonRequestExt, ComponentExt, SwipeDirection, + ButtonRequestExt, ComponentExt, EventCtx, SwipeDirection, }, flow::{ base::{DecisionBuilder as _, StateChange}, FlowMsg, FlowState, SwipeFlow, }, layout::obj::LayoutObj, - model_mercury::component::SwipeContent, + model_mercury::component::{InternallySwipable, InternallySwipableContent, SwipeContent}, }, }; use heapless::Vec; use super::super::{ - component::{Frame, FrameMsg, PromptScreen, ShareWords}, + component::{Footer, Frame, FrameMsg, Header, PromptScreen, ShareWords}, theme, }; @@ -65,6 +65,24 @@ pub extern "C" fn new_show_share_words(n_args: usize, args: *const Obj, kwargs: unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, ShowShareWords::new_obj) } } +fn header_updating_func( + content: &InternallySwipableContent, + ctx: &mut EventCtx, + header: &mut Header, +) { + let (subtitle, subtitle_style) = content.inner().subtitle(); + header.update_subtitle(ctx, subtitle, Some(*subtitle_style)); +} +fn footer_updating_func( + content: &InternallySwipableContent, + ctx: &mut EventCtx, + footer: &mut Footer, +) { + let current_page = content.inner().current_page(); + let total_pages = content.inner().num_pages(); + footer.update_page_counter(ctx, current_page, Some(total_pages)); +} + impl ShowShareWords { fn new_obj(_args: &[Obj], kwargs: &Map) -> Result { let title: TString = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?; @@ -77,7 +95,6 @@ impl ShowShareWords { .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 highlight_repeated: bool = kwargs.get(Qstr::MP_QSTR_highlight_repeated)?.try_into()?; let nwords = share_words_vec.len(); let mut instructions_paragraphs = ParagraphVecShort::new(); @@ -101,8 +118,19 @@ impl ShowShareWords { .one_button_request(ButtonRequestCode::ResetDevice.with_name("share_words")) .with_pages(move |_| nwords + 2); - let content_words = - ShareWords::new(title, subtitle, share_words_vec, highlight_repeated).map(|_| None); + let n_words = share_words_vec.len(); + let content_words = Frame::left_aligned( + title, + InternallySwipableContent::new(ShareWords::new(share_words_vec, subtitle)), + ) + .with_swipe(SwipeDirection::Up, SwipeSettings::default()) + .with_swipe(SwipeDirection::Down, SwipeSettings::default()) + .with_vertical_pages() + .with_subtitle(subtitle) + .register_header_update_fn(header_updating_func) + .with_footer_counter(TR::instructions__swipe_up.into(), n_words as u8) + .register_footer_update_fn(footer_updating_func) + .map(|_| None); let content_confirm = Frame::left_aligned( text_confirm, diff --git a/core/embed/rust/src/ui/model_mercury/layout.rs b/core/embed/rust/src/ui/model_mercury/layout.rs index 7f02f41dc..dd0478406 100644 --- a/core/embed/rust/src/ui/model_mercury/layout.rs +++ b/core/embed/rust/src/ui/model_mercury/layout.rs @@ -1524,7 +1524,6 @@ pub static mp_module_trezorui2: Module = obj_module! { /// description: str, /// text_info: Iterable[str], /// text_confirm: str, - /// highlight_repeated: bool, /// ) -> LayoutObj[UiResult]: /// """Show wallet backup words preceded by an instruction screen and followed by /// confirmation.""" diff --git a/core/mocks/generated/trezorui2.pyi b/core/mocks/generated/trezorui2.pyi index d83b7d20a..59e58ca27 100644 --- a/core/mocks/generated/trezorui2.pyi +++ b/core/mocks/generated/trezorui2.pyi @@ -403,7 +403,6 @@ def flow_show_share_words( description: str, text_info: Iterable[str], text_confirm: str, - highlight_repeated: bool, ) -> LayoutObj[UiResult]: """Show wallet backup words preceded by an instruction screen and followed by confirmation.""" diff --git a/core/src/trezor/ui/layouts/mercury/reset.py b/core/src/trezor/ui/layouts/mercury/reset.py index 03f5c33e6..a3c5ca8da 100644 --- a/core/src/trezor/ui/layouts/mercury/reset.py +++ b/core/src/trezor/ui/layouts/mercury/reset.py @@ -23,7 +23,6 @@ async def show_share_words( ) -> None: title = TR.reset__recovery_wallet_backup_title - highlight_repeated = True if share_index is None: subtitle = "" elif group_index is None: @@ -52,7 +51,6 @@ async def show_share_words( description=description, text_info=text_info, text_confirm=text_confirm, - highlight_repeated=highlight_repeated, ) )