From 06960906913cc2efcdf01f9fc2912dc8538c8b9c Mon Sep 17 00:00:00 2001 From: obrusvit Date: Mon, 23 Dec 2024 14:45:50 +0100 Subject: [PATCH] feat(eckhart): implement basic UiFirmware fns - confirm_action, confirm_value - few show_xyz - provisional homescreen --- .../rust/src/ui/api/firmware_micropython.rs | 2 +- .../rust/src/ui/layout_eckhart/theme/mod.rs | 35 ++- .../rust/src/ui/layout_eckhart/ui_firmware.rs | 275 ++++++++++++++---- .../src/trezor/ui/layouts/eckhart/__init__.py | 6 +- 4 files changed, 263 insertions(+), 55 deletions(-) diff --git a/core/embed/rust/src/ui/api/firmware_micropython.rs b/core/embed/rust/src/ui/api/firmware_micropython.rs index edaf39da8a..135123c3de 100644 --- a/core/embed/rust/src/ui/api/firmware_micropython.rs +++ b/core/embed/rust/src/ui/api/firmware_micropython.rs @@ -1667,7 +1667,7 @@ pub static mp_module_trezorui_api: Module = obj_module! { /// title: str, /// button: str, /// description: str = "", - /// allow_cancel: bool = True, + /// allow_cancel: bool = False, /// time_ms: int = 0, /// ) -> LayoutObj[UiResult]: /// """Success modal. No buttons shown when `button` is empty string.""" diff --git a/core/embed/rust/src/ui/layout_eckhart/theme/mod.rs b/core/embed/rust/src/ui/layout_eckhart/theme/mod.rs index 9c2231fa33..097d3564df 100644 --- a/core/embed/rust/src/ui/layout_eckhart/theme/mod.rs +++ b/core/embed/rust/src/ui/layout_eckhart/theme/mod.rs @@ -12,6 +12,7 @@ use super::{ fonts, }; +pub const CONFIRM_HOLD_DURATION: Duration = Duration::from_millis(1500); pub const ERASE_HOLD_DURATION: Duration = Duration::from_millis(1500); // Color palette. @@ -136,6 +137,12 @@ pub const TEXT_MONO_LIGHT: TextStyle = TextStyle::new( GREY_EXTRA_LIGHT, ); +/// Decide the text style of chunkified text according to its length. +pub fn get_chunkified_text_style(_character_length: usize) -> &'static TextStyle { + // TODO: implement properly for Eckhart, see Delizia implemenation + &TEXT_MONO_MEDIUM +} + pub const fn label_title_main() -> TextStyle { TEXT_SMALL } @@ -147,7 +154,7 @@ pub const fn button_default() -> ButtonStyleSheet { font: fonts::FONT_SATOSHI_MEDIUM_26, text_color: GREY_LIGHT, button_color: BG, - icon_color: FG, + icon_color: GREY_LIGHT, background_color: BG, }, active: &ButtonStyle { @@ -167,6 +174,32 @@ pub const fn button_default() -> ButtonStyleSheet { } } +pub const fn button_confirm() -> ButtonStyleSheet { + ButtonStyleSheet { + normal: &ButtonStyle { + font: fonts::FONT_SATOSHI_MEDIUM_26, + text_color: GREEN_LIGHT, + button_color: BG, + icon_color: GREEN_LIGHT, + background_color: BG, + }, + active: &ButtonStyle { + font: fonts::FONT_SATOSHI_MEDIUM_26, + text_color: GREEN, + button_color: GREY_SUPER_DARK, + icon_color: GREEN, + background_color: BG, + }, + disabled: &ButtonStyle { + font: fonts::FONT_SATOSHI_MEDIUM_26, + text_color: GREY_LIGHT, + button_color: GREY_DARK, + icon_color: GREY_LIGHT, + background_color: GREY_DARK, + }, + } +} + pub const fn button_header() -> ButtonStyleSheet { ButtonStyleSheet { diff --git a/core/embed/rust/src/ui/layout_eckhart/ui_firmware.rs b/core/embed/rust/src/ui/layout_eckhart/ui_firmware.rs index 0b0be59cf6..1303178b81 100644 --- a/core/embed/rust/src/ui/layout_eckhart/ui_firmware.rs +++ b/core/embed/rust/src/ui/layout_eckhart/ui_firmware.rs @@ -1,13 +1,20 @@ use crate::{ error::Error, io::BinaryData, - micropython::{gc::Gc, list::List, obj::Obj}, + micropython::{gc::Gc, iter::IterBuf, list::List, obj::Obj, util}, strutil::TString, + translations::TR, ui::{ - component::Empty, + component::{ + text::{ + op::OpTextLayout, + paragraphs::{Paragraph, ParagraphSource, ParagraphVecShort, Paragraphs, VecExt}, + }, + Empty, FormattedText, + }, layout::{ obj::{LayoutMaybeTrace, LayoutObj, RootComponent}, - util::RecoveryType, + util::{ConfirmValueParams, RecoveryType, StrOrBytes}, }, ui_firmware::{ FirmwareUI, MAX_CHECKLIST_ITEMS, MAX_GROUP_SHARE_LINES, MAX_WORD_QUIZ_ITEMS, @@ -17,25 +24,60 @@ use crate::{ }; use super::{ - component::{ActionBar, Button, GenericScreen, Header, Hint}, - theme, UIEckhart, + component::{ActionBar, Button, Header, HeaderMsg, Hint, TextScreen}, + fonts, theme, UIEckhart, }; impl FirmwareUI for UIEckhart { fn confirm_action( - _title: TString<'static>, - _action: Option>, - _description: Option>, + title: TString<'static>, + action: Option>, + description: Option>, _subtitle: Option>, - _verb: Option>, + verb: Option>, _verb_cancel: Option>, - _hold: bool, + hold: bool, _hold_danger: bool, - _reverse: bool, + reverse: bool, _prompt_screen: bool, _prompt_title: Option>, ) -> Result { - Err::, Error>(Error::ValueError(c"not implemented")) + let action = action.unwrap_or("".into()); + let description = description.unwrap_or("".into()); + let formatted_text = { + let ops = if !reverse { + OpTextLayout::new(theme::TEXT_NORMAL) + .color(theme::GREY_LIGHT) + .text(action, fonts::FONT_SATOSHI_REGULAR_38) + .newline() + .color(theme::GREY) + .text(description, fonts::FONT_SATOSHI_REGULAR_22) + } else { + OpTextLayout::new(theme::TEXT_NORMAL) + .color(theme::GREY) + .text(description, fonts::FONT_SATOSHI_REGULAR_22) + .newline() + .color(theme::GREY_LIGHT) + .text(action, fonts::FONT_SATOSHI_REGULAR_38) + }; + FormattedText::new(ops).vertically_centered() + }; + + let verb = verb.unwrap_or(TR::buttons__confirm.into()); + let right_button = if hold { + Button::with_text(verb).with_long_press(theme::CONFIRM_HOLD_DURATION) + } else { + Button::with_text(verb) + }; + let screen = TextScreen::new(formatted_text) + .with_header(Header::new(title).with_menu_button()) + .with_hint(Hint::new_instruction(description, None)) + .with_action_bar(ActionBar::new_double( + Button::with_icon(theme::ICON_CHEVRON_LEFT), + right_button, + )); + let layout = RootComponent::new(screen); + Ok(layout) } fn confirm_address( @@ -146,22 +188,65 @@ impl FirmwareUI for UIEckhart { } fn confirm_value( - _title: TString<'static>, - _value: Obj, - _description: Option>, - _is_data: bool, - _extra: Option>, + title: TString<'static>, + value: Obj, + description: Option>, + is_data: bool, + extra: Option>, _subtitle: Option>, - _verb: Option>, + verb: Option>, _verb_cancel: Option>, - _info: bool, - _hold: bool, - _chunkify: bool, - _page_counter: bool, + info: bool, + hold: bool, + chunkify: bool, + page_counter: bool, _prompt_screen: bool, - _cancel: bool, + cancel: bool, ) -> Result, Error> { - Err::, Error>(Error::ValueError(c"not implemented")) + let paragraphs = ConfirmValueParams { + description: description.unwrap_or("".into()), + extra: extra.unwrap_or("".into()), + value: if value != Obj::const_none() { + value.try_into()? + } else { + StrOrBytes::Str("".into()) + }, + font: if chunkify { + let value: TString = value.try_into()?; + theme::get_chunkified_text_style(value.len()) + } else if is_data { + &theme::TEXT_MONO_MEDIUM + } else { + &theme::TEXT_MEDIUM + }, + description_font: &theme::TEXT_SMALL, + extra_font: &theme::TEXT_SMALL, + } + .into_paragraphs(); + + let verb = verb.unwrap_or(TR::buttons__confirm.into()); + let right_button = if hold { + Button::with_text(verb).with_long_press(theme::CONFIRM_HOLD_DURATION) + } else { + Button::with_text(verb) + }; + let header = if info { + Header::new(title) + .with_right_button(Button::with_icon(theme::ICON_INFO), HeaderMsg::Menu) + } else { + Header::new(title) + }; + + let mut screen = TextScreen::new(paragraphs) + .with_header(header) + .with_action_bar(ActionBar::new_double( + Button::with_icon(theme::ICON_CROSS), + right_button, + )); + if page_counter { + screen = screen.with_hint(Hint::new_page_counter()); + } + LayoutObj::new(screen) } fn confirm_value_intro( @@ -369,12 +454,22 @@ impl FirmwareUI for UIEckhart { } fn show_homescreen( - _label: TString<'static>, + label: TString<'static>, _hold: bool, - _notification: Option>, + notification: Option>, _notification_level: u8, ) -> Result { - Err::, Error>(Error::ValueError(c"not implemented")) + let paragraphs = ParagraphVecShort::from_iter([ + Paragraph::new(&theme::TEXT_NORMAL, label), + Paragraph::new( + &theme::TEXT_NORMAL, + notification.unwrap_or(TString::empty()), + ), + ]) + .into_paragraphs(); + + let layout = RootComponent::new(paragraphs); + Ok(layout) } fn show_info( @@ -387,12 +482,31 @@ impl FirmwareUI for UIEckhart { } fn show_info_with_cancel( - _title: TString<'static>, - _items: Obj, + title: TString<'static>, + items: Obj, _horizontal: bool, - _chunkify: bool, + chunkify: bool, ) -> Result { - Err::, Error>(Error::ValueError(c"not implemented")) + let mut paragraphs = ParagraphVecShort::new(); + for para in IterBuf::new().try_iterate(items)? { + let [key, value]: [Obj; 2] = util::iter_into_array(para)?; + let key: TString = key.try_into()?; + let value: TString = value.try_into()?; + paragraphs.add(Paragraph::new(&theme::TEXT_MEDIUM, key).no_break()); + if chunkify { + paragraphs.add(Paragraph::new( + theme::get_chunkified_text_style(value.len()), + value, + )); + } else { + paragraphs.add(Paragraph::new(&theme::TEXT_MONO_MEDIUM, value)); + } + } + + let screen = TextScreen::new(paragraphs.into_paragraphs()) + .with_header(Header::new(title).with_close_button()); + let layout = RootComponent::new(screen); + Ok(layout) } fn show_lockscreen( @@ -403,8 +517,22 @@ impl FirmwareUI for UIEckhart { Err::, Error>(Error::ValueError(c"not implemented")) } - fn show_mismatch(_title: TString<'static>) -> Result { - Err::, Error>(Error::ValueError(c"not implemented")) + fn show_mismatch(title: TString<'static>) -> Result { + let description: TString = TR::addr_mismatch__contact_support_at.into(); + let url: TString = TR::addr_mismatch__support_url.into(); + let button: TString = TR::buttons__quit.into(); + + let paragraphs = ParagraphVecShort::from_iter([ + Paragraph::new(&theme::TEXT_REGULAR, description).centered(), + Paragraph::new(&theme::TEXT_MONO_MEDIUM, url).centered(), + ]) + .into_paragraphs(); + let screen = TextScreen::new(paragraphs) + .with_header(Header::new(title)) + .with_action_bar(ActionBar::new_single(Button::with_text(button))); + + let layout = RootComponent::new(screen); + Ok(layout) } fn show_progress( @@ -447,35 +575,82 @@ impl FirmwareUI for UIEckhart { fn show_simple( text: TString<'static>, - _title: Option>, - _button: Option>, + title: Option>, + button: Option>, ) -> Result, Error> { - Err::, Error>(Error::ValueError(c"not implemented")) + let paragraphs = Paragraph::new(&theme::TEXT_REGULAR, text).into_paragraphs(); + + let mut screen = TextScreen::new(paragraphs); + if let Some(title) = title { + screen = screen.with_header(Header::new(title)); + } + if let Some(button) = button { + screen = screen.with_action_bar(ActionBar::new_single(Button::with_text(button))); + } + + let obj = LayoutObj::new(screen)?; + Ok(obj) } fn show_success( - _title: TString<'static>, - _button: TString<'static>, - _description: TString<'static>, - _allow_cancel: bool, + title: TString<'static>, + button: TString<'static>, + description: TString<'static>, + allow_cancel: bool, _time_ms: u32, ) -> Result, Error> { - Err::, Error>(Error::ValueError(c"not implemented")) + let paragraphs = Paragraph::new(&theme::TEXT_REGULAR, description).into_paragraphs(); + let header = Header::new(title).with_icon(theme::ICON_DONE, theme::GREEN_LIGHT); + let action_bar = if allow_cancel { + ActionBar::new_double( + Button::with_icon(theme::ICON_CROSS), + Button::with_text(button), + ) + } else { + ActionBar::new_single(Button::with_text(button)) + }; + let screen = TextScreen::new(paragraphs) + .with_header(header) + .with_action_bar(action_bar); + let layout = LayoutObj::new(screen)?; + Ok(layout) } - fn show_wait_text(_text: TString<'static>) -> Result { - Err::, Error>(Error::ValueError(c"not implemented")) + fn show_wait_text(text: TString<'static>) -> Result { + let paragraphs = Paragraph::new(&theme::TEXT_REGULAR, text).into_paragraphs(); + let screen = TextScreen::new(paragraphs); + let layout = RootComponent::new(screen); + Ok(layout) } fn show_warning( - _title: TString<'static>, - _button: TString<'static>, - _value: TString<'static>, - _description: TString<'static>, - _allow_cancel: bool, - _danger: bool, + title: TString<'static>, + button: TString<'static>, + value: TString<'static>, + description: TString<'static>, + allow_cancel: bool, + danger: bool, // TODO: review if `danger` needed in all layouts since we have show_danger ) -> Result, Error> { - Err::, Error>(Error::ValueError(c"not implemented")) + let paragraphs = ParagraphVecShort::from_iter([ + Paragraph::new(&theme::TEXT_SMALL, description), + Paragraph::new(&theme::TEXT_REGULAR, value), + ]) + .into_paragraphs(); + + let header = Header::new(title).with_icon(theme::ICON_INFO, theme::GREEN_LIGHT); + let action_bar = if allow_cancel { + ActionBar::new_double( + Button::with_icon(theme::ICON_CROSS), + Button::with_text(button), + ) + } else { + ActionBar::new_single(Button::with_text(button)) + }; + let screen = TextScreen::new(paragraphs) + .with_header(header) + .with_action_bar(action_bar); + let layout = LayoutObj::new(screen)?; + Ok(layout) } fn tutorial() -> Result { diff --git a/core/src/trezor/ui/layouts/eckhart/__init__.py b/core/src/trezor/ui/layouts/eckhart/__init__.py index d06980e716..77d13dbe13 100644 --- a/core/src/trezor/ui/layouts/eckhart/__init__.py +++ b/core/src/trezor/ui/layouts/eckhart/__init__.py @@ -351,9 +351,9 @@ def show_success( ) -> Awaitable[None]: return raise_if_not_confirmed( trezorui_api.show_success( - title=content, - button="", - description=subheader if subheader else "", + title=subheader if subheader else "", + button=button if button else "", + description=content, ), br_name, ButtonRequestType.Success,