diff --git a/core/embed/rust/librust_qstr.h b/core/embed/rust/librust_qstr.h index a6c8e52b7..639ec4d91 100644 --- a/core/embed/rust/librust_qstr.h +++ b/core/embed/rust/librust_qstr.h @@ -358,6 +358,7 @@ static void _librust_qstrs(void) { MP_QSTR_progress__x_seconds_left_template; MP_QSTR_progress_event; MP_QSTR_prompt; + MP_QSTR_prompt_screen; MP_QSTR_qr_title; MP_QSTR_reboot_to_bootloader__just_a_moment; MP_QSTR_reboot_to_bootloader__restart; diff --git a/core/embed/rust/src/ui/model_mercury/flow/confirm_action.rs b/core/embed/rust/src/ui/model_mercury/flow/confirm_action.rs new file mode 100644 index 000000000..64f956b96 --- /dev/null +++ b/core/embed/rust/src/ui/model_mercury/flow/confirm_action.rs @@ -0,0 +1,210 @@ +use crate::{ + error, + strutil::TString, + translations::TR, + ui::{ + component::{text::paragraphs::Paragraph, ComponentExt, SwipeDirection}, + flow::{base::Decision, flow_store, FlowMsg, FlowState, FlowStore, SwipeFlow, SwipePage}, + }, +}; + +use super::super::{ + component::{Frame, FrameMsg, PromptScreen, VerticalMenu, VerticalMenuChoiceMsg}, + theme, +}; + +// TODO: merge with code from https://github.com/trezor/trezor-firmware/pull/3805 +// when ready + +#[derive(Copy, Clone, PartialEq, Eq, ToPrimitive)] +pub enum ConfirmAction { + Intro, + Menu, + Confirm, +} + +/// 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, ToPrimitive)] +pub enum ConfirmActionSimple { + Intro, + Menu, +} + +impl FlowState for ConfirmAction { + fn handle_swipe(&self, direction: SwipeDirection) -> Decision { + match (self, direction) { + (ConfirmAction::Intro, SwipeDirection::Left) => { + Decision::Goto(ConfirmAction::Menu, direction) + } + (ConfirmAction::Menu, SwipeDirection::Right) => { + Decision::Goto(ConfirmAction::Intro, direction) + } + (ConfirmAction::Intro, SwipeDirection::Up) => { + Decision::Goto(ConfirmAction::Confirm, direction) + } + (ConfirmAction::Confirm, SwipeDirection::Down) => { + Decision::Goto(ConfirmAction::Intro, direction) + } + (ConfirmAction::Confirm, SwipeDirection::Left) => { + Decision::Goto(ConfirmAction::Menu, direction) + } + _ => Decision::Nothing, + } + } + + fn handle_event(&self, msg: FlowMsg) -> Decision { + match (self, msg) { + (ConfirmAction::Intro, FlowMsg::Info) => { + Decision::Goto(ConfirmAction::Menu, SwipeDirection::Left) + } + (ConfirmAction::Menu, FlowMsg::Cancelled) => { + Decision::Goto(ConfirmAction::Intro, SwipeDirection::Right) + } + (ConfirmAction::Menu, FlowMsg::Choice(0)) => Decision::Return(FlowMsg::Cancelled), + (ConfirmAction::Confirm, FlowMsg::Confirmed) => Decision::Return(FlowMsg::Confirmed), + (ConfirmAction::Confirm, FlowMsg::Info) => { + Decision::Goto(ConfirmAction::Menu, SwipeDirection::Left) + } + _ => Decision::Nothing, + } + } +} + +impl FlowState for ConfirmActionSimple { + fn handle_swipe(&self, direction: SwipeDirection) -> Decision { + match (self, direction) { + (ConfirmActionSimple::Intro, SwipeDirection::Left) => { + Decision::Goto(ConfirmActionSimple::Menu, direction) + } + (ConfirmActionSimple::Menu, SwipeDirection::Right) => { + Decision::Goto(ConfirmActionSimple::Intro, direction) + } + (ConfirmActionSimple::Intro, SwipeDirection::Up) => { + Decision::Return(FlowMsg::Confirmed) + } + _ => Decision::Nothing, + } + } + + fn handle_event(&self, msg: FlowMsg) -> Decision { + match (self, msg) { + (ConfirmActionSimple::Intro, FlowMsg::Info) => { + Decision::Goto(ConfirmActionSimple::Menu, SwipeDirection::Left) + } + (ConfirmActionSimple::Menu, FlowMsg::Cancelled) => { + Decision::Goto(ConfirmActionSimple::Intro, SwipeDirection::Right) + } + (ConfirmActionSimple::Menu, FlowMsg::Choice(0)) => Decision::Return(FlowMsg::Cancelled), + _ => Decision::Nothing, + } + } +} + +use crate::{ + micropython::{map::Map, obj::Obj, qstr::Qstr, util}, + ui::{ + component::text::paragraphs::{ParagraphSource, ParagraphVecShort, VecExt}, + layout::obj::LayoutObj, + }, +}; + +#[allow(clippy::not_unsafe_ptr_arg_deref)] +pub extern "C" fn new_confirm_action(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj { + unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, new_confirm_action_obj) } +} + +fn new_confirm_action_obj(_args: &[Obj], kwargs: &Map) -> Result { + let title: TString = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?; + let action: Option = kwargs.get(Qstr::MP_QSTR_action)?.try_into_option()?; + let description: Option = kwargs.get(Qstr::MP_QSTR_description)?.try_into_option()?; + // let verb: Option = kwargs + // .get(Qstr::MP_QSTR_verb) + // .unwrap_or_else(|_| Obj::const_none()) + // .try_into_option()?; + let verb_cancel: Option = kwargs + .get(Qstr::MP_QSTR_verb_cancel) + .unwrap_or_else(|_| Obj::const_none()) + .try_into_option()?; + let reverse: bool = kwargs.get_or(Qstr::MP_QSTR_reverse, false)?; + let hold: bool = kwargs.get_or(Qstr::MP_QSTR_hold, false)?; + // let hold_danger: bool = kwargs.get_or(Qstr::MP_QSTR_hold_danger, false)?; + let prompt_screen: bool = kwargs.get_or(Qstr::MP_QSTR_prompt_screen, false)?; + + let paragraphs = { + let action = action.unwrap_or("".into()); + let description = description.unwrap_or("".into()); + let mut paragraphs = ParagraphVecShort::new(); + if !reverse { + paragraphs + .add(Paragraph::new(&theme::TEXT_MAIN_GREY_LIGHT, action)) + .add(Paragraph::new(&theme::TEXT_MAIN_GREY_LIGHT, description)); + } else { + paragraphs + .add(Paragraph::new(&theme::TEXT_MAIN_GREY_LIGHT, description)) + .add(Paragraph::new(&theme::TEXT_MAIN_GREY_LIGHT, action)); + } + paragraphs.into_paragraphs() + }; + + let content_intro = Frame::left_aligned(title, SwipePage::vertical(paragraphs)) + .with_menu_button() + .with_footer(TR::instructions__swipe_up.into(), None) + .map(|msg| matches!(msg, FrameMsg::Button(_)).then_some(FlowMsg::Info)); + + let content_menu = if let Some(verb_cancel) = verb_cancel { + Frame::left_aligned( + "".into(), + VerticalMenu::empty().danger(theme::ICON_CANCEL, verb_cancel.into()), + ) + } else { + Frame::left_aligned( + "".into(), + VerticalMenu::empty().danger(theme::ICON_CANCEL, TR::buttons__cancel.into()), + ) + } + .with_cancel_button() + .map(move |msg| match msg { + FrameMsg::Content(VerticalMenuChoiceMsg::Selected(_)) => Some(FlowMsg::Choice(0)), + FrameMsg::Button(_) => Some(FlowMsg::Cancelled), + }); + + if !prompt_screen { + let store = flow_store().add(content_intro)?.add(content_menu)?; + let res = SwipeFlow::new(ConfirmActionSimple::Intro, store)?; + return Ok(LayoutObj::new(res)?.into()); + } else { + let (prompt, prompt_action) = if hold { + ( + PromptScreen::new_hold_to_confirm(), + TR::instructions__hold_to_confirm.into(), + ) + } else { + ( + PromptScreen::new_tap_to_confirm(), + TR::instructions__tap_to_confirm.into(), + ) + }; + + let content_confirm = Frame::left_aligned(title, prompt) + .with_footer(prompt_action, None) + .with_menu_button(); + // .with_overlapping_content(); + + // if let Some(subtitle) = subtitle { + // content_confirm = content_confirm.with_subtitle(subtitle); + // } + + let content_confirm = content_confirm.map(move |msg| match msg { + FrameMsg::Content(()) => Some(FlowMsg::Confirmed), + FrameMsg::Button(_) => Some(FlowMsg::Info), + }); + + let store = flow_store() + .add(content_intro)? + .add(content_menu)? + .add(content_confirm)?; + let res = SwipeFlow::new(ConfirmAction::Intro, store)?; + return Ok(LayoutObj::new(res)?.into()); + }; +} diff --git a/core/embed/rust/src/ui/model_mercury/flow/mod.rs b/core/embed/rust/src/ui/model_mercury/flow/mod.rs index af52f0367..cf7533821 100644 --- a/core/embed/rust/src/ui/model_mercury/flow/mod.rs +++ b/core/embed/rust/src/ui/model_mercury/flow/mod.rs @@ -1,3 +1,4 @@ +pub mod confirm_action; pub mod confirm_reset_create; pub mod confirm_reset_recover; pub mod confirm_set_new_pin; @@ -6,6 +7,7 @@ pub mod prompt_backup; pub mod show_share_words; pub mod warning_hi_prio; +pub use confirm_action::new_confirm_action; pub use confirm_reset_create::ConfirmResetCreate; pub use confirm_reset_recover::ConfirmResetRecover; pub use confirm_set_new_pin::SetNewPin; diff --git a/core/embed/rust/src/ui/model_mercury/layout.rs b/core/embed/rust/src/ui/model_mercury/layout.rs index 2f14470e0..df100680a 100644 --- a/core/embed/rust/src/ui/model_mercury/layout.rs +++ b/core/embed/rust/src/ui/model_mercury/layout.rs @@ -331,54 +331,6 @@ impl ComponentMsgObj for super::component::bl_confirm::Confirm<'_> { } } -extern "C" fn new_confirm_action(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 action: Option = kwargs.get(Qstr::MP_QSTR_action)?.try_into_option()?; - let description: Option = - kwargs.get(Qstr::MP_QSTR_description)?.try_into_option()?; - let verb: Option = kwargs - .get(Qstr::MP_QSTR_verb) - .unwrap_or_else(|_| Obj::const_none()) - .try_into_option()?; - let verb_cancel: Option = kwargs - .get(Qstr::MP_QSTR_verb_cancel) - .unwrap_or_else(|_| Obj::const_none()) - .try_into_option()?; - let reverse: bool = kwargs.get_or(Qstr::MP_QSTR_reverse, false)?; - let hold: bool = kwargs.get_or(Qstr::MP_QSTR_hold, false)?; - let hold_danger: bool = kwargs.get_or(Qstr::MP_QSTR_hold_danger, false)?; - - let paragraphs = { - let action = action.unwrap_or("".into()); - let description = description.unwrap_or("".into()); - let mut paragraphs = ParagraphVecShort::new(); - if !reverse { - paragraphs - .add(Paragraph::new(&theme::TEXT_DEMIBOLD, action)) - .add(Paragraph::new(&theme::TEXT_NORMAL, description)); - } else { - paragraphs - .add(Paragraph::new(&theme::TEXT_NORMAL, description)) - .add(Paragraph::new(&theme::TEXT_DEMIBOLD, action)); - } - paragraphs.into_paragraphs() - }; - - let mut page = if hold { - ButtonPage::new(paragraphs, theme::BG).with_hold()? - } else { - ButtonPage::new(paragraphs, theme::BG).with_cancel_confirm(verb_cancel, verb) - }; - if hold && hold_danger { - page = page.with_confirm_style(theme::button_danger()) - } - let obj = LayoutObj::new(Frame::left_aligned(title, page))?; - Ok(obj.into()) - }; - unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } -} - extern "C" fn new_confirm_emphasized(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()?; @@ -1610,14 +1562,16 @@ pub static mp_module_trezorui2: Module = obj_module! { /// title: str, /// action: str | None, /// description: str | None, + /// subtitle: str | None = None, /// verb: str | None = None, /// verb_cancel: str | None = None, /// hold: bool = False, /// hold_danger: bool = False, /// reverse: bool = False, + /// prompt_screen: bool = False, /// ) -> LayoutObj[UiResult]: /// """Confirm action.""" - Qstr::MP_QSTR_confirm_action => obj_fn_kw!(0, new_confirm_action).as_obj(), + Qstr::MP_QSTR_confirm_action => obj_fn_kw!(0, flow::confirm_action::new_confirm_action).as_obj(), /// def confirm_emphasized( /// *, diff --git a/core/mocks/generated/trezorui2.pyi b/core/mocks/generated/trezorui2.pyi index a18d10966..97f0eef91 100644 --- a/core/mocks/generated/trezorui2.pyi +++ b/core/mocks/generated/trezorui2.pyi @@ -76,11 +76,13 @@ def confirm_action( title: str, action: str | None, description: str | None, + subtitle: str | None = None, verb: str | None = None, verb_cancel: str | None = None, hold: bool = False, hold_danger: bool = False, reverse: bool = False, + prompt_screen: bool = False, ) -> LayoutObj[UiResult]: """Confirm action."""