diff --git a/core/embed/rust/src/ui/model_tr/component/homescreen.rs b/core/embed/rust/src/ui/model_tr/component/homescreen.rs index 61ca4c9692..687350c4bd 100644 --- a/core/embed/rust/src/ui/model_tr/component/homescreen.rs +++ b/core/embed/rust/src/ui/model_tr/component/homescreen.rs @@ -12,7 +12,7 @@ use crate::{ use super::{ super::constant, common::display_center, theme, ButtonController, ButtonControllerMsg, - ButtonLayout, + ButtonLayout, ButtonPos, CancelConfirmMsg, }; const AREA: Rect = constant::screen(); @@ -220,6 +220,70 @@ where } } +pub struct ConfirmHomescreen +where + T: StringType, +{ + title: Child>, + buffer_func: F, + buttons: Child>, +} + +impl ConfirmHomescreen +where + T: StringType + Clone, +{ + pub fn new(title: T, buffer_func: F) -> Self { + let btn_layout = ButtonLayout::cancel_none_text("CHANGE".into()); + ConfirmHomescreen { + title: Child::new(Label::centered(title, theme::TEXT_BOLD)), + buffer_func, + buttons: Child::new(ButtonController::new(btn_layout)), + } + } +} + +impl<'a, T, F> Component for ConfirmHomescreen +where + T: StringType + Clone, + F: Fn() -> &'a [u8], +{ + type Msg = CancelConfirmMsg; + + fn place(&mut self, bounds: Rect) -> Rect { + let (title_content_area, button_area) = bounds.split_bottom(theme::BUTTON_HEIGHT); + let title_height = theme::TEXT_BOLD.text_font.line_height(); + let (title_area, _) = title_content_area.split_top(title_height); + self.title.place(title_area); + self.buttons.place(button_area); + bounds + } + + fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { + // Left button cancels, right confirms + if let Some(ButtonControllerMsg::Triggered(pos)) = self.buttons.event(ctx, event) { + match pos { + ButtonPos::Left => return Some(CancelConfirmMsg::Cancelled), + ButtonPos::Right => return Some(CancelConfirmMsg::Confirmed), + _ => {} + } + } + None + } + + fn paint(&mut self) { + // Drawing the image full-screen first and then other things on top + let toif_data = unwrap!(Toif::new((self.buffer_func)())); + toif_data.draw(TOP_CENTER, Alignment2D::TOP_CENTER, theme::FG, theme::BG); + // Need to make all the title background black, so the title text is well + // visible + let title_area = self.title.inner().area(); + rect_fill(title_area, theme::BG); + self.title.paint(); + self.buttons.paint(); + } +} + // DEBUG-ONLY SECTION BELOW #[cfg(feature = "ui_debug")] @@ -243,3 +307,14 @@ where t.child("label", &self.label); } } + +#[cfg(feature = "ui_debug")] +impl crate::trace::Trace for ConfirmHomescreen +where + T: StringType, +{ + fn trace(&self, t: &mut dyn crate::trace::Tracer) { + t.component("ConfirmHomescreen"); + t.child("title", &self.title); + } +} diff --git a/core/embed/rust/src/ui/model_tr/component/mod.rs b/core/embed/rust/src/ui/model_tr/component/mod.rs index c116265770..50592b5a89 100644 --- a/core/embed/rust/src/ui/model_tr/component/mod.rs +++ b/core/embed/rust/src/ui/model_tr/component/mod.rs @@ -50,7 +50,7 @@ pub use flow::Flow; pub use flow_pages::{FlowPages, Page}; pub use frame::{Frame, ScrollableContent, ScrollableFrame}; #[cfg(feature = "micropython")] -pub use homescreen::{Homescreen, Lockscreen}; +pub use homescreen::{ConfirmHomescreen, Homescreen, Lockscreen}; pub use input_methods::{ number_input::NumberInput, passphrase::PassphraseEntry, diff --git a/core/embed/rust/src/ui/model_tr/layout.rs b/core/embed/rust/src/ui/model_tr/layout.rs index ee1b015388..a013fc8eb2 100644 --- a/core/embed/rust/src/ui/model_tr/layout.rs +++ b/core/embed/rust/src/ui/model_tr/layout.rs @@ -6,8 +6,15 @@ use crate::{ error::Error, maybe_trace::MaybeTrace, micropython::{ - buffer::StrBuffer, gc::Gc, iter::IterBuf, list::List, map::Map, module::Module, obj::Obj, - qstr::Qstr, util, + buffer::{get_buffer, StrBuffer}, + gc::Gc, + iter::IterBuf, + list::List, + map::Map, + module::Module, + obj::Obj, + qstr::Qstr, + util, }, strutil::StringType, ui::{ @@ -38,9 +45,10 @@ use crate::{ use super::{ component::{ AddressDetails, ButtonActions, ButtonDetails, ButtonLayout, ButtonPage, CancelConfirmMsg, - CancelInfoConfirmMsg, CoinJoinProgress, Flow, FlowPages, Frame, Homescreen, Lockscreen, - NumberInput, Page, PassphraseEntry, PinEntry, Progress, ScrollableContent, ScrollableFrame, - ShareWords, ShowMore, SimpleChoice, WelcomeScreen, WordlistEntry, WordlistType, + CancelInfoConfirmMsg, CoinJoinProgress, ConfirmHomescreen, Flow, FlowPages, Frame, + Homescreen, Lockscreen, NumberInput, Page, PassphraseEntry, PinEntry, Progress, + ScrollableContent, ScrollableFrame, ShareWords, ShowMore, SimpleChoice, WelcomeScreen, + WordlistEntry, WordlistType, }, constant, theme, }; @@ -242,6 +250,19 @@ where } } +impl<'a, T, F> ComponentMsgObj for ConfirmHomescreen +where + T: StringType + Clone, + F: Fn() -> &'a [u8], +{ + fn msg_try_into_obj(&self, msg: Self::Msg) -> Result { + match msg { + CancelConfirmMsg::Confirmed => Ok(CONFIRMED.as_obj()), + CancelConfirmMsg::Cancelled => Ok(CANCELLED.as_obj()), + } + } +} + /// Function to create and call a `ButtonPage` dialog based on paginable content /// (e.g. `Paragraphs` or `FormattedText`). /// Has optional title (supply empty `StrBuffer` for that) and hold-to-confirm @@ -392,6 +413,24 @@ extern "C" fn new_confirm_properties(n_args: usize, args: *const Obj, kwargs: *m unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } } +extern "C" fn new_confirm_homescreen(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj { + let block = move |_args: &[Obj], kwargs: &Map| { + let title: StrBuffer = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?; + let data: Obj = kwargs.get(Qstr::MP_QSTR_image)?; + + // Layout needs to hold the Obj to play nice with GC. Obj is resolved to &[u8] + // in every paint pass. + // SAFETY: We expect no existing mutable reference. Resulting reference is + // discarded before returning to micropython. + let buffer_func = move || unsafe { unwrap!(get_buffer(data)) }; + + let obj = LayoutObj::new(ConfirmHomescreen::new(title, buffer_func))?; + Ok(obj.into()) + }; + + unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } +} + extern "C" fn new_confirm_reset_device(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj { let block = move |_args: &[Obj], kwargs: &Map| { let title: StrBuffer = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?; @@ -1587,6 +1626,14 @@ pub static mp_module_trezorui2: Module = obj_module! { /// """Confirm action.""" Qstr::MP_QSTR_confirm_action => obj_fn_kw!(0, new_confirm_action).as_obj(), + /// def confirm_homescreen( + /// *, + /// title: str, + /// image: bytes, + /// ) -> object: + /// """Confirm homescreen.""" + Qstr::MP_QSTR_confirm_homescreen => obj_fn_kw!(0, new_confirm_homescreen).as_obj(), + /// def confirm_blob( /// *, /// title: str, diff --git a/core/mocks/generated/trezorui2.pyi b/core/mocks/generated/trezorui2.pyi index fbd9789f11..66d85354ad 100644 --- a/core/mocks/generated/trezorui2.pyi +++ b/core/mocks/generated/trezorui2.pyi @@ -29,6 +29,15 @@ def confirm_action( """Confirm action.""" +# rust/src/ui/model_tr/layout.rs +def confirm_homescreen( + *, + title: str, + image: bytes, +) -> object: + """Confirm homescreen.""" + + # rust/src/ui/model_tr/layout.rs def confirm_blob( *, diff --git a/core/src/apps/management/apply_settings.py b/core/src/apps/management/apply_settings.py index 1dd067afd1..6bf3fffea6 100644 --- a/core/src/apps/management/apply_settings.py +++ b/core/src/apps/management/apply_settings.py @@ -3,7 +3,7 @@ from typing import TYPE_CHECKING import trezorui2 from trezor import utils from trezor.enums import ButtonRequestType -from trezor.ui.layouts import confirm_action, confirm_homescreen, confirm_single +from trezor.ui.layouts import confirm_action from trezor.wire import DataError if TYPE_CHECKING: @@ -151,6 +151,8 @@ async def apply_settings(msg: ApplySettings) -> Success: async def _require_confirm_change_homescreen(homescreen: bytes) -> None: + from trezor.ui.layouts import confirm_homescreen + if homescreen == b"": await confirm_action( "set_homescreen", @@ -159,12 +161,12 @@ async def _require_confirm_change_homescreen(homescreen: bytes) -> None: br_code=BRT_PROTECT_CALL, ) else: - await confirm_homescreen( - homescreen, - ) + await confirm_homescreen(homescreen) async def _require_confirm_change_label(label: str) -> None: + from trezor.ui.layouts import confirm_single + await confirm_single( "set_label", "Device name", diff --git a/core/src/trezor/ui/layouts/tr/__init__.py b/core/src/trezor/ui/layouts/tr/__init__.py index f7e445cb5b..b4aeee1b0c 100644 --- a/core/src/trezor/ui/layouts/tr/__init__.py +++ b/core/src/trezor/ui/layouts/tr/__init__.py @@ -443,12 +443,17 @@ async def confirm_path_warning( async def confirm_homescreen( image: bytes, ) -> None: - # TODO: show homescreen preview? - await confirm_action( - "set_homescreen", - "Set homescreen", - description="Do you really want to set new homescreen image?", - br_code=ButtonRequestType.ProtectCall, + await raise_if_not_confirmed( + interact( + RustLayout( + trezorui2.confirm_homescreen( + title="CHANGE HOMESCREEN?", + image=image, + ) + ), + "set_homesreen", + ButtonRequestType.ProtectCall, + ) )