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
487467c83e
commit
3836293a6e
@ -112,6 +112,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;
|
||||
@ -258,6 +259,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;
|
||||
@ -373,6 +375,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;
|
||||
@ -670,6 +673,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: u8 = 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, 1> = 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()?;
|
||||
@ -1561,6 +1574,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: int,
|
||||
/// paired_devices: Iterable[str],
|
||||
/// ) -> LayoutObj[UiResult]:
|
||||
/// """Show the device menu."""
|
||||
Qstr::MP_QSTR_show_device_menu => obj_fn_kw!(0, new_show_device_menu).as_obj(),
|
||||
|
||||
/// def show_info(
|
||||
/// *,
|
||||
/// title: str,
|
||||
|
@ -7,7 +7,7 @@ use crate::ui::{
|
||||
|
||||
const ELLIPSIS: &str = "...";
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
#[derive(PartialEq, Eq, Copy, Clone)]
|
||||
pub enum LineBreaking {
|
||||
/// Break line only at whitespace, if possible. If we don't find any
|
||||
/// whitespace, break words.
|
||||
@ -19,7 +19,7 @@ pub enum LineBreaking {
|
||||
BreakWordsNoHyphen,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
#[derive(PartialEq, Eq, Copy, Clone)]
|
||||
pub enum PageBreaking {
|
||||
/// Stop after hitting the bottom-right edge of the bounds.
|
||||
Cut,
|
||||
@ -54,7 +54,7 @@ pub struct TextLayout {
|
||||
}
|
||||
|
||||
/// Configuration for chunkifying the text into smaller parts.
|
||||
#[derive(Copy, Clone)]
|
||||
#[derive(PartialEq, Eq, Copy, Clone)]
|
||||
pub struct Chunks {
|
||||
/// How many characters will be grouped in one chunk.
|
||||
pub chunk_size: usize,
|
||||
@ -79,7 +79,7 @@ impl Chunks {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
#[derive(PartialEq, Eq, Copy, Clone)]
|
||||
pub struct TextStyle {
|
||||
/// Text font ID.
|
||||
pub text_font: Font,
|
||||
|
@ -861,6 +861,16 @@ impl FirmwareUI for UIBolt {
|
||||
Ok(layout)
|
||||
}
|
||||
|
||||
fn show_device_menu(
|
||||
_failed_backup: bool,
|
||||
_battery_percentage: u8,
|
||||
_paired_devices: heapless::Vec<TString<'static>, 1>,
|
||||
) -> 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>,
|
||||
|
@ -1027,6 +1027,16 @@ impl FirmwareUI for UICaesar {
|
||||
Ok(layout)
|
||||
}
|
||||
|
||||
fn show_device_menu(
|
||||
_failed_backup: bool,
|
||||
_battery_percentage: u8,
|
||||
_paired_devices: Vec<TString<'static>, 1>,
|
||||
) -> 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>,
|
||||
|
@ -884,6 +884,16 @@ impl FirmwareUI for UIDelizia {
|
||||
Ok(layout)
|
||||
}
|
||||
|
||||
fn show_device_menu(
|
||||
_failed_backup: bool,
|
||||
_battery_percentage: u8,
|
||||
_paired_devices: heapless::Vec<TString<'static>, 1>,
|
||||
) -> 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>,
|
||||
|
@ -14,6 +14,9 @@ use crate::{
|
||||
},
|
||||
};
|
||||
|
||||
#[cfg(feature = "bootloader")]
|
||||
use super::super::fonts;
|
||||
|
||||
use super::super::theme;
|
||||
|
||||
pub enum ButtonMsg {
|
||||
@ -28,7 +31,7 @@ pub struct Button {
|
||||
touch_expand: Option<Insets>,
|
||||
content: ButtonContent,
|
||||
content_offset: Offset,
|
||||
styles: ButtonStyleSheet,
|
||||
stylesheet: ButtonStyleSheet,
|
||||
text_align: Alignment,
|
||||
radius: Option<u8>,
|
||||
state: State,
|
||||
@ -41,9 +44,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"))]
|
||||
pub const SUBTEXT_STYLE_GREEN: TextStyle = theme::label_menu_item_subtitle_green();
|
||||
#[cfg(feature = "bootloader")]
|
||||
pub 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 {
|
||||
@ -51,7 +68,7 @@ impl Button {
|
||||
content_offset: Offset::zero(),
|
||||
area: Rect::zero(),
|
||||
touch_expand: None,
|
||||
styles: theme::button_default(),
|
||||
stylesheet: theme::button_default(),
|
||||
text_align: Alignment::Center,
|
||||
radius: None,
|
||||
state: State::Initial,
|
||||
@ -62,12 +79,41 @@ impl Button {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_menu_item(text: TString<'static>, stylesheet: ButtonStyleSheet) -> Self {
|
||||
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 fn new_menu_item_with_subtext(
|
||||
text: TString<'static>,
|
||||
stylesheet: ButtonStyleSheet,
|
||||
subtext: TString<'static>,
|
||||
subtext_style: Option<TextStyle>,
|
||||
) -> Self {
|
||||
Self::with_text_and_subtext(text, subtext, subtext_style)
|
||||
.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))
|
||||
}
|
||||
|
||||
pub const fn with_text_and_subtext(text: TString<'static>, subtext: TString<'static>) -> Self {
|
||||
Self::new(ButtonContent::TextAndSubtext(text, subtext))
|
||||
pub fn with_text_and_subtext(
|
||||
text: TString<'static>,
|
||||
subtext: TString<'static>,
|
||||
subtext_style: Option<TextStyle>,
|
||||
) -> Self {
|
||||
Self::new(ButtonContent::TextAndSubtext {
|
||||
text,
|
||||
subtext,
|
||||
subtext_style: subtext_style.unwrap_or(Self::DEFAULT_SUBTEXT_STYLE),
|
||||
})
|
||||
}
|
||||
|
||||
pub const fn with_icon(icon: Icon) -> Self {
|
||||
@ -87,8 +133,8 @@ 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
|
||||
}
|
||||
|
||||
@ -205,32 +251,32 @@ impl Button {
|
||||
let icon_height = child.icon.toif.height();
|
||||
text_height.max(icon_height)
|
||||
}
|
||||
ButtonContent::TextAndSubtext(_, _) => {
|
||||
ButtonContent::TextAndSubtext { subtext_style, .. } => {
|
||||
self.style().font.allcase_text_height()
|
||||
+ Self::LINE_SPACING
|
||||
+ Self::SUBTEXT_STYLE.text_font.allcase_text_height()
|
||||
+ 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 {
|
||||
@ -317,10 +363,10 @@ impl Button {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render_content<'s>(
|
||||
fn render_content<'s>(
|
||||
&self,
|
||||
target: &mut impl Renderer<'s>,
|
||||
style: &ButtonStyle,
|
||||
stylesheet: &ButtonStyle,
|
||||
alpha: u8,
|
||||
) {
|
||||
match &self.content {
|
||||
@ -333,16 +379,20 @@ 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);
|
||||
});
|
||||
}
|
||||
ButtonContent::TextAndSubtext(text, subtext) => {
|
||||
ButtonContent::TextAndSubtext {
|
||||
text,
|
||||
subtext,
|
||||
subtext_style,
|
||||
} => {
|
||||
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,
|
||||
@ -352,17 +402,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);
|
||||
@ -371,7 +421,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);
|
||||
}
|
||||
@ -384,8 +434,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);
|
||||
@ -394,7 +444,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 {
|
||||
@ -542,7 +592,7 @@ impl crate::trace::Trace for Button {
|
||||
t.string("text", content.text);
|
||||
t.bool("icon", true);
|
||||
}
|
||||
ButtonContent::TextAndSubtext(text, _) => {
|
||||
ButtonContent::TextAndSubtext { text, .. } => {
|
||||
t.string("text", *text);
|
||||
}
|
||||
#[cfg(feature = "micropython")]
|
||||
@ -563,7 +613,11 @@ enum State {
|
||||
pub enum ButtonContent {
|
||||
Empty,
|
||||
Text(TString<'static>),
|
||||
TextAndSubtext(TString<'static>, TString<'static>),
|
||||
TextAndSubtext {
|
||||
text: TString<'static>,
|
||||
subtext: TString<'static>,
|
||||
subtext_style: TextStyle,
|
||||
},
|
||||
Icon(Icon),
|
||||
IconAndText(IconText),
|
||||
#[cfg(feature = "micropython")]
|
||||
|
@ -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::BackupFailed => "BackupFailed".try_into(),
|
||||
DeviceMenuMsg::DevicePair => "DevicePair".try_into(),
|
||||
DeviceMenuMsg::DeviceDisconnect(_) => "DeviceDisconnect".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,526 @@
|
||||
use crate::{
|
||||
strutil::TString,
|
||||
ui::{
|
||||
component::{
|
||||
text::{
|
||||
paragraphs::{Paragraph, Paragraphs},
|
||||
TextStyle,
|
||||
},
|
||||
Component, Event, EventCtx,
|
||||
},
|
||||
geometry::Rect,
|
||||
layout_eckhart::{
|
||||
component::{Button, ButtonStyleSheet},
|
||||
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_SUBSCREENS: usize = 10;
|
||||
|
||||
const DISCONNECT_DEVICE_MENU_INDEX: usize = 1;
|
||||
|
||||
#[derive(Clone)]
|
||||
enum Action {
|
||||
// Go to another registered subscreen
|
||||
GoTo(usize),
|
||||
|
||||
// Return a DeviceMenuMsg to the caller
|
||||
Return(DeviceMenuMsg),
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub enum DeviceMenuMsg {
|
||||
// Root menu
|
||||
BackupFailed,
|
||||
|
||||
// "Pair & Connect"
|
||||
DevicePair, // pair a new device
|
||||
DeviceDisconnect(
|
||||
usize, /* which device to disconnect, index in the list of devices */
|
||||
),
|
||||
|
||||
// Security menu
|
||||
CheckBackup,
|
||||
WipeDevice,
|
||||
|
||||
// Device menu
|
||||
ScreenBrightness,
|
||||
|
||||
// nothing selected
|
||||
Close,
|
||||
}
|
||||
|
||||
struct MenuItem {
|
||||
text: TString<'static>,
|
||||
subtext: Option<(TString<'static>, Option<TextStyle>)>,
|
||||
stylesheet: ButtonStyleSheet,
|
||||
action: Option<Action>,
|
||||
}
|
||||
|
||||
impl MenuItem {
|
||||
pub fn new(text: TString<'static>, action: Option<Action>) -> Self {
|
||||
Self {
|
||||
text,
|
||||
subtext: None,
|
||||
stylesheet: theme::menu_item_title(),
|
||||
action,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_subtext(mut self, subtext: Option<(TString<'static>, Option<TextStyle>)>) -> Self {
|
||||
self.subtext = subtext;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_stylesheet(mut self, stylesheet: ButtonStyleSheet) -> Self {
|
||||
self.stylesheet = stylesheet;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
struct SubmenuScreen {
|
||||
header_text: TString<'static>,
|
||||
show_battery: bool,
|
||||
items: Vec<MenuItem, MENU_MAX_ITEMS>,
|
||||
}
|
||||
|
||||
impl SubmenuScreen {
|
||||
pub fn new(header_text: TString<'static>, items: Vec<MenuItem, MENU_MAX_ITEMS>) -> Self {
|
||||
Self {
|
||||
header_text,
|
||||
show_battery: false,
|
||||
items,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_battery(mut self) -> Self {
|
||||
self.show_battery = true;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
// Each subscreen of the DeviceMenuScreen is one of these
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
enum Subscreen {
|
||||
// A menu, with associated items and actions
|
||||
Submenu(SubmenuScreen),
|
||||
|
||||
// A screen allowing the user to to disconnect a device
|
||||
DeviceScreen(
|
||||
TString<'static>, /* device name */
|
||||
usize, /* index in the list of devices */
|
||||
),
|
||||
|
||||
// The about screen
|
||||
AboutScreen,
|
||||
}
|
||||
|
||||
pub struct DeviceMenuScreen<'a> {
|
||||
bounds: Rect,
|
||||
|
||||
battery_percentage: u8,
|
||||
|
||||
// These correspond to the currently active subscreen,
|
||||
// which is one of the possible kinds of subscreens
|
||||
// as defined by `enum Subscreen`
|
||||
// The active one will be Some(...) and the other two will be None.
|
||||
// This way we only need to keep one screen at any time in memory.
|
||||
menu_screen: Option<VerticalMenuScreen>,
|
||||
paired_device_screen: Option<VerticalMenuScreen>,
|
||||
about_screen: Option<TextScreen<Paragraphs<[Paragraph<'a>; 2]>>>,
|
||||
|
||||
// Information needed to construct any subscreen on demand
|
||||
subscreens: Vec<Subscreen, MAX_SUBSCREENS>,
|
||||
|
||||
// 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: u8,
|
||||
// NB: we currently only support one device at a time.
|
||||
// if we ever increase this size, we will need a way to return the correct
|
||||
// device index on Disconnect back to uPy
|
||||
// (see component_msg_obj.rs, which currently just returns "DeviceDisconnect" with no
|
||||
// index!)
|
||||
paired_devices: Vec<TString<'static>, 1>,
|
||||
) -> Self {
|
||||
let mut screen = Self {
|
||||
bounds: Rect::zero(),
|
||||
battery_percentage,
|
||||
menu_screen: None,
|
||||
paired_device_screen: None,
|
||||
about_screen: None,
|
||||
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, 1> = Vec::new();
|
||||
for (i, device) in paired_devices.iter().enumerate() {
|
||||
unwrap!(paired_device_indices
|
||||
.push(screen.add_subscreen(Subscreen::DeviceScreen(*device, i))));
|
||||
}
|
||||
|
||||
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, pair_and_connect, settings);
|
||||
|
||||
screen.set_active_subscreen(root);
|
||||
|
||||
screen
|
||||
}
|
||||
|
||||
fn is_low_battery(&self) -> bool {
|
||||
self.battery_percentage < 20
|
||||
}
|
||||
|
||||
fn add_paired_devices_menu(
|
||||
&mut self,
|
||||
paired_devices: Vec<TString<'static>, 1>,
|
||||
paired_device_indices: Vec<usize, 1>,
|
||||
) -> usize {
|
||||
let mut items: Vec<MenuItem, MENU_MAX_ITEMS> = Vec::new();
|
||||
for (device, idx) in paired_devices.iter().zip(paired_device_indices) {
|
||||
unwrap!(items.push(
|
||||
MenuItem::new(*device, Some(Action::GoTo(idx))).with_subtext(Some((
|
||||
"Connected".into(),
|
||||
Some(Button::SUBTEXT_STYLE_GREEN)
|
||||
))) // TODO: this should be a boolean feature of the device
|
||||
));
|
||||
}
|
||||
|
||||
self.add_subscreen(Subscreen::Submenu(SubmenuScreen::new(
|
||||
"Manage paired devices".into(),
|
||||
items,
|
||||
)))
|
||||
}
|
||||
|
||||
fn add_pair_and_connect_menu(&mut self, manage_devices_index: usize) -> usize {
|
||||
let mut items: Vec<MenuItem, MENU_MAX_ITEMS> = Vec::new();
|
||||
unwrap!(items.push(
|
||||
MenuItem::new(
|
||||
"Manage paired devices".into(),
|
||||
Some(Action::GoTo(manage_devices_index)),
|
||||
)
|
||||
.with_subtext(Some((
|
||||
"1 device connected".into(),
|
||||
Some(Button::SUBTEXT_STYLE_GREEN)
|
||||
)))
|
||||
));
|
||||
unwrap!(items.push(MenuItem::new(
|
||||
"Pair new device".into(),
|
||||
Some(Action::Return(DeviceMenuMsg::DevicePair)),
|
||||
)));
|
||||
|
||||
self.add_subscreen(Subscreen::Submenu(SubmenuScreen::new(
|
||||
"Pair & connect".into(),
|
||||
items,
|
||||
)))
|
||||
}
|
||||
|
||||
fn add_settings_menu(&mut self, security_index: usize, device_index: usize) -> usize {
|
||||
let mut items: Vec<MenuItem, MENU_MAX_ITEMS> = Vec::new();
|
||||
unwrap!(items.push(MenuItem::new(
|
||||
"Security".into(),
|
||||
Some(Action::GoTo(security_index))
|
||||
)));
|
||||
unwrap!(items.push(MenuItem::new(
|
||||
"Device".into(),
|
||||
Some(Action::GoTo(device_index))
|
||||
)));
|
||||
|
||||
self.add_subscreen(Subscreen::Submenu(SubmenuScreen::new(
|
||||
"Settings".into(),
|
||||
items,
|
||||
)))
|
||||
}
|
||||
|
||||
fn add_security_menu(&mut self) -> usize {
|
||||
let mut items: Vec<MenuItem, MENU_MAX_ITEMS> = Vec::new();
|
||||
unwrap!(items.push(MenuItem::new(
|
||||
"Check backup".into(),
|
||||
Some(Action::Return(DeviceMenuMsg::CheckBackup)),
|
||||
)));
|
||||
unwrap!(items.push(MenuItem::new(
|
||||
"Wipe device".into(),
|
||||
Some(Action::Return(DeviceMenuMsg::WipeDevice))
|
||||
)));
|
||||
|
||||
self.add_subscreen(Subscreen::Submenu(SubmenuScreen::new(
|
||||
"Security".into(),
|
||||
items,
|
||||
)))
|
||||
}
|
||||
|
||||
fn add_device_menu(&mut self, device_name: TString<'static>, about_index: usize) -> usize {
|
||||
let mut items: Vec<MenuItem, MENU_MAX_ITEMS> = Vec::new();
|
||||
unwrap!(
|
||||
items.push(MenuItem::new("Name".into(), None).with_subtext(Some((device_name, None))))
|
||||
);
|
||||
unwrap!(items.push(MenuItem::new(
|
||||
"Screen brightness".into(),
|
||||
Some(Action::Return(DeviceMenuMsg::ScreenBrightness)),
|
||||
)));
|
||||
unwrap!(items.push(MenuItem::new(
|
||||
"About".into(),
|
||||
Some(Action::GoTo(about_index))
|
||||
)));
|
||||
|
||||
self.add_subscreen(Subscreen::Submenu(SubmenuScreen::new(
|
||||
"Device".into(),
|
||||
items,
|
||||
)))
|
||||
}
|
||||
|
||||
fn add_root_menu(
|
||||
&mut self,
|
||||
failed_backup: bool,
|
||||
pair_and_connect_index: usize,
|
||||
settings_index: usize,
|
||||
) -> usize {
|
||||
let mut items: Vec<MenuItem, MENU_MAX_ITEMS> = Vec::new();
|
||||
if failed_backup {
|
||||
unwrap!(items.push(
|
||||
MenuItem::new(
|
||||
"Backup failed".into(),
|
||||
Some(Action::Return(DeviceMenuMsg::BackupFailed)),
|
||||
)
|
||||
.with_subtext(Some(("Review".into(), None)))
|
||||
.with_stylesheet(theme::menu_item_title_red()),
|
||||
));
|
||||
}
|
||||
unwrap!(items.push(
|
||||
MenuItem::new(
|
||||
"Pair & connect".into(),
|
||||
Some(Action::GoTo(pair_and_connect_index)),
|
||||
)
|
||||
.with_subtext(Some((
|
||||
"1 device connected".into(),
|
||||
Some(Button::SUBTEXT_STYLE_GREEN)
|
||||
)))
|
||||
));
|
||||
unwrap!(items.push(MenuItem::new(
|
||||
"Settings".into(),
|
||||
Some(Action::GoTo(settings_index)),
|
||||
)));
|
||||
self.add_subscreen(Subscreen::Submenu(
|
||||
SubmenuScreen::new("".into(), items).with_battery(),
|
||||
))
|
||||
}
|
||||
|
||||
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;
|
||||
self.build_active_subscreen();
|
||||
}
|
||||
|
||||
fn build_active_subscreen(&mut self) {
|
||||
match self.subscreens[self.active_subscreen] {
|
||||
Subscreen::Submenu(ref mut submenu) => {
|
||||
self.paired_device_screen = None;
|
||||
self.about_screen = None;
|
||||
let mut menu = VerticalMenu::empty().with_separators();
|
||||
for item in &submenu.items {
|
||||
let button = if let Some((subtext, subtext_style)) = item.subtext {
|
||||
Button::new_menu_item_with_subtext(
|
||||
item.text,
|
||||
item.stylesheet,
|
||||
subtext,
|
||||
subtext_style,
|
||||
)
|
||||
} else {
|
||||
Button::new_menu_item(item.text, item.stylesheet)
|
||||
};
|
||||
menu = menu.item(button);
|
||||
}
|
||||
let mut header = Header::new(submenu.header_text)
|
||||
.with_right_button(Button::with_icon(theme::ICON_CROSS), HeaderMsg::Cancelled);
|
||||
if submenu.show_battery {
|
||||
header = header.with_icon(
|
||||
theme::ICON_BATTERY_ZAP,
|
||||
if self.is_low_battery() {
|
||||
theme::YELLOW
|
||||
} else {
|
||||
theme::GREEN_LIME
|
||||
},
|
||||
);
|
||||
} else {
|
||||
header = header.with_left_button(
|
||||
Button::with_icon(theme::ICON_CHEVRON_LEFT),
|
||||
HeaderMsg::Back,
|
||||
);
|
||||
}
|
||||
self.menu_screen = Some(VerticalMenuScreen::new(menu).with_header(header));
|
||||
}
|
||||
Subscreen::DeviceScreen(device, _) => {
|
||||
self.menu_screen = None;
|
||||
self.about_screen = None;
|
||||
let mut menu = VerticalMenu::empty().with_separators();
|
||||
menu = menu.item(Button::new_menu_item(device, theme::menu_item_title()));
|
||||
menu = menu.item(Button::new_menu_item(
|
||||
"Disconnect".into(),
|
||||
theme::menu_item_title_red(),
|
||||
));
|
||||
self.paired_device_screen = Some(
|
||||
VerticalMenuScreen::new(menu).with_header(
|
||||
Header::new("Manage".into())
|
||||
.with_right_button(
|
||||
Button::with_icon(theme::ICON_CROSS),
|
||||
HeaderMsg::Cancelled,
|
||||
)
|
||||
.with_left_button(
|
||||
Button::with_icon(theme::ICON_CHEVRON_LEFT),
|
||||
HeaderMsg::Back,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
Subscreen::AboutScreen => {
|
||||
self.menu_screen = None;
|
||||
self.paired_device_screen = None;
|
||||
let about_content = Paragraphs::new([
|
||||
Paragraph::new(&theme::firmware::TEXT_REGULAR, "Firmware version"),
|
||||
Paragraph::new(&theme::firmware::TEXT_REGULAR, "2.3.1"), // TODO
|
||||
]);
|
||||
|
||||
self.about_screen = Some(
|
||||
TextScreen::new(about_content)
|
||||
.with_header(Header::new("About".into()).with_close_button()),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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.items[idx].action {
|
||||
Some(Action::GoTo(menu)) => {
|
||||
self.menu_screen.as_mut().unwrap().update_menu(ctx);
|
||||
unwrap!(self.parent_subscreens.push(self.active_subscreen));
|
||||
self.set_active_subscreen(menu);
|
||||
self.place(self.bounds);
|
||||
}
|
||||
Some(Action::Return(msg)) => return Some(msg),
|
||||
None => {}
|
||||
};
|
||||
}
|
||||
_ => {
|
||||
panic!("Expected a submenu!");
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn go_back(&mut self) -> Option<DeviceMenuMsg> {
|
||||
if let Some(parent) = self.parent_subscreens.pop() {
|
||||
self.set_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(..) => self.menu_screen.place(bounds),
|
||||
Subscreen::DeviceScreen(..) => self.paired_device_screen.place(bounds),
|
||||
Subscreen::AboutScreen => self.about_screen.place(bounds),
|
||||
};
|
||||
|
||||
bounds
|
||||
}
|
||||
|
||||
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||
// Handle the event for the active menu
|
||||
match self.subscreens[self.active_subscreen] {
|
||||
Subscreen::Submenu(..) => match self.menu_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::DeviceScreen(_, i) => match self.paired_device_screen.event(ctx, event) {
|
||||
Some(VerticalMenuScreenMsg::Selected(index)) => {
|
||||
if index == DISCONNECT_DEVICE_MENU_INDEX {
|
||||
return Some(DeviceMenuMsg::DeviceDisconnect(i));
|
||||
}
|
||||
}
|
||||
Some(VerticalMenuScreenMsg::Back) => {
|
||||
return self.go_back();
|
||||
}
|
||||
Some(VerticalMenuScreenMsg::Close) => {
|
||||
return Some(DeviceMenuMsg::Close);
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
Subscreen::AboutScreen => {
|
||||
if let Some(TextScreenMsg::Cancelled) = self.about_screen.event(ctx, event) {
|
||||
return self.go_back();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||
match &self.subscreens[self.active_subscreen] {
|
||||
Subscreen::Submenu(..) => self.menu_screen.render(target),
|
||||
Subscreen::DeviceScreen(..) => self.paired_device_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");
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -233,6 +233,7 @@ pub fn new_continue_recovery_homepage(
|
||||
Button::with_text_and_subtext(
|
||||
TR::words__recovery_share.into(),
|
||||
TR::buttons__more_info.into(),
|
||||
None,
|
||||
)
|
||||
.styled(theme::menu_item_title())
|
||||
.with_text_align(Alignment::Start)
|
||||
|
@ -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,13 +32,15 @@ use crate::{
|
||||
use super::{
|
||||
component::Button,
|
||||
firmware::{
|
||||
ActionBar, Bip39Input, ConfirmHomescreen, Header, HeaderMsg, Hint, Homescreen,
|
||||
MnemonicKeyboard, NumberInputScreen, PinKeyboard, SelectWordCountScreen, SelectWordScreen,
|
||||
SetBrightnessScreen, Slip39Input, TextScreen,
|
||||
ActionBar, Bip39Input, ConfirmHomescreen, DeviceMenuScreen, Header, HeaderMsg, Hint,
|
||||
Homescreen, MnemonicKeyboard, NumberInputScreen, PinKeyboard, SelectWordCountScreen,
|
||||
SelectWordScreen, SetBrightnessScreen, Slip39Input, TextScreen,
|
||||
},
|
||||
flow, fonts, theme, UIEckhart,
|
||||
};
|
||||
|
||||
use heapless::Vec;
|
||||
|
||||
impl FirmwareUI for UIEckhart {
|
||||
fn confirm_action(
|
||||
title: TString<'static>,
|
||||
@ -781,6 +783,19 @@ impl FirmwareUI for UIEckhart {
|
||||
Ok(layout)
|
||||
}
|
||||
|
||||
fn show_device_menu(
|
||||
failed_backup: bool,
|
||||
battery_percentage: u8,
|
||||
paired_devices: Vec<TString<'static>, 1>,
|
||||
) -> 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>,
|
||||
|
@ -303,6 +303,12 @@ pub trait FirmwareUI {
|
||||
notification_level: u8,
|
||||
) -> Result<impl LayoutMaybeTrace, Error>;
|
||||
|
||||
fn show_device_menu(
|
||||
failed_backup: bool,
|
||||
battery_percentage: u8,
|
||||
paired_devices: Vec<TString<'static>, 1>,
|
||||
) -> Result<impl LayoutMaybeTrace, Error>;
|
||||
|
||||
fn show_info(
|
||||
title: TString<'static>,
|
||||
description: TString<'static>,
|
||||
|
@ -532,6 +532,16 @@ def show_homescreen(
|
||||
"""Idle homescreen."""
|
||||
|
||||
|
||||
# rust/src/ui/api/firmware_micropython.rs
|
||||
def show_device_menu(
|
||||
*,
|
||||
failed_backup: bool,
|
||||
battery_percentage: int,
|
||||
paired_devices: Iterable[str],
|
||||
) -> LayoutObj[UiResult]:
|
||||
"""Show the device menu."""
|
||||
|
||||
|
||||
# rust/src/ui/api/firmware_micropython.rs
|
||||
def show_info(
|
||||
*,
|
||||
|
@ -3,8 +3,10 @@ from typing import Coroutine
|
||||
import storage
|
||||
import storage.cache
|
||||
import storage.device
|
||||
import trezorui_api
|
||||
from trezor import config, wire
|
||||
from trezor.enums import MessageType
|
||||
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
|
||||
@ -53,11 +55,43 @@ async def homescreen() -> None:
|
||||
hold_to_lock=config.has_pin(),
|
||||
)
|
||||
try:
|
||||
await obj.get_result()
|
||||
res = await obj.get_result()
|
||||
finally:
|
||||
obj.__del__()
|
||||
|
||||
lock_device()
|
||||
if res is trezorui_api.INFO:
|
||||
|
||||
# MOCK DATA
|
||||
failed_backup = True
|
||||
battery_percentage = 22
|
||||
paired_devices = ["Suite on my de-Googled Phone"]
|
||||
#
|
||||
|
||||
menu_result = await raise_if_not_confirmed(
|
||||
trezorui_api.show_device_menu(
|
||||
failed_backup=failed_backup,
|
||||
battery_percentage=battery_percentage,
|
||||
paired_devices=paired_devices,
|
||||
),
|
||||
"device_menu",
|
||||
)
|
||||
print(menu_result)
|
||||
if menu_result == "DevicePair":
|
||||
|
||||
await raise_if_not_confirmed(
|
||||
trezorui_api.show_pairing_device_name(
|
||||
device_name="My Trez",
|
||||
),
|
||||
"device_name",
|
||||
)
|
||||
await raise_if_not_confirmed(
|
||||
trezorui_api.show_pairing_code(
|
||||
code="123456",
|
||||
),
|
||||
"pairing_code",
|
||||
)
|
||||
else:
|
||||
lock_device()
|
||||
|
||||
|
||||
async def _lockscreen(screensaver: bool = False) -> None:
|
||||
|
Loading…
Reference in New Issue
Block a user