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:
parent
a619962031
commit
f519d1f073
@ -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;
|
||||
|
@ -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,
|
||||
|
@ -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>,
|
||||
|
@ -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>,
|
||||
|
@ -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>,
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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");
|
||||
}
|
||||
}
|
@ -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(
|
||||
|
@ -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;
|
||||
|
@ -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 {
|
||||
|
@ -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>,
|
||||
|
@ -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>,
|
||||
|
@ -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(
|
||||
*,
|
||||
|
@ -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:
|
||||
|
Loading…
Reference in New Issue
Block a user