1
0
mirror of https://github.com/trezor/trezor-firmware.git synced 2024-12-19 12:58:13 +00:00

refactor(core/mercury): self-updating footer

This commit enables registering function for updating footer and header
based on the content. This eliminates the need to create wrappers around
Frame to update them.

[no changelog]
This commit is contained in:
obrusvit 2024-09-04 14:15:40 +02:00 committed by Vít Obrusník
parent 7a992a593d
commit d15ecfb859
10 changed files with 139 additions and 267 deletions

View File

@ -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;

View File

@ -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<F: Fn(usize) -> TString<'static>>(
Frame<SwipeContent<SwipePage<PagedVerticalMenu<F>>>>,
);
impl<F: Fn(usize) -> TString<'static>> ChooseCredential<F> {
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<F: Fn(usize) -> TString<'static>> Component for ChooseCredential<F> {
type Msg = FrameMsg<VerticalMenuChoiceMsg>;
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<Self::Msg> {
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<F: Fn(usize) -> TString<'static>> Swipable for ChooseCredential<F> {
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<F: Fn(usize) -> TString<'static>> crate::trace::Trace for ChooseCredential<F> {
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
self.0.trace(t)
}
}

View File

@ -85,7 +85,9 @@ pub struct Frame<T> {
bounds: Rect,
content: T,
header: Header,
header_update_fn: Option<fn(&T, &mut EventCtx, &mut Header)>,
footer: Option<Footer<'static>>,
footer_update_fn: Option<fn(&T, &mut EventCtx, &mut Footer)>,
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<TextStyle>,
) {
self.header.update_subtitle(ctx, new_subtitle, new_style);
}
pub fn update_content<F, R>(&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<usize>,
) {
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
}

View File

@ -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;

View File

@ -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<u8, MAX_WORDS>;
/// 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<TString<'a>, MAX_WORDS>,
subtitle: TString<'static>,
frame: Frame<InternallySwipableContent<ShareWordsInner<'a>>>,
repeated_indices: Option<IndexVec>,
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<TString<'a>, 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<TString<'a>, 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<Never>;
fn place(&mut self, bounds: Rect) -> Rect {
self.frame.place(bounds);
bounds
}
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
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<TString<'a>, 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<TString<'a>, 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];

View File

@ -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<SwipePage<PagedVerticalMenu<impl Fn(usize) -> 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);

View File

@ -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<ShareWords>,
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<ShareWords>,
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<Obj, error::Error> {
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,

View File

@ -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."""

View File

@ -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."""

View File

@ -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,
)
)