1
0
mirror of https://github.com/trezor/trezor-firmware.git synced 2025-07-04 05:42:34 +00:00

TR-core/rust: implement Coinjoin progress screen

This commit is contained in:
grdddj 2023-04-13 15:26:19 +02:00
parent cde8d29e51
commit e1252e9ee9
8 changed files with 230 additions and 85 deletions

View File

@ -7,9 +7,11 @@ pub mod toif;
use heapless::String; use heapless::String;
use core::fmt::Write;
use super::{ use super::{
constant, constant,
geometry::{Offset, Point, Rect}, geometry::{Alignment, Offset, Point, Rect},
}; };
#[cfg(feature = "dma2d")] #[cfg(feature = "dma2d")]
use crate::trezorhal::{ use crate::trezorhal::{
@ -853,50 +855,86 @@ pub fn paint_point(point: &Point, color: Color) {
} }
/// Draws longer multiline texts inside an area. /// Draws longer multiline texts inside an area.
/// Does not add any characters on the line boundaries. /// Splits lines on word boundaries/whitespace.
/// When a word is too long to fit one line, splitting
/// it on multiple lines with "-" at the line-ends.
/// ///
/// If it fits, returns the rest of the area. /// If it fits, returns the rest of the area.
/// If it does not fit, returns `None`. /// If it does not fit, returns `None`.
pub fn text_multiline( pub fn text_multiline_split_words(
area: Rect, area: Rect,
text: &str, text: &str,
font: Font, font: Font,
fg_color: Color, fg_color: Color,
bg_color: Color, bg_color: Color,
alignment: Alignment,
) -> Option<Rect> { ) -> Option<Rect> {
let line_height = font.line_height(); let line_height = font.line_height();
let characters_overall = text.chars().count();
let mut taken_from_top = 0; let mut taken_from_top = 0;
let mut characters_drawn = 0; let mut chars_processed = 0;
let mut text_iter = text.split_whitespace();
let mut word_from_prev_line = None;
'lines: loop { 'lines: loop {
let baseline = area.top_left() + Offset::y(line_height + taken_from_top); let baseline_left = area.top_left() + Offset::y(line_height + taken_from_top);
if !area.contains(baseline) { if !area.contains(baseline_left) {
// The whole area was consumed. // The whole area was consumed.
return None; return None;
} }
let mut line_text: String<50> = String::new(); let mut line_text: String<100> = String::new();
'characters: loop {
if let Some(character) = text.chars().nth(characters_drawn) { 'words: while let Some(word) = word_from_prev_line.take().or_else(|| text_iter.next()) {
characters_drawn += 1; let prev_line_text_len = line_text.len();
if character == '\n' { if !line_text.is_empty() {
// The line is forced to end. // Putting spaces in between words.
break 'characters; unwrap!(line_text.push(' '));
} }
unwrap!(line_text.push(character)); if write!(&mut line_text, "{}", word).is_err() {
// We have a word/line longer than 100 chars.
// Add just 50 characters, that is enough for this line.
unwrap!(write!(&mut line_text, "{}", &word[..50]));
}
if font.text_width(&line_text) <= area.width() {
chars_processed += word.chars().count() + 1;
} else { } else {
// No more characters to draw. // The word does not fit on the line anymore.
break 'characters; // Word can be longer than the whole line - in that case splitting it to more
} // lines
if font.text_width(&line_text) > area.width() { if prev_line_text_len == 0 {
// Cannot fit on the line anymore. for (idx, _) in word.char_indices() {
line_text.pop(); if font.text_width(&word[..idx]) > area.width() {
characters_drawn -= 1; let split_idx = idx - 1;
break 'characters; let chars_fitting_this_line = split_idx - 1; // accounting for the hyphen we will add
line_text = String::from(&word[..chars_fitting_this_line]);
unwrap!(line_text.push('-'));
chars_processed += chars_fitting_this_line;
word_from_prev_line = Some(&word[chars_fitting_this_line..]);
break;
}
}
} else {
line_text.truncate(prev_line_text_len);
word_from_prev_line = Some(word);
}
break 'words;
}
}
match alignment {
Alignment::Start => text_left(baseline_left, &line_text, font, fg_color, bg_color),
Alignment::Center => {
let baseline_center = baseline_left + Offset::x(area.width() / 2);
text_center(baseline_center, &line_text, font, fg_color, bg_color)
}
Alignment::End => {
let baseline_right = baseline_left + Offset::x(area.width());
text_right(baseline_right, &line_text, font, fg_color, bg_color)
} }
} }
text_left(baseline, &line_text, font, fg_color, bg_color);
taken_from_top += line_height; taken_from_top += line_height;
if characters_drawn == characters_overall { if chars_processed >= text.chars().count() {
// No more lines to draw. // No more lines to draw.
break 'lines; break 'lines;
} }

View File

@ -0,0 +1,83 @@
use crate::{
micropython::buffer::StrBuffer,
ui::{
component::{base::Never, Component, Event, EventCtx},
display::{text_multiline_split_words, Font},
geometry::{Alignment, Rect},
model_tr::theme,
},
};
const HEADER: &str = "COINJOIN IN PROGRESS";
const FOOTER: &str = "Don't disconnect your Trezor";
pub struct CoinJoinProgress {
text: StrBuffer,
area: Rect,
}
impl CoinJoinProgress {
pub fn new(text: StrBuffer, _indeterminate: bool) -> Self {
Self {
text,
area: Rect::zero(),
}
}
}
impl Component for CoinJoinProgress {
type Msg = Never;
fn place(&mut self, bounds: Rect) -> Rect {
self.area = bounds;
bounds
}
fn event(&mut self, _ctx: &mut EventCtx, _event: Event) -> Option<Self::Msg> {
None
}
fn paint(&mut self) {
// Trying to paint all three parts into the area, stopping if any of them
// doesn't fit.
let mut possible_rest = text_multiline_split_words(
self.area,
HEADER,
Font::NORMAL,
theme::FG,
theme::BG,
Alignment::Center,
);
if let Some(rest) = possible_rest {
possible_rest = text_multiline_split_words(
rest,
self.text.as_ref(),
Font::MONO,
theme::FG,
theme::BG,
Alignment::Center,
);
} else {
return;
}
if let Some(rest) = possible_rest {
text_multiline_split_words(
rest,
FOOTER,
Font::BOLD,
theme::FG,
theme::BG,
Alignment::Center,
);
}
}
}
#[cfg(feature = "ui_debug")]
impl crate::trace::Trace for CoinJoinProgress {
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.open("CoinJoinProgress");
t.string(self.text.as_ref());
t.close();
}
}

View File

@ -2,6 +2,7 @@ mod address_details;
mod button; mod button;
mod button_controller; mod button_controller;
mod changing_text; mod changing_text;
mod coinjoin_progress;
mod common; mod common;
mod flow; mod flow;
mod flow_pages; mod flow_pages;
@ -34,6 +35,7 @@ pub use hold_to_confirm::{HoldToConfirm, HoldToConfirmMsg};
pub use button_controller::{ButtonController, ButtonControllerMsg}; pub use button_controller::{ButtonController, ButtonControllerMsg};
pub use changing_text::ChangingTextLine; pub use changing_text::ChangingTextLine;
pub use coinjoin_progress::CoinJoinProgress;
pub use flow::{Flow, FlowMsg}; pub use flow::{Flow, FlowMsg};
pub use flow_pages::{FlowPages, Page}; pub use flow_pages::{FlowPages, Page};
pub use frame::{Frame, ScrollableContent, ScrollableFrame}; pub use frame::{Frame, ScrollableContent, ScrollableFrame};

View File

@ -2,8 +2,8 @@ use crate::{
micropython::buffer::StrBuffer, micropython::buffer::StrBuffer,
ui::{ ui::{
component::{Component, Event, EventCtx, Never, Paginate}, component::{Component, Event, EventCtx, Never, Paginate},
display::{text_multiline, Font}, display::{text_multiline_split_words, Font},
geometry::{Offset, Rect}, geometry::{Alignment, Offset, Rect},
model_tr::theme, model_tr::theme,
}, },
}; };
@ -77,40 +77,42 @@ impl<const N: usize> ShareWords<N> {
50, 50,
"Write all ", "Write all ",
inttostr!(self.share_words.len() as u8), inttostr!(self.share_words.len() as u8),
"\nwords in order on\nrecovery seed card." " words in order on recovery seed card."
) )
} }
/// Display the first page with user information. /// Display the first page with user information.
fn paint_entry_page(&mut self) { fn paint_entry_page(&mut self) {
text_multiline( text_multiline_split_words(
self.area.split_top(15).1, self.area.split_top(15).1,
&self.get_first_text(), &self.get_first_text(),
Font::BOLD, Font::BOLD,
theme::FG, theme::FG,
theme::BG, theme::BG,
Alignment::Start,
); );
} }
fn get_second_text(&self) -> String<50> { fn get_second_text(&self) -> String<50> {
build_string!(50, "Do NOT make\ndigital copies!") build_string!(50, "Do NOT make digital copies!")
} }
/// Display the second page with user information. /// Display the second page with user information.
fn paint_second_page(&mut self) { fn paint_second_page(&mut self) {
text_multiline( text_multiline_split_words(
self.area.split_top(15).1, self.area.split_top(15).1,
&self.get_second_text(), &self.get_second_text(),
Font::MONO, Font::MONO,
theme::FG, theme::FG,
theme::BG, theme::BG,
Alignment::Start,
); );
} }
fn get_final_text(&self) -> String<50> { fn get_final_text(&self) -> String<50> {
build_string!( build_string!(
50, 50,
"I wrote down all\n", "I wrote down all ",
inttostr!(self.share_words.len() as u8), inttostr!(self.share_words.len() as u8),
" words in order." " words in order."
) )
@ -118,12 +120,13 @@ impl<const N: usize> ShareWords<N> {
/// Display the final page with user confirmation. /// Display the final page with user confirmation.
fn paint_final_page(&mut self) { fn paint_final_page(&mut self) {
text_multiline( text_multiline_split_words(
self.area.split_top(12).1, self.area.split_top(12).1,
&self.get_final_text(), &self.get_final_text(),
Font::MONO, Font::MONO,
theme::FG, theme::FG,
theme::BG, theme::BG,
Alignment::Start,
); );
} }

View File

@ -44,8 +44,8 @@ use crate::{
use super::{ use super::{
component::{ component::{
AddressDetails, AddressDetailsMsg, ButtonActions, ButtonDetails, ButtonLayout, ButtonPage, AddressDetails, AddressDetailsMsg, ButtonActions, ButtonDetails, ButtonLayout, ButtonPage,
CancelInfoConfirmMsg, Flow, FlowMsg, FlowPages, Frame, Homescreen, HomescreenMsg, CancelInfoConfirmMsg, CoinJoinProgress, Flow, FlowMsg, FlowPages, Frame, Homescreen,
Lockscreen, NoBtnDialog, NoBtnDialogMsg, NumberInput, NumberInputMsg, Page, HomescreenMsg, Lockscreen, NoBtnDialog, NoBtnDialogMsg, NumberInput, NumberInputMsg, Page,
PassphraseEntry, PassphraseEntryMsg, PinEntry, PinEntryMsg, Progress, ShareWords, ShowMore, PassphraseEntry, PassphraseEntryMsg, PinEntry, PinEntryMsg, Progress, ShareWords, ShowMore,
SimpleChoice, SimpleChoiceMsg, WelcomeScreen, WordlistEntry, WordlistEntryMsg, SimpleChoice, SimpleChoiceMsg, WelcomeScreen, WordlistEntry, WordlistEntryMsg,
WordlistType, WordlistType,
@ -132,6 +132,19 @@ impl ComponentMsgObj for PinEntry {
} }
} }
// Clippy complains about conflicting implementations
#[cfg(not(feature = "clippy"))]
impl<T> ComponentMsgObj for (Timeout, T)
where
T: Component<Msg = TimeoutMsg>,
{
fn msg_try_into_obj(&self, msg: Self::Msg) -> Result<Obj, Error> {
match msg {
TimeoutMsg::TimedOut => Ok(CANCELLED.as_obj()),
}
}
}
impl ComponentMsgObj for AddressDetails { impl ComponentMsgObj for AddressDetails {
fn msg_try_into_obj(&self, msg: Self::Msg) -> Result<Obj, Error> { fn msg_try_into_obj(&self, msg: Self::Msg) -> Result<Obj, Error> {
match msg { match msg {
@ -140,6 +153,12 @@ impl ComponentMsgObj for AddressDetails {
} }
} }
impl ComponentMsgObj for CoinJoinProgress {
fn msg_try_into_obj(&self, _msg: Self::Msg) -> Result<Obj, Error> {
unreachable!();
}
}
impl ComponentMsgObj for NumberInput { impl ComponentMsgObj for NumberInput {
fn msg_try_into_obj(&self, msg: Self::Msg) -> Result<Obj, Error> { fn msg_try_into_obj(&self, msg: Self::Msg) -> Result<Obj, Error> {
match msg { match msg {
@ -900,9 +919,9 @@ extern "C" fn new_confirm_coinjoin(n_args: usize, args: *const Obj, kwargs: *mut
let max_feerate: StrBuffer = kwargs.get(Qstr::MP_QSTR_max_feerate)?.try_into()?; let max_feerate: StrBuffer = kwargs.get(Qstr::MP_QSTR_max_feerate)?.try_into()?;
let paragraphs = Paragraphs::new([ let paragraphs = Paragraphs::new([
Paragraph::new(&theme::TEXT_BOLD, "Maximum rounds:".into()), Paragraph::new(&theme::TEXT_BOLD, "Max rounds".into()),
Paragraph::new(&theme::TEXT_MONO, max_rounds), Paragraph::new(&theme::TEXT_MONO, max_rounds),
Paragraph::new(&theme::TEXT_BOLD, "Maximum mining fee:".into()).no_break(), Paragraph::new(&theme::TEXT_BOLD, "Max mining fee".into()).no_break(),
Paragraph::new(&theme::TEXT_MONO, max_feerate), Paragraph::new(&theme::TEXT_MONO, max_feerate),
]); ]);
@ -1138,6 +1157,29 @@ extern "C" fn new_show_progress(n_args: usize, args: *const Obj, kwargs: *mut Ma
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
} }
extern "C" fn new_show_progress_coinjoin(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
let block = move |_args: &[Obj], kwargs: &Map| {
let title: StrBuffer = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?;
let indeterminate: bool = kwargs.get_or(Qstr::MP_QSTR_indeterminate, false)?;
let time_ms: u32 = kwargs.get_or(Qstr::MP_QSTR_time_ms, 0)?;
let skip_first_paint: bool = kwargs.get_or(Qstr::MP_QSTR_skip_first_paint, false)?;
// The second type parameter is actually not used in `new()` but we need to
// provide it.
let progress = CoinJoinProgress::new(title, indeterminate);
let obj = if time_ms > 0 && indeterminate {
let timeout = Timeout::new(time_ms);
LayoutObj::new((timeout, progress.map(|_msg| None)))?
} else {
LayoutObj::new(progress)?
};
if skip_first_paint {
obj.skip_first_paint();
}
Ok(obj.into())
};
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
}
extern "C" fn new_show_homescreen(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj { extern "C" fn new_show_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 label: StrBuffer = kwargs.get(Qstr::MP_QSTR_label)?.try_into()?; let label: StrBuffer = kwargs.get(Qstr::MP_QSTR_label)?.try_into()?;
@ -1171,33 +1213,6 @@ extern "C" fn new_show_lockscreen(n_args: usize, args: *const Obj, kwargs: *mut
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
} }
extern "C" fn new_show_busyscreen(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
let block = move |_args: &[Obj], kwargs: &Map| {
let title: StrBuffer = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?;
let description: StrBuffer = kwargs.get(Qstr::MP_QSTR_description)?.try_into()?;
let time_ms: u32 = kwargs.get(Qstr::MP_QSTR_time_ms)?.try_into()?;
let skip_first_paint: bool = kwargs.get(Qstr::MP_QSTR_skip_first_paint)?.try_into()?;
let content = Paragraphs::new([
Paragraph::new(&theme::TEXT_BOLD, title),
Paragraph::new(&theme::TEXT_MONO, description),
]);
let obj = LayoutObj::new(NoBtnDialog::new(
content,
Timeout::new(time_ms).map(|msg| {
(matches!(msg, TimeoutMsg::TimedOut)).then(|| CancelConfirmMsg::Confirmed)
}),
))?;
if skip_first_paint {
obj.skip_first_paint();
}
Ok(obj.into())
};
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
}
extern "C" fn draw_welcome_screen() -> Obj { extern "C" fn draw_welcome_screen() -> Obj {
// No need of util::try_or_raise, this does not allocate // No need of util::try_or_raise, this does not allocate
let mut screen = WelcomeScreen::new(); let mut screen = WelcomeScreen::new();
@ -1510,6 +1525,17 @@ pub static mp_module_trezorui2: Module = obj_module! {
/// make sure the initial description has at least that amount of lines.""" /// make sure the initial description has at least that amount of lines."""
Qstr::MP_QSTR_show_progress => obj_fn_kw!(0, new_show_progress).as_obj(), Qstr::MP_QSTR_show_progress => obj_fn_kw!(0, new_show_progress).as_obj(),
/// def show_progress_coinjoin(
/// *,
/// title: str,
/// indeterminate: bool = False,
/// time_ms: int = 0,
/// skip_first_paint: bool = False,
/// ) -> object:
/// """Show progress loader for coinjoin. Returns CANCELLED after a specified time when
/// time_ms timeout is passed."""
Qstr::MP_QSTR_show_progress_coinjoin => obj_fn_kw!(0, new_show_progress_coinjoin).as_obj(),
/// def show_homescreen( /// def show_homescreen(
/// *, /// *,
/// label: str, /// label: str,
@ -1530,16 +1556,6 @@ pub static mp_module_trezorui2: Module = obj_module! {
/// """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(),
/// def show_busyscreen(
/// *,
/// title: str,
/// description: str,
/// time_ms: int,
/// skip_first_paint: bool,
/// ) -> CANCELLED:
/// """Homescreen used for indicating coinjoin in progress."""
Qstr::MP_QSTR_show_busyscreen => obj_fn_kw!(0, new_show_busyscreen).as_obj(),
/// def draw_welcome_screen() -> None: /// def draw_welcome_screen() -> None:
/// """Show logo icon with the model name at the bottom and return.""" /// """Show logo icon with the model name at the bottom and return."""
Qstr::MP_QSTR_draw_welcome_screen => obj_fn_0!(draw_welcome_screen).as_obj(), Qstr::MP_QSTR_draw_welcome_screen => obj_fn_0!(draw_welcome_screen).as_obj(),

View File

@ -114,10 +114,9 @@ class Busyscreen(HomescreenBase):
def __init__(self, delay_ms: int) -> None: def __init__(self, delay_ms: int) -> None:
skip = storage_cache.homescreen_shown is self.RENDER_INDICATOR skip = storage_cache.homescreen_shown is self.RENDER_INDICATOR
super().__init__( super().__init__(
# TODO: remove show_busyscreen in favor of show_progress_coinjoin layout=trezorui2.show_progress_coinjoin(
layout=trezorui2.show_busyscreen( title="Waiting for others",
title="PLEASE WAIT", indeterminate=True,
description="Coinjoin in progress.\n\nDo not disconnect your Trezor.",
time_ms=delay_ms, time_ms=delay_ms,
skip_first_paint=skip, skip_first_paint=skip,
) )

View File

@ -99,8 +99,10 @@ def read_words(debug: "DebugLink", backup_type: messages.BackupType) -> list[str
else: else:
assert layout.text_content().startswith("RECOVERY SEED") assert layout.text_content().startswith("RECOVERY SEED")
# Swiping through all the page and loading the words # Swiping through all the pages and loading the words
for _ in range(layout.page_count() - 1): for i in range(layout.page_count() - 1):
# In model R, first two pages are just informational
if not (debug.model == "R" and i < 2):
words.extend(layout.seed_words()) words.extend(layout.seed_words())
layout = debug.input(swipe=messages.DebugSwipeDirection.UP, wait=True) layout = debug.input(swipe=messages.DebugSwipeDirection.UP, wait=True)
assert layout is not None assert layout is not None

View File

@ -341,8 +341,10 @@ def read_and_confirm_mnemonic_tr(
mnemonic: list[str] = [] mnemonic: list[str] = []
br = yield br = yield
assert br.pages is not None assert br.pages is not None
for _ in range(br.pages - 1): for i in range(br.pages - 1):
layout = debug.wait_layout() layout = debug.wait_layout()
# First two pages have just instructions
if i > 1:
words = layout.seed_words() words = layout.seed_words()
mnemonic.extend(words) mnemonic.extend(words)
debug.press_right() debug.press_right()