diff --git a/core/embed/rust/librust_qstr.h b/core/embed/rust/librust_qstr.h index a707c6f48c..f0bc19ad4c 100644 --- a/core/embed/rust/librust_qstr.h +++ b/core/embed/rust/librust_qstr.h @@ -94,8 +94,10 @@ static void _librust_qstrs(void) { MP_QSTR_authenticate__confirm_template; MP_QSTR_authenticate__header; MP_QSTR_auto_lock__change_template; + MP_QSTR_auto_lock__description; MP_QSTR_auto_lock__title; MP_QSTR_auto_lock__turned_on; + MP_QSTR_auto_lock_delay; MP_QSTR_backlight_fade; MP_QSTR_backlight_set; MP_QSTR_backup__can_back_up_anytime; @@ -248,6 +250,7 @@ static void _librust_qstrs(void) { MP_QSTR_device_name__title; MP_QSTR_disable_animation; MP_QSTR_disconnect; + MP_QSTR_duration_ms; MP_QSTR_encode; MP_QSTR_encoded_length; MP_QSTR_entropy__send; @@ -352,10 +355,12 @@ static void _librust_qstrs(void) { MP_QSTR_max_count; MP_QSTR_max_feerate; MP_QSTR_max_len; + MP_QSTR_max_ms; MP_QSTR_max_rounds; MP_QSTR_menu_title; MP_QSTR_message; MP_QSTR_min_count; + MP_QSTR_min_ms; MP_QSTR_misc__decrypt_value; MP_QSTR_misc__enable_labeling; MP_QSTR_misc__encrypt_value; @@ -428,6 +433,7 @@ static void _librust_qstrs(void) { MP_QSTR_pin__turn_on; MP_QSTR_pin__wrong_pin; MP_QSTR_plurals__contains_x_keys; + MP_QSTR_plurals__lock_after_x_days; MP_QSTR_plurals__lock_after_x_hours; MP_QSTR_plurals__lock_after_x_milliseconds; MP_QSTR_plurals__lock_after_x_minutes; @@ -513,6 +519,7 @@ static void _librust_qstrs(void) { MP_QSTR_remaining_shares; MP_QSTR_request_bip39; MP_QSTR_request_complete_repaint; + MP_QSTR_request_duration; MP_QSTR_request_number; MP_QSTR_request_passphrase; MP_QSTR_request_pin; diff --git a/core/embed/rust/src/time.rs b/core/embed/rust/src/time.rs index 2f38f53b25..edbfa04371 100644 --- a/core/embed/rust/src/time.rs +++ b/core/embed/rust/src/time.rs @@ -6,6 +6,9 @@ use core::{ use crate::trezorhal::time; const MILLIS_PER_SEC: u32 = 1000; +const MILLIS_PER_MINUTE: u32 = MILLIS_PER_SEC * 60; +const MILLIS_PER_HOUR: u32 = MILLIS_PER_MINUTE * 60; +const MILLIS_PER_DAY: u32 = MILLIS_PER_HOUR * 24; #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Default)] pub struct Duration { @@ -20,15 +23,45 @@ impl Duration { } pub const fn from_secs(secs: u32) -> Self { - Self { - millis: secs * MILLIS_PER_SEC, - } + // Check for potential overflow + debug_assert!(secs < u32::MAX / MILLIS_PER_SEC); + Self::from_millis(secs * MILLIS_PER_SEC) + } + + pub const fn from_mins(mins: u32) -> Self { + // Check for potential overflow + debug_assert!(mins < u32::MAX / MILLIS_PER_MINUTE); + Self::from_millis(mins * MILLIS_PER_MINUTE) + } + + pub const fn from_hours(hours: u32) -> Self { + // Check for potential overflow + debug_assert!(hours < u32::MAX / MILLIS_PER_HOUR); + Self::from_millis(hours * MILLIS_PER_HOUR) + } + pub const fn from_days(days: u32) -> Self { + // Check for potential overflow + debug_assert!(days < u32::MAX / MILLIS_PER_DAY); + Self::from_millis(days * MILLIS_PER_DAY) } pub fn to_millis(self) -> u32 { self.millis } + pub fn to_secs(self) -> u32 { + self.millis / MILLIS_PER_SEC + } + pub fn to_mins(self) -> u32 { + self.millis / MILLIS_PER_MINUTE + } + pub fn to_hours(self) -> u32 { + self.millis / MILLIS_PER_HOUR + } + pub fn to_days(self) -> u32 { + self.millis / MILLIS_PER_DAY + } + pub fn checked_add(self, rhs: Self) -> Option { self.millis.checked_add(rhs.millis).map(Self::from_millis) } @@ -36,6 +69,74 @@ impl Duration { pub fn checked_sub(self, rhs: Self) -> Option { self.millis.checked_sub(rhs.millis).map(Self::from_millis) } + + /// Returns a new Duration containing only the largest complete time unit + /// (days, hours, minutes, or seconds) + /// + /// Examples: + /// - 1 day, 3 hours → 1 day + /// - 3 hours, 45 minutes → 3 hours + /// - 59 seconds → 59 seconds + pub fn crop_to_largest_unit(self) -> Self { + if self.millis >= MILLIS_PER_DAY { + Duration::from_days(self.to_days()) + } else if self.millis >= MILLIS_PER_HOUR { + Duration::from_hours(self.to_hours()) + } else if self.millis >= MILLIS_PER_MINUTE { + Duration::from_mins(self.to_mins()) + } else { + Duration::from_secs(self.to_secs()) + } + } + + /// Increment by one unit based on the current magnitude + /// + /// Examples: + /// - 59s → 1m (moves to the next unit when crossing a boundary) + /// - 1m → 2m + /// - 23h → 1d + /// + /// Returns None if addition would overflow + pub fn increment_unit(self) -> Option { + let base = self.crop_to_largest_unit(); + + let step = if base.millis < MILLIS_PER_MINUTE { + Duration::from_secs(1) + } else if base.millis < MILLIS_PER_HOUR { + Duration::from_mins(1) + } else if base.millis < MILLIS_PER_DAY { + Duration::from_hours(1) + } else { + Duration::from_days(1) + }; + + base.checked_add(step) + } + + /// Decrement by one unit based on the current magnitude + /// + /// Examples: + /// - 1m → 59s (moves to the previous unit at boundaries) + /// - 2m → 1m + /// - 1h → 59m + /// - 1d → 23h + /// + /// Returns None if subtraction would result in negative duration + pub fn decrement_unit(self) -> Option { + let base = self.crop_to_largest_unit(); + + let step = if base.millis <= MILLIS_PER_MINUTE { + Duration::from_secs(1) + } else if base.millis <= MILLIS_PER_HOUR { + Duration::from_mins(1) + } else if base.millis <= MILLIS_PER_DAY { + Duration::from_hours(1) + } else { + Duration::from_days(1) + }; + + base.checked_sub(step) + } } impl Mul for Duration { @@ -280,4 +381,67 @@ mod tests { assert!(!sw.is_running_within(Duration::from_millis(5))); assert!(!sw.is_running_within(Duration::from_millis(10000))); } + + #[test] + fn test_crop_to_largest_unit() { + assert_eq!( + Duration::from_secs(59).crop_to_largest_unit(), + Duration::from_secs(59) + ); + assert_eq!( + Duration::from_secs(60).crop_to_largest_unit(), + Duration::from_mins(1) + ); + assert_eq!( + Duration::from_secs(61).crop_to_largest_unit(), + Duration::from_mins(1) + ); + assert_eq!( + Duration::from_secs(3600).crop_to_largest_unit(), + Duration::from_hours(1) + ); + assert_eq!( + Duration::from_secs(86399).crop_to_largest_unit(), + Duration::from_hours(23) + ); + } + + #[test] + fn test_increment_decrement_unit() { + // Increment + assert_eq!( + unwrap!(Duration::from_secs(59).increment_unit()), + Duration::from_mins(1) + ); + assert_eq!( + unwrap!(Duration::from_mins(1).increment_unit()), + Duration::from_mins(2) + ); + assert_eq!( + unwrap!(Duration::from_secs(61).increment_unit()), + Duration::from_mins(2) + ); + assert_eq!( + unwrap!(Duration::from_days(3).increment_unit()), + Duration::from_days(4) + ); + + // Decrement + assert_eq!( + unwrap!(Duration::from_mins(1).decrement_unit()), + Duration::from_secs(59) + ); + assert_eq!( + unwrap!(Duration::from_secs(61).decrement_unit()), + Duration::from_secs(59) + ); + assert_eq!( + unwrap!(Duration::from_mins(3).decrement_unit()), + Duration::from_mins(2) + ); + assert_eq!( + unwrap!(Duration::from_hours(1).decrement_unit()), + Duration::from_mins(59) + ); + } } diff --git a/core/embed/rust/src/translations/generated/translated_string.rs b/core/embed/rust/src/translations/generated/translated_string.rs index fce7d06ccd..0ff9328e87 100644 --- a/core/embed/rust/src/translations/generated/translated_string.rs +++ b/core/embed/rust/src/translations/generated/translated_string.rs @@ -27,7 +27,7 @@ pub enum TranslatedString { authenticate__confirm_template = 13, // "Allow connected computer to confirm your {0} is genuine?" authenticate__header = 14, // "Authenticate device" auto_lock__change_template = 15, // "Auto-lock Trezor after {0} of inactivity?" - auto_lock__title = 16, // "Auto-lock delay" + auto_lock__title = 16, // {"Bolt": "Auto-lock delay", "Caesar": "Auto-lock delay", "Delizia": "Auto-lock delay", "Eckhart": "Auto-lock"} backup__can_back_up_anytime = 17, // "You can back up your Trezor once, at any time." backup__it_should_be_backed_up = 18, // {"Bolt": "You should back up your new wallet right now.", "Caesar": "You should back up your new wallet right now.", "Delizia": "You should back up your new wallet right now.", "Eckhart": "Back up your new wallet now."} backup__it_should_be_backed_up_now = 19, // "It should be backed up now!" @@ -1438,7 +1438,14 @@ impl TranslatedString { (Self::authenticate__confirm_template, "Allow connected computer to confirm your {0} is genuine?"), (Self::authenticate__header, "Authenticate device"), (Self::auto_lock__change_template, "Auto-lock Trezor after {0} of inactivity?"), + #[cfg(feature = "layout_bolt")] (Self::auto_lock__title, "Auto-lock delay"), + #[cfg(feature = "layout_caesar")] + (Self::auto_lock__title, "Auto-lock delay"), + #[cfg(feature = "layout_delizia")] + (Self::auto_lock__title, "Auto-lock delay"), + #[cfg(feature = "layout_eckhart")] + (Self::auto_lock__title, "Auto-lock"), (Self::backup__can_back_up_anytime, "You can back up your Trezor once, at any time."), (Self::backup__it_should_be_backed_up, "You should back up your new wallet right now."), (Self::backup__it_should_be_backed_up_now, "It should be backed up now!"), @@ -2856,6 +2863,7 @@ impl TranslatedString { (Qstr::MP_QSTR_authenticate__confirm_template, Self::authenticate__confirm_template), (Qstr::MP_QSTR_authenticate__header, Self::authenticate__header), (Qstr::MP_QSTR_auto_lock__change_template, Self::auto_lock__change_template), + (Qstr::MP_QSTR_auto_lock__description, Self::auto_lock__description), (Qstr::MP_QSTR_auto_lock__title, Self::auto_lock__title), (Qstr::MP_QSTR_auto_lock__turned_on, Self::auto_lock__turned_on), (Qstr::MP_QSTR_backup__can_back_up_anytime, Self::backup__can_back_up_anytime), @@ -3674,6 +3682,7 @@ impl TranslatedString { (Qstr::MP_QSTR_pin__turn_on, Self::pin__turn_on), (Qstr::MP_QSTR_pin__wrong_pin, Self::pin__wrong_pin), (Qstr::MP_QSTR_plurals__contains_x_keys, Self::plurals__contains_x_keys), + (Qstr::MP_QSTR_plurals__lock_after_x_days, Self::plurals__lock_after_x_days), (Qstr::MP_QSTR_plurals__lock_after_x_hours, Self::plurals__lock_after_x_hours), (Qstr::MP_QSTR_plurals__lock_after_x_milliseconds, Self::plurals__lock_after_x_milliseconds), (Qstr::MP_QSTR_plurals__lock_after_x_minutes, Self::plurals__lock_after_x_minutes), diff --git a/core/embed/rust/src/ui/api/firmware_micropython.rs b/core/embed/rust/src/ui/api/firmware_micropython.rs index e0321d8b17..77372c0c8d 100644 --- a/core/embed/rust/src/ui/api/firmware_micropython.rs +++ b/core/embed/rust/src/ui/api/firmware_micropython.rs @@ -653,6 +653,23 @@ extern "C" fn new_request_number(n_args: usize, args: *const Obj, kwargs: *mut M unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } } +extern "C" fn new_request_duration(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 duration_ms: u32 = kwargs.get(Qstr::MP_QSTR_duration_ms)?.try_into()?; + let min_ms: u32 = kwargs.get(Qstr::MP_QSTR_min_ms)?.try_into()?; + let max_ms: u32 = kwargs.get(Qstr::MP_QSTR_max_ms)?.try_into()?; + let description: Option = kwargs + .get(Qstr::MP_QSTR_description) + .unwrap_or_else(|_| Obj::const_none()) + .try_into_option()?; + + let layout = ModelUI::request_duration(title, duration_ms, min_ms, max_ms, description)?; + Ok(LayoutObj::new_root(layout)?.into()) + }; + unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } +} + extern "C" fn new_request_pin(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj { let block = move |_args: &[Obj], kwargs: &Map| { let prompt: TString = kwargs.get(Qstr::MP_QSTR_prompt)?.try_into()?; @@ -828,12 +845,15 @@ extern "C" fn new_show_device_menu(n_args: usize, args: *const Obj, kwargs: *mut let device_name: TString = kwargs.get(Qstr::MP_QSTR_device_name)?.try_into()?; let paired_devices: Obj = kwargs.get(Qstr::MP_QSTR_paired_devices)?; let paired_devices: Vec = util::iter_into_vec(paired_devices)?; + let auto_lock_delay: TString<'static> = + kwargs.get(Qstr::MP_QSTR_auto_lock_delay)?.try_into()?; let layout = ModelUI::show_device_menu( failed_backup, battery_percentage, firmware_version, device_name, paired_devices, + auto_lock_delay, )?; let layout_obj = LayoutObj::new_root(layout)?; Ok(layout_obj.into()) @@ -1522,6 +1542,17 @@ pub static mp_module_trezorui_api: Module = obj_module! { /// description.""" Qstr::MP_QSTR_request_number => obj_fn_kw!(0, new_request_number).as_obj(), + /// def request_duration( + /// *, + /// title: str, + /// duration_ms: int, + /// min_ms: int, + /// max_ms: int, + /// description: str | None = None, + /// ) -> LayoutObj[tuple[UiResult, int]]: + /// """Duration input with + and - buttons, optional static description. """ + Qstr::MP_QSTR_request_duration => obj_fn_kw!(0, new_request_duration).as_obj(), + /// def request_pin( /// *, /// prompt: str, @@ -1633,6 +1664,7 @@ pub static mp_module_trezorui_api: Module = obj_module! { /// firmware_version: str, /// device_name: str, /// paired_devices: Iterable[str], + /// auto_lock_delay: str, /// ) -> LayoutObj[UiResult]: /// """Show the device menu.""" Qstr::MP_QSTR_show_device_menu => obj_fn_kw!(0, new_show_device_menu).as_obj(), diff --git a/core/embed/rust/src/ui/layout_bolt/ui_firmware.rs b/core/embed/rust/src/ui/layout_bolt/ui_firmware.rs index b705124e26..21f009bc62 100644 --- a/core/embed/rust/src/ui/layout_bolt/ui_firmware.rs +++ b/core/embed/rust/src/ui/layout_bolt/ui_firmware.rs @@ -666,6 +666,16 @@ impl FirmwareUI for UIBolt { Ok(layout) } + fn request_duration( + _title: TString<'static>, + _duration_ms: u32, + _min_ms: u32, + _max_ms: u32, + _description: Option>, + ) -> Result { + Err::, Error>(ERROR_NOT_IMPLEMENTED) + } + fn request_pin( prompt: TString<'static>, subprompt: TString<'static>, @@ -874,6 +884,7 @@ impl FirmwareUI for UIBolt { _firmware_version: TString<'static>, _device_name: TString<'static>, _paired_devices: heapless::Vec, 1>, + _auto_lock_delay: TString<'static>, ) -> Result { Err::, Error>(Error::ValueError( c"show_device_menu not supported", diff --git a/core/embed/rust/src/ui/layout_caesar/ui_firmware.rs b/core/embed/rust/src/ui/layout_caesar/ui_firmware.rs index 27f54c9c16..cd52aaf0b1 100644 --- a/core/embed/rust/src/ui/layout_caesar/ui_firmware.rs +++ b/core/embed/rust/src/ui/layout_caesar/ui_firmware.rs @@ -872,6 +872,16 @@ impl FirmwareUI for UICaesar { Ok(layout) } + fn request_duration( + _title: TString<'static>, + _duration_ms: u32, + _min_ms: u32, + _max_ms: u32, + _description: Option>, + ) -> Result { + Err::, Error>(ERROR_NOT_IMPLEMENTED) + } + fn request_pin( prompt: TString<'static>, subprompt: TString<'static>, @@ -1067,6 +1077,7 @@ impl FirmwareUI for UICaesar { _firmware_version: TString<'static>, _device_name: TString<'static>, _paired_devices: Vec, 1>, + _auto_lock_delay: TString<'static>, ) -> Result { Err::, Error>(Error::ValueError( c"show_device_menu not supported", diff --git a/core/embed/rust/src/ui/layout_delizia/ui_firmware.rs b/core/embed/rust/src/ui/layout_delizia/ui_firmware.rs index a9b382c6d2..3828255a7d 100644 --- a/core/embed/rust/src/ui/layout_delizia/ui_firmware.rs +++ b/core/embed/rust/src/ui/layout_delizia/ui_firmware.rs @@ -720,6 +720,16 @@ impl FirmwareUI for UIDelizia { Ok(flow) } + fn request_duration( + _title: TString<'static>, + _duration_ms: u32, + _min_ms: u32, + _max_ms: u32, + _description: Option>, + ) -> Result { + Err::, Error>(ERROR_NOT_IMPLEMENTED) + } + fn request_pin( prompt: TString<'static>, subprompt: TString<'static>, @@ -896,6 +906,7 @@ impl FirmwareUI for UIDelizia { _firmware_version: TString<'static>, _device_name: TString<'static>, _paired_devices: heapless::Vec, 1>, + _auto_lock_delay: TString<'static>, ) -> Result { Err::, Error>(Error::ValueError( c"show_device_menu not supported", diff --git a/core/embed/rust/src/ui/layout_eckhart/component_msg_obj.rs b/core/embed/rust/src/ui/layout_eckhart/component_msg_obj.rs index 8218b5b7e7..6d03c043c2 100644 --- a/core/embed/rust/src/ui/layout_eckhart/component_msg_obj.rs +++ b/core/embed/rust/src/ui/layout_eckhart/component_msg_obj.rs @@ -17,7 +17,8 @@ use super::firmware::{ AllowedTextContent, ConfirmHomescreen, ConfirmHomescreenMsg, DeviceMenuMsg, DeviceMenuScreen, Homescreen, HomescreenMsg, MnemonicInput, MnemonicKeyboard, MnemonicKeyboardMsg, PinKeyboard, PinKeyboardMsg, ProgressScreen, SelectWordCountMsg, SelectWordCountScreen, SelectWordMsg, - SelectWordScreen, SetBrightnessScreen, TextScreen, TextScreenMsg, + SelectWordScreen, SetBrightnessScreen, TextScreen, TextScreenMsg, ValueInput, ValueInputScreen, + ValueInputScreenMsg, }; impl ComponentMsgObj for PinKeyboard<'_> { @@ -116,6 +117,19 @@ impl ComponentMsgObj for SelectWordCountScreen { } } +impl ComponentMsgObj for ValueInputScreen { + fn msg_try_into_obj(&self, msg: Self::Msg) -> Result { + match msg { + ValueInputScreenMsg::Confirmed(i) => i.try_into(), + ValueInputScreenMsg::Cancelled => Ok(CANCELLED.as_obj()), + // menu message is handled only in the flow + ValueInputScreenMsg::Menu => unreachable!(), + // changed value message is handled only in the flow + ValueInputScreenMsg::Changed(_) => unreachable!(), + } + } +} + impl ComponentMsgObj for ConfirmHomescreen { fn msg_try_into_obj(&self, msg: Self::Msg) -> Result { match msg { @@ -140,6 +154,7 @@ impl<'a> ComponentMsgObj for DeviceMenuScreen<'a> { DeviceMenuMsg::CheckBackup => "CheckBackup".try_into(), DeviceMenuMsg::WipeDevice => "WipeDevice".try_into(), DeviceMenuMsg::ScreenBrightness => "ScreenBrightness".try_into(), + DeviceMenuMsg::AutoLockDelay => "AutoLockDelay".try_into(), DeviceMenuMsg::Close => Ok(CANCELLED.as_obj()), } } diff --git a/core/embed/rust/src/ui/layout_eckhart/firmware/device_menu_screen.rs b/core/embed/rust/src/ui/layout_eckhart/firmware/device_menu_screen.rs index 622b51622c..8a61693f59 100644 --- a/core/embed/rust/src/ui/layout_eckhart/firmware/device_menu_screen.rs +++ b/core/embed/rust/src/ui/layout_eckhart/firmware/device_menu_screen.rs @@ -4,6 +4,7 @@ use crate::{ error::Error, micropython::gc::GcBox, strutil::TString, + trezorhal::storage::has_pin, ui::{ component::{ text::{ @@ -60,6 +61,7 @@ pub enum DeviceMenuMsg { // Device menu ScreenBrightness, + AutoLockDelay, // nothing selected Close, @@ -166,6 +168,7 @@ impl<'a> DeviceMenuScreen<'a> { // (see component_msg_obj.rs, which currently just returns "DeviceDisconnect" with no // index!) paired_devices: Vec, 1>, + auto_lock_delay: TString<'static>, ) -> Result { let mut screen = Self { bounds: Rect::zero(), @@ -181,7 +184,7 @@ impl<'a> DeviceMenuScreen<'a> { let about = screen.add_subscreen(Subscreen::AboutScreen); let security = screen.add_security_menu(); - let device = screen.add_device_menu(device_name, about); + let device = screen.add_device_menu(device_name, about, auto_lock_delay); let settings = screen.add_settings_menu(security, device); let mut paired_device_indices: Vec = Vec::new(); @@ -274,7 +277,12 @@ impl<'a> DeviceMenuScreen<'a> { self.add_subscreen(Subscreen::Submenu(submenu_index)) } - fn add_device_menu(&mut self, device_name: TString<'static>, about_index: usize) -> usize { + fn add_device_menu( + &mut self, + device_name: TString<'static>, + about_index: usize, + auto_lock_delay: TString<'static>, + ) -> usize { let mut items: Vec = Vec::new(); unwrap!( items.push(MenuItem::new("Name".into(), None).with_subtext(Some((device_name, None)))) @@ -283,6 +291,17 @@ impl<'a> DeviceMenuScreen<'a> { "Screen brightness".into(), Some(Action::Return(DeviceMenuMsg::ScreenBrightness)), ))); + + if has_pin() { + unwrap!(items.push( + MenuItem::new( + "Auto-lock delay".into(), + Some(Action::Return(DeviceMenuMsg::AutoLockDelay)), + ) + .with_subtext(Some((auto_lock_delay, None))) + )); + } + unwrap!(items.push(MenuItem::new( "About".into(), Some(Action::GoTo(about_index)) diff --git a/core/embed/rust/src/ui/layout_eckhart/firmware/mod.rs b/core/embed/rust/src/ui/layout_eckhart/firmware/mod.rs index 250508a9f8..d17885c5f9 100644 --- a/core/embed/rust/src/ui/layout_eckhart/firmware/mod.rs +++ b/core/embed/rust/src/ui/layout_eckhart/firmware/mod.rs @@ -10,13 +10,13 @@ mod hint; mod hold_to_confirm; mod homescreen; mod keyboard; -mod number_input_screen; mod progress_screen; mod qr_screen; mod select_word_screen; mod share_words; mod text_screen; mod updatable_info_screen; +mod value_input_screen; mod vertical_menu; mod vertical_menu_screen; @@ -37,13 +37,15 @@ pub use keyboard::{ slip39::Slip39Input, word_count_screen::{SelectWordCountMsg, SelectWordCountScreen}, }; -pub use number_input_screen::{NumberInputScreen, NumberInputScreenMsg}; pub use progress_screen::ProgressScreen; pub use qr_screen::{QrMsg, QrScreen}; pub use select_word_screen::{SelectWordMsg, SelectWordScreen}; pub use share_words::{ShareWordsScreen, ShareWordsScreenMsg}; pub use text_screen::{AllowedTextContent, TextScreen, TextScreenMsg}; pub use updatable_info_screen::UpdatableInfoScreen; +pub use value_input_screen::{ + DurationInput, NumberInput, ValueInput, ValueInputScreen, ValueInputScreenMsg, +}; pub use vertical_menu::{ LongMenuGc, MenuItems, ShortMenuVec, VerticalMenu, VerticalMenuMsg, LONG_MENU_ITEMS, SHORT_MENU_ITEMS, diff --git a/core/embed/rust/src/ui/layout_eckhart/firmware/value_input_screen.rs b/core/embed/rust/src/ui/layout_eckhart/firmware/value_input_screen.rs new file mode 100644 index 0000000000..efc552aed9 --- /dev/null +++ b/core/embed/rust/src/ui/layout_eckhart/firmware/value_input_screen.rs @@ -0,0 +1,543 @@ +use crate::{ + strutil::{self, plural_form, ShortString, TString}, + time::Duration, + translations::TR, + ui::{ + component::{swipe_detect::SwipeConfig, Component, Event, EventCtx, Label, Maybe, Timer}, + flow::Swipable, + geometry::{Alignment, Insets, Offset, Rect}, + shape::{self, Renderer}, + util::Pager, + }, +}; + +use super::{ + super::{ + super::constant::SCREEN, + component::{Button, ButtonMsg}, + fonts, theme, + }, + ActionBar, ActionBarMsg, Header, HeaderMsg, +}; + +pub enum ValueInputScreenMsg { + Cancelled, + Confirmed(u32), + Changed(u32), + Menu, +} + +pub struct ValueInputScreen { + /// Screen header + header: Header, + /// Screeen description + description: Label<'static>, + /// Value input dialog + input_dialog: ValueInputDialog, + /// Return message when the value is changed + /// This is only used in the flows + changed_value_handling: bool, + /// Screen action bar + action_bar: ActionBar, +} + +impl ValueInputScreen { + const DESCRIPTION_HEIGHT: i16 = 123; + const INPUT_HEIGHT: i16 = 170; + pub fn new(value: T, text: TString<'static>) -> Self { + Self { + header: Header::new(TString::empty()), + action_bar: ActionBar::new_cancel_confirm(), + input_dialog: ValueInputDialog::new(value), + changed_value_handling: false, + description: Label::new(text, Alignment::Start, theme::TEXT_MEDIUM), + } + } + + pub fn with_header(mut self, header: Header) -> Self { + self.header = header; + self + } + + pub fn with_action_bar(mut self, action_bar: ActionBar) -> Self { + self.action_bar = action_bar; + self + } + + pub fn with_changed_value_handling(mut self) -> Self { + self.changed_value_handling = true; + self + } + + pub fn value(&self) -> u32 { + self.input_dialog.value_input.num() + } +} + +impl Component for ValueInputScreen { + type Msg = ValueInputScreenMsg; + + fn place(&mut self, bounds: Rect) -> Rect { + // assert full screen + debug_assert_eq!(bounds.height(), SCREEN.height()); + debug_assert_eq!(bounds.width(), SCREEN.width()); + + let (header_area, rest) = bounds.split_top(Header::HEADER_HEIGHT); + let (rest, action_bar_area) = rest.split_bottom(ActionBar::ACTION_BAR_HEIGHT); + let (description_area, rest) = rest.split_top(Self::DESCRIPTION_HEIGHT); + let (input_area, rest) = rest.split_top(Self::INPUT_HEIGHT); + + // Set touch expansion for the action bar not to overlap with the input area + self.action_bar + .set_touch_expansion(Insets::top(rest.height())); + + let description_area = description_area.inset(Insets::sides(24)); + + self.header.place(header_area); + self.description.place(description_area); + self.input_dialog.place(input_area); + self.action_bar.place(action_bar_area); + + bounds + } + + fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { + if let Some(HeaderMsg::Menu) = self.header.event(ctx, event) { + return Some(ValueInputScreenMsg::Menu); + } + + if let Some(msg) = self.action_bar.event(ctx, event) { + match msg { + ActionBarMsg::Confirmed => { + return Some(ValueInputScreenMsg::Confirmed(self.value())) + } + ActionBarMsg::Cancelled => return Some(ValueInputScreenMsg::Cancelled), + _ => {} + } + } + + self.input_dialog + .event(ctx, event) + .filter(|_| self.changed_value_handling) + .map(ValueInputScreenMsg::Changed) + } + + fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { + self.header.render(target); + self.description.render(target); + self.input_dialog.render(target); + self.action_bar.render(target); + } +} + +#[cfg(feature = "micropython")] +impl Swipable for ValueInputScreen { + fn get_swipe_config(&self) -> SwipeConfig { + SwipeConfig::new() + } + + fn get_pager(&self) -> Pager { + Pager::single_page() + } +} + +#[cfg(feature = "ui_debug")] +impl crate::trace::Trace for ValueInputScreen { + fn trace(&self, t: &mut dyn crate::trace::Tracer) { + t.component("ValueInputScreen"); + t.child("description", &self.description); + t.child("Header", &self.header); + t.child("input_dialog", &self.input_dialog); + } +} + +struct ValueInputDialog { + area: Rect, + dec: Maybe