1
0
mirror of https://github.com/trezor/trezor-firmware.git synced 2025-04-21 01:29:02 +00:00

feat(eckhart): implement trait functions: confirm_value_intro,

confirm_modify_output, confirm_modify_fee, confirm_with_info,
show_info_with_cancel and confirm_props
This commit is contained in:
Lukas Bielesch 2025-04-04 15:24:41 +02:00
parent f5acf7b052
commit 9de999f13b
5 changed files with 454 additions and 35 deletions

View File

@ -0,0 +1,133 @@
use crate::{
error,
micropython::obj::Obj,
strutil::TString,
translations::TR,
ui::{
component::{
text::paragraphs::{Paragraph, ParagraphSource},
ComponentExt,
},
flow::{
base::{Decision, DecisionBuilder as _},
FlowController, FlowMsg, SwipeFlow,
},
geometry::{Alignment, Direction, LinearPlacement, Offset},
layout::util::StrOrBytes,
},
};
use super::super::{
component::Button,
firmware::{
ActionBar, Header, TextScreen, TextScreenMsg, VerticalMenu, VerticalMenuScreen,
VerticalMenuScreenMsg,
},
theme,
};
const TIMEOUT_MS: u32 = 2000;
#[derive(Copy, Clone, PartialEq, Eq)]
pub enum ConfirmValueIntro {
Intro,
Menu,
}
impl FlowController for ConfirmValueIntro {
#[inline]
fn index(&'static self) -> usize {
*self as usize
}
fn handle_swipe(&'static self, _direction: Direction) -> Decision {
self.do_nothing()
}
fn handle_event(&'static self, msg: FlowMsg) -> Decision {
match (self, msg) {
(Self::Intro, FlowMsg::Confirmed) => self.return_msg(FlowMsg::Confirmed),
(Self::Intro, FlowMsg::Info) => Self::Menu.goto(),
(Self::Menu, FlowMsg::Choice(0)) => self.return_msg(FlowMsg::Info),
(Self::Menu, FlowMsg::Choice(1)) => self.return_msg(FlowMsg::Cancelled),
(Self::Menu, FlowMsg::Cancelled) => Self::Intro.goto(),
_ => self.do_nothing(),
}
}
}
#[allow(clippy::too_many_arguments)]
pub fn new_confirm_value_intro(
title: TString<'static>,
subtitle: Option<TString<'static>>,
value: Obj,
value_menu_label: TString<'static>,
cancel_menu_label: Option<TString<'static>>,
confirm_button_label: Option<TString<'static>>,
hold: bool,
chunkify: bool,
) -> Result<SwipeFlow, error::Error> {
let cancel_menu_label = cancel_menu_label.unwrap_or(TR::buttons__cancel.into());
// Intro
let mut confirm_button = Button::with_text(
confirm_button_label.unwrap_or(TR::sign_message__confirm_without_review.into()),
)
.styled(theme::button_confirm());
if hold {
confirm_button = confirm_button.with_long_press(theme::LOCK_HOLD_DURATION);
}
let value = if value != Obj::const_none() {
unwrap!(value.try_into())
} else {
StrOrBytes::Str("".into())
};
let intro_style = if chunkify {
&theme::TEXT_MONO_ADDRESS_CHUNKS
} else {
&theme::TEXT_MONO_ADDRESS
};
let content_intro = TextScreen::new(
Paragraph::new(intro_style, value.as_str_offset(0))
.into_paragraphs()
.with_placement(LinearPlacement::vertical()),
)
.with_page_limit(1)
.with_header(Header::new(title).with_menu_button())
.with_subtitle(subtitle.unwrap_or(TString::empty()))
.with_action_bar(ActionBar::new_single(confirm_button))
.map(|msg| match msg {
TextScreenMsg::Confirmed => Some(FlowMsg::Confirmed),
TextScreenMsg::Cancelled => Some(FlowMsg::Cancelled),
TextScreenMsg::Menu => Some(FlowMsg::Info),
});
let menu_items = VerticalMenu::empty()
.item(
Button::with_text(value_menu_label)
.styled(theme::menu_item_title())
.with_text_align(Alignment::Start)
.with_content_offset(Offset::x(12)),
)
.item(
Button::with_text(cancel_menu_label)
.styled(theme::menu_item_title_orange())
.with_text_align(Alignment::Start)
.with_content_offset(Offset::x(12)),
);
let content_menu = VerticalMenuScreen::new(menu_items)
.with_header(Header::new(TString::empty()).with_close_button())
.map(move |msg| match msg {
VerticalMenuScreenMsg::Selected(i) => Some(FlowMsg::Choice(i)),
VerticalMenuScreenMsg::Close => Some(FlowMsg::Cancelled),
_ => None,
});
let mut res = SwipeFlow::new(&ConfirmValueIntro::Intro)?;
res.add_page(&ConfirmValueIntro::Intro, content_intro)?
.add_page(&ConfirmValueIntro::Menu, content_menu)?;
Ok(res)
}

View File

@ -0,0 +1,113 @@
use crate::{
error,
maybe_trace::MaybeTrace,
strutil::TString,
translations::TR,
ui::{
component::ComponentExt,
flow::{
base::{Decision, DecisionBuilder as _},
FlowController, FlowMsg, SwipeFlow,
},
geometry::{Alignment, Direction, Offset},
},
};
use super::super::{
component::Button,
firmware::{
ActionBar, AllowedTextContent, Header, TextScreen, TextScreenMsg, VerticalMenu,
VerticalMenuScreen, VerticalMenuScreenMsg,
},
theme,
};
const TIMEOUT_MS: u32 = 2000;
#[derive(Copy, Clone, PartialEq, Eq)]
pub enum ConfirmWithMenu {
Value,
Menu,
}
impl FlowController for ConfirmWithMenu {
#[inline]
fn index(&'static self) -> usize {
*self as usize
}
fn handle_swipe(&'static self, _direction: Direction) -> Decision {
self.do_nothing()
}
fn handle_event(&'static self, msg: FlowMsg) -> Decision {
match (self, msg) {
(Self::Value, FlowMsg::Confirmed) => self.return_msg(FlowMsg::Confirmed),
(Self::Value, FlowMsg::Info) => Self::Menu.goto(),
(Self::Menu, FlowMsg::Choice(0)) => self.return_msg(FlowMsg::Info),
(Self::Menu, FlowMsg::Choice(1)) => self.return_msg(FlowMsg::Cancelled),
(Self::Menu, FlowMsg::Cancelled) => Self::Value.goto(),
_ => self.do_nothing(),
}
}
}
#[allow(clippy::too_many_arguments)]
pub fn new_confirm_with_menu<T: AllowedTextContent + MaybeTrace + 'static>(
title: TString<'static>,
subtitle: Option<TString<'static>>,
content: T,
confirm_label: Option<TString<'static>>,
hold: bool,
extra_menu_label: Option<TString<'static>>,
cancel_menu_label: Option<TString<'static>>,
) -> Result<SwipeFlow, error::Error> {
let cancel_menu_label = cancel_menu_label.unwrap_or(TR::buttons__cancel.into());
let confirm_label = confirm_label.unwrap_or(TR::buttons__confirm.into());
// Value
let mut confirm_button = Button::with_text(confirm_label).styled(theme::button_confirm());
if hold {
confirm_button = confirm_button.with_long_press(theme::LOCK_HOLD_DURATION);
}
let content_value = TextScreen::new(content)
.with_header(Header::new(title).with_menu_button())
.with_action_bar(ActionBar::new_single(confirm_button))
.with_subtitle(subtitle.unwrap_or(TString::empty()))
.map(|msg| match msg {
TextScreenMsg::Confirmed => Some(FlowMsg::Confirmed),
TextScreenMsg::Cancelled => Some(FlowMsg::Cancelled),
TextScreenMsg::Menu => Some(FlowMsg::Info),
});
let mut menu_items = VerticalMenu::empty();
if let Some(extra_menu_label) = extra_menu_label {
menu_items = menu_items.item(
Button::with_text(extra_menu_label)
.styled(theme::menu_item_title())
.with_text_align(Alignment::Start)
.with_content_offset(Offset::x(12)),
);
}
menu_items = menu_items.item(
Button::with_text(cancel_menu_label)
.styled(theme::menu_item_title_orange())
.with_text_align(Alignment::Start)
.with_content_offset(Offset::x(12)),
);
let content_menu = VerticalMenuScreen::new(menu_items)
.with_header(Header::new(TString::empty()).with_close_button())
.map(move |msg| match msg {
VerticalMenuScreenMsg::Selected(i) => Some(FlowMsg::Choice(i)),
VerticalMenuScreenMsg::Close => Some(FlowMsg::Cancelled),
_ => None,
});
let mut res = SwipeFlow::new(&ConfirmWithMenu::Value)?;
res.add_page(&ConfirmWithMenu::Value, content_value)?
.add_page(&ConfirmWithMenu::Menu, content_menu)?;
Ok(res)
}

View File

@ -2,6 +2,8 @@ pub mod confirm_output;
pub mod confirm_reset;
pub mod confirm_set_new_pin;
pub mod confirm_summary;
pub mod confirm_value_intro;
pub mod confirm_with_menu;
pub mod continue_recovery_homepage;
pub mod get_address;
pub mod prompt_backup;
@ -14,6 +16,8 @@ pub use confirm_output::new_confirm_output;
pub use confirm_reset::new_confirm_reset;
pub use confirm_set_new_pin::new_set_new_pin;
pub use confirm_summary::new_confirm_summary;
pub use confirm_value_intro::new_confirm_value_intro;
pub use confirm_with_menu::new_confirm_with_menu;
pub use continue_recovery_homepage::new_continue_recovery_homepage;
pub use get_address::GetAddress;
pub use prompt_backup::PromptBackup;

View File

@ -14,13 +14,14 @@ use crate::{
Checklist, Paragraph, ParagraphSource, ParagraphVecLong, ParagraphVecShort,
Paragraphs, VecExt,
},
TextStyle,
},
Empty, FormattedText,
},
geometry::{Alignment, LinearPlacement, Offset},
layout::{
obj::{LayoutMaybeTrace, LayoutObj, RootComponent},
util::{ConfirmValueParams, RecoveryType, StrOrBytes},
util::{ConfirmValueParams, PropsList, RecoveryType, StrOrBytes},
},
ui_firmware::{
FirmwareUI, MAX_CHECKLIST_ITEMS, MAX_GROUP_SHARE_LINES, MAX_WORD_QUIZ_ITEMS,
@ -179,21 +180,84 @@ impl FirmwareUI for UIEckhart {
}
fn confirm_modify_fee(
_title: TString<'static>,
_sign: i32,
_user_fee_change: TString<'static>,
_total_fee_new: TString<'static>,
title: TString<'static>,
sign: i32,
user_fee_change: TString<'static>,
total_fee_new: TString<'static>,
_fee_rate_amount: Option<TString<'static>>,
) -> Result<impl LayoutMaybeTrace, Error> {
Err::<RootComponent<Empty, ModelUI>, Error>(Error::ValueError(c"not implemented"))
let (description, change, total_label) = match sign {
s if s < 0 => (
TR::modify_fee__decrease_fee,
user_fee_change,
TR::modify_fee__new_transaction_fee,
),
s if s > 0 => (
TR::modify_fee__increase_fee,
user_fee_change,
TR::modify_fee__new_transaction_fee,
),
_ => (
TR::modify_fee__no_change,
"".into(),
TR::modify_fee__transaction_fee,
),
};
let paragraphs = ParagraphVecShort::from_iter([
Paragraph::new(&theme::TEXT_SMALL_LIGHT, description),
Paragraph::new(&theme::TEXT_MONO_MEDIUM_LIGHT, change),
Paragraph::new(&theme::TEXT_SMALL_LIGHT, total_label),
Paragraph::new(&theme::TEXT_MONO_MEDIUM_LIGHT, total_fee_new),
]);
let flow = flow::new_confirm_with_menu(
title,
None,
paragraphs
.into_paragraphs()
.with_spacing(12)
.with_placement(LinearPlacement::vertical()),
None,
false,
Some(TR::words__title_information.into()),
None,
)?;
Ok(flow)
}
fn confirm_modify_output(
_sign: i32,
_amount_change: TString<'static>,
_amount_new: TString<'static>,
sign: i32,
amount_change: TString<'static>,
amount_new: TString<'static>,
) -> Result<impl LayoutMaybeTrace, Error> {
Err::<RootComponent<Empty, ModelUI>, Error>(Error::ValueError(c"not implemented"))
let description = if sign < 0 {
TR::modify_amount__decrease_amount
} else {
TR::modify_amount__increase_amount
};
let paragraphs = ParagraphVecShort::from_iter([
Paragraph::new(&theme::TEXT_SMALL_LIGHT, description),
Paragraph::new(&theme::TEXT_MONO_MEDIUM_LIGHT, amount_change),
Paragraph::new(&theme::TEXT_SMALL_LIGHT, TR::modify_amount__new_amount),
Paragraph::new(&theme::TEXT_MONO_MEDIUM_LIGHT, amount_new),
]);
let layout = RootComponent::new(
TextScreen::new(
paragraphs
.into_paragraphs()
.with_placement(LinearPlacement::vertical())
.with_spacing(12),
)
.with_header(Header::new(TR::modify_amount__title.into()))
.with_action_bar(ActionBar::new_double(
Button::with_icon(theme::ICON_CROSS),
Button::with_text(TR::buttons__confirm.into()),
)),
);
Ok(layout)
}
fn confirm_more(
@ -261,11 +325,30 @@ impl FirmwareUI for UIEckhart {
}
fn confirm_properties(
_title: TString<'static>,
_items: Obj,
_hold: bool,
title: TString<'static>,
items: Obj,
hold: bool,
) -> Result<impl LayoutMaybeTrace, Error> {
Err::<RootComponent<Empty, ModelUI>, Error>(Error::ValueError(c"not implemented"))
let paragraphs = PropsList::new(
items,
&theme::TEXT_SMALL_LIGHT,
&theme::TEXT_MEDIUM,
&theme::TEXT_MONO_LIGHT,
)?;
let flow = flow::new_confirm_with_menu(
title,
None,
paragraphs
.into_paragraphs()
.with_spacing(12)
.with_placement(LinearPlacement::vertical()),
None,
hold,
None,
None,
)?;
Ok(flow)
}
fn confirm_value(
@ -333,25 +416,65 @@ impl FirmwareUI for UIEckhart {
}
fn confirm_value_intro(
_title: TString<'static>,
_value: Obj,
_subtitle: Option<TString<'static>>,
_verb: Option<TString<'static>>,
_verb_cancel: Option<TString<'static>>,
_hold: bool,
_chunkify: bool,
title: TString<'static>,
value: Obj,
subtitle: Option<TString<'static>>,
verb: Option<TString<'static>>,
verb_cancel: Option<TString<'static>>,
hold: bool,
chunkify: bool,
) -> Result<Gc<LayoutObj>, Error> {
Err::<Gc<LayoutObj>, Error>(Error::ValueError(c"confirm_value_intro not implemented"))
let flow = flow::new_confirm_value_intro(
title,
subtitle,
value,
TR::buttons__view_all_data.into(),
verb_cancel,
verb,
hold,
chunkify,
)?;
LayoutObj::new_root(flow)
}
fn confirm_with_info(
_title: TString<'static>,
_items: Obj,
_verb: TString<'static>,
_verb_info: TString<'static>,
title: TString<'static>,
items: Obj,
verb: TString<'static>,
verb_info: TString<'static>,
_verb_cancel: Option<TString<'static>>,
) -> Result<impl LayoutMaybeTrace, Error> {
Err::<RootComponent<Empty, ModelUI>, Error>(Error::ValueError(c"not implemented"))
let mut paragraphs = ParagraphVecShort::new();
for para in IterBuf::new().try_iterate(items)? {
let [text, is_data]: [Obj; 2] = util::iter_into_array(para)?;
let is_data = is_data.try_into()?;
let style: &TextStyle = if is_data {
&theme::TEXT_SMALL_LIGHT
} else {
&theme::TEXT_MONO_MEDIUM_LIGHT
};
let text: TString = text.try_into()?;
paragraphs.add(Paragraph::new(style, text));
if paragraphs.is_full() {
break;
}
}
let flow = flow::new_confirm_with_menu(
title,
None,
paragraphs
.into_paragraphs()
.with_placement(LinearPlacement::vertical())
.with_spacing(12),
Some(verb),
false,
Some(verb_info),
None,
)?;
Ok(flow)
}
fn check_homescreen_format(image: BinaryData, _accept_toif: bool) -> bool {
@ -902,19 +1025,24 @@ impl FirmwareUI for UIEckhart {
let [key, value]: [Obj; 2] = util::iter_into_array(para)?;
let key: TString = key.try_into()?;
let value: TString = value.try_into()?;
paragraphs.add(Paragraph::new(&theme::TEXT_MEDIUM, key).no_break());
paragraphs.add(Paragraph::new(&theme::TEXT_SMALL_LIGHT, key).no_break());
if chunkify {
paragraphs.add(Paragraph::new(
theme::get_chunkified_text_style(value.len()),
value,
));
} else {
paragraphs.add(Paragraph::new(&theme::TEXT_MONO_MEDIUM, value));
paragraphs.add(Paragraph::new(&theme::TEXT_MONO_LIGHT, value));
}
}
let screen = TextScreen::new(paragraphs.into_paragraphs())
.with_header(Header::new(title).with_close_button());
let screen = TextScreen::new(
paragraphs
.into_paragraphs()
.with_spacing(12)
.with_placement(LinearPlacement::vertical()),
)
.with_header(Header::new(title).with_close_button());
let layout = RootComponent::new(screen);
Ok(layout)
}

View File

@ -899,8 +899,36 @@ async def confirm_modify_output(
amount_change: str,
amount_new: str,
) -> None:
# FIXME: not implemented
raise NotImplementedError
address_layout = trezorui_api.confirm_value(
title=TR.modify_amount__title,
value=address,
verb=TR.buttons__continue,
verb_cancel=None,
description=f"{TR.words__address}:",
)
modify_layout = trezorui_api.confirm_modify_output(
sign=sign,
amount_change=amount_change,
amount_new=amount_new,
)
send_button_request = True
while True:
await raise_if_not_confirmed(
address_layout,
"modify_output" if send_button_request else None,
ButtonRequestType.ConfirmOutput,
)
result = await interact(
modify_layout,
"modify_output" if send_button_request else None,
ButtonRequestType.ConfirmOutput,
raise_on_cancel=None,
)
send_button_request = False
if result is CONFIRMED:
break
def confirm_modify_fee(
@ -910,8 +938,21 @@ def confirm_modify_fee(
total_fee_new: str,
fee_rate_amount: str | None = None,
) -> Awaitable[None]:
# FIXME: not implemented
raise NotImplementedError
fee_layout = trezorui_api.confirm_modify_fee(
title=title,
sign=sign,
user_fee_change=user_fee_change,
total_fee_new=total_fee_new,
fee_rate_amount=fee_rate_amount,
)
items: list[tuple[str, str]] = []
if fee_rate_amount:
items.append((TR.bitcoin__new_fee_rate, fee_rate_amount))
info_layout = trezorui_api.show_info_with_cancel(
title=TR.confirm_total__title_fee,
items=items,
)
return with_info(fee_layout, info_layout, "modify_fee", ButtonRequestType.SignTx)
def confirm_coinjoin(max_rounds: int, max_fee_per_vbyte: str) -> Awaitable[None]: