fix(core): rebase on current drawlib

Port new render function signature to model_mercury.
Fix some rebase errors.
Also port recent changes from mp <-> rust iface.

[no changelog]
pull/3662/head
obrusvit 2 months ago
parent d13634cf66
commit e35987790b

@ -103,7 +103,7 @@ impl<'a> Component for Intro<'a> {
self.menu.paint(); self.menu.paint();
} }
fn render(&mut self, target: &mut impl Renderer) { fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
self.bg.render(target); self.bg.render(target);
self.title.render(target); self.title.render(target);
self.text.render(target); self.text.render(target);

@ -109,7 +109,7 @@ impl Component for Menu {
self.reset.paint(); self.reset.paint();
} }
fn render(&mut self, target: &mut impl Renderer) { fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
self.bg.render(target); self.bg.render(target);
self.title.render(target); self.title.render(target);
self.close.render(target); self.close.render(target);

@ -59,7 +59,7 @@ impl Component for Welcome {
); );
} }
fn render(&mut self, target: &mut impl Renderer) { fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
self.bg.render(target); self.bg.render(target);
shape::Text::new(screen().top_center() + Offset::y(102), "Get started with") shape::Text::new(screen().top_center() + Offset::y(102), "Get started with")
.with_align(Alignment::Center) .with_align(Alignment::Center)

@ -182,7 +182,7 @@ where
} }
} }
fn render(&mut self, target: &mut impl Renderer) { fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
match self.current_page { match self.current_page {
0 => self.qr_code.render(target), 0 => self.qr_code.render(target),
1 => self.details.render(target), 1 => self.details.render(target),

@ -236,11 +236,11 @@ where
} }
} }
fn render(&mut self, target: &mut impl Renderer) { fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
self.bg.render(target); self.bg.render(target);
self.content_pad.render(target); self.content_pad.render(target);
if let Some(info) = self.info.as_mut() { if let Some(info) = self.info.as_ref() {
if self.show_info { if self.show_info {
info.close_button.render(target); info.close_button.render(target);
info.title.render(target); info.title.render(target);
@ -259,7 +259,7 @@ where
self.alert.render(target); self.alert.render(target);
self.left_button.render(target); self.left_button.render(target);
self.right_button.render(target); self.right_button.render(target);
match &mut self.title { match &self.title {
ConfirmTitle::Text(label) => label.render(target), ConfirmTitle::Text(label) => label.render(target),
ConfirmTitle::Icon(icon) => { ConfirmTitle::Icon(icon) => {
shape::ToifImage::new(Point::new(screen().center().x, ICON_TOP), icon.toif) shape::ToifImage::new(Point::new(screen().center().x, ICON_TOP), icon.toif)

@ -191,7 +191,7 @@ impl<T> Button<T> {
} }
} }
pub fn render_background(&self, target: &mut impl Renderer, style: &ButtonStyle) { pub fn render_background<'s>(&self, target: &mut impl Renderer<'s>, style: &ButtonStyle) {
match &self.content { match &self.content {
ButtonContent::IconBlend(_, _, _) => {} ButtonContent::IconBlend(_, _, _) => {}
_ => shape::Bar::new(self.area) _ => shape::Bar::new(self.area)
@ -244,7 +244,7 @@ impl<T> Button<T> {
} }
} }
pub fn render_content(&self, target: &mut impl Renderer, style: &ButtonStyle) pub fn render_content<'s>(&self, target: &mut impl Renderer<'s>, style: &ButtonStyle)
where where
T: AsRef<str>, T: AsRef<str>,
{ {
@ -375,7 +375,7 @@ where
self.paint_content(style); self.paint_content(style);
} }
fn render(&mut self, target: &mut impl Renderer) { fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
let style = self.style(); let style = self.style();
self.render_background(target, style); self.render_background(target, style);
self.render_content(target, style); self.render_content(target, style);

@ -8,8 +8,7 @@ use crate::{
ui::{ ui::{
canvas::algo::PI4, canvas::algo::PI4,
component::{ component::{
base::Never, painter, Child, Component, ComponentExt, Empty, Event, EventCtx, Label, base::Never, Bar, Child, Component, ComponentExt, Empty, Event, EventCtx, Label, Split,
Split,
}, },
constant, constant,
display::loader::{loader_circular_uncompress, LoaderDimensions}, display::loader::{loader_circular_uncompress, LoaderDimensions},
@ -55,7 +54,7 @@ where
style, style,
) )
.vertically_centered(); .vertically_centered();
let bg = painter::rect_painter(style.background_color, theme::BG); let bg = Bar::new(style.background_color, theme::BG, 2);
let inner = (bg, label); let inner = (bg, label);
CoinJoinProgress::with_background(text, inner, indeterminate) CoinJoinProgress::with_background(text, inner, indeterminate)
} }
@ -133,7 +132,7 @@ where
self.label.paint(); self.label.paint();
} }
fn render(&mut self, target: &mut impl Renderer) { fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
self.content.render(target); self.content.render(target);
let center = constant::screen().center() + Offset::y(LOADER_OFFSET); let center = constant::screen().center() + Offset::y(LOADER_OFFSET);
@ -141,10 +140,10 @@ where
let background_color = theme::BG; let background_color = theme::BG;
let inactive_color = background_color.blend(active_color, 85); let inactive_color = background_color.blend(active_color, 85);
let start = (self.value - 100) % 1000; let start = (self.value as i32 - 100) % 1000;
let end = (self.value + 100) % 1000; let end = (self.value as i32 + 100) % 1000;
let start = ((start as i32 * 8 * PI4 as i32) / 1000) as i16; let start = ((start * 8 * PI4 as i32) / 1000) as i16;
let end = ((end as i32 * 8 * PI4 as i32) / 1000) as i16; let end = ((end * 8 * PI4 as i32) / 1000) as i16;
shape::Circle::new(center, LOADER_OUTER) shape::Circle::new(center, LOADER_OUTER)
.with_bg(inactive_color) .with_bg(inactive_color)

@ -72,7 +72,7 @@ where
self.controls.paint(); self.controls.paint();
} }
fn render(&mut self, target: &mut impl Renderer) { fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
self.content.render(target); self.content.render(target);
self.controls.render(target); self.controls.render(target);
} }
@ -204,7 +204,7 @@ where
self.controls.paint(); self.controls.paint();
} }
fn render(&mut self, target: &mut impl Renderer) { fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
self.image.render(target); self.image.render(target);
self.paragraphs.render(target); self.paragraphs.render(target);
self.controls.render(target); self.controls.render(target);

@ -89,7 +89,7 @@ impl<T: AsRef<str>> Component for ErrorScreen<'_, T> {
self.footer.paint(); self.footer.paint();
} }
fn render(&mut self, target: &mut impl Renderer) { fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
self.bg.render(target); self.bg.render(target);
let icon = ICON_WARNING40; let icon = ICON_WARNING40;

@ -12,6 +12,7 @@ use crate::ui::{
}; };
use super::CancelConfirmMsg; use super::CancelConfirmMsg;
use core::cell::Cell;
const ICON_HEIGHT: i16 = 70; const ICON_HEIGHT: i16 = 70;
const SCROLLBAR_INSET_TOP: i16 = 5; const SCROLLBAR_INSET_TOP: i16 = 5;
@ -32,7 +33,7 @@ pub struct FidoConfirm<F: Fn(usize) -> T, T, U> {
/// Function/closure that will return appropriate page on demand. /// Function/closure that will return appropriate page on demand.
get_account: F, get_account: F,
scrollbar: ScrollBar, scrollbar: ScrollBar,
fade: bool, fade: Cell<bool>,
controls: U, controls: U,
} }
@ -61,14 +62,28 @@ where
page_swipe.allow_right = scrollbar.has_previous_page(); page_swipe.allow_right = scrollbar.has_previous_page();
page_swipe.allow_left = scrollbar.has_next_page(); page_swipe.allow_left = scrollbar.has_next_page();
// NOTE: This is an ugly hotfix for the erroneous behavior of
// TextLayout used in the account_name Label. In this
// particular case, TextLayout calculates the wrong height of
// fitted text that's higher than the TextLayout bound itself.
//
// The following two lines should be swapped when the problem with
// TextLayout is fixed.
//
// See also, continuation of this hotfix in the place() function.
// let current_account = get_account(scrollbar.active_page);
let current_account = "".into();
Self { Self {
app_name: Label::centered(app_name, theme::TEXT_DEMIBOLD), app_name: Label::centered(app_name, theme::TEXT_DEMIBOLD),
account_name: Label::centered("".into(), theme::TEXT_DEMIBOLD), account_name: Label::centered(current_account, theme::TEXT_DEMIBOLD),
page_swipe, page_swipe,
icon: Child::new(Image::new(icon_data)), icon: Child::new(Image::new(icon_data)),
get_account, get_account,
scrollbar, scrollbar,
fade: false, fade: Cell::new(false),
controls, controls,
} }
} }
@ -89,11 +104,14 @@ where
self.page_swipe.allow_right = self.scrollbar.has_previous_page(); self.page_swipe.allow_right = self.scrollbar.has_previous_page();
self.page_swipe.allow_left = self.scrollbar.has_next_page(); self.page_swipe.allow_left = self.scrollbar.has_next_page();
let current_account = (self.get_account)(self.active_page());
self.account_name.set_text(current_account);
// Redraw the page. // Redraw the page.
ctx.request_paint(); ctx.request_paint();
// Reset backlight to normal level on next paint. // Reset backlight to normal level on next paint.
self.fade = true; self.fade.set(true);
} }
fn active_page(&self) -> usize { fn active_page(&self) -> usize {
@ -141,6 +159,11 @@ where
self.app_name.place(app_name_area); self.app_name.place(app_name_area);
self.account_name.place(account_name_area); self.account_name.place(account_name_area);
// NOTE: This is a hotfix used due to the erroneous behavior of TextLayout.
// This line should be removed when the problem with TextLayout is fixed.
// See also the code for FidoConfirm::new().
self.account_name.set_text((self.get_account)(self.scrollbar.active_page));
bounds bounds
} }
@ -168,8 +191,6 @@ where
self.scrollbar.paint(); self.scrollbar.paint();
} }
let current_account = (self.get_account)(self.active_page());
// Erasing the old text content before writing the new one. // Erasing the old text content before writing the new one.
let account_name_area = self.account_name.area(); let account_name_area = self.account_name.area();
let real_area = account_name_area let real_area = account_name_area
@ -179,21 +200,19 @@ where
// Account name is optional. // Account name is optional.
// Showing it only if it differs from app name. // Showing it only if it differs from app name.
// (Dummy requests usually have some text as both app_name and account_name.) // (Dummy requests usually have some text as both app_name and account_name.)
if !current_account.as_ref().is_empty() let account_name = self.account_name.text().as_ref();
&& current_account.as_ref() != self.app_name.text().as_ref() let app_name = self.app_name.text().as_ref();
{ if !account_name.is_empty() && account_name != app_name {
self.account_name.set_text(current_account);
self.account_name.paint(); self.account_name.paint();
} }
if self.fade { if self.fade.take() {
self.fade = false;
// Note that this is blocking and takes some time. // Note that this is blocking and takes some time.
display::fade_backlight(theme::BACKLIGHT_NORMAL); display::fade_backlight(theme::BACKLIGHT_NORMAL);
} }
} }
fn render(&mut self, target: &mut impl Renderer) { fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
self.icon.render(target); self.icon.render(target);
self.controls.render(target); self.controls.render(target);
self.app_name.render(target); self.app_name.render(target);
@ -202,8 +221,6 @@ where
self.scrollbar.render(target); self.scrollbar.render(target);
} }
let current_account = (self.get_account)(self.active_page());
// Erasing the old text content before writing the new one. // Erasing the old text content before writing the new one.
let account_name_area = self.account_name.area(); let account_name_area = self.account_name.area();
let real_area = account_name_area let real_area = account_name_area
@ -213,15 +230,13 @@ where
// Account name is optional. // Account name is optional.
// Showing it only if it differs from app name. // Showing it only if it differs from app name.
// (Dummy requests usually have some text as both app_name and account_name.) // (Dummy requests usually have some text as both app_name and account_name.)
if !current_account.as_ref().is_empty() let account_name = self.account_name.text().as_ref();
&& current_account.as_ref() != self.app_name.text().as_ref() let app_name = self.app_name.text().as_ref();
{ if !account_name.is_empty() && account_name != app_name {
self.account_name.set_text(current_account);
self.account_name.render(target); self.account_name.render(target);
} }
if self.fade { if self.fade.take() {
self.fade = false;
// Note that this is blocking and takes some time. // Note that this is blocking and takes some time.
display::fade_backlight(theme::BACKLIGHT_NORMAL); display::fade_backlight(theme::BACKLIGHT_NORMAL);
} }

@ -161,7 +161,7 @@ where
self.content.paint(); self.content.paint();
} }
fn render(&mut self, target: &mut impl Renderer) { fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
self.title.render(target); self.title.render(target);
self.subtitle.render(target); self.subtitle.render(target);
self.button.render(target); self.button.render(target);

@ -123,7 +123,7 @@ impl Homescreen {
self.loader.paint() self.loader.paint()
} }
fn render_loader(&mut self, target: &mut impl Renderer) { fn render_loader<'s>(&'s self, target: &mut impl Renderer<'s>) {
TR::progress__locking_device.map_translated(|t| { TR::progress__locking_device.map_translated(|t| {
shape::Text::new(TOP_CENTER + Offset::y(HOLD_Y), t) shape::Text::new(TOP_CENTER + Offset::y(HOLD_Y), t)
.with_align(Alignment::Center) .with_align(Alignment::Center)
@ -267,13 +267,13 @@ impl Component for Homescreen {
} }
} }
fn render(&mut self, target: &mut impl Renderer) { fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
self.pad.render(target); self.pad.render(target);
if self.loader.is_animating() || self.loader.is_completely_grown(Instant::now()) { if self.loader.is_animating() || self.loader.is_completely_grown(Instant::now()) {
self.render_loader(target); self.render_loader(target);
} else { } else {
let img_data = match self.custom_image { let img_data = match self.custom_image {
Some(ref img) => IMAGE_HOMESCREEN, //img.as_ref(), !@# solve lifetime problem Some(ref img) => img.as_ref(),
None => IMAGE_HOMESCREEN, None => IMAGE_HOMESCREEN,
}; };
@ -458,9 +458,9 @@ impl Component for Lockscreen {
} }
} }
fn render(&mut self, target: &mut impl Renderer) { fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
let img_data = match self.custom_image { let img_data = match self.custom_image {
Some(ref img) => IMAGE_HOMESCREEN, //img.as_ref(), !@# solve lifetime problem Some(ref img) => img.as_ref(),
None => IMAGE_HOMESCREEN, None => IMAGE_HOMESCREEN,
}; };

@ -504,12 +504,12 @@ impl BlurringContext {
fn vertical_avg(&mut self) { fn vertical_avg(&mut self) {
let lines = &mut self.mem.buffer[0..DECOMP_LINES]; let lines = &mut self.mem.buffer[0..DECOMP_LINES];
for i in 0..HOMESCREEN_IMAGE_WIDTH as usize { for i in 0..HOMESCREEN_IMAGE_WIDTH as usize {
self.totals.buffer[RED_IDX][i] += self.totals.buffer[RED_IDX][i] += lines[self.add_idx][RED_IDX][i];
lines[self.add_idx][RED_IDX][i] - lines[self.rem_idx][RED_IDX][i]; self.totals.buffer[GREEN_IDX][i] += lines[self.add_idx][GREEN_IDX][i];
self.totals.buffer[GREEN_IDX][i] += self.totals.buffer[BLUE_IDX][i] += lines[self.add_idx][BLUE_IDX][i];
lines[self.add_idx][GREEN_IDX][i] - lines[self.rem_idx][GREEN_IDX][i]; self.totals.buffer[RED_IDX][i] -= lines[self.rem_idx][RED_IDX][i];
self.totals.buffer[BLUE_IDX][i] += self.totals.buffer[GREEN_IDX][i] -= lines[self.rem_idx][GREEN_IDX][i];
lines[self.add_idx][BLUE_IDX][i] - lines[self.rem_idx][BLUE_IDX][i]; self.totals.buffer[BLUE_IDX][i] -= lines[self.rem_idx][BLUE_IDX][i];
} }
} }

@ -156,7 +156,7 @@ impl Component for Bip39Input {
} }
} }
fn render(&mut self, target: &mut impl Renderer) { fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
let area = self.button.area(); let area = self.button.area();
let style = self.button.style(); let style = self.button.style();

@ -131,8 +131,8 @@ pub fn paint_pending_marker(text_baseline: Point, text: &str, font: Font, color:
} }
/// Create a visible "underscoring" of the last letter of a text. /// Create a visible "underscoring" of the last letter of a text.
pub fn render_pending_marker( pub fn render_pending_marker<'s>(
target: &mut impl Renderer, target: &mut impl Renderer<'s>,
text_baseline: Point, text_baseline: Point,
text: &str, text: &str,
font: Font, font: Font,

@ -183,12 +183,12 @@ where
} }
} }
fn render(&mut self, target: &mut impl Renderer) { fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
self.prompt.render(target); self.prompt.render(target);
self.input.render(target); self.input.render(target);
self.back.render(target); self.back.render(target);
for btn in &mut self.keys { for btn in &self.keys {
btn.render(target); btn.render(target);
} }
} }

@ -15,6 +15,8 @@ use crate::ui::{
util::long_line_content_with_ellipsis, util::long_line_content_with_ellipsis,
}; };
use core::cell::Cell;
pub enum PassphraseKeyboardMsg { pub enum PassphraseKeyboardMsg {
Confirmed, Confirmed,
Cancelled, Cancelled,
@ -27,7 +29,7 @@ pub struct PassphraseKeyboard {
confirm: Child<Button<&'static str>>, confirm: Child<Button<&'static str>>,
keys: [Child<Button<&'static str>>; KEY_COUNT], keys: [Child<Button<&'static str>>; KEY_COUNT],
scrollbar: ScrollBar, scrollbar: ScrollBar,
fade: bool, fade: Cell<bool>,
} }
const STARTING_PAGE: usize = 1; const STARTING_PAGE: usize = 1;
@ -65,7 +67,7 @@ impl PassphraseKeyboard {
Child::new(Button::new(Self::key_content(text)).styled(theme::button_pin())) Child::new(Button::new(Self::key_content(text)).styled(theme::button_pin()))
}), }),
scrollbar: ScrollBar::horizontal(), scrollbar: ScrollBar::horizontal(),
fade: false, fade: Cell::new(false),
} }
} }
@ -101,7 +103,7 @@ impl PassphraseKeyboard {
// Update buttons. // Update buttons.
self.replace_button_content(ctx, key_page); self.replace_button_content(ctx, key_page);
// Reset backlight to normal level on next paint. // Reset backlight to normal level on next paint.
self.fade = true; self.fade.set(true);
// So that swipe does not visually enable the input buttons when max length // So that swipe does not visually enable the input buttons when max length
// reached // reached
self.update_input_btns_state(ctx); self.update_input_btns_state(ctx);
@ -290,23 +292,21 @@ impl Component for PassphraseKeyboard {
for btn in &mut self.keys { for btn in &mut self.keys {
btn.paint(); btn.paint();
} }
if self.fade { if self.fade.take() {
self.fade = false;
// Note that this is blocking and takes some time. // Note that this is blocking and takes some time.
display::fade_backlight(theme::BACKLIGHT_NORMAL); display::fade_backlight(theme::BACKLIGHT_NORMAL);
} }
} }
fn render(&mut self, target: &mut impl Renderer) { fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
self.input.render(target); self.input.render(target);
self.scrollbar.render(target); self.scrollbar.render(target);
self.confirm.render(target); self.confirm.render(target);
self.back.render(target); self.back.render(target);
for btn in &mut self.keys { for btn in &self.keys {
btn.render(target); btn.render(target);
} }
if self.fade { if self.fade.take() {
self.fade = false;
// Note that this is blocking and takes some time. // Note that this is blocking and takes some time.
display::fade_backlight(theme::BACKLIGHT_NORMAL); display::fade_backlight(theme::BACKLIGHT_NORMAL);
} }
@ -392,7 +392,7 @@ impl Component for Input {
} }
} }
fn render(&mut self, target: &mut impl Renderer) { fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
let style = theme::label_keyboard(); let style = theme::label_keyboard();
let text_baseline = self.area.top_left() + Offset::y(style.text_font.text_height()) let text_baseline = self.area.top_left() + Offset::y(style.text_font.text_height())

@ -269,11 +269,11 @@ where
} }
} }
fn render(&mut self, target: &mut impl Renderer) { fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
self.erase_btn.render(target); self.erase_btn.render(target);
self.textbox_pad.render(target); self.textbox_pad.render(target);
if self.textbox.inner().is_empty() { if self.textbox.inner().is_empty() {
if let Some(ref mut w) = self.major_warning { if let Some(ref w) = self.major_warning {
w.render(target); w.render(target);
} else { } else {
self.major_prompt.render(target); self.major_prompt.render(target);
@ -284,7 +284,7 @@ where
self.textbox.render(target); self.textbox.render(target);
} }
self.confirm_btn.render(target); self.confirm_btn.render(target);
for btn in &mut self.digit_btns { for btn in &self.digit_btns {
btn.render(target); btn.render(target);
} }
} }
@ -389,7 +389,7 @@ impl PinDots {
} }
} }
fn render_digits(&self, area: Rect, target: &mut impl Renderer) { fn render_digits<'s>(&self, area: Rect, target: &mut impl Renderer<'s>) {
let center = area.center() + Offset::y(Font::MONO.text_height() / 2); let center = area.center() + Offset::y(Font::MONO.text_height() / 2);
let right = center + Offset::x(Font::MONO.text_width("0") * (MAX_VISIBLE_DOTS as i16) / 2); let right = center + Offset::x(Font::MONO.text_width("0") * (MAX_VISIBLE_DOTS as i16) / 2);
let digits = self.digits.len(); let digits = self.digits.len();
@ -454,7 +454,7 @@ impl PinDots {
} }
} }
fn render_dots(&self, area: Rect, target: &mut impl Renderer) { fn render_dots<'s>(&self, area: Rect, target: &mut impl Renderer<'s>) {
let mut cursor = self.size().snap(area.center(), Alignment2D::CENTER); let mut cursor = self.size().snap(area.center(), Alignment2D::CENTER);
let digits = self.digits.len(); let digits = self.digits.len();
@ -533,7 +533,7 @@ impl Component for PinDots {
} }
} }
fn render(&mut self, target: &mut impl Renderer) { fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
let dot_area = self.area.inset(HEADER_PADDING); let dot_area = self.area.inset(HEADER_PADDING);
self.pad.render(target); self.pad.render(target);
if self.display_digits { if self.display_digits {

@ -187,7 +187,7 @@ impl Component for Slip39Input {
} }
} }
fn render(&mut self, target: &mut impl Renderer) { fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
let area = self.button.area(); let area = self.button.area();
let style = self.button.style(); let style = self.button.style();

@ -58,8 +58,8 @@ impl Component for SelectWordCount {
} }
} }
fn render(&mut self, target: &mut impl Renderer) { fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
for btn in self.button.iter_mut() { for btn in self.button.iter() {
btn.render(target) btn.render(target)
} }
} }

@ -209,7 +209,7 @@ impl Component for Loader {
} }
} }
fn render(&mut self, target: &mut impl Renderer) { fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
// TODO: Consider passing the current instant along with the event -- that way, // TODO: Consider passing the current instant along with the event -- that way,
// we could synchronize painting across the component tree. Also could be useful // we could synchronize painting across the component tree. Also could be useful
// in automated tests. // in automated tests.
@ -250,7 +250,7 @@ impl Component for Loader {
if let Some((icon, color)) = style.icon { if let Some((icon, color)) = style.icon {
shape::ToifImage::new(center, icon.toif) shape::ToifImage::new(center, icon.toif)
.with_align(Alignment2D::CENTER) .with_align(Alignment2D::CENTER)
.with_bg(color) .with_fg(color)
.render(target); .render(target);
} }
} }

@ -124,7 +124,7 @@ where
self.confirm_button.paint(); self.confirm_button.paint();
} }
fn render(&mut self, target: &mut impl Renderer) { fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
self.input.render(target); self.input.render(target);
self.paragraphs_pad.render(target); self.paragraphs_pad.render(target);
self.paragraphs.render(target); self.paragraphs.render(target);
@ -240,7 +240,7 @@ impl Component for NumberInput {
self.inc.paint(); self.inc.paint();
} }
fn render(&mut self, target: &mut impl Renderer) { fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
let mut buf = [0u8; 10]; let mut buf = [0u8; 10];
if let Some(text) = strutil::format_i64(self.value as i64, &mut buf) { if let Some(text) = strutil::format_i64(self.value as i64, &mut buf) {

@ -18,6 +18,8 @@ use super::{
SwipeDirection, SwipeDirection,
}; };
use core::cell::Cell;
/// Allows pagination of inner component. Shows scroll bar, confirm & cancel /// Allows pagination of inner component. Shows scroll bar, confirm & cancel
/// buttons. Optionally handles hold-to-confirm with loader. /// buttons. Optionally handles hold-to-confirm with loader.
pub struct ButtonPage<T, U> { pub struct ButtonPage<T, U> {
@ -41,7 +43,7 @@ pub struct ButtonPage<T, U> {
/// Whether to pass-through right swipe to parent component. /// Whether to pass-through right swipe to parent component.
swipe_right: bool, swipe_right: bool,
/// Fade to given backlight level on next paint(). /// Fade to given backlight level on next paint().
fade: Option<u16>, fade: Cell<Option<u16>>,
} }
impl<T> ButtonPage<T, StrBuffer> impl<T> ButtonPage<T, StrBuffer>
@ -77,7 +79,7 @@ where
cancel_from_any_page: false, cancel_from_any_page: false,
swipe_left: false, swipe_left: false,
swipe_right: false, swipe_right: false,
fade: None, fade: Cell::new(None),
} }
} }
@ -157,7 +159,7 @@ where
// Swipe has dimmed the screen, so fade back to normal backlight after the next // Swipe has dimmed the screen, so fade back to normal backlight after the next
// paint. // paint.
self.fade = Some(theme::BACKLIGHT_NORMAL); self.fade.set(Some(theme::BACKLIGHT_NORMAL));
} }
fn is_cancel_visible(&self) -> bool { fn is_cancel_visible(&self) -> bool {
@ -413,7 +415,7 @@ where
} }
} }
fn render(&mut self, target: &mut impl Renderer) { fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
self.pad.render(target); self.pad.render(target);
match &self.loader { match &self.loader {
Some(l) if l.is_animating() => self.loader.render(target), Some(l) if l.is_animating() => self.loader.render(target),

@ -123,7 +123,7 @@ where
self.description.paint(); self.description.paint();
} }
fn render(&mut self, target: &mut impl Renderer) { fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
self.title.render(target); self.title.render(target);
let center = constant::screen().center() + Offset::y(self.loader_y_offset); let center = constant::screen().center() + Offset::y(self.loader_y_offset);

@ -92,7 +92,7 @@ impl<T: AsRef<str>> Component for ResultFooter<'_, T> {
self.text.paint(); self.text.paint();
} }
fn render(&mut self, target: &mut impl Renderer) { fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
// divider line // divider line
let bar = Rect::from_center_and_size( let bar = Rect::from_center_and_size(
Point::new(self.area.center().x, self.area.y0), Point::new(self.area.center().x, self.area.y0),
@ -181,7 +181,7 @@ impl<'a, T: StringType> Component for ResultScreen<'a, T> {
self.footer.paint(); self.footer.paint();
} }
fn render(&mut self, target: &mut impl Renderer) { fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
self.bg.render(target); self.bg.render(target);
self.footer_pad.render(target); self.footer_pad.render(target);

@ -124,7 +124,7 @@ impl Component for ScrollBar {
} }
} }
fn render(&mut self, target: &mut impl Renderer) { fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
fn dotsize(distance: usize, nhidden: usize) -> Icon { fn dotsize(distance: usize, nhidden: usize) -> Icon {
match (nhidden.saturating_sub(distance)).min(2 - distance) { match (nhidden.saturating_sub(distance)).min(2 - distance) {
0 => theme::DOT_INACTIVE, 0 => theme::DOT_INACTIVE,

@ -6,6 +6,7 @@ use crate::ui::{
}; };
use super::{theme, ScrollBar, Swipe, SwipeDirection}; use super::{theme, ScrollBar, Swipe, SwipeDirection};
use core::cell::Cell;
const SCROLLBAR_HEIGHT: i16 = 18; const SCROLLBAR_HEIGHT: i16 = 18;
const SCROLLBAR_BORDER: i16 = 4; const SCROLLBAR_BORDER: i16 = 4;
@ -17,7 +18,7 @@ pub struct SimplePage<T> {
scrollbar: ScrollBar, scrollbar: ScrollBar,
axis: Axis, axis: Axis,
swipe_right_to_go_back: bool, swipe_right_to_go_back: bool,
fade: Option<u16>, fade: Cell<Option<u16>>,
} }
impl<T> SimplePage<T> impl<T> SimplePage<T>
@ -33,7 +34,7 @@ where
scrollbar: ScrollBar::new(axis), scrollbar: ScrollBar::new(axis),
axis, axis,
swipe_right_to_go_back: false, swipe_right_to_go_back: false,
fade: None, fade: Cell::new(None),
} }
} }
@ -80,7 +81,7 @@ where
// Swipe has dimmed the screen, so fade back to normal backlight after the next // Swipe has dimmed the screen, so fade back to normal backlight after the next
// paint. // paint.
self.fade = Some(theme::BACKLIGHT_NORMAL); self.fade.set(Some(theme::BACKLIGHT_NORMAL));
} }
fn is_horizontal(&self) -> bool { fn is_horizontal(&self) -> bool {
@ -165,7 +166,7 @@ where
} }
} }
fn render(&mut self, target: &mut impl Renderer) { fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
self.pad.render(target); self.pad.render(target);
self.content.render(target); self.content.render(target);
if self.scrollbar.has_pages() { if self.scrollbar.has_pages() {

@ -161,5 +161,5 @@ impl Component for Swipe {
fn paint(&mut self) {} fn paint(&mut self) {}
fn render(&mut self, _target: &mut impl Renderer) {} fn render<'s>(&'s self, _target: &mut impl Renderer<'s>) {}
} }

@ -71,7 +71,7 @@ impl Component for WelcomeScreen {
); );
} }
fn render(&mut self, target: &mut impl Renderer) { fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
let logo = if self.empty_lock { let logo = if self.empty_lock {
theme::ICON_LOGO_EMPTY theme::ICON_LOGO_EMPTY
} else { } else {

@ -21,8 +21,8 @@ use crate::{
base::ComponentExt, base::ComponentExt,
connect::Connect, connect::Connect,
image::BlendedImage, image::BlendedImage,
jpeg::{ImageBuffer, Jpeg},
paginated::{PageMsg, Paginate}, paginated::{PageMsg, Paginate},
painter,
placed::GridPlaced, placed::GridPlaced,
text::{ text::{
op::OpTextLayout, op::OpTextLayout,
@ -211,10 +211,7 @@ where
} }
} }
impl<F> ComponentMsgObj for painter::Painter<F> impl ComponentMsgObj for Jpeg {
where
F: FnMut(geometry::Rect),
{
fn msg_try_into_obj(&self, _msg: Self::Msg) -> Result<Obj, Error> { fn msg_try_into_obj(&self, _msg: Self::Msg) -> Result<Obj, Error> {
unreachable!() unreachable!()
} }
@ -646,33 +643,25 @@ extern "C" fn new_confirm_properties(n_args: usize, args: *const Obj, kwargs: *m
extern "C" fn new_confirm_homescreen(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj { extern "C" fn new_confirm_homescreen(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
let block = move |_args: &[Obj], kwargs: &Map| { let block = move |_args: &[Obj], kwargs: &Map| {
let title: StrBuffer = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?; let title: StrBuffer = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?;
let data: Obj = kwargs.get(Qstr::MP_QSTR_image)?; let image: Obj = kwargs.get(Qstr::MP_QSTR_image)?;
// Layout needs to hold the Obj to play nice with GC. Obj is resolved to &[u8] let mut jpeg = unwrap!(ImageBuffer::from_object(image));
// in every paint pass.
let buffer_func = move || { if jpeg.is_empty() {
// SAFETY: We expect no existing mutable reference. Resulting reference is // Incoming data may be empty, meaning we should
// discarded before returning to micropython. // display default homescreen image.
let buffer = unsafe { unwrap!(get_buffer(data)) }; jpeg = ImageBuffer::from_slice(theme::IMAGE_HOMESCREEN);
// Incoming data may be empty, meaning we should display default homescreen }
// image.
if buffer.is_empty() {
theme::IMAGE_HOMESCREEN
} else {
buffer
}
};
let size = match jpeg_info(buffer_func()) { if let None = jpeg_info(jpeg.data()) {
Some(info) => info.0, return Err(value_error!("Invalid image."));
_ => return Err(value_error!("Invalid image.")),
}; };
let tr_change: StrBuffer = TR::buttons__change.try_into()?; let tr_change: StrBuffer = TR::buttons__change.try_into()?;
let buttons = Button::cancel_confirm_text(None, Some(tr_change)); let buttons = Button::cancel_confirm_text(None, Some(tr_change));
let obj = LayoutObj::new(Frame::centered( let obj = LayoutObj::new(Frame::centered(
title, title,
Dialog::new(painter::jpeg_painter(buffer_func, size, 1), buttons), Dialog::new(Jpeg::new(jpeg, 1), buttons),
))?; ))?;
Ok(obj.into()) Ok(obj.into())
}; };
@ -1662,15 +1651,86 @@ extern "C" fn new_show_wait_text(message: Obj) -> Obj {
#[no_mangle] #[no_mangle]
pub static mp_module_trezorui2: Module = obj_module! { pub static mp_module_trezorui2: Module = obj_module! {
/// from trezor import utils
///
/// T = TypeVar("T")
///
/// class LayoutObj(Generic[T]):
/// """Representation of a Rust-based layout object.
/// see `trezor::ui::layout::obj::LayoutObj`.
/// """
///
/// def attach_timer_fn(self, fn: Callable[[int, int], None]) -> None:
/// """Attach a timer setter function.
///
/// The layout object can call the timer setter with two arguments,
/// `token` and `deadline`. When `deadline` is reached, the layout object
/// expects a callback to `self.timer(token)`.
/// """
///
/// if utils.USE_TOUCH:
/// def touch_event(self, event: int, x: int, y: int) -> T | None:
/// """Receive a touch event `event` at coordinates `x`, `y`."""
///
/// if utils.USE_BUTTON:
/// def button_event(self, event: int, button: int) -> T | None:
/// """Receive a button event `event` for button `button`."""
///
/// def progress_event(self, value: int, description: str) -> T | None:
/// """Receive a progress event."""
///
/// def usb_event(self, connected: bool) -> T | None:
/// """Receive a USB connect/disconnect event."""
///
/// def timer(self, token: int) -> T | None:
/// """Callback for the timer set by `attach_timer_fn`.
///
/// This function should be called by the executor after the corresponding
/// deadline is reached.
/// """
///
/// def paint(self) -> bool:
/// """Paint the layout object on screen.
///
/// Will only paint updated parts of the layout as required.
/// Returns True if any painting actually happened.
/// """
///
/// def request_complete_repaint(self) -> None:
/// """Request a complete repaint of the screen.
///
/// Does not repaint the screen, a subsequent call to `paint()` is required.
/// """
///
/// if __debug__:
/// def trace(self, tracer: Callable[[str], None]) -> None:
/// """Generate a JSON trace of the layout object.
///
/// The JSON can be emitted as a sequence of calls to `tracer`, each of
/// which is not necessarily a valid JSON chunk. The caller must
/// reassemble the chunks to get a sensible result.
/// """
///
/// def bounds(self) -> None:
/// """Paint bounds of individual components on screen."""
///
/// def page_count(self) -> int:
/// """Return the number of pages in the layout object."""
///
/// class UiResult:
/// """Result of a UI operation."""
/// pass
///
/// mock:global
Qstr::MP_QSTR___name__ => Qstr::MP_QSTR_trezorui2.to_obj(), Qstr::MP_QSTR___name__ => Qstr::MP_QSTR_trezorui2.to_obj(),
/// CONFIRMED: object /// CONFIRMED: UiResult
Qstr::MP_QSTR_CONFIRMED => CONFIRMED.as_obj(), Qstr::MP_QSTR_CONFIRMED => CONFIRMED.as_obj(),
/// CANCELLED: object /// CANCELLED: UiResult
Qstr::MP_QSTR_CANCELLED => CANCELLED.as_obj(), Qstr::MP_QSTR_CANCELLED => CANCELLED.as_obj(),
/// INFO: object /// INFO: UiResult
Qstr::MP_QSTR_INFO => INFO.as_obj(), Qstr::MP_QSTR_INFO => INFO.as_obj(),
/// def disable_animation(disable: bool) -> None: /// def disable_animation(disable: bool) -> None:
@ -1691,7 +1751,7 @@ pub static mp_module_trezorui2: Module = obj_module! {
/// hold: bool = False, /// hold: bool = False,
/// hold_danger: bool = False, /// hold_danger: bool = False,
/// reverse: bool = False, /// reverse: bool = False,
/// ) -> object: /// ) -> LayoutObj[UiResult]:
/// """Confirm action.""" /// """Confirm action."""
Qstr::MP_QSTR_confirm_action => obj_fn_kw!(0, new_confirm_action).as_obj(), Qstr::MP_QSTR_confirm_action => obj_fn_kw!(0, new_confirm_action).as_obj(),
@ -1700,7 +1760,7 @@ pub static mp_module_trezorui2: Module = obj_module! {
/// title: str, /// title: str,
/// items: Iterable[str | tuple[bool, str]], /// items: Iterable[str | tuple[bool, str]],
/// verb: str | None = None, /// verb: str | None = None,
/// ) -> object: /// ) -> LayoutObj[UiResult]:
/// """Confirm formatted text that has been pre-split in python. For tuples /// """Confirm formatted text that has been pre-split in python. For tuples
/// the first component is a bool indicating whether this part is emphasized.""" /// the first component is a bool indicating whether this part is emphasized."""
Qstr::MP_QSTR_confirm_emphasized => obj_fn_kw!(0, new_confirm_emphasized).as_obj(), Qstr::MP_QSTR_confirm_emphasized => obj_fn_kw!(0, new_confirm_emphasized).as_obj(),
@ -1709,7 +1769,7 @@ pub static mp_module_trezorui2: Module = obj_module! {
/// *, /// *,
/// title: str, /// title: str,
/// image: bytes, /// image: bytes,
/// ) -> object: /// ) -> LayoutObj[UiResult]:
/// """Confirm homescreen.""" /// """Confirm homescreen."""
Qstr::MP_QSTR_confirm_homescreen => obj_fn_kw!(0, new_confirm_homescreen).as_obj(), Qstr::MP_QSTR_confirm_homescreen => obj_fn_kw!(0, new_confirm_homescreen).as_obj(),
@ -1723,7 +1783,7 @@ pub static mp_module_trezorui2: Module = obj_module! {
/// verb_cancel: str | None = None, /// verb_cancel: str | None = None,
/// hold: bool = False, /// hold: bool = False,
/// chunkify: bool = False, /// chunkify: bool = False,
/// ) -> object: /// ) -> LayoutObj[UiResult]:
/// """Confirm byte sequence data.""" /// """Confirm byte sequence data."""
Qstr::MP_QSTR_confirm_blob => obj_fn_kw!(0, new_confirm_blob).as_obj(), Qstr::MP_QSTR_confirm_blob => obj_fn_kw!(0, new_confirm_blob).as_obj(),
@ -1735,7 +1795,7 @@ pub static mp_module_trezorui2: Module = obj_module! {
/// verb: str | None = "CONFIRM", /// verb: str | None = "CONFIRM",
/// extra: str | None, /// extra: str | None,
/// chunkify: bool = False, /// chunkify: bool = False,
/// ) -> object: /// ) -> LayoutObj[UiResult]:
/// """Confirm address. Similar to `confirm_blob` but has corner info button /// """Confirm address. Similar to `confirm_blob` but has corner info button
/// and allows left swipe which does the same thing as the button.""" /// and allows left swipe which does the same thing as the button."""
Qstr::MP_QSTR_confirm_address => obj_fn_kw!(0, new_confirm_address).as_obj(), Qstr::MP_QSTR_confirm_address => obj_fn_kw!(0, new_confirm_address).as_obj(),
@ -1745,7 +1805,7 @@ pub static mp_module_trezorui2: Module = obj_module! {
/// title: str, /// title: str,
/// items: list[tuple[str | None, str | bytes | None, bool]], /// items: list[tuple[str | None, str | bytes | None, bool]],
/// hold: bool = False, /// hold: bool = False,
/// ) -> object: /// ) -> LayoutObj[UiResult]:
/// """Confirm list of key-value pairs. The third component in the tuple should be True if /// """Confirm list of key-value pairs. The third component in the tuple should be True if
/// the value is to be rendered as binary with monospace font, False otherwise.""" /// the value is to be rendered as binary with monospace font, False otherwise."""
Qstr::MP_QSTR_confirm_properties => obj_fn_kw!(0, new_confirm_properties).as_obj(), Qstr::MP_QSTR_confirm_properties => obj_fn_kw!(0, new_confirm_properties).as_obj(),
@ -1754,7 +1814,7 @@ pub static mp_module_trezorui2: Module = obj_module! {
/// *, /// *,
/// title: str, /// title: str,
/// button: str, /// button: str,
/// ) -> object: /// ) -> LayoutObj[UiResult]:
/// """Confirm TOS before device setup.""" /// """Confirm TOS before device setup."""
Qstr::MP_QSTR_confirm_reset_device => obj_fn_kw!(0, new_confirm_reset_device).as_obj(), Qstr::MP_QSTR_confirm_reset_device => obj_fn_kw!(0, new_confirm_reset_device).as_obj(),
@ -1767,7 +1827,7 @@ pub static mp_module_trezorui2: Module = obj_module! {
/// account: str | None, /// account: str | None,
/// path: str | None, /// path: str | None,
/// xpubs: list[tuple[str, str]], /// xpubs: list[tuple[str, str]],
/// ) -> object: /// ) -> LayoutObj[UiResult]:
/// """Show address details - QR code, account, path, cosigner xpubs.""" /// """Show address details - QR code, account, path, cosigner xpubs."""
Qstr::MP_QSTR_show_address_details => obj_fn_kw!(0, new_show_address_details).as_obj(), Qstr::MP_QSTR_show_address_details => obj_fn_kw!(0, new_show_address_details).as_obj(),
@ -1777,7 +1837,7 @@ pub static mp_module_trezorui2: Module = obj_module! {
/// items: Iterable[Tuple[str, str]], /// items: Iterable[Tuple[str, str]],
/// horizontal: bool = False, /// horizontal: bool = False,
/// chunkify: bool = False, /// chunkify: bool = False,
/// ) -> object: /// ) -> LayoutObj[UiResult]:
/// """Show metadata for outgoing transaction.""" /// """Show metadata for outgoing transaction."""
Qstr::MP_QSTR_show_info_with_cancel => obj_fn_kw!(0, new_show_info_with_cancel).as_obj(), Qstr::MP_QSTR_show_info_with_cancel => obj_fn_kw!(0, new_show_info_with_cancel).as_obj(),
@ -1793,7 +1853,7 @@ pub static mp_module_trezorui2: Module = obj_module! {
/// hold: bool = False, /// hold: bool = False,
/// chunkify: bool = False, /// chunkify: bool = False,
/// text_mono: bool = True, /// text_mono: bool = True,
/// ) -> object: /// ) -> LayoutObj[UiResult]:
/// """Confirm value. Merge of confirm_total and confirm_output.""" /// """Confirm value. Merge of confirm_total and confirm_output."""
Qstr::MP_QSTR_confirm_value => obj_fn_kw!(0, new_confirm_value).as_obj(), Qstr::MP_QSTR_confirm_value => obj_fn_kw!(0, new_confirm_value).as_obj(),
@ -1803,7 +1863,7 @@ pub static mp_module_trezorui2: Module = obj_module! {
/// items: Iterable[tuple[str, str]], /// items: Iterable[tuple[str, str]],
/// info_button: bool = False, /// info_button: bool = False,
/// cancel_arrow: bool = False, /// cancel_arrow: bool = False,
/// ) -> object: /// ) -> LayoutObj[UiResult]:
/// """Transaction summary. Always hold to confirm.""" /// """Transaction summary. Always hold to confirm."""
Qstr::MP_QSTR_confirm_total => obj_fn_kw!(0, new_confirm_total).as_obj(), Qstr::MP_QSTR_confirm_total => obj_fn_kw!(0, new_confirm_total).as_obj(),
@ -1812,7 +1872,7 @@ pub static mp_module_trezorui2: Module = obj_module! {
/// sign: int, /// sign: int,
/// amount_change: str, /// amount_change: str,
/// amount_new: str, /// amount_new: str,
/// ) -> object: /// ) -> LayoutObj[UiResult]:
/// """Decrease or increase output amount.""" /// """Decrease or increase output amount."""
Qstr::MP_QSTR_confirm_modify_output => obj_fn_kw!(0, new_confirm_modify_output).as_obj(), Qstr::MP_QSTR_confirm_modify_output => obj_fn_kw!(0, new_confirm_modify_output).as_obj(),
@ -1823,7 +1883,7 @@ pub static mp_module_trezorui2: Module = obj_module! {
/// user_fee_change: str, /// user_fee_change: str,
/// total_fee_new: str, /// total_fee_new: str,
/// fee_rate_amount: str | None, # ignored /// fee_rate_amount: str | None, # ignored
/// ) -> object: /// ) -> LayoutObj[UiResult]:
/// """Decrease or increase transaction fee.""" /// """Decrease or increase transaction fee."""
Qstr::MP_QSTR_confirm_modify_fee => obj_fn_kw!(0, new_confirm_modify_fee).as_obj(), Qstr::MP_QSTR_confirm_modify_fee => obj_fn_kw!(0, new_confirm_modify_fee).as_obj(),
@ -1833,7 +1893,7 @@ pub static mp_module_trezorui2: Module = obj_module! {
/// app_name: str, /// app_name: str,
/// icon_name: str | None, /// icon_name: str | None,
/// accounts: list[str | None], /// accounts: list[str | None],
/// ) -> int | object: /// ) -> LayoutObj[int | UiResult]:
/// """FIDO confirmation. /// """FIDO confirmation.
/// ///
/// Returns page index in case of confirmation and CANCELLED otherwise. /// Returns page index in case of confirmation and CANCELLED otherwise.
@ -1847,7 +1907,7 @@ pub static mp_module_trezorui2: Module = obj_module! {
/// description: str = "", /// description: str = "",
/// allow_cancel: bool = False, /// allow_cancel: bool = False,
/// time_ms: int = 0, /// time_ms: int = 0,
/// ) -> object: /// ) -> LayoutObj[UiResult]:
/// """Error modal. No buttons shown when `button` is empty string.""" /// """Error modal. No buttons shown when `button` is empty string."""
Qstr::MP_QSTR_show_error => obj_fn_kw!(0, new_show_error).as_obj(), Qstr::MP_QSTR_show_error => obj_fn_kw!(0, new_show_error).as_obj(),
@ -1859,7 +1919,7 @@ pub static mp_module_trezorui2: Module = obj_module! {
/// description: str = "", /// description: str = "",
/// allow_cancel: bool = False, /// allow_cancel: bool = False,
/// time_ms: int = 0, /// time_ms: int = 0,
/// ) -> object: /// ) -> LayoutObj[UiResult]:
/// """Warning modal. No buttons shown when `button` is empty string.""" /// """Warning modal. No buttons shown when `button` is empty string."""
Qstr::MP_QSTR_show_warning => obj_fn_kw!(0, new_show_warning).as_obj(), Qstr::MP_QSTR_show_warning => obj_fn_kw!(0, new_show_warning).as_obj(),
@ -1870,7 +1930,7 @@ pub static mp_module_trezorui2: Module = obj_module! {
/// description: str = "", /// description: str = "",
/// allow_cancel: bool = False, /// allow_cancel: bool = False,
/// time_ms: int = 0, /// time_ms: int = 0,
/// ) -> object: /// ) -> LayoutObj[UiResult]:
/// """Success modal. No buttons shown when `button` is empty string.""" /// """Success modal. No buttons shown when `button` is empty string."""
Qstr::MP_QSTR_show_success => obj_fn_kw!(0, new_show_success).as_obj(), Qstr::MP_QSTR_show_success => obj_fn_kw!(0, new_show_success).as_obj(),
@ -1881,11 +1941,11 @@ pub static mp_module_trezorui2: Module = obj_module! {
/// description: str = "", /// description: str = "",
/// allow_cancel: bool = False, /// allow_cancel: bool = False,
/// time_ms: int = 0, /// time_ms: int = 0,
/// ) -> object: /// ) -> LayoutObj[UiResult]:
/// """Info modal. No buttons shown when `button` is empty string.""" /// """Info modal. No buttons shown when `button` is empty string."""
Qstr::MP_QSTR_show_info => obj_fn_kw!(0, new_show_info).as_obj(), Qstr::MP_QSTR_show_info => obj_fn_kw!(0, new_show_info).as_obj(),
/// def show_mismatch(*, title: str) -> object: /// def show_mismatch(*, title: str) -> LayoutObj[UiResult]:
/// """Warning modal, receiving address mismatch.""" /// """Warning modal, receiving address mismatch."""
Qstr::MP_QSTR_show_mismatch => obj_fn_kw!(0, new_show_mismatch).as_obj(), Qstr::MP_QSTR_show_mismatch => obj_fn_kw!(0, new_show_mismatch).as_obj(),
@ -1894,7 +1954,7 @@ pub static mp_module_trezorui2: Module = obj_module! {
/// title: str | None, /// title: str | None,
/// description: str = "", /// description: str = "",
/// button: str = "", /// button: str = "",
/// ) -> object: /// ) -> LayoutObj[UiResult]:
/// """Simple dialog with text and one button.""" /// """Simple dialog with text and one button."""
Qstr::MP_QSTR_show_simple => obj_fn_kw!(0, new_show_simple).as_obj(), Qstr::MP_QSTR_show_simple => obj_fn_kw!(0, new_show_simple).as_obj(),
@ -1904,7 +1964,7 @@ pub static mp_module_trezorui2: Module = obj_module! {
/// button: str, /// button: str,
/// info_button: str, /// info_button: str,
/// items: Iterable[tuple[int, str]], /// items: Iterable[tuple[int, str]],
/// ) -> object: /// ) -> LayoutObj[UiResult]:
/// """Confirm given items but with third button. Always single page /// """Confirm given items but with third button. Always single page
/// without scrolling.""" /// without scrolling."""
Qstr::MP_QSTR_confirm_with_info => obj_fn_kw!(0, new_confirm_with_info).as_obj(), Qstr::MP_QSTR_confirm_with_info => obj_fn_kw!(0, new_confirm_with_info).as_obj(),
@ -1914,7 +1974,7 @@ pub static mp_module_trezorui2: Module = obj_module! {
/// title: str, /// title: str,
/// button: str, /// button: str,
/// items: Iterable[tuple[int, str]], /// items: Iterable[tuple[int, str]],
/// ) -> object: /// ) -> LayoutObj[UiResult]:
/// """Confirm long content with the possibility to go back from any page. /// """Confirm long content with the possibility to go back from any page.
/// Meant to be used with confirm_with_info.""" /// Meant to be used with confirm_with_info."""
Qstr::MP_QSTR_confirm_more => obj_fn_kw!(0, new_confirm_more).as_obj(), Qstr::MP_QSTR_confirm_more => obj_fn_kw!(0, new_confirm_more).as_obj(),
@ -1923,7 +1983,7 @@ pub static mp_module_trezorui2: Module = obj_module! {
/// *, /// *,
/// max_rounds: str, /// max_rounds: str,
/// max_feerate: str, /// max_feerate: str,
/// ) -> object: /// ) -> LayoutObj[UiResult]:
/// """Confirm coinjoin authorization.""" /// """Confirm coinjoin authorization."""
Qstr::MP_QSTR_confirm_coinjoin => obj_fn_kw!(0, new_confirm_coinjoin).as_obj(), Qstr::MP_QSTR_confirm_coinjoin => obj_fn_kw!(0, new_confirm_coinjoin).as_obj(),
@ -1933,7 +1993,7 @@ pub static mp_module_trezorui2: Module = obj_module! {
/// subprompt: str, /// subprompt: str,
/// allow_cancel: bool = True, /// allow_cancel: bool = True,
/// wrong_pin: bool = False, /// wrong_pin: bool = False,
/// ) -> str | object: /// ) -> LayoutObj[str | UiResult]:
/// """Request pin on device.""" /// """Request pin on device."""
Qstr::MP_QSTR_request_pin => obj_fn_kw!(0, new_request_pin).as_obj(), Qstr::MP_QSTR_request_pin => obj_fn_kw!(0, new_request_pin).as_obj(),
@ -1941,7 +2001,7 @@ pub static mp_module_trezorui2: Module = obj_module! {
/// *, /// *,
/// prompt: str, /// prompt: str,
/// max_len: int, /// max_len: int,
/// ) -> str | object: /// ) -> LayoutObj[str | UiResult]:
/// """Passphrase input keyboard.""" /// """Passphrase input keyboard."""
Qstr::MP_QSTR_request_passphrase => obj_fn_kw!(0, new_request_passphrase).as_obj(), Qstr::MP_QSTR_request_passphrase => obj_fn_kw!(0, new_request_passphrase).as_obj(),
@ -1950,7 +2010,7 @@ pub static mp_module_trezorui2: Module = obj_module! {
/// prompt: str, /// prompt: str,
/// prefill_word: str, /// prefill_word: str,
/// can_go_back: bool, /// can_go_back: bool,
/// ) -> str: /// ) -> LayoutObj[str]:
/// """BIP39 word input keyboard.""" /// """BIP39 word input keyboard."""
Qstr::MP_QSTR_request_bip39 => obj_fn_kw!(0, new_request_bip39).as_obj(), Qstr::MP_QSTR_request_bip39 => obj_fn_kw!(0, new_request_bip39).as_obj(),
@ -1959,7 +2019,7 @@ pub static mp_module_trezorui2: Module = obj_module! {
/// prompt: str, /// prompt: str,
/// prefill_word: str, /// prefill_word: str,
/// can_go_back: bool, /// can_go_back: bool,
/// ) -> str: /// ) -> LayoutObj[str]:
/// """SLIP39 word input keyboard.""" /// """SLIP39 word input keyboard."""
Qstr::MP_QSTR_request_slip39 => obj_fn_kw!(0, new_request_slip39).as_obj(), Qstr::MP_QSTR_request_slip39 => obj_fn_kw!(0, new_request_slip39).as_obj(),
@ -1968,7 +2028,7 @@ pub static mp_module_trezorui2: Module = obj_module! {
/// title: str, /// title: str,
/// description: str, /// description: str,
/// words: Iterable[str], /// words: Iterable[str],
/// ) -> int: /// ) -> LayoutObj[int]:
/// """Select mnemonic word from three possibilities - seed check after backup. The /// """Select mnemonic word from three possibilities - seed check after backup. The
/// iterable must be of exact size. Returns index in range `0..3`.""" /// iterable must be of exact size. Returns index in range `0..3`."""
Qstr::MP_QSTR_select_word => obj_fn_kw!(0, new_select_word).as_obj(), Qstr::MP_QSTR_select_word => obj_fn_kw!(0, new_select_word).as_obj(),
@ -1977,7 +2037,7 @@ pub static mp_module_trezorui2: Module = obj_module! {
/// *, /// *,
/// title: str, /// title: str,
/// pages: Iterable[str], /// pages: Iterable[str],
/// ) -> object: /// ) -> LayoutObj[UiResult]:
/// """Show mnemonic for backup. Expects the words pre-divided into individual pages.""" /// """Show mnemonic for backup. Expects the words pre-divided into individual pages."""
Qstr::MP_QSTR_show_share_words => obj_fn_kw!(0, new_show_share_words).as_obj(), Qstr::MP_QSTR_show_share_words => obj_fn_kw!(0, new_show_share_words).as_obj(),
@ -1988,7 +2048,7 @@ pub static mp_module_trezorui2: Module = obj_module! {
/// min_count: int, /// min_count: int,
/// max_count: int, /// max_count: int,
/// description: Callable[[int], str] | None = None, /// description: Callable[[int], str] | None = None,
/// ) -> object: /// ) -> LayoutObj[tuple[UiResult, int]]:
/// """Number input with + and - buttons, description, and info button.""" /// """Number input with + and - buttons, description, and info button."""
Qstr::MP_QSTR_request_number => obj_fn_kw!(0, new_request_number).as_obj(), Qstr::MP_QSTR_request_number => obj_fn_kw!(0, new_request_number).as_obj(),
@ -1998,7 +2058,7 @@ pub static mp_module_trezorui2: Module = obj_module! {
/// items: Iterable[str], /// items: Iterable[str],
/// active: int, /// active: int,
/// button: str, /// button: str,
/// ) -> object: /// ) -> LayoutObj[UiResult]:
/// """Checklist of backup steps. Active index is highlighted, previous items have check /// """Checklist of backup steps. Active index is highlighted, previous items have check
/// mark next to them.""" /// mark next to them."""
Qstr::MP_QSTR_show_checklist => obj_fn_kw!(0, new_show_checklist).as_obj(), Qstr::MP_QSTR_show_checklist => obj_fn_kw!(0, new_show_checklist).as_obj(),
@ -2010,28 +2070,28 @@ pub static mp_module_trezorui2: Module = obj_module! {
/// button: str, /// button: str,
/// dry_run: bool, /// dry_run: bool,
/// info_button: bool = False, /// info_button: bool = False,
/// ) -> object: /// ) -> LayoutObj[UiResult]:
/// """Device recovery homescreen.""" /// """Device recovery homescreen."""
Qstr::MP_QSTR_confirm_recovery => obj_fn_kw!(0, new_confirm_recovery).as_obj(), Qstr::MP_QSTR_confirm_recovery => obj_fn_kw!(0, new_confirm_recovery).as_obj(),
/// def select_word_count( /// def select_word_count(
/// *, /// *,
/// dry_run: bool, /// dry_run: bool,
/// ) -> int | str: # TT returns int /// ) -> LayoutObj[int | str]: # TT returns int
/// """Select mnemonic word count from (12, 18, 20, 24, 33).""" /// """Select mnemonic word count from (12, 18, 20, 24, 33)."""
Qstr::MP_QSTR_select_word_count => obj_fn_kw!(0, new_select_word_count).as_obj(), Qstr::MP_QSTR_select_word_count => obj_fn_kw!(0, new_select_word_count).as_obj(),
/// def show_group_share_success( /// def show_group_share_success(
/// *, /// *,
/// lines: Iterable[str] /// lines: Iterable[str]
/// ) -> int: /// ) -> LayoutObj[UiResult]:
/// """Shown after successfully finishing a group.""" /// """Shown after successfully finishing a group."""
Qstr::MP_QSTR_show_group_share_success => obj_fn_kw!(0, new_show_group_share_success).as_obj(), Qstr::MP_QSTR_show_group_share_success => obj_fn_kw!(0, new_show_group_share_success).as_obj(),
/// def show_remaining_shares( /// def show_remaining_shares(
/// *, /// *,
/// pages: Iterable[tuple[str, str]], /// pages: Iterable[tuple[str, str]],
/// ) -> int: /// ) -> LayoutObj[UiResult]:
/// """Shows SLIP39 state after info button is pressed on `confirm_recovery`.""" /// """Shows SLIP39 state after info button is pressed on `confirm_recovery`."""
Qstr::MP_QSTR_show_remaining_shares => obj_fn_kw!(0, new_show_remaining_shares).as_obj(), Qstr::MP_QSTR_show_remaining_shares => obj_fn_kw!(0, new_show_remaining_shares).as_obj(),
@ -2040,7 +2100,7 @@ pub static mp_module_trezorui2: Module = obj_module! {
/// title: str, /// title: str,
/// indeterminate: bool = False, /// indeterminate: bool = False,
/// description: str = "", /// description: str = "",
/// ) -> object: /// ) -> LayoutObj[UiResult]:
/// """Show progress loader. Please note that the number of lines reserved on screen for /// """Show progress loader. Please note that the number of lines reserved on screen for
/// description is determined at construction time. If you want multiline descriptions /// description is determined at construction time. If you want multiline descriptions
/// make sure the initial description has at least that amount of lines.""" /// make sure the initial description has at least that amount of lines."""
@ -2052,7 +2112,7 @@ pub static mp_module_trezorui2: Module = obj_module! {
/// indeterminate: bool = False, /// indeterminate: bool = False,
/// time_ms: int = 0, /// time_ms: int = 0,
/// skip_first_paint: bool = False, /// skip_first_paint: bool = False,
/// ) -> object: /// ) -> LayoutObj[UiResult]:
/// """Show progress loader for coinjoin. Returns CANCELLED after a specified time when /// """Show progress loader for coinjoin. Returns CANCELLED after a specified time when
/// time_ms timeout is passed.""" /// time_ms timeout is passed."""
Qstr::MP_QSTR_show_progress_coinjoin => obj_fn_kw!(0, new_show_progress_coinjoin).as_obj(), Qstr::MP_QSTR_show_progress_coinjoin => obj_fn_kw!(0, new_show_progress_coinjoin).as_obj(),
@ -2064,7 +2124,7 @@ pub static mp_module_trezorui2: Module = obj_module! {
/// notification: str | None, /// notification: str | None,
/// notification_level: int = 0, /// notification_level: int = 0,
/// skip_first_paint: bool, /// skip_first_paint: bool,
/// ) -> CANCELLED: /// ) -> LayoutObj[UiResult]:
/// """Idle homescreen.""" /// """Idle homescreen."""
Qstr::MP_QSTR_show_homescreen => obj_fn_kw!(0, new_show_homescreen).as_obj(), Qstr::MP_QSTR_show_homescreen => obj_fn_kw!(0, new_show_homescreen).as_obj(),
@ -2074,7 +2134,7 @@ pub static mp_module_trezorui2: Module = obj_module! {
/// bootscreen: bool, /// bootscreen: bool,
/// skip_first_paint: bool, /// skip_first_paint: bool,
/// coinjoin_authorized: bool = False, /// coinjoin_authorized: bool = False,
/// ) -> CANCELLED: /// ) -> LayoutObj[UiResult]:
/// """Homescreen for locked device.""" /// """Homescreen for locked device."""
Qstr::MP_QSTR_show_lockscreen => obj_fn_kw!(0, new_show_lockscreen).as_obj(), Qstr::MP_QSTR_show_lockscreen => obj_fn_kw!(0, new_show_lockscreen).as_obj(),
@ -2082,11 +2142,11 @@ pub static mp_module_trezorui2: Module = obj_module! {
/// *, /// *,
/// description: str, /// description: str,
/// fingerprint: str, /// fingerprint: str,
/// ) -> None: /// ) -> LayoutObj[UiResult]:
/// """Ask whether to update firmware, optionally show fingerprint. Shared with bootloader.""" /// """Ask whether to update firmware, optionally show fingerprint. Shared with bootloader."""
Qstr::MP_QSTR_confirm_firmware_update => obj_fn_kw!(0, new_confirm_firmware_update).as_obj(), Qstr::MP_QSTR_confirm_firmware_update => obj_fn_kw!(0, new_confirm_firmware_update).as_obj(),
/// def show_wait_text(/, message: str) -> None: /// def show_wait_text(message: str, /) -> LayoutObj[None]:
/// """Show single-line text in the middle of the screen.""" /// """Show single-line text in the middle of the screen."""
Qstr::MP_QSTR_show_wait_text => obj_fn_1!(new_show_wait_text).as_obj(), Qstr::MP_QSTR_show_wait_text => obj_fn_1!(new_show_wait_text).as_obj(),
}; };

@ -9,9 +9,9 @@ use crate::trezorhal::bitmap::{BitmapView, Dma2d};
use static_alloc::Bump; use static_alloc::Bump;
pub fn render_on_display<F>(clip: Option<Rect>, bg_color: Option<Color>, mut func: F) pub fn render_on_display<'a, F>(clip: Option<Rect>, bg_color: Option<Color>, func: F)
where where
F: FnMut(&mut ProgressiveRenderer<Bump<[u8; 40 * 1024]>, DisplayModelMercury>), F: FnOnce(&mut ProgressiveRenderer<'_, 'a, Bump<[u8; 40 * 1024]>, DisplayModelMercury>),
{ {
#[link_section = ".no_dma_buffers"] #[link_section = ".no_dma_buffers"]
static mut BUMP_A: Bump<[u8; 40 * 1024]> = Bump::uninit(); static mut BUMP_A: Bump<[u8; 40 * 1024]> = Bump::uninit();
@ -22,6 +22,9 @@ where
let bump_a = unsafe { &mut *core::ptr::addr_of_mut!(BUMP_A) }; let bump_a = unsafe { &mut *core::ptr::addr_of_mut!(BUMP_A) };
let bump_b = unsafe { &mut *core::ptr::addr_of_mut!(BUMP_B) }; let bump_b = unsafe { &mut *core::ptr::addr_of_mut!(BUMP_B) };
{ {
bump_a.reset();
bump_b.reset();
let cache = DrawingCache::new(bump_a, bump_b); let cache = DrawingCache::new(bump_a, bump_b);
let mut canvas = DisplayModelMercury::acquire().unwrap(); let mut canvas = DisplayModelMercury::acquire().unwrap();
@ -29,14 +32,12 @@ where
canvas.set_viewport(Viewport::new(clip)); canvas.set_viewport(Viewport::new(clip));
} }
let mut target = ProgressiveRenderer::new(&mut canvas, bg_color, &cache, bump_a, 30); let mut target = ProgressiveRenderer::new(&mut canvas, bg_color, &cache, bump_a, 45);
func(&mut target); func(&mut target);
target.render(16); target.render(16);
} }
bump_a.reset();
bump_b.reset();
} }
pub struct DisplayModelMercury { pub struct DisplayModelMercury {
@ -65,7 +66,7 @@ impl BasicCanvas for DisplayModelMercury {
self.size self.size
} }
fn fill_rect(&mut self, r: Rect, color: Color) { fn fill_rect(&mut self, r: Rect, color: Color, _alpha: u8) {
let r = r.translate(self.viewport.origin); let r = r.translate(self.viewport.origin);
Dma2d::wnd565_fill(r, self.viewport.clip, color); Dma2d::wnd565_fill(r, self.viewport.clip, color);
} }

@ -1,4 +1,60 @@
from typing import * from typing import *
from trezor import utils
T = TypeVar("T")
# rust/src/ui/model_mercury/layout.rs
class LayoutObj(Generic[T]):
"""Representation of a Rust-based layout object.
see `trezor::ui::layout::obj::LayoutObj`.
"""
def attach_timer_fn(self, fn: Callable[[int, int], None]) -> None:
"""Attach a timer setter function.
The layout object can call the timer setter with two arguments,
`token` and `deadline`. When `deadline` is reached, the layout object
expects a callback to `self.timer(token)`.
"""
if utils.USE_TOUCH:
def touch_event(self, event: int, x: int, y: int) -> T | None:
"""Receive a touch event `event` at coordinates `x`, `y`."""
if utils.USE_BUTTON:
def button_event(self, event: int, button: int) -> T | None:
"""Receive a button event `event` for button `button`."""
def progress_event(self, value: int, description: str) -> T | None:
"""Receive a progress event."""
def usb_event(self, connected: bool) -> T | None:
"""Receive a USB connect/disconnect event."""
def timer(self, token: int) -> T | None:
"""Callback for the timer set by `attach_timer_fn`.
This function should be called by the executor after the corresponding
deadline is reached.
"""
def paint(self) -> bool:
"""Paint the layout object on screen.
Will only paint updated parts of the layout as required.
Returns True if any painting actually happened.
"""
def request_complete_repaint(self) -> None:
"""Request a complete repaint of the screen.
Does not repaint the screen, a subsequent call to `paint()` is required.
"""
if __debug__:
def trace(self, tracer: Callable[[str], None]) -> None:
"""Generate a JSON trace of the layout object.
The JSON can be emitted as a sequence of calls to `tracer`, each of
which is not necessarily a valid JSON chunk. The caller must
reassemble the chunks to get a sensible result.
"""
def bounds(self) -> None:
"""Paint bounds of individual components on screen."""
def page_count(self) -> int:
"""Return the number of pages in the layout object."""
# rust/src/ui/model_mercury/layout.rs
class UiResult:
"""Result of a UI operation."""
pass
CONFIRMED: UiResult CONFIRMED: UiResult
CANCELLED: UiResult CANCELLED: UiResult
INFO: UiResult INFO: UiResult
@ -25,7 +81,7 @@ def confirm_action(
hold: bool = False, hold: bool = False,
hold_danger: bool = False, hold_danger: bool = False,
reverse: bool = False, reverse: bool = False,
) -> object: ) -> LayoutObj[UiResult]:
"""Confirm action.""" """Confirm action."""
@ -35,7 +91,7 @@ def confirm_emphasized(
title: str, title: str,
items: Iterable[str | tuple[bool, str]], items: Iterable[str | tuple[bool, str]],
verb: str | None = None, verb: str | None = None,
) -> object: ) -> LayoutObj[UiResult]:
"""Confirm formatted text that has been pre-split in python. For tuples """Confirm formatted text that has been pre-split in python. For tuples
the first component is a bool indicating whether this part is emphasized.""" the first component is a bool indicating whether this part is emphasized."""
@ -45,7 +101,7 @@ def confirm_homescreen(
*, *,
title: str, title: str,
image: bytes, image: bytes,
) -> object: ) -> LayoutObj[UiResult]:
"""Confirm homescreen.""" """Confirm homescreen."""
@ -60,7 +116,7 @@ def confirm_blob(
verb_cancel: str | None = None, verb_cancel: str | None = None,
hold: bool = False, hold: bool = False,
chunkify: bool = False, chunkify: bool = False,
) -> object: ) -> LayoutObj[UiResult]:
"""Confirm byte sequence data.""" """Confirm byte sequence data."""
@ -73,7 +129,7 @@ def confirm_address(
verb: str | None = "CONFIRM", verb: str | None = "CONFIRM",
extra: str | None, extra: str | None,
chunkify: bool = False, chunkify: bool = False,
) -> object: ) -> LayoutObj[UiResult]:
"""Confirm address. Similar to `confirm_blob` but has corner info button """Confirm address. Similar to `confirm_blob` but has corner info button
and allows left swipe which does the same thing as the button.""" and allows left swipe which does the same thing as the button."""
@ -84,7 +140,7 @@ def confirm_properties(
title: str, title: str,
items: list[tuple[str | None, str | bytes | None, bool]], items: list[tuple[str | None, str | bytes | None, bool]],
hold: bool = False, hold: bool = False,
) -> object: ) -> LayoutObj[UiResult]:
"""Confirm list of key-value pairs. The third component in the tuple should be True if """Confirm list of key-value pairs. The third component in the tuple should be True if
the value is to be rendered as binary with monospace font, False otherwise.""" the value is to be rendered as binary with monospace font, False otherwise."""
@ -94,7 +150,7 @@ def confirm_reset_device(
*, *,
title: str, title: str,
button: str, button: str,
) -> object: ) -> LayoutObj[UiResult]:
"""Confirm TOS before device setup.""" """Confirm TOS before device setup."""
@ -108,7 +164,7 @@ def show_address_details(
account: str | None, account: str | None,
path: str | None, path: str | None,
xpubs: list[tuple[str, str]], xpubs: list[tuple[str, str]],
) -> object: ) -> LayoutObj[UiResult]:
"""Show address details - QR code, account, path, cosigner xpubs.""" """Show address details - QR code, account, path, cosigner xpubs."""
@ -119,7 +175,7 @@ def show_info_with_cancel(
items: Iterable[Tuple[str, str]], items: Iterable[Tuple[str, str]],
horizontal: bool = False, horizontal: bool = False,
chunkify: bool = False, chunkify: bool = False,
) -> object: ) -> LayoutObj[UiResult]:
"""Show metadata for outgoing transaction.""" """Show metadata for outgoing transaction."""
@ -136,7 +192,7 @@ def confirm_value(
hold: bool = False, hold: bool = False,
chunkify: bool = False, chunkify: bool = False,
text_mono: bool = True, text_mono: bool = True,
) -> object: ) -> LayoutObj[UiResult]:
"""Confirm value. Merge of confirm_total and confirm_output.""" """Confirm value. Merge of confirm_total and confirm_output."""
@ -147,7 +203,7 @@ def confirm_total(
items: Iterable[tuple[str, str]], items: Iterable[tuple[str, str]],
info_button: bool = False, info_button: bool = False,
cancel_arrow: bool = False, cancel_arrow: bool = False,
) -> object: ) -> LayoutObj[UiResult]:
"""Transaction summary. Always hold to confirm.""" """Transaction summary. Always hold to confirm."""
@ -157,7 +213,7 @@ def confirm_modify_output(
sign: int, sign: int,
amount_change: str, amount_change: str,
amount_new: str, amount_new: str,
) -> object: ) -> LayoutObj[UiResult]:
"""Decrease or increase output amount.""" """Decrease or increase output amount."""
@ -169,7 +225,7 @@ def confirm_modify_fee(
user_fee_change: str, user_fee_change: str,
total_fee_new: str, total_fee_new: str,
fee_rate_amount: str | None, # ignored fee_rate_amount: str | None, # ignored
) -> object: ) -> LayoutObj[UiResult]:
"""Decrease or increase transaction fee.""" """Decrease or increase transaction fee."""
@ -180,7 +236,7 @@ def confirm_fido(
app_name: str, app_name: str,
icon_name: str | None, icon_name: str | None,
accounts: list[str | None], accounts: list[str | None],
) -> int | object: ) -> LayoutObj[int | UiResult]:
"""FIDO confirmation. """FIDO confirmation.
Returns page index in case of confirmation and CANCELLED otherwise. Returns page index in case of confirmation and CANCELLED otherwise.
""" """
@ -194,7 +250,7 @@ def show_error(
description: str = "", description: str = "",
allow_cancel: bool = False, allow_cancel: bool = False,
time_ms: int = 0, time_ms: int = 0,
) -> object: ) -> LayoutObj[UiResult]:
"""Error modal. No buttons shown when `button` is empty string.""" """Error modal. No buttons shown when `button` is empty string."""
@ -207,7 +263,7 @@ def show_warning(
description: str = "", description: str = "",
allow_cancel: bool = False, allow_cancel: bool = False,
time_ms: int = 0, time_ms: int = 0,
) -> object: ) -> LayoutObj[UiResult]:
"""Warning modal. No buttons shown when `button` is empty string.""" """Warning modal. No buttons shown when `button` is empty string."""
@ -219,7 +275,7 @@ def show_success(
description: str = "", description: str = "",
allow_cancel: bool = False, allow_cancel: bool = False,
time_ms: int = 0, time_ms: int = 0,
) -> object: ) -> LayoutObj[UiResult]:
"""Success modal. No buttons shown when `button` is empty string.""" """Success modal. No buttons shown when `button` is empty string."""
@ -231,12 +287,12 @@ def show_info(
description: str = "", description: str = "",
allow_cancel: bool = False, allow_cancel: bool = False,
time_ms: int = 0, time_ms: int = 0,
) -> object: ) -> LayoutObj[UiResult]:
"""Info modal. No buttons shown when `button` is empty string.""" """Info modal. No buttons shown when `button` is empty string."""
# rust/src/ui/model_mercury/layout.rs # rust/src/ui/model_mercury/layout.rs
def show_mismatch(*, title: str) -> object: def show_mismatch(*, title: str) -> LayoutObj[UiResult]:
"""Warning modal, receiving address mismatch.""" """Warning modal, receiving address mismatch."""
@ -246,7 +302,7 @@ def show_simple(
title: str | None, title: str | None,
description: str = "", description: str = "",
button: str = "", button: str = "",
) -> object: ) -> LayoutObj[UiResult]:
"""Simple dialog with text and one button.""" """Simple dialog with text and one button."""
@ -257,7 +313,7 @@ def confirm_with_info(
button: str, button: str,
info_button: str, info_button: str,
items: Iterable[tuple[int, str]], items: Iterable[tuple[int, str]],
) -> object: ) -> LayoutObj[UiResult]:
"""Confirm given items but with third button. Always single page """Confirm given items but with third button. Always single page
without scrolling.""" without scrolling."""
@ -268,7 +324,7 @@ def confirm_more(
title: str, title: str,
button: str, button: str,
items: Iterable[tuple[int, str]], items: Iterable[tuple[int, str]],
) -> object: ) -> LayoutObj[UiResult]:
"""Confirm long content with the possibility to go back from any page. """Confirm long content with the possibility to go back from any page.
Meant to be used with confirm_with_info.""" Meant to be used with confirm_with_info."""
@ -278,7 +334,7 @@ def confirm_coinjoin(
*, *,
max_rounds: str, max_rounds: str,
max_feerate: str, max_feerate: str,
) -> object: ) -> LayoutObj[UiResult]:
"""Confirm coinjoin authorization.""" """Confirm coinjoin authorization."""
@ -289,7 +345,7 @@ def request_pin(
subprompt: str, subprompt: str,
allow_cancel: bool = True, allow_cancel: bool = True,
wrong_pin: bool = False, wrong_pin: bool = False,
) -> str | object: ) -> LayoutObj[str | UiResult]:
"""Request pin on device.""" """Request pin on device."""
@ -298,7 +354,7 @@ def request_passphrase(
*, *,
prompt: str, prompt: str,
max_len: int, max_len: int,
) -> str | object: ) -> LayoutObj[str | UiResult]:
"""Passphrase input keyboard.""" """Passphrase input keyboard."""
@ -308,7 +364,7 @@ def request_bip39(
prompt: str, prompt: str,
prefill_word: str, prefill_word: str,
can_go_back: bool, can_go_back: bool,
) -> str: ) -> LayoutObj[str]:
"""BIP39 word input keyboard.""" """BIP39 word input keyboard."""
@ -318,7 +374,7 @@ def request_slip39(
prompt: str, prompt: str,
prefill_word: str, prefill_word: str,
can_go_back: bool, can_go_back: bool,
) -> str: ) -> LayoutObj[str]:
"""SLIP39 word input keyboard.""" """SLIP39 word input keyboard."""
@ -328,7 +384,7 @@ def select_word(
title: str, title: str,
description: str, description: str,
words: Iterable[str], words: Iterable[str],
) -> int: ) -> LayoutObj[int]:
"""Select mnemonic word from three possibilities - seed check after backup. The """Select mnemonic word from three possibilities - seed check after backup. The
iterable must be of exact size. Returns index in range `0..3`.""" iterable must be of exact size. Returns index in range `0..3`."""
@ -338,7 +394,7 @@ def show_share_words(
*, *,
title: str, title: str,
pages: Iterable[str], pages: Iterable[str],
) -> object: ) -> LayoutObj[UiResult]:
"""Show mnemonic for backup. Expects the words pre-divided into individual pages.""" """Show mnemonic for backup. Expects the words pre-divided into individual pages."""
@ -350,7 +406,7 @@ def request_number(
min_count: int, min_count: int,
max_count: int, max_count: int,
description: Callable[[int], str] | None = None, description: Callable[[int], str] | None = None,
) -> object: ) -> LayoutObj[tuple[UiResult, int]]:
"""Number input with + and - buttons, description, and info button.""" """Number input with + and - buttons, description, and info button."""
@ -361,7 +417,7 @@ def show_checklist(
items: Iterable[str], items: Iterable[str],
active: int, active: int,
button: str, button: str,
) -> object: ) -> LayoutObj[UiResult]:
"""Checklist of backup steps. Active index is highlighted, previous items have check """Checklist of backup steps. Active index is highlighted, previous items have check
mark next to them.""" mark next to them."""
@ -374,7 +430,7 @@ def confirm_recovery(
button: str, button: str,
dry_run: bool, dry_run: bool,
info_button: bool = False, info_button: bool = False,
) -> object: ) -> LayoutObj[UiResult]:
"""Device recovery homescreen.""" """Device recovery homescreen."""
@ -382,7 +438,7 @@ def confirm_recovery(
def select_word_count( def select_word_count(
*, *,
dry_run: bool, dry_run: bool,
) -> int | str: # TT returns int ) -> LayoutObj[int | str]: # TT returns int
"""Select mnemonic word count from (12, 18, 20, 24, 33).""" """Select mnemonic word count from (12, 18, 20, 24, 33)."""
@ -390,7 +446,7 @@ def select_word_count(
def show_group_share_success( def show_group_share_success(
*, *,
lines: Iterable[str] lines: Iterable[str]
) -> int: ) -> LayoutObj[UiResult]:
"""Shown after successfully finishing a group.""" """Shown after successfully finishing a group."""
@ -398,7 +454,7 @@ def show_group_share_success(
def show_remaining_shares( def show_remaining_shares(
*, *,
pages: Iterable[tuple[str, str]], pages: Iterable[tuple[str, str]],
) -> int: ) -> LayoutObj[UiResult]:
"""Shows SLIP39 state after info button is pressed on `confirm_recovery`.""" """Shows SLIP39 state after info button is pressed on `confirm_recovery`."""
@ -408,7 +464,7 @@ def show_progress(
title: str, title: str,
indeterminate: bool = False, indeterminate: bool = False,
description: str = "", description: str = "",
) -> object: ) -> LayoutObj[UiResult]:
"""Show progress loader. Please note that the number of lines reserved on screen for """Show progress loader. Please note that the number of lines reserved on screen for
description is determined at construction time. If you want multiline descriptions description is determined at construction time. If you want multiline descriptions
make sure the initial description has at least that amount of lines.""" make sure the initial description has at least that amount of lines."""
@ -421,7 +477,7 @@ def show_progress_coinjoin(
indeterminate: bool = False, indeterminate: bool = False,
time_ms: int = 0, time_ms: int = 0,
skip_first_paint: bool = False, skip_first_paint: bool = False,
) -> object: ) -> LayoutObj[UiResult]:
"""Show progress loader for coinjoin. Returns CANCELLED after a specified time when """Show progress loader for coinjoin. Returns CANCELLED after a specified time when
time_ms timeout is passed.""" time_ms timeout is passed."""
@ -434,7 +490,7 @@ def show_homescreen(
notification: str | None, notification: str | None,
notification_level: int = 0, notification_level: int = 0,
skip_first_paint: bool, skip_first_paint: bool,
) -> CANCELLED: ) -> LayoutObj[UiResult]:
"""Idle homescreen.""" """Idle homescreen."""
@ -445,7 +501,7 @@ def show_lockscreen(
bootscreen: bool, bootscreen: bool,
skip_first_paint: bool, skip_first_paint: bool,
coinjoin_authorized: bool = False, coinjoin_authorized: bool = False,
) -> CANCELLED: ) -> LayoutObj[UiResult]:
"""Homescreen for locked device.""" """Homescreen for locked device."""
@ -454,16 +510,16 @@ def confirm_firmware_update(
*, *,
description: str, description: str,
fingerprint: str, fingerprint: str,
) -> None: ) -> LayoutObj[UiResult]:
"""Ask whether to update firmware, optionally show fingerprint. Shared with bootloader.""" """Ask whether to update firmware, optionally show fingerprint. Shared with bootloader."""
# rust/src/ui/model_mercury/layout.rs # rust/src/ui/model_mercury/layout.rs
def show_wait_text(/, message: str) -> None: def show_wait_text(message: str, /) -> LayoutObj[None]:
"""Show single-line text in the middle of the screen.""" """Show single-line text in the middle of the screen."""
CONFIRMED: object CONFIRMED: UiResult
CANCELLED: object CANCELLED: UiResult
INFO: object INFO: UiResult
# rust/src/ui/model_tr/layout.rs # rust/src/ui/model_tr/layout.rs

@ -655,11 +655,16 @@ async def confirm_output(
return return
async def confirm_payment_request( async def should_show_payment_request_details(
recipient_name: str, recipient_name: str,
amount: str, amount: str,
memos: list[str], memos: list[str],
) -> bool: ) -> bool:
"""Return True if the user wants to show payment request details (they click a
special button) and False when the user wants to continue without showing details.
Raises ActionCancelled if the user cancels.
"""
result = await interact( result = await interact(
RustLayout( RustLayout(
trezorui2.confirm_with_info( trezorui2.confirm_with_info(
@ -674,12 +679,10 @@ async def confirm_payment_request(
ButtonRequestType.ConfirmOutput, ButtonRequestType.ConfirmOutput,
) )
# When user pressed INFO, returning False, which gets processed in higher function
# to differentiate it from CONFIRMED. Raising otherwise.
if result is CONFIRMED: if result is CONFIRMED:
return True
elif result is INFO:
return False return False
elif result is INFO:
return True
else: else:
raise ActionCancelled raise ActionCancelled

@ -371,6 +371,7 @@ def test_signmessage_pagination(client: Client, message: str):
@pytest.mark.skip_t1b1 @pytest.mark.skip_t1b1
@pytest.mark.skip_t2b1(reason="Different screen size") @pytest.mark.skip_t2b1(reason="Different screen size")
@pytest.mark.skip_t3t1(reason="Different fonts")
def test_signmessage_pagination_trailing_newline(client: Client): def test_signmessage_pagination_trailing_newline(client: Client):
message = "THIS\nMUST\nNOT\nBE\nPAGINATED\n" message = "THIS\nMUST\nNOT\nBE\nPAGINATED\n"
# The trailing newline must not cause a new paginated screen to appear. # The trailing newline must not cause a new paginated screen to appear.

@ -950,7 +950,7 @@ class InputFlowLockTimeDatetime(InputFlowBase):
def assert_func(self, debug: DebugLink, br: messages.ButtonRequest) -> None: def assert_func(self, debug: DebugLink, br: messages.ButtonRequest) -> None:
layout_text = get_text_possible_pagination(debug, br) layout_text = get_text_possible_pagination(debug, br)
TR.assert_in(layout_text, "bitcoin__locktime_set_to") TR.assert_in(layout_text, "bitcoin__locktime_set_to")
assert self.lock_time_str in layout_text assert self.lock_time_str.replace(' ', '') in layout_text.replace(' ', '')
def input_flow_tt(self) -> BRGeneratorType: def input_flow_tt(self) -> BRGeneratorType:
yield from lock_time_input_flow_tt(self.debug, self.assert_func) yield from lock_time_input_flow_tt(self.debug, self.assert_func)

Loading…
Cancel
Save