1
0
mirror of https://github.com/trezor/trezor-firmware.git synced 2025-04-22 10:09:04 +00:00

feat(eckhart): introduce the device menu

This commit is contained in:
Ioan Bizău 2025-03-28 13:52:36 +01:00
parent a619962031
commit f519d1f073
15 changed files with 620 additions and 47 deletions

View File

@ -98,6 +98,7 @@ static void _librust_qstrs(void) {
MP_QSTR_backup__title_create_wallet_backup;
MP_QSTR_backup__title_skip;
MP_QSTR_backup__want_to_skip;
MP_QSTR_battery_percentage;
MP_QSTR_bitcoin__commitment_data;
MP_QSTR_bitcoin__confirm_locktime;
MP_QSTR_bitcoin__create_proof_of_ownership;
@ -238,6 +239,7 @@ static void _librust_qstrs(void) {
MP_QSTR_extra_item;
MP_QSTR_extra_items;
MP_QSTR_extra_title;
MP_QSTR_failed_backup;
MP_QSTR_fee;
MP_QSTR_fee_items;
MP_QSTR_fee_label;
@ -350,6 +352,7 @@ static void _librust_qstrs(void) {
MP_QSTR_page_counter;
MP_QSTR_pages;
MP_QSTR_paint;
MP_QSTR_paired_devices;
MP_QSTR_passphrase__access_wallet;
MP_QSTR_passphrase__always_on_device;
MP_QSTR_passphrase__continue_with_empty_passphrase;
@ -644,6 +647,7 @@ static void _librust_qstrs(void) {
MP_QSTR_show_address_details;
MP_QSTR_show_checklist;
MP_QSTR_show_danger;
MP_QSTR_show_device_menu;
MP_QSTR_show_error;
MP_QSTR_show_group_share_success;
MP_QSTR_show_homescreen;

View File

@ -802,6 +802,19 @@ extern "C" fn new_show_homescreen(n_args: usize, args: *const Obj, kwargs: *mut
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
}
extern "C" fn new_show_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 battery_percentage: usize = kwargs.get_or(Qstr::MP_QSTR_battery_percentage, 0)?;
let paired_devices: Obj = kwargs.get(Qstr::MP_QSTR_paired_devices)?;
let paired_devices: Vec<TString, 10> = util::iter_into_vec(paired_devices)?;
let layout = ModelUI::show_device_menu(failed_backup, battery_percentage, paired_devices)?;
let layout_obj = LayoutObj::new_root(layout)?;
Ok(layout_obj.into())
};
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
}
extern "C" fn new_show_info(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()?;
@ -1560,6 +1573,15 @@ pub static mp_module_trezorui_api: Module = obj_module! {
/// """Idle homescreen."""
Qstr::MP_QSTR_show_homescreen => obj_fn_kw!(0, new_show_homescreen).as_obj(),
/// def show_device_menu(
/// *,
/// failed_backup: bool,
/// battery_percentage: bool,
/// paired_devices: Iterable[str],
/// ) -> LayoutObj[UiResult]:
/// """Idle homescreen."""
Qstr::MP_QSTR_show_device_menu => obj_fn_kw!(0, new_show_device_menu).as_obj(),
/// def show_info(
/// *,
/// title: str,

View File

@ -870,6 +870,10 @@ impl FirmwareUI for UIBolt {
Ok(layout)
}
fn show_device_menu(_failed_backup: bool, _battery_percentage: usize, _paired_devices: Vec<TString<'static>, 10>) -> Result<impl LayoutMaybeTrace, Error> {
Err::<RootComponent<Empty, ModelUI>, Error>(Error::ValueError(c"show_device_menu not supported"))
}
fn show_info(
title: TString<'static>,
description: TString<'static>,

View File

@ -1036,6 +1036,10 @@ impl FirmwareUI for UICaesar {
Ok(layout)
}
fn show_device_menu(_failed_backup: bool, _battery_percentage: usize, _paired_devices: Vec<TString<'static>, 10>) -> Result<impl LayoutMaybeTrace, Error> {
Err::<RootComponent<Empty, ModelUI>, Error>(Error::ValueError(c"show_device_menu not supported"))
}
fn show_info(
title: TString<'static>,
description: TString<'static>,

View File

@ -888,6 +888,10 @@ impl FirmwareUI for UIDelizia {
Ok(layout)
}
fn show_device_menu(_failed_backup: bool, _battery_percentage: usize, _paired_devices: Vec<TString<'static>, 10>) -> Result<impl LayoutMaybeTrace, Error> {
Err::<RootComponent<Empty, ModelUI>, Error>(Error::ValueError(c"show_device_menu not supported"))
}
fn show_info(
title: TString<'static>,
description: TString<'static>,

View File

@ -13,7 +13,7 @@ use crate::{
},
};
use super::super::theme;
use super::super::{fonts, theme};
pub enum ButtonMsg {
Pressed,
@ -27,7 +27,8 @@ pub struct Button {
touch_expand: Option<Insets>,
content: ButtonContent,
content_offset: Offset,
styles: ButtonStyleSheet,
stylesheet: ButtonStyleSheet,
subtext_style: TextStyle,
text_align: Alignment,
radius: Option<u8>,
state: State,
@ -39,9 +40,23 @@ pub struct Button {
impl Button {
const LINE_SPACING: i16 = 7;
#[cfg(not(feature = "bootloader"))]
const SUBTEXT_STYLE: TextStyle = theme::label_menu_item_subtitle();
const DEFAULT_SUBTEXT_STYLE: TextStyle = theme::label_menu_item_subtitle();
#[cfg(feature = "bootloader")]
const SUBTEXT_STYLE: TextStyle = theme::TEXT_NORMAL;
const DEFAULT_SUBTEXT_STYLE: TextStyle = theme::TEXT_NORMAL;
#[cfg(not(feature = "bootloader"))]
const SUBTEXT_STYLE_GREEN: TextStyle = theme::label_menu_item_subtitle_green();
#[cfg(feature = "bootloader")]
const SUBTEXT_STYLE_GREEN: TextStyle = TextStyle::new(
fonts::FONT_SATOSHI_REGULAR_38,
theme::GREEN,
theme::BG,
theme::GREEN,
theme::GREEN,
);
const MENU_ITEM_RADIUS: u8 = 12;
const MENU_ITEM_ALIGNMENT: Alignment = Alignment::Start;
const MENU_ITEM_CONTENT_OFFSET: Offset = Offset::x(12);
pub const fn new(content: ButtonContent) -> Self {
Self {
@ -49,7 +64,8 @@ impl Button {
content_offset: Offset::zero(),
area: Rect::zero(),
touch_expand: None,
styles: theme::button_default(),
stylesheet: theme::button_default(),
subtext_style: Self::DEFAULT_SUBTEXT_STYLE,
text_align: Alignment::Center,
radius: None,
state: State::Initial,
@ -59,6 +75,25 @@ impl Button {
}
}
pub fn new_menu_item(
text: TString<'static>,
subtext: Option<TString<'static>>,
stylesheet: ButtonStyleSheet,
) -> Self {
match subtext {
Some(subtext) => Self::with_text_and_subtext(text, subtext)
.with_text_align(Self::MENU_ITEM_ALIGNMENT)
.with_content_offset(Self::MENU_ITEM_CONTENT_OFFSET)
.styled(stylesheet)
.with_radius(Self::MENU_ITEM_RADIUS),
None => Self::with_text(text)
.with_text_align(Self::MENU_ITEM_ALIGNMENT)
.with_content_offset(Self::MENU_ITEM_CONTENT_OFFSET)
.styled(stylesheet)
.with_radius(Self::MENU_ITEM_RADIUS),
}
}
pub const fn with_text(text: TString<'static>) -> Self {
Self::new(ButtonContent::Text(text))
}
@ -84,8 +119,13 @@ impl Button {
Self::new(ButtonContent::Empty)
}
pub const fn styled(mut self, styles: ButtonStyleSheet) -> Self {
self.styles = styles;
pub const fn styled(mut self, stylesheet: ButtonStyleSheet) -> Self {
self.stylesheet = stylesheet;
self
}
pub const fn subtext_green(mut self) -> Self {
self.subtext_style = Self::SUBTEXT_STYLE_GREEN;
self
}
@ -196,29 +236,29 @@ impl Button {
ButtonContent::TextAndSubtext(_, _) => {
self.style().font.allcase_text_height()
+ Self::LINE_SPACING
+ Self::SUBTEXT_STYLE.text_font.allcase_text_height()
+ self.subtext_style.text_font.allcase_text_height()
}
#[cfg(feature = "micropython")]
ButtonContent::HomeBar(_) => theme::ACTION_BAR_HEIGHT,
}
}
pub fn set_stylesheet(&mut self, styles: ButtonStyleSheet) {
if self.styles != styles {
self.styles = styles;
pub fn set_stylesheet(&mut self, stylesheet: ButtonStyleSheet) {
if self.stylesheet != stylesheet {
self.stylesheet = stylesheet;
}
}
pub fn style(&self) -> &ButtonStyle {
match self.state {
State::Initial | State::Released => self.styles.normal,
State::Pressed => self.styles.active,
State::Disabled => self.styles.disabled,
State::Initial | State::Released => self.stylesheet.normal,
State::Pressed => self.stylesheet.active,
State::Disabled => self.stylesheet.disabled,
}
}
pub fn style_sheet(&self) -> &ButtonStyleSheet {
&self.styles
pub fn stylesheet(&self) -> &ButtonStyleSheet {
&self.stylesheet
}
pub fn area(&self) -> Rect {
@ -260,10 +300,11 @@ impl Button {
}
}
pub fn render_content<'s>(
fn render_content<'s>(
&self,
target: &mut impl Renderer<'s>,
style: &ButtonStyle,
stylesheet: &ButtonStyle,
subtext_style: &TextStyle,
alpha: u8,
) {
match &self.content {
@ -276,8 +317,8 @@ impl Button {
Alignment::End => self.area.right_center() - self.content_offset,
} + y_offset;
text.map(|text| {
shape::Text::new(start_of_baseline, text, style.font)
.with_fg(style.text_color)
shape::Text::new(start_of_baseline, text, stylesheet.font)
.with_fg(stylesheet.text_color)
.with_align(self.text_align)
.with_alpha(alpha)
.render(target);
@ -285,7 +326,7 @@ impl Button {
}
ButtonContent::TextAndSubtext(text, subtext) => {
let text_y_offset =
Offset::y(self.content_height() / 2 - self.style().font.allcase_text_height());
Offset::y(self.content_height() / 2 - stylesheet.font.allcase_text_height());
let subtext_y_offset = Offset::y(self.content_height() / 2);
let start_of_baseline = match self.text_align {
Alignment::Start => self.area.left_center() + self.content_offset,
@ -295,17 +336,17 @@ impl Button {
let text_baseline = start_of_baseline - text_y_offset;
let subtext_baseline = start_of_baseline + subtext_y_offset;
text.map(|text| {
shape::Text::new(text_baseline, text, style.font)
.with_fg(style.text_color)
text.map(|t| {
shape::Text::new(text_baseline, t, stylesheet.font)
.with_fg(stylesheet.text_color)
.with_align(self.text_align)
.with_alpha(alpha)
.render(target);
});
subtext.map(|subtext| {
shape::Text::new(subtext_baseline, subtext, Self::SUBTEXT_STYLE.text_font)
.with_fg(Self::SUBTEXT_STYLE.text_color)
shape::Text::new(subtext_baseline, subtext, subtext_style.text_font)
.with_fg(subtext_style.text_color)
.with_align(self.text_align)
.with_alpha(alpha)
.render(target);
@ -314,7 +355,7 @@ impl Button {
ButtonContent::Icon(icon) => {
shape::ToifImage::new(self.area.center() + self.content_offset, icon.toif)
.with_align(Alignment2D::CENTER)
.with_fg(style.icon_color)
.with_fg(stylesheet.icon_color)
.with_alpha(alpha)
.render(target);
}
@ -327,8 +368,8 @@ impl Button {
if let Some(text) = text {
const OFFSET_Y: Offset = Offset::y(25);
text.map(|text| {
shape::Text::new(baseline, text, style.font)
.with_fg(style.text_color)
shape::Text::new(baseline, text, stylesheet.font)
.with_fg(stylesheet.text_color)
.with_align(Alignment::Center)
.with_alpha(alpha)
.render(target);
@ -337,7 +378,7 @@ impl Button {
self.area.center() + OFFSET_Y,
theme::ICON_DASH_HORIZONTAL.toif,
)
.with_fg(style.icon_color)
.with_fg(stylesheet.icon_color)
.with_align(Alignment2D::CENTER)
.render(target);
} else {
@ -360,7 +401,7 @@ impl Button {
pub fn render_with_alpha<'s>(&self, target: &mut impl Renderer<'s>, alpha: u8) {
let style = self.style();
self.render_background(target, style, alpha);
self.render_content(target, style, alpha);
self.render_content(target, style, &self.subtext_style, alpha);
}
}
@ -469,7 +510,7 @@ impl Component for Button {
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
let style = self.style();
self.render_background(target, style, 0xFF);
self.render_content(target, style, 0xFF);
self.render_content(target, style, &self.subtext_style, 0xFF);
}
}

View File

@ -14,10 +14,11 @@ use crate::{
};
use super::firmware::{
AllowedTextContent, ConfirmHomescreen, ConfirmHomescreenMsg, Homescreen, HomescreenMsg,
MnemonicInput, MnemonicKeyboard, MnemonicKeyboardMsg, NumberInputScreen, NumberInputScreenMsg,
PinKeyboard, PinKeyboardMsg, SelectWordCountMsg, SelectWordCountScreen, SelectWordMsg,
SelectWordScreen, SetBrightnessScreen, TextScreen, TextScreenMsg,
AllowedTextContent, ConfirmHomescreen, ConfirmHomescreenMsg, DeviceMenuMsg, DeviceMenuScreen,
Homescreen, HomescreenMsg, MnemonicInput, MnemonicKeyboard, MnemonicKeyboardMsg,
NumberInputScreen, NumberInputScreenMsg, PinKeyboard, PinKeyboardMsg, SelectWordCountMsg,
SelectWordCountScreen, SelectWordMsg, SelectWordScreen, SetBrightnessScreen, TextScreen,
TextScreenMsg,
};
impl ComponentMsgObj for PinKeyboard<'_> {
@ -134,3 +135,17 @@ impl ComponentMsgObj for SetBrightnessScreen {
Ok(CONFIRMED.as_obj())
}
}
impl<'a> ComponentMsgObj for DeviceMenuScreen<'a> {
fn msg_try_into_obj(&self, msg: Self::Msg) -> Result<Obj, Error> {
match msg {
DeviceMenuMsg::NotImplemented => "NotImplemented".try_into(),
DeviceMenuMsg::BackupFailed => "BackupFailed".try_into(),
DeviceMenuMsg::PairNewDevice => "PairNewDevice".try_into(),
DeviceMenuMsg::CheckBackup => "CheckBackup".try_into(),
DeviceMenuMsg::WipeDevice => "WipeDevice".try_into(),
DeviceMenuMsg::ScreenBrightness => "ScreenBrightness".try_into(),
DeviceMenuMsg::Close => Ok(CANCELLED.as_obj()),
}
}
}

View File

@ -0,0 +1,432 @@
use crate::{
strutil::TString,
ui::{
component::{
base::AttachType,
text::paragraphs::{Paragraph, Paragraphs},
Component, Event, EventCtx,
},
geometry::Rect,
layout_eckhart::{
component::Button,
constant::SCREEN,
firmware::{
Header, HeaderMsg, TextScreen, TextScreenMsg, VerticalMenu, VerticalMenuScreen,
VerticalMenuScreenMsg, MENU_MAX_ITEMS,
},
},
shape::Renderer,
},
};
use super::theme;
use heapless::Vec;
const MAX_DEPTH: usize = 5;
const MAX_SUBMENUS: usize = 10;
#[derive(Clone, Debug)]
enum Action {
// Go to another registered child screen
GoTo(usize),
// Return a DeviceMenuMsg to the caller
Return(DeviceMenuMsg),
}
struct SubmenuScreen {
pub screen: VerticalMenuScreen,
// actions for the menu items in the VerticalMenuScreen, in order
pub actions: Vec<Option<Action>, MENU_MAX_ITEMS>,
}
enum Subscreen {
Submenu(SubmenuScreen),
AboutScreen,
}
#[derive(Copy, Clone, Debug)]
pub enum DeviceMenuMsg {
NotImplemented,
// Root menu
BackupFailed,
// "Pair & Connect"
PairNewDevice,
// Security menu
CheckBackup,
WipeDevice,
// Device menu
ScreenBrightness,
Close,
}
pub struct DeviceMenuScreen<'a> {
bounds: Rect,
about_screen: TextScreen<Paragraphs<[Paragraph<'a>; 2]>>,
// all the subscreens in the DeviceMenuScreen
// which can be either VerticalMenuScreens with associated Actions
// or some predefined TextScreens, such as "About"
subscreens: Vec<Subscreen, MAX_SUBMENUS>,
// the index of the current subscreen in the list of subscreens
active_subscreen: usize,
// stack of parents that led to the current subscreen
parent_subscreens: Vec<usize, MAX_DEPTH>,
}
impl<'a> DeviceMenuScreen<'a> {
pub fn new(
failed_backup: bool,
battery_percentage: usize,
paired_devices: Vec<TString<'static>, 10>,
) -> Self {
let about_content = Paragraphs::new([
Paragraph::new(&theme::firmware::TEXT_REGULAR, "Firmware version"),
Paragraph::new(&theme::firmware::TEXT_REGULAR, "2.3.1"), // TODO
]);
let about_screen = TextScreen::new(about_content)
.with_header(Header::new("About".into()).with_close_button());
let mut screen = Self {
bounds: Rect::zero(),
about_screen,
active_subscreen: 0,
subscreens: Vec::new(),
parent_subscreens: Vec::new(),
};
let about = screen.add_subscreen(Subscreen::AboutScreen);
let security = screen.add_security_menu();
let device = screen.add_device_menu("My device".into(), about); // TODO: device name
let settings = screen.add_settings_menu(security, device);
let mut paired_device_indices: Vec<usize, 10> = Vec::new();
for device in &paired_devices {
unwrap!(paired_device_indices.push(screen.add_paired_device_menu(*device)));
}
let devices = screen.add_paired_devices_menu(paired_devices, paired_device_indices);
let pair_and_connect = screen.add_pair_and_connect_menu(devices);
let root = screen.add_root_menu(
failed_backup,
battery_percentage,
pair_and_connect,
settings,
);
screen.set_active_subscreen(root);
screen
}
fn add_paired_device_menu(&mut self, device: TString<'static>) -> usize {
let mut actions: Vec<Option<Action>, MENU_MAX_ITEMS> = Vec::new();
let mut menu = VerticalMenu::empty().with_separators();
menu = menu.item(Button::new_menu_item(
device.into(),
None,
theme::menu_item_title(),
));
unwrap!(actions.push(None));
let screen = VerticalMenuScreen::new(menu).with_header(
Header::new("Manage".into())
.with_left_button(Button::with_icon(theme::ICON_CHEVRON_LEFT), HeaderMsg::Back),
);
self.add_subscreen(Subscreen::Submenu(SubmenuScreen { screen, actions }))
}
fn add_paired_devices_menu(
&mut self,
paired_devices: Vec<TString<'static>, 10>,
paired_device_indices: Vec<usize, 10>,
) -> usize {
let mut actions: Vec<Option<Action>, MENU_MAX_ITEMS> = Vec::new();
let mut menu = VerticalMenu::empty().with_separators();
for (device, idx) in paired_devices.iter().zip(paired_device_indices) {
menu = menu.item(Button::new_menu_item(
(*device).into(),
None,
theme::menu_item_title(),
));
unwrap!(actions.push(Some(Action::GoTo(idx))));
}
let screen = VerticalMenuScreen::new(menu).with_header(
Header::new("Manage paired devices".into())
.with_right_button(Button::with_icon(theme::ICON_CROSS), HeaderMsg::Cancelled)
.with_left_button(Button::with_icon(theme::ICON_CHEVRON_LEFT), HeaderMsg::Back),
);
self.add_subscreen(Subscreen::Submenu(SubmenuScreen { screen, actions }))
}
fn add_pair_and_connect_menu(&mut self, manage_devices_index: usize) -> usize {
let mut actions: Vec<Option<Action>, MENU_MAX_ITEMS> = Vec::new();
let mut menu = VerticalMenu::empty().with_separators();
menu = menu.item(
Button::new_menu_item(
"Manage paired devices".into(),
Some("1 device connected".into()), // TODO
theme::menu_item_title(),
)
.subtext_green(),
);
unwrap!(actions.push(Some(Action::GoTo(manage_devices_index))));
menu = menu.item(Button::new_menu_item(
"Pair new device".into(),
None,
theme::menu_item_title(),
));
unwrap!(actions.push(Some(Action::Return(DeviceMenuMsg::PairNewDevice))));
let screen = VerticalMenuScreen::new(menu).with_header(
Header::new("Pair & Connect".into())
.with_right_button(Button::with_icon(theme::ICON_CROSS), HeaderMsg::Cancelled)
.with_left_button(Button::with_icon(theme::ICON_CHEVRON_LEFT), HeaderMsg::Back),
);
self.add_subscreen(Subscreen::Submenu(SubmenuScreen { screen, actions }))
}
fn add_settings_menu(&mut self, security_index: usize, device_index: usize) -> usize {
let mut actions: Vec<Option<Action>, MENU_MAX_ITEMS> = Vec::new();
let mut menu = VerticalMenu::empty().with_separators();
menu = menu.item(Button::new_menu_item(
"Security".into(),
None,
theme::menu_item_title(),
));
unwrap!(actions.push(Some(Action::GoTo(security_index))));
menu = menu.item(Button::new_menu_item(
"Device".into(),
None,
theme::menu_item_title(),
));
unwrap!(actions.push(Some(Action::GoTo(device_index))));
let screen = VerticalMenuScreen::new(menu).with_header(
Header::new("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),
);
self.add_subscreen(Subscreen::Submenu(SubmenuScreen { screen, actions }))
}
fn add_security_menu(&mut self) -> usize {
let mut actions: Vec<Option<Action>, MENU_MAX_ITEMS> = Vec::new();
let mut menu = VerticalMenu::empty().with_separators();
menu = menu.item(Button::new_menu_item(
"Check backup".into(),
None,
theme::menu_item_title(),
));
unwrap!(actions.push(Some(Action::Return(DeviceMenuMsg::CheckBackup))));
menu = menu.item(Button::new_menu_item(
"Wipe device".into(),
None,
theme::menu_item_title(),
));
unwrap!(actions.push(Some(Action::Return(DeviceMenuMsg::WipeDevice))));
let screen = VerticalMenuScreen::new(menu).with_header(
Header::new("Security".into())
.with_right_button(Button::with_icon(theme::ICON_CROSS), HeaderMsg::Cancelled)
.with_left_button(Button::with_icon(theme::ICON_CHEVRON_LEFT), HeaderMsg::Back),
);
self.add_subscreen(Subscreen::Submenu(SubmenuScreen { screen, actions }))
}
fn add_device_menu(&mut self, device_name: TString<'static>, about_index: usize) -> usize {
let mut actions: Vec<Option<Action>, MENU_MAX_ITEMS> = Vec::new();
let mut menu = VerticalMenu::empty().with_separators();
menu = menu.item(Button::new_menu_item(
"Name".into(),
Some(device_name),
theme::menu_item_title(),
));
unwrap!(actions.push(None));
menu = menu.item(Button::new_menu_item(
"Screen brightness".into(),
None,
theme::menu_item_title(),
));
unwrap!(actions.push(Some(Action::Return(DeviceMenuMsg::ScreenBrightness))));
menu = menu.item(Button::new_menu_item(
"About".into(),
None,
theme::menu_item_title(),
));
unwrap!(actions.push(Some(Action::GoTo(about_index))));
let screen = VerticalMenuScreen::new(menu).with_header(
Header::new("Security".into())
.with_right_button(Button::with_icon(theme::ICON_CROSS), HeaderMsg::Cancelled)
.with_left_button(Button::with_icon(theme::ICON_CHEVRON_LEFT), HeaderMsg::Back),
);
self.add_subscreen(Subscreen::Submenu(SubmenuScreen { screen, actions }))
}
fn add_root_menu(
&mut self,
failed_backup: bool,
battery_percentage: usize,
pair_and_connect_index: usize,
settings_index: usize,
) -> usize {
let mut actions: Vec<Option<Action>, MENU_MAX_ITEMS> = Vec::new();
let mut menu = VerticalMenu::empty().with_separators();
if failed_backup {
menu = menu.item(Button::new_menu_item(
"Backup failed".into(),
Some("Review".into()),
theme::menu_item_title_red(),
));
unwrap!(actions.push(Some(Action::Return(DeviceMenuMsg::BackupFailed))));
}
menu = menu.item(
Button::new_menu_item(
"Pair & connect".into(),
Some("1 device connected".into()), // TODO
theme::menu_item_title(),
)
.subtext_green(),
);
unwrap!(actions.push(Some(Action::GoTo(pair_and_connect_index))));
menu = menu.item(Button::new_menu_item(
"Settings".into(),
None,
theme::menu_item_title(),
));
unwrap!(actions.push(Some(Action::GoTo(settings_index))));
let screen = VerticalMenuScreen::new(menu).with_header(
Header::new("".into())
.with_battery(battery_percentage)
.with_right_button(Button::with_icon(theme::ICON_CROSS), HeaderMsg::Cancelled),
);
self.add_subscreen(Subscreen::Submenu(SubmenuScreen { screen, actions }))
}
fn add_subscreen(&mut self, screen: Subscreen) -> usize {
unwrap!(self.subscreens.push(screen));
self.subscreens.len() - 1
}
fn set_active_subscreen(&mut self, idx: usize) {
assert!(idx < self.subscreens.len());
self.active_subscreen = idx;
}
fn handle_submenu(&mut self, ctx: &mut EventCtx, idx: usize) -> Option<DeviceMenuMsg> {
match self.subscreens[self.active_subscreen] {
Subscreen::Submenu(ref mut menu_screen) => {
match menu_screen.actions[idx] {
Some(Action::GoTo(menu)) => {
menu_screen.screen.update_menu(ctx);
unwrap!(self.parent_subscreens.push(self.active_subscreen));
self.active_subscreen = menu;
self.place(self.bounds);
}
Some(Action::Return(msg)) => return Some(msg),
None => {}
};
}
_ => {
assert!(false, "Expected a submenu!");
}
}
None
}
fn go_back(&mut self) -> Option<DeviceMenuMsg> {
if let Some(parent) = self.parent_subscreens.pop() {
self.active_subscreen = parent;
self.place(self.bounds);
None
} else {
Some(DeviceMenuMsg::Close)
}
}
}
impl<'a> Component for DeviceMenuScreen<'a> {
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;
match self.subscreens[self.active_subscreen] {
Subscreen::Submenu(ref mut menu_screen) => menu_screen.screen.place(bounds),
Subscreen::AboutScreen => self.about_screen.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.subscreens[self.active_subscreen] {
Subscreen::Submenu(ref mut menu_screen) => match menu_screen.screen.event(ctx, event) {
Some(VerticalMenuScreenMsg::Selected(index)) => {
return self.handle_submenu(ctx, index);
}
Some(VerticalMenuScreenMsg::Back) => {
return self.go_back();
}
Some(VerticalMenuScreenMsg::Close) => {
return Some(DeviceMenuMsg::Close);
}
_ => {}
},
Subscreen::AboutScreen => match self.about_screen.event(ctx, event) {
Some(TextScreenMsg::Cancelled) => {
return self.go_back();
}
_ => {}
},
}
None
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
match &self.subscreens[self.active_subscreen] {
Subscreen::Submenu(ref menu_screen) => menu_screen.screen.render(target),
Subscreen::AboutScreen => self.about_screen.render(target),
}
}
}
#[cfg(feature = "ui_debug")]
impl<'a> crate::trace::Trace for DeviceMenuScreen<'a> {
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.component("DeviceMenuScreen");
}
}

View File

@ -138,6 +138,19 @@ impl Header {
}
}
pub fn with_battery(self, percentage: usize) -> Self {
let is_low_battery = percentage < 20; // TODO
self.with_icon(
theme::ICON_BATTERY_ZAP,
if is_low_battery {
theme::YELLOW
} else {
theme::GREEN_LIME
},
)
}
#[inline(never)]
pub fn with_menu_button(self) -> Self {
self.with_right_button(

View File

@ -1,6 +1,7 @@
mod action_bar;
mod brightness_screen;
mod confirm_homescreen;
mod device_menu_screen;
mod header;
mod hint;
mod hold_to_confirm;
@ -17,6 +18,7 @@ mod vertical_menu_screen;
pub use action_bar::{ActionBar, ActionBarMsg};
pub use brightness_screen::SetBrightnessScreen;
pub use confirm_homescreen::{ConfirmHomescreen, ConfirmHomescreenMsg};
pub use device_menu_screen::{DeviceMenuMsg, DeviceMenuScreen};
pub use header::{Header, HeaderMsg};
pub use hint::Hint;
pub use hold_to_confirm::HoldToConfirmAnim;

View File

@ -164,6 +164,10 @@ pub const fn label_menu_item_subtitle() -> TextStyle {
TextStyle::new(fonts::FONT_SATOSHI_REGULAR_22, GREY, BG, GREY, GREY)
}
pub const fn label_menu_item_subtitle_green() -> TextStyle {
TextStyle::new(fonts::FONT_SATOSHI_REGULAR_22, GREEN, BG, GREEN, GREEN)
}
// Button styles
pub const fn button_confirm() -> ButtonStyleSheet {
ButtonStyleSheet {
@ -310,6 +314,10 @@ pub const fn menu_item_title_orange() -> ButtonStyleSheet {
menu_item_title!(ORANGE)
}
pub const fn menu_item_title_red() -> ButtonStyleSheet {
menu_item_title!(RED)
}
macro_rules! button_homebar_style {
($text_color:expr, $icon_color:expr) => {
ButtonStyleSheet {

View File

@ -32,6 +32,7 @@ use crate::{
use super::{
component::Button,
firmware::{
DeviceMenuScreen,
ActionBar, Bip39Input, ConfirmHomescreen, Header, HeaderMsg, Hint, Homescreen,
MnemonicKeyboard, NumberInputScreen, PinKeyboard, SelectWordCountScreen, SelectWordScreen,
SetBrightnessScreen, Slip39Input, TextScreen,
@ -39,6 +40,8 @@ use super::{
flow, fonts, theme, UIEckhart,
};
use heapless::Vec;
impl FirmwareUI for UIEckhart {
fn confirm_action(
title: TString<'static>,
@ -780,6 +783,11 @@ impl FirmwareUI for UIEckhart {
Ok(layout)
}
fn show_device_menu(failed_backup: bool, battery_percentage: usize, paired_devices: Vec<TString<'static>, 10>) -> Result<impl LayoutMaybeTrace, Error> {
let layout = RootComponent::new(DeviceMenuScreen::new(failed_backup, battery_percentage, paired_devices));
Ok(layout)
}
fn show_info(
title: TString<'static>,
description: TString<'static>,

View File

@ -301,6 +301,12 @@ pub trait FirmwareUI {
notification_level: u8,
) -> Result<impl LayoutMaybeTrace, Error>;
fn show_device_menu(
failed_backup: bool,
battery_percentage: usize,
paired_devices: Vec<TString<'static>, 10>,
) -> Result<impl LayoutMaybeTrace, Error>;
fn show_info(
title: TString<'static>,
description: TString<'static>,

View File

@ -531,6 +531,16 @@ def show_homescreen(
"""Idle homescreen."""
# rust/src/ui/api/firmware_micropython.rs
def show_device_menu(
*,
failed_backup: bool,
battery_percentage: bool,
paired_devices: Iterable[str],
) -> LayoutObj[UiResult]:
"""Idle homescreen."""
# rust/src/ui/api/firmware_micropython.rs
def show_info(
*,

View File

@ -5,7 +5,7 @@ import storage.cache
import storage.device
from trezor import config, wire
from trezor.enums import MessageType
from trezor.ui.layouts import error_popup, raise_if_not_confirmed
from trezor.ui.layouts import raise_if_not_confirmed
from trezor.ui.layouts.homescreen import Busyscreen, Homescreen, Lockscreen
from apps.base import busy_expiry_ms, lock_device
@ -22,7 +22,7 @@ async def busyscreen() -> None:
async def homescreen() -> None:
from trezor import TR
from trezorui_api import INFO
from trezorui_api import INFO, show_device_menu
if storage.device.is_initialized():
label = storage.device.get_label()
@ -57,15 +57,15 @@ async def homescreen() -> None:
try:
res = await obj.get_result()
if res is INFO:
# trigger device menu
await raise_if_not_confirmed(
error_popup(
"Not implemented",
"DeviceMenu not yet implemented",
button="OK then..",
),
"device_menu",
)
##### MOCK DATA
failed_backup = True
battery_percentage = 22
paired_devices = ["Suite on my de-Googled Phone"]
#####
menu_result = await raise_if_not_confirmed(show_device_menu(failed_backup=failed_backup, battery_percentage=battery_percentage, paired_devices=paired_devices), "device_menu")
print(menu_result)
else:
lock_device()
finally: