1
0
mirror of https://github.com/trezor/trezor-firmware.git synced 2024-11-22 07:28:10 +00:00

feat(core/ui): add cancel button to paginated blobs

[no changelog]
This commit is contained in:
Ioan Bizău 2024-11-14 12:35:31 +01:00
parent 8313c30d5a
commit 2bc787a652
9 changed files with 251 additions and 142 deletions

View File

@ -174,6 +174,7 @@ static void _librust_qstrs(void) {
MP_QSTR_buttons__turn_on;
MP_QSTR_buttons__view_all_data;
MP_QSTR_can_go_back;
MP_QSTR_cancel;
MP_QSTR_cancel_arrow;
MP_QSTR_cancel_cross;
MP_QSTR_cancel_text;

View File

@ -25,94 +25,20 @@ use super::super::{
theme,
};
#[derive(Copy, Clone, PartialEq, Eq)]
pub enum ConfirmAction {
Intro,
Menu,
Confirm,
}
const MENU_ITEM_CANCEL: usize = 0;
const MENU_ITEM_INFO: usize = 1;
impl FlowController for ConfirmAction {
fn index(&'static self) -> usize {
*self as usize
}
fn handle_swipe(&'static self, direction: Direction) -> Decision {
match (self, direction) {
(Self::Intro, Direction::Left) => Self::Menu.swipe(direction),
(Self::Menu, Direction::Right) => Self::Intro.swipe(direction),
(Self::Intro, Direction::Up) => Self::Confirm.swipe(direction),
(Self::Confirm, Direction::Down) => Self::Intro.swipe(direction),
(Self::Confirm, Direction::Left) => Self::Menu.swipe(direction),
_ => self.do_nothing(),
}
}
fn handle_event(&'static self, msg: FlowMsg) -> Decision {
match (self, msg) {
(Self::Intro, FlowMsg::Info) => Self::Menu.goto(),
(Self::Menu, FlowMsg::Cancelled) => Self::Intro.swipe_right(),
(Self::Menu, FlowMsg::Choice(0)) => self.return_msg(FlowMsg::Cancelled),
(Self::Menu, FlowMsg::Choice(1)) => self.return_msg(FlowMsg::Info),
(Self::Confirm, FlowMsg::Confirmed) => self.return_msg(FlowMsg::Confirmed),
(Self::Confirm, FlowMsg::Info) => Self::Menu.goto(),
_ => self.do_nothing(),
}
}
}
/// ConfirmAction flow without a separate "Tap to confirm" or "Hold to confirm"
/// screen. Swiping up directly from the intro screen confirms action.
#[derive(Copy, Clone, PartialEq, Eq)]
pub enum ConfirmActionSimple {
Intro,
Menu,
}
impl FlowController for ConfirmActionSimple {
#[inline]
fn index(&'static self) -> usize {
*self as usize
}
fn handle_swipe(&'static self, direction: Direction) -> Decision {
match (self, direction) {
(Self::Intro, Direction::Left) => Self::Menu.swipe(direction),
(Self::Menu, Direction::Right) => Self::Intro.swipe(direction),
(Self::Intro, Direction::Up) => self.return_msg(FlowMsg::Confirmed),
_ => self.do_nothing(),
}
}
fn handle_event(&'static self, msg: FlowMsg) -> Decision {
match (self, msg) {
(Self::Intro, FlowMsg::Info) => Self::Menu.goto(),
(Self::Menu, FlowMsg::Cancelled) => Self::Intro.swipe_right(),
(Self::Menu, FlowMsg::Choice(0)) => self.return_msg(FlowMsg::Cancelled),
(Self::Menu, FlowMsg::Choice(1)) => self.return_msg(FlowMsg::Info),
_ => self.do_nothing(),
}
}
}
pub struct ConfirmActionMenu {
verb_cancel: Option<TString<'static>>,
info: bool,
verb_info: Option<TString<'static>>,
}
impl ConfirmActionMenu {
pub fn new(
// Extra button at the top-right corner of the Action screen
#[derive(PartialEq)]
pub enum ConfirmActionExtra {
// Opens a menu which can (optionally) lead to an extra Info screen, or cancel the action
Menu {
verb_cancel: Option<TString<'static>>,
info: bool,
has_info: bool,
verb_info: Option<TString<'static>>,
) -> Self {
Self {
verb_cancel,
info,
verb_info,
}
}
},
// Shows a cancel button directly
Cancel,
}
pub struct ConfirmActionStrings {
@ -138,6 +64,106 @@ impl ConfirmActionStrings {
}
}
/// The simplest form of the ConfirmAction flow:
/// no menu, nor a separate "Tap to confirm" or "Hold to confirm".
#[derive(Copy, Clone, PartialEq, Eq)]
enum ConfirmAction {
Action,
}
impl FlowController for ConfirmAction {
#[inline]
fn index(&'static self) -> usize {
*self as usize
}
fn handle_swipe(&'static self, direction: Direction) -> Decision {
match (self, direction) {
(Self::Action, Direction::Up) => self.return_msg(FlowMsg::Confirmed),
_ => self.do_nothing(),
}
}
fn handle_event(&'static self, msg: FlowMsg) -> Decision {
match (self, msg) {
(Self::Action, FlowMsg::Cancelled) => self.return_msg(FlowMsg::Cancelled),
_ => self.do_nothing(),
}
}
}
/// A ConfirmAction flow with a menu which can contain various items
/// as defined by ConfirmActionExtra::Menu.
#[derive(Copy, Clone, PartialEq, Eq)]
enum ConfirmActionWithMenu {
Action,
Menu,
}
impl FlowController for ConfirmActionWithMenu {
#[inline]
fn index(&'static self) -> usize {
*self as usize
}
fn handle_swipe(&'static self, direction: Direction) -> Decision {
match (self, direction) {
(Self::Action, Direction::Left) => Self::Menu.swipe(direction),
(Self::Menu, Direction::Right) => Self::Action.swipe(direction),
(Self::Action, Direction::Up) => self.return_msg(FlowMsg::Confirmed),
_ => self.do_nothing(),
}
}
fn handle_event(&'static self, msg: FlowMsg) -> Decision {
match (self, msg) {
(Self::Action, FlowMsg::Info) => Self::Menu.goto(),
(Self::Menu, FlowMsg::Cancelled) => Self::Action.swipe_right(),
(Self::Menu, FlowMsg::Choice(MENU_ITEM_CANCEL)) => self.return_msg(FlowMsg::Cancelled),
(Self::Menu, FlowMsg::Choice(MENU_ITEM_INFO)) => self.return_msg(FlowMsg::Info),
_ => self.do_nothing(),
}
}
}
// A ConfirmAction flow with a menu and
// a separate "Tap to confirm" or "Hold to confirm" screen.
#[derive(Copy, Clone, PartialEq, Eq)]
enum ConfirmActionWithMenuAndConfirmation {
Action,
Menu,
Confirmation,
}
impl FlowController for ConfirmActionWithMenuAndConfirmation {
fn index(&'static self) -> usize {
*self as usize
}
fn handle_swipe(&'static self, direction: Direction) -> Decision {
match (self, direction) {
(Self::Action, Direction::Left) => Self::Menu.swipe(direction),
(Self::Menu, Direction::Right) => Self::Action.swipe(direction),
(Self::Action, Direction::Up) => Self::Confirmation.swipe(direction),
(Self::Confirmation, Direction::Down) => Self::Action.swipe(direction),
(Self::Confirmation, Direction::Left) => Self::Menu.swipe(direction),
_ => self.do_nothing(),
}
}
fn handle_event(&'static self, msg: FlowMsg) -> Decision {
match (self, msg) {
(Self::Action, FlowMsg::Info) => Self::Menu.goto(),
(Self::Menu, FlowMsg::Cancelled) => Self::Action.swipe_right(),
(Self::Menu, FlowMsg::Choice(MENU_ITEM_CANCEL)) => self.return_msg(FlowMsg::Cancelled),
(Self::Menu, FlowMsg::Choice(MENU_ITEM_INFO)) => self.return_msg(FlowMsg::Info),
(Self::Confirmation, FlowMsg::Confirmed) => self.return_msg(FlowMsg::Confirmed),
(Self::Confirmation, FlowMsg::Info) => Self::Menu.goto(),
_ => self.do_nothing(),
}
}
}
#[allow(clippy::too_many_arguments)]
pub fn new_confirm_action(
title: TString<'static>,
@ -168,7 +194,11 @@ pub fn new_confirm_action(
new_confirm_action_simple(
paragraphs,
ConfirmActionMenu::new(verb_cancel, false, None),
ConfirmActionExtra::Menu {
verb_cancel,
has_info: false,
verb_info: None,
},
ConfirmActionStrings::new(title, subtitle, None, prompt_screen.then_some(prompt_title)),
hold,
None,
@ -179,21 +209,29 @@ pub fn new_confirm_action(
#[inline(never)]
fn new_confirm_action_uni<T: Component + Paginate + MaybeTrace + 'static>(
content: SwipeContent<SwipePage<T>>,
menu: ConfirmActionMenu,
extra: ConfirmActionExtra,
strings: ConfirmActionStrings,
hold: bool,
show_page_counter: bool,
) -> Result<SwipeFlow, error::Error> {
let (prompt_screen, prompt_pages, flow, page) =
create_flow(strings.title, strings.prompt_screen, hold);
create_flow(strings.title, strings.prompt_screen, hold, &extra);
let mut content_intro = Frame::left_aligned(strings.title, content)
let mut content = Frame::left_aligned(strings.title, content)
.with_swipe(Direction::Up, SwipeSettings::default())
.with_swipe(Direction::Left, SwipeSettings::default())
.with_vertical_pages()
.with_menu_button()
.with_footer(TR::instructions__swipe_up.into(), None);
match extra {
ConfirmActionExtra::Menu { .. } => {
content = content.with_menu_button();
}
ConfirmActionExtra::Cancel => {
content = content.with_cancel_button();
}
}
if show_page_counter {
fn footer_update_fn<T: Component + Paginate>(
content: &SwipeContent<SwipePage<T>>,
@ -205,26 +243,25 @@ fn new_confirm_action_uni<T: Component + Paginate + MaybeTrace + 'static>(
footer.update_page_counter(ctx, current_page, page_count);
}
content_intro = content_intro
content = content
.with_footer_counter(TR::instructions__swipe_up.into())
.register_footer_update_fn(footer_update_fn::<T>);
}
if let Some(subtitle) = strings.subtitle {
content_intro = content_intro.with_subtitle(subtitle);
content = content.with_subtitle(subtitle);
}
let content_intro = content_intro
let content = content
.map(move |msg| match msg {
FrameMsg::Button(_) => Some(FlowMsg::Info),
FrameMsg::Button(FlowMsg::Info) => Some(FlowMsg::Info),
FrameMsg::Button(FlowMsg::Cancelled) => Some(FlowMsg::Cancelled),
_ => None,
})
.with_pages(move |intro_pages| intro_pages + prompt_pages);
let flow = flow?.with_page(page, content_intro)?;
let flow = create_menu(flow, menu, prompt_screen)?;
let flow = flow?.with_page(page, content)?;
let flow = create_menu(flow, extra, prompt_screen)?;
let flow = create_confirm(flow, strings.subtitle, hold, prompt_screen)?;
Ok(flow)
@ -234,6 +271,7 @@ fn create_flow(
title: TString<'static>,
prompt_screen: Option<TString<'static>>,
hold: bool,
extra: &ConfirmActionExtra,
) -> (
Option<TString<'static>>,
usize,
@ -242,53 +280,69 @@ fn create_flow(
) {
let prompt_screen = prompt_screen.or_else(|| hold.then_some(title));
let prompt_pages: usize = prompt_screen.is_some().into();
let (flow, page): (Result<SwipeFlow, Error>, &dyn FlowController) = if prompt_screen.is_some() {
(SwipeFlow::new(&ConfirmAction::Intro), &ConfirmAction::Intro)
} else {
(
SwipeFlow::new(&ConfirmActionSimple::Intro),
&ConfirmActionSimple::Intro,
)
let initial_page: &dyn FlowController = match (extra, prompt_screen.is_some()) {
(ConfirmActionExtra::Menu { .. }, false) => &ConfirmActionWithMenu::Action,
(ConfirmActionExtra::Menu { .. }, true) => &ConfirmActionWithMenuAndConfirmation::Action,
_ => &ConfirmAction::Action,
};
(prompt_screen, prompt_pages, flow, page)
(
prompt_screen,
prompt_pages,
SwipeFlow::new(initial_page),
initial_page,
)
}
fn create_menu(
flow: SwipeFlow,
menu: ConfirmActionMenu,
extra: ConfirmActionExtra,
prompt_screen: Option<TString<'static>>,
) -> Result<SwipeFlow, Error> {
let mut menu_choices = VerticalMenu::empty().danger(
theme::ICON_CANCEL,
menu.verb_cancel.unwrap_or(TR::buttons__cancel.into()),
);
if menu.info {
menu_choices = menu_choices.item(
theme::ICON_CHEVRON_RIGHT,
menu.verb_info
.unwrap_or(TR::words__title_information.into()),
if let ConfirmActionExtra::Menu {
verb_cancel,
has_info,
verb_info,
} = extra
{
// NB: The Cancel menu item is always the first,
// because of the MENU_ITEM_CANCEL = 0.
// If we want the cancel item to be somewhere else,
// we would need to account for that and we could not use a constant.
let mut menu_choices = VerticalMenu::empty().danger(
theme::ICON_CANCEL,
verb_cancel.unwrap_or(TR::buttons__cancel.into()),
);
}
let content_menu = Frame::left_aligned("".into(), menu_choices)
.with_cancel_button()
.with_swipe(Direction::Right, SwipeSettings::immediate());
if has_info {
// The Info menu item (if present) has to be the 2nd,
// because of MENU_ITEM_INFO = 1!
menu_choices = menu_choices.item(
theme::ICON_CHEVRON_RIGHT,
verb_info.unwrap_or(TR::words__title_information.into()),
);
}
let content_menu = content_menu.map(move |msg| match msg {
FrameMsg::Content(VerticalMenuChoiceMsg::Selected(i)) => Some(FlowMsg::Choice(i)),
FrameMsg::Button(_) => Some(FlowMsg::Cancelled),
});
let content_menu = Frame::left_aligned("".into(), menu_choices)
.with_cancel_button()
.with_swipe(Direction::Right, SwipeSettings::immediate());
if prompt_screen.is_some() {
flow.with_page(&ConfirmAction::Menu, content_menu)
let content_menu = content_menu.map(move |msg| match msg {
FrameMsg::Content(VerticalMenuChoiceMsg::Selected(i)) => Some(FlowMsg::Choice(i)),
FrameMsg::Button(_) => Some(FlowMsg::Cancelled),
});
if prompt_screen.is_some() {
flow.with_page(&ConfirmActionWithMenuAndConfirmation::Menu, content_menu)
} else {
flow.with_page(&ConfirmActionWithMenu::Menu, content_menu)
}
} else {
flow.with_page(&ConfirmActionSimple::Menu, content_menu)
Ok(flow) // no menu being added
}
}
// Create the extra confirmation screen (optional).
fn create_confirm(
flow: SwipeFlow,
subtitle: Option<TString<'static>>,
@ -324,7 +378,10 @@ fn create_confirm(
_ => None,
});
flow.with_page(&ConfirmAction::Confirm, content_confirm)
flow.with_page(
&ConfirmActionWithMenuAndConfirmation::Confirmation,
content_confirm,
)
} else {
Ok(flow)
}
@ -333,7 +390,7 @@ fn create_confirm(
#[inline(never)]
pub fn new_confirm_action_simple<T: Component + Paginate + MaybeTrace + 'static>(
content: T,
menu: ConfirmActionMenu,
extra: ConfirmActionExtra,
strings: ConfirmActionStrings,
hold: bool,
page_limit: Option<usize>,
@ -341,7 +398,7 @@ pub fn new_confirm_action_simple<T: Component + Paginate + MaybeTrace + 'static>
) -> Result<SwipeFlow, error::Error> {
new_confirm_action_uni(
SwipeContent::new(SwipePage::vertical(content).with_limit(page_limit)),
menu,
extra,
strings,
hold,
show_page_counter,

View File

@ -19,7 +19,7 @@ pub mod util;
pub mod warning_hi_prio;
pub use confirm_action::{
new_confirm_action, new_confirm_action_simple, ConfirmActionMenu, ConfirmActionStrings,
new_confirm_action, new_confirm_action_simple, ConfirmActionExtra, ConfirmActionStrings,
};
#[cfg(feature = "universal_fw")]
pub use confirm_fido::new_confirm_fido;

View File

@ -3,7 +3,7 @@ use super::{
component::{Frame, FrameMsg},
theme,
},
ConfirmActionMenu, ConfirmActionStrings,
ConfirmActionExtra, ConfirmActionStrings,
};
use crate::{
error::Error,
@ -52,6 +52,7 @@ pub struct ConfirmBlobParams {
swipe_up: bool,
swipe_down: bool,
swipe_right: bool,
cancel: bool,
}
impl ConfirmBlobParams {
@ -84,6 +85,7 @@ impl ConfirmBlobParams {
swipe_up: false,
swipe_down: false,
swipe_right: false,
cancel: false,
}
}
@ -152,6 +154,11 @@ impl ConfirmBlobParams {
self
}
pub const fn with_cancel(mut self, cancel: bool) -> Self {
self.cancel = cancel;
self
}
pub const fn with_footer(
mut self,
instruction: TString<'static>,
@ -262,9 +269,19 @@ impl ConfirmBlobParams {
}
.into_paragraphs();
let confirm_extra = if self.cancel {
ConfirmActionExtra::Cancel
} else {
ConfirmActionExtra::Menu {
verb_cancel: self.verb_cancel,
has_info: self.info_button,
verb_info: self.verb_info,
}
};
flow::new_confirm_action_simple(
paragraphs,
ConfirmActionMenu::new(self.verb_cancel, self.info_button, self.verb_info),
confirm_extra,
ConfirmActionStrings::new(
self.title,
self.subtitle,

View File

@ -55,7 +55,7 @@ use crate::{
flow::{
new_confirm_action_simple,
util::{ConfirmBlobParams, ShowInfoParams},
ConfirmActionMenu, ConfirmActionStrings,
ConfirmActionExtra, ConfirmActionStrings,
},
theme::ICON_BULLET_CHECKMARK,
},
@ -246,7 +246,11 @@ extern "C" fn new_confirm_emphasized(n_args: usize, args: *const Obj, kwargs: *m
new_confirm_action_simple(
FormattedText::new(ops).vertically_centered(),
ConfirmActionMenu::new(None, false, None),
ConfirmActionExtra::Menu {
verb_cancel: None,
has_info: false,
verb_info: None,
},
ConfirmActionStrings::new(title, None, None, Some(title)),
false,
None,
@ -296,6 +300,7 @@ extern "C" fn new_confirm_blob(n_args: usize, args: *const Obj, kwargs: *mut Map
.get(Qstr::MP_QSTR_page_limit)
.unwrap_or_else(|_| Obj::const_none())
.try_into_option()?;
let cancel: bool = kwargs.get_or(Qstr::MP_QSTR_cancel, false)?;
let (description, description_font) = if page_limit == Some(1) {
(
@ -318,6 +323,7 @@ extern "C" fn new_confirm_blob(n_args: usize, args: *const Obj, kwargs: *mut Map
.with_chunkify(chunkify)
.with_page_counter(page_counter)
.with_page_limit(page_limit)
.with_cancel(cancel)
.with_prompt(prompt_screen)
.with_hold(hold)
.into_flow()
@ -389,7 +395,11 @@ extern "C" fn new_confirm_address(n_args: usize, args: *const Obj, kwargs: *mut
flow::new_confirm_action_simple(
paragraphs,
ConfirmActionMenu::new(None, false, None),
ConfirmActionExtra::Menu {
verb_cancel: None,
has_info: false,
verb_info: None,
},
ConfirmActionStrings::new(title, None, None, None),
false,
None,
@ -432,7 +442,11 @@ extern "C" fn new_confirm_properties(n_args: usize, args: *const Obj, kwargs: *m
new_confirm_action_simple(
paragraphs.into_paragraphs(),
ConfirmActionMenu::new(None, false, None),
ConfirmActionExtra::Menu {
verb_cancel: None,
has_info: false,
verb_info: None,
},
ConfirmActionStrings::new(title, None, None, hold.then_some(title)),
hold,
None,
@ -462,7 +476,11 @@ extern "C" fn new_confirm_homescreen(n_args: usize, args: *const Obj, kwargs: *m
new_confirm_action_simple(
paragraphs,
ConfirmActionMenu::new(None, false, None),
ConfirmActionExtra::Menu {
verb_cancel: None,
has_info: false,
verb_info: None,
},
ConfirmActionStrings::new(
TR::homescreen__settings_title.into(),
Some(TR::homescreen__settings_subtitle.into()),
@ -774,7 +792,11 @@ extern "C" fn new_confirm_total(n_args: usize, args: *const Obj, kwargs: *mut Ma
new_confirm_action_simple(
paragraphs.into_paragraphs(),
ConfirmActionMenu::new(None, true, None),
ConfirmActionExtra::Menu {
verb_cancel: None,
has_info: true,
verb_info: None,
},
ConfirmActionStrings::new(title, None, None, Some(title)),
true,
None,
@ -1091,7 +1113,11 @@ extern "C" fn new_confirm_coinjoin(n_args: usize, args: *const Obj, kwargs: *mut
new_confirm_action_simple(
paragraphs,
ConfirmActionMenu::new(None, false, None),
ConfirmActionExtra::Menu {
verb_cancel: None,
has_info: false,
verb_info: None,
},
ConfirmActionStrings::new(
TR::coinjoin__title.into(),
None,
@ -1587,6 +1613,7 @@ pub static mp_module_trezorui2: Module = obj_module! {
/// page_counter: bool = False,
/// prompt_screen: bool = False,
/// page_limit: int | None = None,
/// cancel: bool = False,
/// ) -> LayoutObj[UiResult]:
/// """Confirm byte sequence data."""
Qstr::MP_QSTR_confirm_blob => obj_fn_kw!(0, new_confirm_blob).as_obj(),

View File

@ -1712,6 +1712,7 @@ pub static mp_module_trezorui2: Module = obj_module! {
/// page_counter: bool = False,
/// prompt_screen: bool = False,
/// page_limit: int | None = None,
/// cancel: bool = False,
/// ) -> LayoutObj[UiResult]:
/// """Confirm byte sequence data."""
Qstr::MP_QSTR_confirm_blob => obj_fn_kw!(0, new_confirm_blob).as_obj(),

View File

@ -1806,6 +1806,7 @@ pub static mp_module_trezorui2: Module = obj_module! {
/// page_counter: bool = False,
/// prompt_screen: bool = False,
/// page_limit: int | None = None,
/// cancel: bool = False,
/// ) -> LayoutObj[UiResult]:
/// """Confirm byte sequence data."""
Qstr::MP_QSTR_confirm_blob => obj_fn_kw!(0, new_confirm_blob).as_obj(),

View File

@ -71,6 +71,7 @@ def confirm_blob(
page_counter: bool = False,
prompt_screen: bool = False,
page_limit: int | None = None,
cancel: bool = False,
) -> LayoutObj[UiResult]:
"""Confirm byte sequence data."""
@ -644,6 +645,7 @@ def confirm_blob(
page_counter: bool = False,
prompt_screen: bool = False,
page_limit: int | None = None,
cancel: bool = False,
) -> LayoutObj[UiResult]:
"""Confirm byte sequence data."""
@ -1216,6 +1218,7 @@ def confirm_blob(
page_counter: bool = False,
prompt_screen: bool = False,
page_limit: int | None = None,
cancel: bool = False,
) -> LayoutObj[UiResult]:
"""Confirm byte sequence data."""

View File

@ -480,6 +480,7 @@ def confirm_blob(
info_layout = trezorui2.confirm_blob(
title=title,
data=data,
subtitle=description,
description=None,
verb=None,
verb_cancel=verb_cancel,
@ -488,6 +489,7 @@ def confirm_blob(
chunkify=chunkify,
page_counter=True,
prompt_screen=False,
cancel=True,
)
return with_info(