diff --git a/core/embed/rust/src/ui/api/firmware_upy.rs b/core/embed/rust/src/ui/api/firmware_upy.rs index c253a23786..86e49a6676 100644 --- a/core/embed/rust/src/ui/api/firmware_upy.rs +++ b/core/embed/rust/src/ui/api/firmware_upy.rs @@ -1,12 +1,43 @@ -use crate::micropython::map::Map; -use crate::ui::layout::obj::ATTACH_TYPE_OBJ; -use crate::ui::layout::base::LAYOUT_STATE; -use crate::ui::backlight::BACKLIGHT_LEVELS_OBJ; use crate::{ - micropython::{macros::obj_module, module::Module, qstr::Qstr}, - ui::layout::result::{CANCELLED, CONFIRMED, INFO}, + micropython::{ + macros::{obj_fn_kw, obj_module}, + map::Map, + module::Module, + obj::Obj, + qstr::Qstr, + util, + }, + strutil::TString, + ui::{ + backlight::BACKLIGHT_LEVELS_OBJ, + layout::{ + base::LAYOUT_STATE, + obj::{LayoutObj, ATTACH_TYPE_OBJ}, + result::{CANCELLED, CONFIRMED, INFO}, + }, + ui_features::ModelUI, + ui_features_fw::UIFeaturesFirmware, + }, }; +// free-standing functions exported to MicroPython mirror `trait +// UIFeaturesFirmware` + +extern "C" fn show_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()?; + let description: TString = kwargs.get(Qstr::MP_QSTR_description)?.try_into()?; + let button: TString = kwargs + .get_or(Qstr::MP_QSTR_button, TString::empty())? + .try_into()?; + let time_ms: u32 = kwargs.get_or(Qstr::MP_QSTR_time_ms, 0)?.try_into()?; + + let obj = ModelUI::show_info(title, description, button, time_ms)?; + Ok(obj.into()) + }; + unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } +} + #[no_mangle] pub static mp_module_trezorui_api: Module = obj_module! { /// from trezor import utils @@ -103,6 +134,16 @@ pub static mp_module_trezorui_api: Module = obj_module! { /// INFO: UiResult Qstr::MP_QSTR_INFO => INFO.as_obj(), + /// def show_info( + /// *, + /// title: str, + /// description: str = "", + /// button: str = "", + /// time_ms: int = 0, + /// ) -> LayoutObj[UiResult]: + /// """Info screen.""" + Qstr::MP_QSTR_show_info => obj_fn_kw!(0, show_info).as_obj(), + /// class BacklightLevels: /// """Backlight levels. Values dynamically update based on user settings.""" /// MAX: ClassVar[int] @@ -130,4 +171,5 @@ pub static mp_module_trezorui_api: Module = obj_module! { /// TRANSITIONING: "ClassVar[LayoutState]" /// DONE: "ClassVar[LayoutState]" Qstr::MP_QSTR_LayoutState => LAYOUT_STATE.as_obj(), + }; diff --git a/core/embed/rust/src/ui/layout/obj.rs b/core/embed/rust/src/ui/layout/obj.rs index 3ab4d64ea5..2c74c378d5 100644 --- a/core/embed/rust/src/ui/layout/obj.rs +++ b/core/embed/rust/src/ui/layout/obj.rs @@ -94,7 +94,7 @@ pub trait ComponentMsgObj: Component { pub trait ComponentMaybeTrace: Component + ComponentMsgObj + MaybeTrace {} impl ComponentMaybeTrace for T where T: Component + ComponentMsgObj + MaybeTrace {} -struct RootComponent +pub struct RootComponent where T: Component, M: UIFeaturesCommon, diff --git a/core/embed/rust/src/ui/mod.rs b/core/embed/rust/src/ui/mod.rs index e64bb0eef8..7636c026a3 100644 --- a/core/embed/rust/src/ui/mod.rs +++ b/core/embed/rust/src/ui/mod.rs @@ -23,6 +23,7 @@ pub mod model_mercury; pub mod model_tr; #[cfg(feature = "model_tt")] pub mod model_tt; + pub mod ui_features; #[cfg(feature = "micropython")] pub mod ui_features_fw; diff --git a/core/embed/rust/src/ui/model_mercury/layout.rs b/core/embed/rust/src/ui/model_mercury/layout.rs index fb5e082b78..c7fdda14da 100644 --- a/core/embed/rust/src/ui/model_mercury/layout.rs +++ b/core/embed/rust/src/ui/model_mercury/layout.rs @@ -964,21 +964,6 @@ extern "C" fn new_show_success(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_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()?; - let description: TString = kwargs.get(Qstr::MP_QSTR_description)?.try_into()?; - let content = Paragraphs::new(Paragraph::new(&theme::TEXT_MAIN_GREY_LIGHT, description)); - let obj = LayoutObj::new(SwipeUpScreen::new( - Frame::left_aligned(title, SwipeContent::new(content)) - .with_footer(TR::instructions__swipe_up.into(), None) - .with_swipe(Direction::Up, SwipeSettings::default()), - ))?; - Ok(obj.into()) - }; - unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } -} - extern "C" fn new_show_mismatch(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()?; @@ -1512,7 +1497,6 @@ pub static mp_module_trezorui2: Module = obj_module! { /// Qstr::MP_QSTR___name__ => Qstr::MP_QSTR_trezorui2.to_obj(), - /// def disable_animation(disable: bool) -> None: /// """Disable animations, debug builds only.""" Qstr::MP_QSTR_disable_animation => obj_fn_1!(upy_disable_animation).as_obj(), @@ -1716,17 +1700,6 @@ pub static mp_module_trezorui2: Module = obj_module! { /// """Success screen. Description is used in the footer.""" Qstr::MP_QSTR_show_success => obj_fn_kw!(0, new_show_success).as_obj(), - /// def show_info( - /// *, - /// title: str, - /// button: str = "CONTINUE", - /// description: str = "", - /// allow_cancel: bool = False, - /// time_ms: int = 0, - /// ) -> LayoutObj[UiResult]: - /// """Info modal. No buttons shown when `button` is empty string.""" - Qstr::MP_QSTR_show_info => obj_fn_kw!(0, new_show_info).as_obj(), - /// def show_mismatch(*, title: str) -> LayoutObj[UiResult]: /// """Warning modal, receiving address mismatch.""" Qstr::MP_QSTR_show_mismatch => obj_fn_kw!(0, new_show_mismatch).as_obj(), 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 new file mode 100644 index 0000000000..e4bb653400 --- /dev/null +++ b/core/embed/rust/src/ui/model_mercury/ui_features_fw.rs @@ -0,0 +1,37 @@ +use crate::{ + error::Error, + micropython::gc::Gc, + strutil::TString, + translations::TR, + ui::{ + component::{ + swipe_detect::SwipeSettings, + text::paragraphs::{Paragraph, Paragraphs}, + }, + geometry::Direction, + layout::obj::{LayoutMaybeTrace, LayoutObj, RootComponent}, + ui_features_fw::UIFeaturesFirmware, + }, +}; + +use super::{ + component::{Frame, SwipeContent, SwipeUpScreen}, + theme, ModelMercuryFeatures, +}; + +impl UIFeaturesFirmware for ModelMercuryFeatures { + fn show_info( + title: TString<'static>, + description: TString<'static>, + _button: TString<'static>, + _time_ms: u32, + ) -> Result, Error> { + let content = Paragraphs::new(Paragraph::new(&theme::TEXT_MAIN_GREY_LIGHT, description)); + let obj = LayoutObj::new(SwipeUpScreen::new( + Frame::left_aligned(title, SwipeContent::new(content)) + .with_footer(TR::instructions__swipe_up.into(), None) + .with_swipe(Direction::Up, SwipeSettings::default()), + ))?; + Ok(obj) + } +} diff --git a/core/embed/rust/src/ui/model_tr/layout.rs b/core/embed/rust/src/ui/model_tr/layout.rs index 91f58bc98c..730958b74e 100644 --- a/core/embed/rust/src/ui/model_tr/layout.rs +++ b/core/embed/rust/src/ui/model_tr/layout.rs @@ -1098,31 +1098,6 @@ extern "C" fn new_show_warning(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_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()?; - let description: TString = kwargs.get_or(Qstr::MP_QSTR_description, "".into())?; - let time_ms: u32 = kwargs.get_or(Qstr::MP_QSTR_time_ms, 0)?; - - let content = Frame::new( - title, - Paragraphs::new([Paragraph::new(&theme::TEXT_NORMAL, description)]), - ); - let obj = if time_ms == 0 { - // No timer, used when we only want to draw the dialog once and - // then throw away the layout object. - LayoutObj::new(content)? - } else { - // Timeout. - let timeout = Timeout::new(time_ms); - LayoutObj::new((timeout, content.map(|_| None)))? - }; - - Ok(obj.into()) - }; - unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } -} - extern "C" fn new_show_passphrase() -> Obj { let block = move || { let text: TString = TR::passphrase__please_enter.into(); @@ -1871,15 +1846,6 @@ pub static mp_module_trezorui2: Module = obj_module! { /// """Warning modal with middle button and centered text.""" Qstr::MP_QSTR_show_warning => obj_fn_kw!(0, new_show_warning).as_obj(), - /// def show_info( - /// *, - /// title: str, - /// description: str = "", - /// time_ms: int = 0, - /// ) -> LayoutObj[UiResult]: - /// """Info modal.""" - Qstr::MP_QSTR_show_info => obj_fn_kw!(0, new_show_info).as_obj(), - /// def show_passphrase() -> LayoutObj[UiResult]: /// """Show passphrase on host dialog.""" Qstr::MP_QSTR_show_passphrase => obj_fn_0!(new_show_passphrase).as_obj(), diff --git a/core/embed/rust/src/ui/model_tr/mod.rs b/core/embed/rust/src/ui/model_tr/mod.rs index b68581716c..8ad9254362 100644 --- a/core/embed/rust/src/ui/model_tr/mod.rs +++ b/core/embed/rust/src/ui/model_tr/mod.rs @@ -12,6 +12,7 @@ mod screens; pub mod theme; pub struct ModelTRFeatures {} +pub mod ui_features_fw; impl UIFeaturesCommon for ModelTRFeatures { const SCREEN: Rect = constant::SCREEN; 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 new file mode 100644 index 0000000000..c914a99e55 --- /dev/null +++ b/core/embed/rust/src/ui/model_tr/ui_features_fw.rs @@ -0,0 +1,38 @@ +use crate::{ + error::Error, + micropython::gc::Gc, + strutil::TString, + ui::{ + component::{ + text::paragraphs::{Paragraph, Paragraphs}, ComponentExt, Timeout + }, + layout::obj::{LayoutMaybeTrace, LayoutObj, RootComponent}, + ui_features_fw::UIFeaturesFirmware, + }, +}; + +use super::{component::Frame, theme, ModelTRFeatures}; + +impl UIFeaturesFirmware for ModelTRFeatures { + fn show_info( + title: TString<'static>, + description: TString<'static>, + _button: TString<'static>, + time_ms: u32, + ) -> Result, Error> { + let content = Frame::new( + title, + Paragraphs::new([Paragraph::new(&theme::TEXT_NORMAL, description)]), + ); + let obj = if time_ms == 0 { + // No timer, used when we only want to draw the dialog once and + // then throw away the layout object. + LayoutObj::new(content)? + } else { + // Timeout. + let timeout = Timeout::new(time_ms); + LayoutObj::new((timeout, content.map(|_| None)))? + }; + Ok(obj) + } +} diff --git a/core/embed/rust/src/ui/model_tt/layout.rs b/core/embed/rust/src/ui/model_tt/layout.rs index b0d034faa3..5624e0f8b4 100644 --- a/core/embed/rust/src/ui/model_tt/layout.rs +++ b/core/embed/rust/src/ui/model_tt/layout.rs @@ -1024,20 +1024,6 @@ extern "C" fn new_show_success(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_info(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj { - let block = move |_args: &[Obj], kwargs: &Map| { - let icon = BlendedImage::new( - theme::IMAGE_BG_CIRCLE, - theme::IMAGE_FG_INFO, - theme::INFO_COLOR, - theme::FG, - theme::BG, - ); - new_show_modal(kwargs, icon, theme::button_info()) - }; - unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } -} - extern "C" fn new_show_mismatch(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()?; @@ -1653,7 +1639,6 @@ pub static mp_module_trezorui2: Module = obj_module! { /// from trezor import utils /// from trezorui_api import * /// - /// def disable_animation(disable: bool) -> None: /// """Disable animations, debug builds only.""" Qstr::MP_QSTR_disable_animation => obj_fn_1!(upy_disable_animation).as_obj(), @@ -1865,17 +1850,6 @@ pub static mp_module_trezorui2: Module = obj_module! { /// """Success modal. No buttons shown when `button` is empty string.""" Qstr::MP_QSTR_show_success => obj_fn_kw!(0, new_show_success).as_obj(), - /// def show_info( - /// *, - /// title: str, - /// button: str = "CONTINUE", - /// description: str = "", - /// allow_cancel: bool = False, - /// time_ms: int = 0, - /// ) -> LayoutObj[UiResult]: - /// """Info modal. No buttons shown when `button` is empty string.""" - Qstr::MP_QSTR_show_info => obj_fn_kw!(0, new_show_info).as_obj(), - /// def show_mismatch(*, title: str) -> LayoutObj[UiResult]: /// """Warning modal, receiving address mismatch.""" Qstr::MP_QSTR_show_mismatch => obj_fn_kw!(0, new_show_mismatch).as_obj(), diff --git a/core/embed/rust/src/ui/model_tt/mod.rs b/core/embed/rust/src/ui/model_tt/mod.rs index 63042191f7..e3570ac104 100644 --- a/core/embed/rust/src/ui/model_tt/mod.rs +++ b/core/embed/rust/src/ui/model_tt/mod.rs @@ -19,6 +19,7 @@ use crate::ui::{ }; pub struct ModelTTFeatures; +pub mod ui_features_fw; impl UIFeaturesCommon for ModelTTFeatures { #[cfg(feature = "backlight")] 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 new file mode 100644 index 0000000000..129e207db3 --- /dev/null +++ b/core/embed/rust/src/ui/model_tt/ui_features_fw.rs @@ -0,0 +1,111 @@ +use crate::{ + error::Error, + micropython::gc::Gc, + strutil::TString, + ui::{ + component::{image::BlendedImage, ComponentExt, Empty, Timeout}, + layout::obj::{LayoutMaybeTrace, LayoutObj, RootComponent}, + ui_features_fw::UIFeaturesFirmware, + }, +}; + +use super::{ + component::{Button, ButtonMsg, ButtonStyleSheet, CancelConfirmMsg, IconDialog}, + theme, ModelTTFeatures, +}; + +impl UIFeaturesFirmware for ModelTTFeatures { + fn show_info( + title: TString<'static>, + description: TString<'static>, + button: TString<'static>, + time_ms: u32, + ) -> Result, Error> { + assert!( + !button.is_empty() || time_ms > 0, + "either button or timeout must be set" + ); + + let icon = BlendedImage::new( + theme::IMAGE_BG_CIRCLE, + theme::IMAGE_FG_INFO, + theme::INFO_COLOR, + theme::FG, + theme::BG, + ); + let res = new_show_modal( + title, + TString::empty(), + description, + TString::empty(), + false, + time_ms, + icon, + theme::button_info(), + )?; + Ok(res) + } +} + +fn new_show_modal( + title: TString<'static>, + value: TString<'static>, + description: TString<'static>, + button: TString<'static>, + allow_cancel: bool, + time_ms: u32, + icon: BlendedImage, + button_style: ButtonStyleSheet, +) -> Result, Error> { + let no_buttons = button.is_empty(); + let obj = if no_buttons && time_ms == 0 { + // No buttons and no timer, used when we only want to draw the dialog once and + // then throw away the layout object. + LayoutObj::new( + IconDialog::new(icon, title, Empty) + .with_value(value) + .with_description(description), + )? + } else if no_buttons && time_ms > 0 { + // Timeout, no buttons. + LayoutObj::new( + IconDialog::new( + icon, + title, + Timeout::new(time_ms).map(|_| Some(CancelConfirmMsg::Confirmed)), + ) + .with_value(value) + .with_description(description), + )? + } else if allow_cancel { + // Two buttons. + LayoutObj::new( + IconDialog::new( + icon, + title, + Button::cancel_confirm( + Button::with_icon(theme::ICON_CANCEL), + Button::with_text(button).styled(button_style), + false, + ), + ) + .with_value(value) + .with_description(description), + )? + } else { + // Single button. + LayoutObj::new( + IconDialog::new( + icon, + title, + theme::button_bar(Button::with_text(button).styled(button_style).map(|msg| { + (matches!(msg, ButtonMsg::Clicked)).then(|| CancelConfirmMsg::Confirmed) + })), + ) + .with_value(value) + .with_description(description), + )? + }; + + Ok(obj) +} diff --git a/core/embed/rust/src/ui/ui_features_fw.rs b/core/embed/rust/src/ui/ui_features_fw.rs new file mode 100644 index 0000000000..f721394cb3 --- /dev/null +++ b/core/embed/rust/src/ui/ui_features_fw.rs @@ -0,0 +1,12 @@ +use crate::{error::Error, micropython::gc::Gc, strutil::TString}; + +use super::layout::obj::{LayoutMaybeTrace, LayoutObj}; + +pub trait UIFeaturesFirmware { + fn show_info( + title: TString<'static>, + description: TString<'static>, + button: TString<'static>, + time_ms: u32, + ) -> Result, Error>; +} diff --git a/core/mocks/generated/trezorui2.pyi b/core/mocks/generated/trezorui2.pyi index 6765d58f2d..8a594f1c9c 100644 --- a/core/mocks/generated/trezorui2.pyi +++ b/core/mocks/generated/trezorui2.pyi @@ -223,18 +223,6 @@ def show_success( """Success screen. Description is used in the footer.""" -# rust/src/ui/model_mercury/layout.rs -def show_info( - *, - title: str, - button: str = "CONTINUE", - description: str = "", - allow_cancel: bool = False, - time_ms: int = 0, -) -> LayoutObj[UiResult]: - """Info modal. No buttons shown when `button` is empty string.""" - - # rust/src/ui/model_mercury/layout.rs def show_mismatch(*, title: str) -> LayoutObj[UiResult]: """Warning modal, receiving address mismatch.""" @@ -793,16 +781,6 @@ def show_warning( """Warning modal with middle button and centered text.""" -# rust/src/ui/model_tr/layout.rs -def show_info( - *, - title: str, - description: str = "", - time_ms: int = 0, -) -> LayoutObj[UiResult]: - """Info modal.""" - - # rust/src/ui/model_tr/layout.rs def show_passphrase() -> LayoutObj[UiResult]: """Show passphrase on host dialog.""" @@ -1251,18 +1229,6 @@ def show_success( """Success modal. No buttons shown when `button` is empty string.""" -# rust/src/ui/model_tt/layout.rs -def show_info( - *, - title: str, - button: str = "CONTINUE", - description: str = "", - allow_cancel: bool = False, - time_ms: int = 0, -) -> LayoutObj[UiResult]: - """Info modal. No buttons shown when `button` is empty string.""" - - # rust/src/ui/model_tt/layout.rs def show_mismatch(*, title: str) -> LayoutObj[UiResult]: """Warning modal, receiving address mismatch.""" diff --git a/core/mocks/generated/trezorui_api.pyi b/core/mocks/generated/trezorui_api.pyi index aac8e6b8ac..17f0c7f367 100644 --- a/core/mocks/generated/trezorui_api.pyi +++ b/core/mocks/generated/trezorui_api.pyi @@ -68,6 +68,17 @@ CANCELLED: UiResult INFO: UiResult +# rust/src/ui/api/firmware_upy.rs +def show_info( + *, + title: str, + description: str = "", + button: str = "", + time_ms: int = 0, +) -> LayoutObj[UiResult]: + """Info screen.""" + + # rust/src/ui/api/firmware_upy.rs class BacklightLevels: """Backlight levels. Values dynamically update based on user settings.""" diff --git a/core/src/trezor/ui/layouts/mercury/reset.py b/core/src/trezor/ui/layouts/mercury/reset.py index 48ec5352d9..1e9d853a82 100644 --- a/core/src/trezor/ui/layouts/mercury/reset.py +++ b/core/src/trezor/ui/layouts/mercury/reset.py @@ -290,8 +290,9 @@ async def show_intro_backup(single_share: bool, num_of_words: int | None) -> Non description = TR.backup__info_multi_share_backup await interact( - trezorui2.show_info( - title=TR.backup__title_create_wallet_backup, description=description + trezorui_api.show_info( + title=TR.backup__title_create_wallet_backup, + description=description, ), "backup_intro", ButtonRequestType.ResetDevice, diff --git a/core/src/trezor/ui/layouts/tr/__init__.py b/core/src/trezor/ui/layouts/tr/__init__.py index 622c744782..f0d5c543a8 100644 --- a/core/src/trezor/ui/layouts/tr/__init__.py +++ b/core/src/trezor/ui/layouts/tr/__init__.py @@ -1136,7 +1136,7 @@ def error_popup( description = description.format(description_param) if subtitle: description = f"{subtitle}\n{description}" - return trezorui2.show_info( + return trezorui_api.show_info( title=title, description=description, time_ms=timeout_ms, diff --git a/core/src/trezor/ui/layouts/tt/reset.py b/core/src/trezor/ui/layouts/tt/reset.py index b3b380c0d2..2aa2a7a70a 100644 --- a/core/src/trezor/ui/layouts/tt/reset.py +++ b/core/src/trezor/ui/layouts/tt/reset.py @@ -313,11 +313,10 @@ def show_intro_backup(single_share: bool, num_of_words: int | None) -> Awaitable description = TR.backup__info_multi_share_backup return raise_if_not_confirmed( - trezorui2.show_info( + trezorui_api.show_info( title="", - button=TR.buttons__continue, description=description, - allow_cancel=False, + button=TR.buttons__continue, ), "backup_intro", ButtonRequestType.ResetDevice, @@ -326,10 +325,10 @@ def show_intro_backup(single_share: bool, num_of_words: int | None) -> Awaitable def show_warning_backup() -> Awaitable[trezorui_api.UiResult]: return interact( - trezorui2.show_info( + trezorui_api.show_info( title=TR.reset__never_make_digital_copy, + description="", button=TR.buttons__ok_i_understand, - allow_cancel=False, ), "backup_warning", ButtonRequestType.ResetDevice,