diff --git a/core/embed/rust/src/ui/api/firmware_upy.rs b/core/embed/rust/src/ui/api/firmware_upy.rs index ef6b271c6d..fcece645f7 100644 --- a/core/embed/rust/src/ui/api/firmware_upy.rs +++ b/core/embed/rust/src/ui/api/firmware_upy.rs @@ -358,6 +358,14 @@ extern "C" fn new_show_wait_text(message: Obj) -> Obj { unsafe { util::try_or_raise(block) } } +extern "C" fn new_tutorial(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj { + let block = |_args: &[Obj], _kwargs: &Map| { + let layout = ModelUI::tutorial()?; + Ok(LayoutObj::new_root(layout)?.into()) + }; + unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } +} + pub extern "C" fn upy_check_homescreen_format(data: Obj) -> Obj { let block = || { let buffer = data.try_into()?; @@ -654,6 +662,10 @@ pub static mp_module_trezorui_api: Module = obj_module! { /// """Show single-line text in the middle of the screen.""" Qstr::MP_QSTR_show_wait_text => obj_fn_1!(new_show_wait_text).as_obj(), + /// def tutorial() -> LayoutObj[UiResult]: + /// """Show user how to interact with the device.""" + Qstr::MP_QSTR_tutorial => obj_fn_kw!(0, new_tutorial).as_obj(), + /// class BacklightLevels: /// """Backlight levels. Values dynamically update based on user settings.""" /// MAX: ClassVar[int] diff --git a/core/embed/rust/src/ui/model_mercury/layout.rs b/core/embed/rust/src/ui/model_mercury/layout.rs index cfbae04566..f542f5c410 100644 --- a/core/embed/rust/src/ui/model_mercury/layout.rs +++ b/core/embed/rust/src/ui/model_mercury/layout.rs @@ -1028,15 +1028,6 @@ extern "C" fn new_prompt_backup() -> Obj { unsafe { util::try_or_raise(block) } } -extern "C" fn new_show_tutorial() -> Obj { - let block = || { - let flow = flow::show_tutorial::new_show_tutorial()?; - let obj = LayoutObj::new_root(flow)?; - Ok(obj.into()) - }; - unsafe { util::try_or_raise(block) } -} - extern "C" fn new_show_group_share_success( n_args: usize, args: *const Obj, @@ -1341,10 +1332,6 @@ pub static mp_module_trezorui2: Module = obj_module! { /// """Shown after successfully finishing a group.""" Qstr::MP_QSTR_show_group_share_success => obj_fn_kw!(0, new_show_group_share_success).as_obj(), - /// def tutorial() -> LayoutObj[UiResult]: - /// """Show user how to interact with the device.""" - Qstr::MP_QSTR_tutorial => obj_fn_0!(new_show_tutorial).as_obj(), - /// def flow_get_address( /// *, /// address: str | bytes, 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 index 919b68ec92..371d42ff7e 100644 --- a/core/embed/rust/src/ui/model_mercury/ui_features_fw.rs +++ b/core/embed/rust/src/ui/model_mercury/ui_features_fw.rs @@ -359,4 +359,9 @@ impl UIFeaturesFirmware for ModelMercuryFeatures { let layout = RootComponent::new(Connect::new(text, theme::FG, theme::BG)); Ok(layout) } + + fn tutorial() -> Result { + let flow = flow::show_tutorial::new_show_tutorial()?; + Ok(flow) + } } diff --git a/core/embed/rust/src/ui/model_tr/layout.rs b/core/embed/rust/src/ui/model_tr/layout.rs index 86e797d706..a92d82963a 100644 --- a/core/embed/rust/src/ui/model_tr/layout.rs +++ b/core/embed/rust/src/ui/model_tr/layout.rs @@ -753,95 +753,6 @@ extern "C" fn new_confirm_address(n_args: usize, args: *const Obj, kwargs: *mut unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } } -/// General pattern of most tutorial screens. -/// (title, text, btn_layout, btn_actions, text_y_offset) -fn tutorial_screen( - title: TString<'static>, - text: TR, - btn_layout: ButtonLayout, - btn_actions: ButtonActions, -) -> Page { - let ops = OpTextLayout::new(theme::TEXT_NORMAL).text_normal(text); - let formatted = FormattedText::new(ops).vertically_centered(); - Page::new(btn_layout, btn_actions, formatted).with_title(title) -} - -extern "C" fn tutorial(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj { - let block = |_args: &[Obj], _kwargs: &Map| { - const PAGE_COUNT: usize = 7; - - let get_page = move |page_index| { - // Lazy-loaded list of screens to show, with custom content, - // buttons and actions triggered by these buttons. - // Cancelling the first screen will point to the last one, - // which asks for confirmation whether user wants to - // really cancel the tutorial. - match page_index { - // title, text, btn_layout, btn_actions - 0 => tutorial_screen( - TR::tutorial__title_hello.into(), - TR::tutorial__welcome_press_right, - ButtonLayout::cancel_none_arrow(), - ButtonActions::last_none_next(), - ), - 1 => tutorial_screen( - "".into(), - TR::tutorial__use_trezor, - ButtonLayout::arrow_none_arrow(), - ButtonActions::prev_none_next(), - ), - 2 => tutorial_screen( - TR::buttons__hold_to_confirm.into(), - TR::tutorial__press_and_hold, - ButtonLayout::arrow_none_htc(TR::buttons__hold_to_confirm.into()), - ButtonActions::prev_none_next(), - ), - 3 => tutorial_screen( - TR::tutorial__title_screen_scroll.into(), - TR::tutorial__scroll_down, - ButtonLayout::arrow_none_text(TR::buttons__continue.into()), - ButtonActions::prev_none_next(), - ), - 4 => tutorial_screen( - TR::buttons__confirm.into(), - TR::tutorial__middle_click, - ButtonLayout::none_armed_none(TR::buttons__confirm.into()), - ButtonActions::none_next_none(), - ), - 5 => tutorial_screen( - TR::tutorial__title_tutorial_complete.into(), - TR::tutorial__ready_to_use, - ButtonLayout::text_none_text( - TR::buttons__again.into(), - TR::buttons__continue.into(), - ), - ButtonActions::beginning_none_confirm(), - ), - 6 => tutorial_screen( - TR::tutorial__title_skip.into(), - TR::tutorial__sure_you_want_skip, - ButtonLayout::arrow_none_text(TR::buttons__skip.into()), - ButtonActions::beginning_none_cancel(), - ), - _ => unreachable!(), - } - }; - - let pages = FlowPages::new(get_page, PAGE_COUNT); - - // Setting the ignore-second-button to mimic all the Choice pages, to teach user - // that they should really press both buttons at the same time to achieve - // middle-click. - let obj = LayoutObj::new( - Flow::new(pages) - .with_scrollbar(false) - .with_ignore_second_button_ms(constant::IGNORE_OTHER_BTN_MS), - )?; - Ok(obj.into()) - }; - unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } -} - extern "C" fn new_confirm_modify_fee(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj { let block = move |_args: &[Obj], kwargs: &Map| { let sign: i32 = kwargs.get(Qstr::MP_QSTR_sign)?.try_into()?; @@ -1365,10 +1276,6 @@ pub static mp_module_trezorui2: Module = obj_module! { /// """Confirm details about altcoin transaction.""" Qstr::MP_QSTR_altcoin_tx_summary => obj_fn_kw!(0, new_altcoin_tx_summary).as_obj(), - /// def tutorial() -> LayoutObj[UiResult]: - /// """Show user how to interact with the device.""" - Qstr::MP_QSTR_tutorial => obj_fn_kw!(0, tutorial).as_obj(), - /// def confirm_modify_fee( /// *, /// title: str, # ignored 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 index a8c485035e..96e94c0931 100644 --- a/core/embed/rust/src/ui/model_tr/ui_features_fw.rs +++ b/core/embed/rust/src/ui/model_tr/ui_features_fw.rs @@ -23,7 +23,10 @@ use crate::{ obj::{LayoutMaybeTrace, LayoutObj, RootComponent}, util::RecoveryType, }, - model_tr::component::{ButtonActions, ButtonLayout, Page}, + model_tr::{ + component::{ButtonActions, ButtonLayout, Page}, + constant, + }, ui_features_fw::UIFeaturesFirmware, }, }; @@ -389,6 +392,79 @@ impl UIFeaturesFirmware for ModelTRFeatures { let layout = RootComponent::new(Connect::new(text, theme::FG, theme::BG)); Ok(layout) } + + fn tutorial() -> Result { + const PAGE_COUNT: usize = 7; + + let get_page = move |page_index| { + // Lazy-loaded list of screens to show, with custom content, + // buttons and actions triggered by these buttons. + // Cancelling the first screen will point to the last one, + // which asks for confirmation whether user wants to + // really cancel the tutorial. + match page_index { + // title, text, btn_layout, btn_actions + 0 => tutorial_screen( + TR::tutorial__title_hello.into(), + TR::tutorial__welcome_press_right, + ButtonLayout::cancel_none_arrow(), + ButtonActions::last_none_next(), + ), + 1 => tutorial_screen( + "".into(), + TR::tutorial__use_trezor, + ButtonLayout::arrow_none_arrow(), + ButtonActions::prev_none_next(), + ), + 2 => tutorial_screen( + TR::buttons__hold_to_confirm.into(), + TR::tutorial__press_and_hold, + ButtonLayout::arrow_none_htc(TR::buttons__hold_to_confirm.into()), + ButtonActions::prev_none_next(), + ), + 3 => tutorial_screen( + TR::tutorial__title_screen_scroll.into(), + TR::tutorial__scroll_down, + ButtonLayout::arrow_none_text(TR::buttons__continue.into()), + ButtonActions::prev_none_next(), + ), + 4 => tutorial_screen( + TR::buttons__confirm.into(), + TR::tutorial__middle_click, + ButtonLayout::none_armed_none(TR::buttons__confirm.into()), + ButtonActions::none_next_none(), + ), + 5 => tutorial_screen( + TR::tutorial__title_tutorial_complete.into(), + TR::tutorial__ready_to_use, + ButtonLayout::text_none_text( + TR::buttons__again.into(), + TR::buttons__continue.into(), + ), + ButtonActions::beginning_none_confirm(), + ), + 6 => tutorial_screen( + TR::tutorial__title_skip.into(), + TR::tutorial__sure_you_want_skip, + ButtonLayout::arrow_none_text(TR::buttons__skip.into()), + ButtonActions::beginning_none_cancel(), + ), + _ => unreachable!(), + } + }; + + let pages = FlowPages::new(get_page, PAGE_COUNT); + + // Setting the ignore-second-button to mimic all the Choice pages, to teach user + // that they should really press both buttons at the same time to achieve + // middle-click. + let layout = RootComponent::new( + Flow::new(pages) + .with_scrollbar(false) + .with_ignore_second_button_ms(constant::IGNORE_OTHER_BTN_MS), + ); + Ok(layout) + } } /// Function to create and call a `ButtonPage` dialog based on paginable content @@ -427,3 +503,16 @@ fn content_in_button_page( Ok(RootComponent::new(frame)) } + +/// General pattern of most tutorial screens. +/// (title, text, btn_layout, btn_actions, text_y_offset) +fn tutorial_screen( + title: TString<'static>, + text: TR, + btn_layout: ButtonLayout, + btn_actions: ButtonActions, +) -> Page { + let ops = OpTextLayout::new(theme::TEXT_NORMAL).text_normal(text); + let formatted = FormattedText::new(ops).vertically_centered(); + Page::new(btn_layout, btn_actions, formatted).with_title(title) +} 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 index e653c0db1b..dcdff4d91c 100644 --- a/core/embed/rust/src/ui/model_tt/ui_features_fw.rs +++ b/core/embed/rust/src/ui/model_tt/ui_features_fw.rs @@ -440,6 +440,12 @@ impl UIFeaturesFirmware for ModelTTFeatures { let layout = RootComponent::new(Connect::new(text, theme::FG, theme::BG)); Ok(layout) } + + fn tutorial() -> Result { + Err::, Error>(Error::ValueError( + c"tutorial not supported", + )) + } } fn new_show_modal( diff --git a/core/embed/rust/src/ui/ui_features_fw.rs b/core/embed/rust/src/ui/ui_features_fw.rs index a15a3d8f37..f719c2b6da 100644 --- a/core/embed/rust/src/ui/ui_features_fw.rs +++ b/core/embed/rust/src/ui/ui_features_fw.rs @@ -120,4 +120,6 @@ pub trait UIFeaturesFirmware { ) -> Result, Error>; // TODO: return LayoutMaybeTrace fn show_wait_text(text: TString<'static>) -> Result; + + fn tutorial() -> Result; } diff --git a/core/mocks/generated/trezorui2.pyi b/core/mocks/generated/trezorui2.pyi index e6b4df8908..18b26d1273 100644 --- a/core/mocks/generated/trezorui2.pyi +++ b/core/mocks/generated/trezorui2.pyi @@ -267,11 +267,6 @@ def show_group_share_success( """Shown after successfully finishing a group.""" -# rust/src/ui/model_mercury/layout.rs -def tutorial() -> LayoutObj[UiResult]: - """Show user how to interact with the device.""" - - # rust/src/ui/model_mercury/layout.rs def flow_get_address( *, @@ -487,11 +482,6 @@ def altcoin_tx_summary( """Confirm details about altcoin transaction.""" -# rust/src/ui/model_tr/layout.rs -def tutorial() -> LayoutObj[UiResult]: - """Show user how to interact with the device.""" - - # rust/src/ui/model_tr/layout.rs def confirm_modify_fee( *, diff --git a/core/mocks/generated/trezorui_api.pyi b/core/mocks/generated/trezorui_api.pyi index ec2c2f5e01..fb8fd4a73b 100644 --- a/core/mocks/generated/trezorui_api.pyi +++ b/core/mocks/generated/trezorui_api.pyi @@ -281,6 +281,11 @@ def show_wait_text(message: str, /) -> LayoutObj[None]: """Show single-line text in the middle of the screen.""" +# rust/src/ui/api/firmware_upy.rs +def tutorial() -> LayoutObj[UiResult]: + """Show user how to interact with the device.""" + + # 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/__init__.py b/core/src/trezor/ui/layouts/mercury/__init__.py index eb34f3d683..63c60b02cd 100644 --- a/core/src/trezor/ui/layouts/mercury/__init__.py +++ b/core/src/trezor/ui/layouts/mercury/__init__.py @@ -1204,7 +1204,7 @@ def set_brightness(current: int | None = None) -> Awaitable[None]: def tutorial(br_code: ButtonRequestType = BR_CODE_OTHER) -> Awaitable[None]: """Showing users how to interact with the device.""" return raise_if_not_confirmed( - trezorui2.tutorial(), + trezorui_api.tutorial(), "tutorial", br_code, ) diff --git a/core/src/trezor/ui/layouts/tr/__init__.py b/core/src/trezor/ui/layouts/tr/__init__.py index bdb68de860..f26003df99 100644 --- a/core/src/trezor/ui/layouts/tr/__init__.py +++ b/core/src/trezor/ui/layouts/tr/__init__.py @@ -487,7 +487,7 @@ async def confirm_output( def tutorial(br_code: ButtonRequestType = BR_CODE_OTHER) -> Awaitable[ui.UiResult]: """Showing users how to interact with the device.""" - return interact(trezorui2.tutorial(), "tutorial", br_code) + return interact(trezorui_api.tutorial(), "tutorial", br_code) async def should_show_payment_request_details(