mirror of
https://github.com/trezor/trezor-firmware.git
synced 2025-02-22 04:22:07 +00:00
feat(eckhart): Add full-screen device menu component
device menu cont
This commit is contained in:
parent
b84a983678
commit
4c7aec0635
@ -208,6 +208,7 @@ static void _librust_qstrs(void) {
|
|||||||
MP_QSTR_confirm_value;
|
MP_QSTR_confirm_value;
|
||||||
MP_QSTR_confirm_value_intro;
|
MP_QSTR_confirm_value_intro;
|
||||||
MP_QSTR_confirm_with_info;
|
MP_QSTR_confirm_with_info;
|
||||||
|
MP_QSTR_connections;
|
||||||
MP_QSTR_continue_recovery_homepage;
|
MP_QSTR_continue_recovery_homepage;
|
||||||
MP_QSTR_count;
|
MP_QSTR_count;
|
||||||
MP_QSTR_current;
|
MP_QSTR_current;
|
||||||
@ -220,6 +221,19 @@ static void _librust_qstrs(void) {
|
|||||||
MP_QSTR_deinit;
|
MP_QSTR_deinit;
|
||||||
MP_QSTR_description;
|
MP_QSTR_description;
|
||||||
MP_QSTR_details_title;
|
MP_QSTR_details_title;
|
||||||
|
MP_QSTR_device_menu;
|
||||||
|
MP_QSTR_device_menu__1_connection;
|
||||||
|
MP_QSTR_device_menu__about;
|
||||||
|
MP_QSTR_device_menu__active_connections;
|
||||||
|
MP_QSTR_device_menu__backup_failed_description;
|
||||||
|
MP_QSTR_device_menu__backup_failed_title;
|
||||||
|
MP_QSTR_device_menu__battery_low_description;
|
||||||
|
MP_QSTR_device_menu__battery_low_title;
|
||||||
|
MP_QSTR_device_menu__bluetooth;
|
||||||
|
MP_QSTR_device_menu__brightness;
|
||||||
|
MP_QSTR_device_menu__connections_title;
|
||||||
|
MP_QSTR_device_menu__fw_version;
|
||||||
|
MP_QSTR_device_menu__language;
|
||||||
MP_QSTR_device_name__change_template;
|
MP_QSTR_device_name__change_template;
|
||||||
MP_QSTR_device_name__title;
|
MP_QSTR_device_name__title;
|
||||||
MP_QSTR_disable_animation;
|
MP_QSTR_disable_animation;
|
||||||
@ -235,6 +249,7 @@ static void _librust_qstrs(void) {
|
|||||||
MP_QSTR_extra;
|
MP_QSTR_extra;
|
||||||
MP_QSTR_extra_items;
|
MP_QSTR_extra_items;
|
||||||
MP_QSTR_extra_title;
|
MP_QSTR_extra_title;
|
||||||
|
MP_QSTR_failed_backup;
|
||||||
MP_QSTR_fee;
|
MP_QSTR_fee;
|
||||||
MP_QSTR_fee_items;
|
MP_QSTR_fee_items;
|
||||||
MP_QSTR_fee_label;
|
MP_QSTR_fee_label;
|
||||||
@ -317,6 +332,7 @@ static void _librust_qstrs(void) {
|
|||||||
MP_QSTR_lockscreen__tap_to_unlock;
|
MP_QSTR_lockscreen__tap_to_unlock;
|
||||||
MP_QSTR_lockscreen__title_locked;
|
MP_QSTR_lockscreen__title_locked;
|
||||||
MP_QSTR_lockscreen__title_not_connected;
|
MP_QSTR_lockscreen__title_not_connected;
|
||||||
|
MP_QSTR_low_battery;
|
||||||
MP_QSTR_max_count;
|
MP_QSTR_max_count;
|
||||||
MP_QSTR_max_feerate;
|
MP_QSTR_max_feerate;
|
||||||
MP_QSTR_max_len;
|
MP_QSTR_max_len;
|
||||||
|
@ -1383,6 +1383,18 @@ pub enum TranslatedString {
|
|||||||
#[cfg(feature = "universal_fw")]
|
#[cfg(feature = "universal_fw")]
|
||||||
ethereum__unknown_contract_address_short = 974, // "Unknown contract address."
|
ethereum__unknown_contract_address_short = 974, // "Unknown contract address."
|
||||||
reset__share_words_first = 975, // "Write down the first word from the backup."
|
reset__share_words_first = 975, // "Write down the first word from the backup."
|
||||||
|
device_menu__1_connection = 976, // "1 active connection"
|
||||||
|
device_menu__about = 977, // "About"
|
||||||
|
device_menu__active_connections = 978, // "{0} active connections"
|
||||||
|
device_menu__backup_failed_description = 979, // "Review"
|
||||||
|
device_menu__backup_failed_title = 980, // "Backup failed"
|
||||||
|
device_menu__battery_low_description = 981, // "Recharge soon"
|
||||||
|
device_menu__battery_low_title = 982, // "Battery low"
|
||||||
|
device_menu__bluetooth = 983, // "Bluetooth management"
|
||||||
|
device_menu__brightness = 984, // "Brightness"
|
||||||
|
device_menu__connections_title = 985, // "Pair & Connect"
|
||||||
|
device_menu__fw_version = 986, // "Firmware version"
|
||||||
|
device_menu__language = 987, // "Language"
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TranslatedString {
|
impl TranslatedString {
|
||||||
@ -2776,6 +2788,18 @@ impl TranslatedString {
|
|||||||
#[cfg(feature = "universal_fw")]
|
#[cfg(feature = "universal_fw")]
|
||||||
Self::ethereum__unknown_contract_address_short => "Unknown contract address.",
|
Self::ethereum__unknown_contract_address_short => "Unknown contract address.",
|
||||||
Self::reset__share_words_first => "Write down the first word from the backup.",
|
Self::reset__share_words_first => "Write down the first word from the backup.",
|
||||||
|
Self::device_menu__1_connection => "1 active connection",
|
||||||
|
Self::device_menu__about => "About",
|
||||||
|
Self::device_menu__active_connections => "{0} active connections",
|
||||||
|
Self::device_menu__backup_failed_description => "Review",
|
||||||
|
Self::device_menu__backup_failed_title => "Backup failed",
|
||||||
|
Self::device_menu__battery_low_description => "Recharge soon",
|
||||||
|
Self::device_menu__battery_low_title => "Battery low",
|
||||||
|
Self::device_menu__bluetooth => "Bluetooth management",
|
||||||
|
Self::device_menu__brightness => "Brightness",
|
||||||
|
Self::device_menu__connections_title => "Pair & Connect",
|
||||||
|
Self::device_menu__fw_version => "Firmware version",
|
||||||
|
Self::device_menu__language => "Language",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -4154,6 +4178,18 @@ impl TranslatedString {
|
|||||||
#[cfg(feature = "universal_fw")]
|
#[cfg(feature = "universal_fw")]
|
||||||
Qstr::MP_QSTR_ethereum__unknown_contract_address_short => Some(Self::ethereum__unknown_contract_address_short),
|
Qstr::MP_QSTR_ethereum__unknown_contract_address_short => Some(Self::ethereum__unknown_contract_address_short),
|
||||||
Qstr::MP_QSTR_reset__share_words_first => Some(Self::reset__share_words_first),
|
Qstr::MP_QSTR_reset__share_words_first => Some(Self::reset__share_words_first),
|
||||||
|
Qstr::MP_QSTR_device_menu__1_connection => Some(Self::device_menu__1_connection),
|
||||||
|
Qstr::MP_QSTR_device_menu__about => Some(Self::device_menu__about),
|
||||||
|
Qstr::MP_QSTR_device_menu__active_connections => Some(Self::device_menu__active_connections),
|
||||||
|
Qstr::MP_QSTR_device_menu__backup_failed_description => Some(Self::device_menu__backup_failed_description),
|
||||||
|
Qstr::MP_QSTR_device_menu__backup_failed_title => Some(Self::device_menu__backup_failed_title),
|
||||||
|
Qstr::MP_QSTR_device_menu__battery_low_description => Some(Self::device_menu__battery_low_description),
|
||||||
|
Qstr::MP_QSTR_device_menu__battery_low_title => Some(Self::device_menu__battery_low_title),
|
||||||
|
Qstr::MP_QSTR_device_menu__bluetooth => Some(Self::device_menu__bluetooth),
|
||||||
|
Qstr::MP_QSTR_device_menu__brightness => Some(Self::device_menu__brightness),
|
||||||
|
Qstr::MP_QSTR_device_menu__connections_title => Some(Self::device_menu__connections_title),
|
||||||
|
Qstr::MP_QSTR_device_menu__fw_version => Some(Self::device_menu__fw_version),
|
||||||
|
Qstr::MP_QSTR_device_menu__language => Some(Self::device_menu__language),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -419,6 +419,19 @@ extern "C" fn new_continue_recovery_homepage(
|
|||||||
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
|
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extern "C" fn new_device_menu(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
|
||||||
|
let block = move |_args: &[Obj], kwargs: &Map| {
|
||||||
|
let failed_backup: bool = kwargs.get(Qstr::MP_QSTR_failed_backup)?.try_into()?;
|
||||||
|
let low_battery: bool = kwargs.get(Qstr::MP_QSTR_low_battery)?.try_into()?;
|
||||||
|
let connections: TString = kwargs.get(Qstr::MP_QSTR_connections)?.try_into()?;
|
||||||
|
|
||||||
|
|
||||||
|
let layout = ModelUI::device_menu(failed_backup, low_battery, connections)?;
|
||||||
|
Ok(LayoutObj::new_root(layout)?.into())
|
||||||
|
};
|
||||||
|
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
|
||||||
|
}
|
||||||
|
|
||||||
extern "C" fn new_flow_confirm_output(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
|
extern "C" fn new_flow_confirm_output(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
|
||||||
let block = move |_args: &[Obj], kwargs: &Map| {
|
let block = move |_args: &[Obj], kwargs: &Map| {
|
||||||
let title: Option<TString> = kwargs.get(Qstr::MP_QSTR_title)?.try_into_option()?;
|
let title: Option<TString> = kwargs.get(Qstr::MP_QSTR_title)?.try_into_option()?;
|
||||||
@ -1318,6 +1331,16 @@ pub static mp_module_trezorui_api: Module = obj_module! {
|
|||||||
/// """Device recovery homescreen."""
|
/// """Device recovery homescreen."""
|
||||||
Qstr::MP_QSTR_continue_recovery_homepage => obj_fn_kw!(0, new_continue_recovery_homepage).as_obj(),
|
Qstr::MP_QSTR_continue_recovery_homepage => obj_fn_kw!(0, new_continue_recovery_homepage).as_obj(),
|
||||||
|
|
||||||
|
/// def device_menu(
|
||||||
|
/// *,
|
||||||
|
/// failed_backup: bool,
|
||||||
|
/// low_battery: bool,
|
||||||
|
/// connections: str | None,
|
||||||
|
/// ) -> LayoutObj[int]:
|
||||||
|
/// """Show eckhart device menu."""
|
||||||
|
Qstr::MP_QSTR_device_menu => obj_fn_kw!(0, new_device_menu).as_obj(),
|
||||||
|
|
||||||
|
|
||||||
/// def flow_confirm_output(
|
/// def flow_confirm_output(
|
||||||
/// *,
|
/// *,
|
||||||
/// title: str | None,
|
/// title: str | None,
|
||||||
|
@ -536,6 +536,14 @@ impl FirmwareUI for UIBolt {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn device_menu(
|
||||||
|
_failed_backup: bool,
|
||||||
|
_low_battery: bool,
|
||||||
|
_connections: TString<'static>,
|
||||||
|
) -> Result<impl LayoutMaybeTrace, Error> {
|
||||||
|
Err::<Gc<LayoutObj>, Error>(Error::ValueError(c"not implemented"))
|
||||||
|
}
|
||||||
|
|
||||||
fn flow_confirm_output(
|
fn flow_confirm_output(
|
||||||
_title: Option<TString<'static>>,
|
_title: Option<TString<'static>>,
|
||||||
_subtitle: Option<TString<'static>>,
|
_subtitle: Option<TString<'static>>,
|
||||||
|
@ -671,6 +671,14 @@ impl FirmwareUI for UICaesar {
|
|||||||
LayoutObj::new_root(layout)
|
LayoutObj::new_root(layout)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn device_menu(
|
||||||
|
_failed_backup: bool,
|
||||||
|
_low_battery: bool,
|
||||||
|
_connections: TString<'static>,
|
||||||
|
) -> Result<impl LayoutMaybeTrace, Error> {
|
||||||
|
Err::<Gc<LayoutObj>, Error>(Error::ValueError(c"not implemented"))
|
||||||
|
}
|
||||||
|
|
||||||
fn flow_confirm_output(
|
fn flow_confirm_output(
|
||||||
_title: Option<TString<'static>>,
|
_title: Option<TString<'static>>,
|
||||||
_subtitle: Option<TString<'static>>,
|
_subtitle: Option<TString<'static>>,
|
||||||
|
@ -507,6 +507,14 @@ impl FirmwareUI for UIDelizia {
|
|||||||
LayoutObj::new_root(flow)
|
LayoutObj::new_root(flow)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn device_menu(
|
||||||
|
_failed_backup: bool,
|
||||||
|
_low_battery: bool,
|
||||||
|
_connections: TString<'static>,
|
||||||
|
) -> Result<impl LayoutMaybeTrace, Error> {
|
||||||
|
Err::<Gc<LayoutObj>, Error>(Error::ValueError(c"not implemented"))
|
||||||
|
}
|
||||||
|
|
||||||
fn flow_confirm_output(
|
fn flow_confirm_output(
|
||||||
title: Option<TString<'static>>,
|
title: Option<TString<'static>>,
|
||||||
subtitle: Option<TString<'static>>,
|
subtitle: Option<TString<'static>>,
|
||||||
|
@ -0,0 +1,143 @@
|
|||||||
|
use crate::ui::{
|
||||||
|
component::{base::AttachType, Component, Event, EventCtx},
|
||||||
|
geometry::Rect,
|
||||||
|
layout_eckhart::{
|
||||||
|
component::{Button, VerticalMenuScreen, VerticalMenuScreenMsg, MENU_MAX_ITEMS},
|
||||||
|
constant::SCREEN,
|
||||||
|
theme,
|
||||||
|
},
|
||||||
|
shape::{self, Renderer},
|
||||||
|
};
|
||||||
|
|
||||||
|
use heapless::Vec;
|
||||||
|
|
||||||
|
// Max number of menu screens
|
||||||
|
const MAX_MENUS: usize = 5;
|
||||||
|
|
||||||
|
type VerticalMenus = Vec<Button, MAX_MENUS>;
|
||||||
|
|
||||||
|
pub enum DeviceMenuMsg {
|
||||||
|
Selected(usize),
|
||||||
|
/// Right header button clicked
|
||||||
|
Close,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Linear map of vertical menus.
|
||||||
|
pub struct DeviceMenuScreen {
|
||||||
|
/// Bounds of the menu screen
|
||||||
|
bounds: Rect,
|
||||||
|
/// Index of the currently active menu
|
||||||
|
active_menu: usize,
|
||||||
|
/// Stack of parent menus for back navigation
|
||||||
|
menu_parents: Vec<usize, MAX_MENUS>,
|
||||||
|
/// Stack of menu screens and their children
|
||||||
|
menu_stack: Vec<(VerticalMenuScreen, Vec<Option<usize>, MENU_MAX_ITEMS>), 5>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DeviceMenuScreen {
|
||||||
|
pub fn empty() -> Self {
|
||||||
|
Self {
|
||||||
|
bounds: Rect::zero(),
|
||||||
|
active_menu: 0, // Start with the first menu by default
|
||||||
|
menu_stack: Vec::new(),
|
||||||
|
menu_parents: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add an internal menu screen with children and return the index of the new
|
||||||
|
// menu within the stack. The children are optional indices of the sub-menus in
|
||||||
|
// the stack
|
||||||
|
pub fn add_inner_menu(
|
||||||
|
&mut self,
|
||||||
|
menu: VerticalMenuScreen,
|
||||||
|
children: Vec<Option<usize>, MENU_MAX_ITEMS>,
|
||||||
|
) -> usize {
|
||||||
|
unwrap!(self.menu_stack.push((menu, children)));
|
||||||
|
self.menu_stack.len() - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a leaf menu screen (without any children)
|
||||||
|
pub fn add_leaf_menu(&mut self, menu: VerticalMenuScreen) -> usize {
|
||||||
|
let mut children = Vec::new();
|
||||||
|
for _ in 0..MENU_MAX_ITEMS {
|
||||||
|
unwrap!(children.push(None));
|
||||||
|
}
|
||||||
|
|
||||||
|
self.add_inner_menu(menu, children)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the index of the active menu
|
||||||
|
pub fn set_active_menu(&mut self, menu: usize) {
|
||||||
|
assert!(menu < self.menu_stack.len());
|
||||||
|
self.active_menu = menu;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Navigate to a different menu based on selection.
|
||||||
|
fn handle_menu_selection(&mut self, ctx: &mut EventCtx, index: usize) -> Option<DeviceMenuMsg> {
|
||||||
|
if self.menu_stack[self.active_menu].1[index].is_some() {
|
||||||
|
unwrap!(self.menu_parents.push(self.active_menu));
|
||||||
|
self.active_menu = self.menu_stack[self.active_menu].1[index].unwrap();
|
||||||
|
self.place(self.bounds);
|
||||||
|
self.menu_stack[self.active_menu].0.update_menu(ctx);
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle back navigation to previously active menu
|
||||||
|
fn handle_back_navigation(&mut self) -> Option<DeviceMenuMsg> {
|
||||||
|
if self.menu_parents.is_empty() {
|
||||||
|
Some(DeviceMenuMsg::Close)
|
||||||
|
} else {
|
||||||
|
self.active_menu = self.menu_parents.pop().unwrap();
|
||||||
|
self.place(self.bounds);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Component for DeviceMenuScreen {
|
||||||
|
type Msg = DeviceMenuMsg;
|
||||||
|
|
||||||
|
fn place(&mut self, bounds: Rect) -> Rect {
|
||||||
|
// assert full screen
|
||||||
|
debug_assert_eq!(bounds.height(), SCREEN.height());
|
||||||
|
debug_assert_eq!(bounds.width(), SCREEN.width());
|
||||||
|
|
||||||
|
self.bounds = bounds;
|
||||||
|
// Place the active menu
|
||||||
|
self.menu_stack[self.active_menu].0.place(bounds);
|
||||||
|
bounds
|
||||||
|
}
|
||||||
|
|
||||||
|
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||||
|
// Update the menu when the screen is attached
|
||||||
|
if let Event::Attach(AttachType::Initial) = event {}
|
||||||
|
// Handle the event for the active menu
|
||||||
|
match self.menu_stack[self.active_menu].0.event(ctx, event) {
|
||||||
|
Some(VerticalMenuScreenMsg::Selected(index)) => {
|
||||||
|
// Navigate to the selected menu (if any)
|
||||||
|
return self.handle_menu_selection(ctx, index);
|
||||||
|
}
|
||||||
|
Some(VerticalMenuScreenMsg::Back) => {
|
||||||
|
return self.handle_back_navigation();
|
||||||
|
}
|
||||||
|
Some(VerticalMenuScreenMsg::Close) => {
|
||||||
|
return Some(DeviceMenuMsg::Close);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||||
|
// Render the active menu if any
|
||||||
|
self.menu_stack[self.active_menu].0.render(target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "ui_debug")]
|
||||||
|
impl crate::trace::Trace for DeviceMenuScreen {
|
||||||
|
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||||
|
t.component("DeviceMenuScreen");
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,7 @@
|
|||||||
mod action_bar;
|
mod action_bar;
|
||||||
pub mod bl_confirm;
|
pub mod bl_confirm;
|
||||||
mod button;
|
mod button;
|
||||||
|
mod device_menu_screen;
|
||||||
mod error;
|
mod error;
|
||||||
mod header;
|
mod header;
|
||||||
mod hint;
|
mod hint;
|
||||||
@ -14,6 +15,7 @@ mod welcome_screen;
|
|||||||
|
|
||||||
pub use action_bar::{ActionBar, ActionBarMsg};
|
pub use action_bar::{ActionBar, ActionBarMsg};
|
||||||
pub use button::{Button, ButtonContent, ButtonMsg, ButtonStyle, ButtonStyleSheet, IconText};
|
pub use button::{Button, ButtonContent, ButtonMsg, ButtonStyle, ButtonStyleSheet, IconText};
|
||||||
|
pub use device_menu_screen::{DeviceMenuMsg, DeviceMenuScreen};
|
||||||
pub use error::ErrorScreen;
|
pub use error::ErrorScreen;
|
||||||
pub use header::{Header, HeaderMsg};
|
pub use header::{Header, HeaderMsg};
|
||||||
pub use hint::Hint;
|
pub use hint::Hint;
|
||||||
|
@ -14,7 +14,8 @@ use crate::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use super::component::{
|
use super::component::{
|
||||||
AllowedTextContent, SelectWordMsg, SelectWordScreen, TextScreen, TextScreenMsg,
|
AllowedTextContent, DeviceMenuMsg, DeviceMenuScreen, SelectWordMsg, SelectWordScreen,
|
||||||
|
TextScreen, TextScreenMsg, VerticalMenuScreen, VerticalMenuScreenMsg,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Clippy/compiler complains about conflicting implementations
|
// Clippy/compiler complains about conflicting implementations
|
||||||
@ -61,3 +62,22 @@ impl ComponentMsgObj for SelectWordScreen {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl ComponentMsgObj for VerticalMenuScreen {
|
||||||
|
fn msg_try_into_obj(&self, msg: Self::Msg) -> Result<Obj, Error> {
|
||||||
|
match msg {
|
||||||
|
VerticalMenuScreenMsg::Back => Ok(CANCELLED.as_obj()),
|
||||||
|
VerticalMenuScreenMsg::Close => Ok(CANCELLED.as_obj()),
|
||||||
|
VerticalMenuScreenMsg::Selected(_) => Ok(CONFIRMED.as_obj()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ComponentMsgObj for DeviceMenuScreen {
|
||||||
|
fn msg_try_into_obj(&self, msg: Self::Msg) -> Result<Obj, Error> {
|
||||||
|
match msg {
|
||||||
|
DeviceMenuMsg::Close => Ok(CANCELLED.as_obj()),
|
||||||
|
DeviceMenuMsg::Selected(_) => Ok(CONFIRMED.as_obj()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -12,6 +12,7 @@ use crate::{
|
|||||||
},
|
},
|
||||||
Empty, FormattedText,
|
Empty, FormattedText,
|
||||||
},
|
},
|
||||||
|
geometry::Alignment,
|
||||||
layout::{
|
layout::{
|
||||||
obj::{LayoutMaybeTrace, LayoutObj, RootComponent},
|
obj::{LayoutMaybeTrace, LayoutObj, RootComponent},
|
||||||
util::{ConfirmValueParams, RecoveryType, StrOrBytes},
|
util::{ConfirmValueParams, RecoveryType, StrOrBytes},
|
||||||
@ -24,10 +25,15 @@ use crate::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
component::{ActionBar, Button, Header, HeaderMsg, Hint, SelectWordScreen, TextScreen},
|
component::{
|
||||||
|
ActionBar, Button, ButtonStyleSheet, DeviceMenuScreen, Header, HeaderMsg, Hint,
|
||||||
|
SelectWordScreen, TextScreen, VerticalMenu, VerticalMenuScreen, MENU_MAX_ITEMS,
|
||||||
|
},
|
||||||
flow, fonts, theme, UIEckhart,
|
flow, fonts, theme, UIEckhart,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use heapless::Vec;
|
||||||
|
|
||||||
impl FirmwareUI for UIEckhart {
|
impl FirmwareUI for UIEckhart {
|
||||||
fn confirm_action(
|
fn confirm_action(
|
||||||
title: TString<'static>,
|
title: TString<'static>,
|
||||||
@ -285,6 +291,148 @@ impl FirmwareUI for UIEckhart {
|
|||||||
Err::<Gc<LayoutObj>, Error>(Error::ValueError(c"not implemented"))
|
Err::<Gc<LayoutObj>, Error>(Error::ValueError(c"not implemented"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn device_menu(
|
||||||
|
failed_backup: bool,
|
||||||
|
low_battery: bool,
|
||||||
|
connections: TString<'static>,
|
||||||
|
) -> Result<impl LayoutMaybeTrace, Error> {
|
||||||
|
const BUTTON_RADIUS: u8 = 12;
|
||||||
|
const BUTTON_ALIGNMENT: Alignment = Alignment::Start;
|
||||||
|
|
||||||
|
// Function to add a menu item with optional subtext to the VerticalMenu
|
||||||
|
fn add_menu_item(
|
||||||
|
menu: VerticalMenu,
|
||||||
|
text: TString<'static>,
|
||||||
|
subtext: Option<TString<'static>>,
|
||||||
|
style: ButtonStyleSheet,
|
||||||
|
include: bool,
|
||||||
|
) -> VerticalMenu {
|
||||||
|
if include {
|
||||||
|
let button = match subtext {
|
||||||
|
Some(sub) => Button::with_text_and_subtext(text, sub)
|
||||||
|
.with_text_align(BUTTON_ALIGNMENT)
|
||||||
|
.styled(style)
|
||||||
|
.with_radius(BUTTON_RADIUS),
|
||||||
|
None => Button::with_text(text)
|
||||||
|
.with_text_align(BUTTON_ALIGNMENT)
|
||||||
|
.styled(style)
|
||||||
|
.with_radius(BUTTON_RADIUS),
|
||||||
|
};
|
||||||
|
menu.item(button)
|
||||||
|
} else {
|
||||||
|
menu
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the device menu screen
|
||||||
|
let mut device_menu = DeviceMenuScreen::empty();
|
||||||
|
|
||||||
|
// Define menu items for settings
|
||||||
|
let settings_menu_items = [
|
||||||
|
(TR::device_menu__language, None, true),
|
||||||
|
(TR::device_menu__bluetooth, None, true),
|
||||||
|
(TR::device_menu__brightness, None, true),
|
||||||
|
(TR::device_menu__fw_version, None, true),
|
||||||
|
(TR::device_menu__about, None, true),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Build the settings menu dynamically
|
||||||
|
let settings_menu = settings_menu_items
|
||||||
|
.iter()
|
||||||
|
.fold(
|
||||||
|
VerticalMenu::empty().with_separators(),
|
||||||
|
|menu, &(text, subtext, include)| {
|
||||||
|
add_menu_item(
|
||||||
|
menu,
|
||||||
|
text.into(),
|
||||||
|
subtext.into(),
|
||||||
|
theme::menu_item_title(),
|
||||||
|
include,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.with_separators();
|
||||||
|
|
||||||
|
// Create the settings screen
|
||||||
|
let settings_screen = VerticalMenuScreen::new(settings_menu).with_header(
|
||||||
|
Header::new(TR::words__settings.into())
|
||||||
|
.with_right_button(Button::with_icon(theme::ICON_CROSS), HeaderMsg::Cancelled)
|
||||||
|
.with_left_button(Button::with_icon(theme::ICON_CHEVRON_LEFT), HeaderMsg::Back),
|
||||||
|
);
|
||||||
|
let setting_index = device_menu.add_leaf_menu(settings_screen);
|
||||||
|
|
||||||
|
// Determine battery color based on low_battery flag
|
||||||
|
let battery_color = if low_battery {
|
||||||
|
theme::YELLOW
|
||||||
|
} else {
|
||||||
|
theme::GREEN_LIME
|
||||||
|
};
|
||||||
|
|
||||||
|
// Define root menu items
|
||||||
|
let root_menu_items = [
|
||||||
|
(
|
||||||
|
TR::device_menu__backup_failed_title,
|
||||||
|
Some(TR::device_menu__backup_failed_description.into()),
|
||||||
|
theme::menu_item_title_red(),
|
||||||
|
failed_backup,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
TR::device_menu__battery_low_title,
|
||||||
|
Some(TR::device_menu__battery_low_description.into()),
|
||||||
|
theme::menu_item_title_yellow(),
|
||||||
|
low_battery,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
TR::device_menu__connections_title,
|
||||||
|
Some(connections.into()),
|
||||||
|
theme::menu_item_title(),
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
(TR::words__settings, None, theme::menu_item_title(), true),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Build the root menu dynamically
|
||||||
|
let root_menu = root_menu_items.iter().fold(
|
||||||
|
VerticalMenu::empty().with_separators(),
|
||||||
|
|menu, &(text, subtext, style, include)| {
|
||||||
|
add_menu_item(menu, text.into(), subtext.into(), style, include)
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Initialize root children
|
||||||
|
let mut root_children: Vec<Option<usize>, MENU_MAX_ITEMS> = Vec::new();
|
||||||
|
|
||||||
|
// Optional failed backup child
|
||||||
|
if failed_backup {
|
||||||
|
root_children.push(None).unwrap();
|
||||||
|
}
|
||||||
|
// Optional low battery child
|
||||||
|
if low_battery {
|
||||||
|
root_children.push(None).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remaining children
|
||||||
|
root_children
|
||||||
|
.extend_from_slice(&[None, Some(setting_index)])
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Create the root screen
|
||||||
|
let root_screen = VerticalMenuScreen::new(root_menu).with_header(
|
||||||
|
Header::new("".into())
|
||||||
|
.with_right_button(Button::with_icon(theme::ICON_CROSS), HeaderMsg::Cancelled)
|
||||||
|
.with_icon(theme::ICON_BATTERY_ZAP, battery_color),
|
||||||
|
);
|
||||||
|
let root_index: usize = device_menu.add_inner_menu(root_screen, root_children);
|
||||||
|
|
||||||
|
// Set root menu as active
|
||||||
|
device_menu.set_active_menu(root_index);
|
||||||
|
|
||||||
|
// Create and return the layout
|
||||||
|
let layout = RootComponent::new(device_menu);
|
||||||
|
|
||||||
|
Ok(layout)
|
||||||
|
}
|
||||||
|
|
||||||
fn flow_confirm_output(
|
fn flow_confirm_output(
|
||||||
_title: Option<TString<'static>>,
|
_title: Option<TString<'static>>,
|
||||||
_subtitle: Option<TString<'static>>,
|
_subtitle: Option<TString<'static>>,
|
||||||
|
@ -156,6 +156,12 @@ pub trait FirmwareUI {
|
|||||||
|
|
||||||
fn check_homescreen_format(image: BinaryData, accept_toif: bool) -> bool;
|
fn check_homescreen_format(image: BinaryData, accept_toif: bool) -> bool;
|
||||||
|
|
||||||
|
fn device_menu(
|
||||||
|
failed_backup: bool,
|
||||||
|
low_battery: bool,
|
||||||
|
connections: TString<'static>,
|
||||||
|
) -> Result<impl LayoutMaybeTrace, Error>;
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
fn flow_confirm_output(
|
fn flow_confirm_output(
|
||||||
title: Option<TString<'static>>,
|
title: Option<TString<'static>>,
|
||||||
|
@ -306,6 +306,16 @@ def continue_recovery_homepage(
|
|||||||
"""Device recovery homescreen."""
|
"""Device recovery homescreen."""
|
||||||
|
|
||||||
|
|
||||||
|
# rust/src/ui/api/firmware_micropython.rs
|
||||||
|
def device_menu(
|
||||||
|
*,
|
||||||
|
failed_backup: bool,
|
||||||
|
low_battery: bool,
|
||||||
|
connections: str | None,
|
||||||
|
) -> LayoutObj[int]:
|
||||||
|
"""Show eckhart device menu."""
|
||||||
|
|
||||||
|
|
||||||
# rust/src/ui/api/firmware_micropython.rs
|
# rust/src/ui/api/firmware_micropython.rs
|
||||||
def flow_confirm_output(
|
def flow_confirm_output(
|
||||||
*,
|
*,
|
||||||
|
@ -251,6 +251,18 @@ class TR:
|
|||||||
confirm_total__title_sending_from: str = "Sending from"
|
confirm_total__title_sending_from: str = "Sending from"
|
||||||
debug__loading_seed: str = "Loading seed"
|
debug__loading_seed: str = "Loading seed"
|
||||||
debug__loading_seed_not_recommended: str = "Loading private seed is not recommended."
|
debug__loading_seed_not_recommended: str = "Loading private seed is not recommended."
|
||||||
|
device_menu__1_connection: str = "1 active connection"
|
||||||
|
device_menu__about: str = "About"
|
||||||
|
device_menu__active_connections: str = "{0} active connections"
|
||||||
|
device_menu__backup_failed_description: str = "Review"
|
||||||
|
device_menu__backup_failed_title: str = "Backup failed"
|
||||||
|
device_menu__battery_low_description: str = "Recharge soon"
|
||||||
|
device_menu__battery_low_title: str = "Battery low"
|
||||||
|
device_menu__bluetooth: str = "Bluetooth management"
|
||||||
|
device_menu__brightness: str = "Brightness"
|
||||||
|
device_menu__connections_title: str = "Pair & Connect"
|
||||||
|
device_menu__fw_version: str = "Firmware version"
|
||||||
|
device_menu__language: str = "Language"
|
||||||
device_name__change_template: str = "Change device name to {0}?"
|
device_name__change_template: str = "Change device name to {0}?"
|
||||||
device_name__title: str = "Device name"
|
device_name__title: str = "Device name"
|
||||||
entropy__send: str = "Do you really want to send entropy?"
|
entropy__send: str = "Do you really want to send entropy?"
|
||||||
|
@ -919,6 +919,35 @@ async def confirm_signverify(
|
|||||||
raise NotImplemented
|
raise NotImplemented
|
||||||
|
|
||||||
|
|
||||||
|
async def device_menu(
|
||||||
|
failed_backup: bool,
|
||||||
|
battery_level: int,
|
||||||
|
connections: int,
|
||||||
|
) -> None:
|
||||||
|
|
||||||
|
# Currently arbitrary value
|
||||||
|
if battery_level < 20:
|
||||||
|
low_battery = True
|
||||||
|
else:
|
||||||
|
low_battery = False
|
||||||
|
|
||||||
|
if connections == 1:
|
||||||
|
connections_str = TR.device_menu__1_connection
|
||||||
|
elif connections == 0:
|
||||||
|
connections_str = TR.device_menu__connections_title.format("No")
|
||||||
|
else:
|
||||||
|
connections_str = TR.device_menu__connections_title.format(connections)
|
||||||
|
|
||||||
|
await interact(
|
||||||
|
trezorui_api.device_menu(
|
||||||
|
failed_backup=failed_backup,
|
||||||
|
low_battery=low_battery,
|
||||||
|
connections=connections_str,
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def error_popup(
|
def error_popup(
|
||||||
title: str,
|
title: str,
|
||||||
description: str,
|
description: str,
|
||||||
|
@ -253,6 +253,18 @@
|
|||||||
"confirm_total__title_sending_from": "Sending from",
|
"confirm_total__title_sending_from": "Sending from",
|
||||||
"debug__loading_seed": "Loading seed",
|
"debug__loading_seed": "Loading seed",
|
||||||
"debug__loading_seed_not_recommended": "Loading private seed is not recommended.",
|
"debug__loading_seed_not_recommended": "Loading private seed is not recommended.",
|
||||||
|
"device_menu__brightness": "Brightness",
|
||||||
|
"device_menu__language": "Language",
|
||||||
|
"device_menu__bluetooth": "Bluetooth management",
|
||||||
|
"device_menu__fw_version": "Firmware version",
|
||||||
|
"device_menu__about": "About",
|
||||||
|
"device_menu__backup_failed_title": "Backup failed",
|
||||||
|
"device_menu__backup_failed_description": "Review",
|
||||||
|
"device_menu__battery_low_title": "Battery low",
|
||||||
|
"device_menu__battery_low_description": "Recharge soon",
|
||||||
|
"device_menu__connections_title": "Pair & Connect",
|
||||||
|
"device_menu__1_connection": "1 active connection",
|
||||||
|
"device_menu__active_connections": "{0} active connections",
|
||||||
"device_name__change_template": "Change device name to {0}?",
|
"device_name__change_template": "Change device name to {0}?",
|
||||||
"device_name__title": "Device name",
|
"device_name__title": "Device name",
|
||||||
"entropy__send": "Do you really want to send entropy?",
|
"entropy__send": "Do you really want to send entropy?",
|
||||||
|
@ -974,5 +974,17 @@
|
|||||||
"972": "ethereum__interaction_contract",
|
"972": "ethereum__interaction_contract",
|
||||||
"973": "misc__enable_labeling",
|
"973": "misc__enable_labeling",
|
||||||
"974": "ethereum__unknown_contract_address_short",
|
"974": "ethereum__unknown_contract_address_short",
|
||||||
"975": "reset__share_words_first"
|
"975": "reset__share_words_first",
|
||||||
|
"976": "device_menu__1_connection",
|
||||||
|
"977": "device_menu__about",
|
||||||
|
"978": "device_menu__active_connections",
|
||||||
|
"979": "device_menu__backup_failed_description",
|
||||||
|
"980": "device_menu__backup_failed_title",
|
||||||
|
"981": "device_menu__battery_low_description",
|
||||||
|
"982": "device_menu__battery_low_title",
|
||||||
|
"983": "device_menu__bluetooth",
|
||||||
|
"984": "device_menu__brightness",
|
||||||
|
"985": "device_menu__connections_title",
|
||||||
|
"986": "device_menu__fw_version",
|
||||||
|
"987": "device_menu__language"
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"current": {
|
"current": {
|
||||||
"merkle_root": "43c08e81d71c1c28d77b1650fc96b0dfcd473fde0c922717e5588baeeb581bd3",
|
"merkle_root": "f3c02816770393fd369280c0668d5d93812be27ba55413957d66a73abce201b5",
|
||||||
"datetime": "2025-02-14T14:18:48.730778",
|
"datetime": "2025-02-16T11:06:40.774900",
|
||||||
"commit": "8ecb8718c89547bcc8c0bb2f2cf9e823a9a75be3"
|
"commit": "71e2c51c5b52f783f85a87f38d943750d7e2e332"
|
||||||
},
|
},
|
||||||
"history": [
|
"history": [
|
||||||
{
|
{
|
||||||
|
Loading…
Reference in New Issue
Block a user