diff --git a/ci/shell.nix b/ci/shell.nix index 6f5a3f1d2..233b558dc 100644 --- a/ci/shell.nix +++ b/ci/shell.nix @@ -75,6 +75,7 @@ stdenvNoCC.mkDerivation ({ curl # for connect tests editorconfig-checker gcc-arm-embedded + gdb git gitAndTools.git-subrepo gnumake diff --git a/core/Makefile b/core/Makefile index b3d3b999b..845a6776c 100644 --- a/core/Makefile +++ b/core/Makefile @@ -107,7 +107,7 @@ test_emu_click: ## run click tests test_emu_ui: ## run ui integration tests $(EMU_TEST) $(PYTEST) $(TESTPATH)/device_tests $(TESTOPTS) \ - --ui=test --ui-check-missing --not-generate-report-after-each-test + --ui=test --ui-check-missing test_emu_ui_multicore: ## run ui integration tests using multiple cores $(PYTEST) -n auto $(TESTPATH)/device_tests $(TESTOPTS) \ diff --git a/core/embed/rust/src/ui/component/label.rs b/core/embed/rust/src/ui/component/label.rs index 713b38995..424d4b9aa 100644 --- a/core/embed/rust/src/ui/component/label.rs +++ b/core/embed/rust/src/ui/component/label.rs @@ -1,10 +1,10 @@ use crate::ui::{ component::{Component, Event, EventCtx, Never}, display::Font, - geometry::{Alignment, Offset, Rect}, + geometry::{Offset, Rect}, }; -use super::{text::TextStyle, TextLayout}; +use super::{text::{TextAlign, TextStyle}, TextLayout}; pub struct Label { text: T, @@ -15,7 +15,7 @@ impl Label where T: AsRef, { - pub fn new(text: T, align: Alignment, style: TextStyle) -> Self { + pub fn new(text: T, align: TextAlign, style: TextStyle) -> Self { Self { text, layout: TextLayout::new(style).with_align(align), @@ -23,15 +23,15 @@ where } pub fn left_aligned(text: T, style: TextStyle) -> Self { - Self::new(text, Alignment::Start, style) + Self::new(text, TextAlign::Left, style) } pub fn right_aligned(text: T, style: TextStyle) -> Self { - Self::new(text, Alignment::End, style) + Self::new(text, TextAlign::Right, style) } pub fn centered(text: T, style: TextStyle) -> Self { - Self::new(text, Alignment::Center, style) + Self::new(text, TextAlign::Center, style) } pub fn text(&self) -> &T { diff --git a/core/embed/rust/src/ui/component/text/layout.rs b/core/embed/rust/src/ui/component/text/layout.rs index c2b40448c..149802b43 100644 --- a/core/embed/rust/src/ui/component/text/layout.rs +++ b/core/embed/rust/src/ui/component/text/layout.rs @@ -2,7 +2,7 @@ use super::iter::GlyphMetrics; use crate::ui::{ display, display::{Color, Font}, - geometry::{Alignment, Dimensions, Offset, Point, Rect}, + geometry::{Dimensions, Offset, Point, Rect}, }; #[derive(Copy, Clone)] @@ -26,6 +26,14 @@ pub enum PageBreaking { CutAndInsertEllipsis, } +#[derive(Copy, Clone)] +pub enum TextAlign { + Left, + Center, + Right, + Block, +} + /// Visual instructions for laying out a formatted block of text. #[derive(Copy, Clone)] pub struct TextLayout { @@ -42,7 +50,7 @@ pub struct TextLayout { /// Fonts, colors, line/page breaking behavior. pub style: TextStyle, /// Horizontal alignment. - pub align: Alignment, + pub align: TextAlign, } #[derive(Copy, Clone)] @@ -63,6 +71,8 @@ pub struct TextStyle { pub line_breaking: LineBreaking, /// Specifies what to do at the end of the page. pub page_breaking: PageBreaking, + /// Increase or decrease space size, in pixels. + pub space_adjust: i16, } impl TextStyle { @@ -81,6 +91,7 @@ impl TextStyle { ellipsis_color, line_breaking: LineBreaking::BreakAtWhitespace, page_breaking: PageBreaking::CutAndInsertEllipsis, + space_adjust: 0, } } @@ -93,6 +104,11 @@ impl TextStyle { self.page_breaking = page_breaking; self } + + pub const fn with_space_adjust(mut self, space_adjust: i16) -> Self { + self.space_adjust = space_adjust; + self + } } impl TextLayout { @@ -104,7 +120,7 @@ impl TextLayout { padding_top: 0, padding_bottom: 0, style, - align: Alignment::Start, + align: TextAlign::Left, } } @@ -113,7 +129,7 @@ impl TextLayout { self } - pub fn with_align(mut self, align: Alignment) -> Self { + pub fn with_align(mut self, align: TextAlign) -> Self { self.align = align; self } @@ -197,18 +213,29 @@ impl TextLayout { let span = Span::fit_horizontally( remaining_text, remaining_width, - self.style.text_font, + AdjustSpace::new(self.style.text_font, self.style.space_adjust), self.style.line_breaking, ); cursor.x += match self.align { - Alignment::Start => 0, - Alignment::Center => (remaining_width - span.advance.x) / 2, - Alignment::End => remaining_width - span.advance.x, + TextAlign::Left | TextAlign::Block => 0, + TextAlign::Center => (remaining_width - span.advance.x) / 2, + TextAlign::Right => remaining_width - span.advance.x, + }; + let space_adjust = match self.align { + TextAlign::Block if self.style.space_adjust == 0 => { + let spaces: i16 = unwrap!(remaining_text[..span.length].chars().filter(|c| *c == ' ').count().try_into()); + if spaces > 0 { + ((remaining_width - span.advance.x) / spaces).min(self.style.text_font.char_width(' ') * 8 / 7) + } else { + 0 + } + } + _ => self.style.space_adjust, }; // Report the span at the cursor position. - sink.text(*cursor, self, &remaining_text[..span.length]); + sink.text(*cursor, self, &remaining_text[..span.length], space_adjust); // Continue with the rest of the remaining_text. remaining_text = &remaining_text[span.length + span.skip_next_chars..]; @@ -300,7 +327,7 @@ impl LayoutFit { /// Visitor for text segment operations. pub trait LayoutSink { - fn text(&mut self, _cursor: Point, _layout: &TextLayout, _text: &str) {} + fn text(&mut self, _cursor: Point, _layout: &TextLayout, _text: &str, _space_adjust: i16) {} fn hyphen(&mut self, _cursor: Point, _layout: &TextLayout) {} fn ellipsis(&mut self, _cursor: Point, _layout: &TextLayout) {} fn line_break(&mut self, _cursor: Point) {} @@ -314,10 +341,23 @@ impl LayoutSink for TextNoOp {} pub struct TextRenderer; impl LayoutSink for TextRenderer { - fn text(&mut self, cursor: Point, layout: &TextLayout, text: &str) { + fn text(&mut self, cursor: Point, layout: &TextLayout, text: &str, space_adjust: i16) { + let mut c = cursor; + let mut t = text; + while let Some(i) = t.find(' ') { + display::text( + c, + &t[..i+1], + layout.style.text_font, + layout.style.text_color, + layout.style.background_color, + ); + c.x += layout.style.text_font.text_width(&t[..i+1]) + space_adjust; + t = &t[i+1..]; + } display::text( - cursor, - text, + c, + t, layout.style.text_font, layout.style.text_color, layout.style.background_color, @@ -345,6 +385,29 @@ impl LayoutSink for TextRenderer { } } +struct AdjustSpace(T, i16); + +impl AdjustSpace { + fn new(metrics: T, adjust: i16) -> Self { + Self(metrics, adjust) + } +} + +impl GlyphMetrics for AdjustSpace { + fn char_width(&self, ch: char) -> i16 { + let width = self.0.char_width(ch); + if ch == ' ' { + width + self.1 + } else { + width + } + } + + fn line_height(&self) -> i16 { + self.0.line_height() + } +} + #[cfg(feature = "ui_debug")] pub mod trace { use crate::ui::geometry::Point; @@ -354,7 +417,7 @@ pub mod trace { pub struct TraceSink<'a>(pub &'a mut dyn crate::trace::Tracer); impl<'a> LayoutSink for TraceSink<'a> { - fn text(&mut self, _cursor: Point, _layout: &TextLayout, text: &str) { + fn text(&mut self, _cursor: Point, _layout: &TextLayout, text: &str, _space_adjust: i16) { self.0.string(text); } diff --git a/core/embed/rust/src/ui/component/text/mod.rs b/core/embed/rust/src/ui/component/text/mod.rs index 517840e64..28df8463b 100644 --- a/core/embed/rust/src/ui/component/text/mod.rs +++ b/core/embed/rust/src/ui/component/text/mod.rs @@ -3,4 +3,4 @@ mod iter; pub mod layout; pub mod paragraphs; -pub use layout::{LineBreaking, PageBreaking, TextStyle}; +pub use layout::{LineBreaking, PageBreaking, TextAlign, TextStyle}; diff --git a/core/embed/rust/src/ui/component/text/paragraphs.rs b/core/embed/rust/src/ui/component/text/paragraphs.rs index f654013d8..1c6d41d49 100644 --- a/core/embed/rust/src/ui/component/text/paragraphs.rs +++ b/core/embed/rust/src/ui/component/text/paragraphs.rs @@ -3,10 +3,10 @@ use heapless::Vec; use crate::ui::{ component::{Component, Event, EventCtx, Never, Paginate}, display, - geometry::{Alignment, Insets, LinearPlacement, Offset, Point, Rect}, + geometry::{Insets, LinearPlacement, Offset, Point, Rect}, }; -use super::layout::{LayoutFit, TextLayout, TextStyle}; +use super::layout::{LayoutFit, TextLayout, TextStyle, TextAlign}; /// Used as an upper bound of number of different styles we may render on single /// page. @@ -252,7 +252,7 @@ pub struct Paragraph { /// Paragraph style. style: &'static TextStyle, /// Paragraph alignment. - align: Alignment, + align: TextAlign, /// Place next paragraph on new page. break_after: bool, /// Try to keep this and the next paragraph on the same page. NOTE: doesn't @@ -265,14 +265,19 @@ impl Paragraph { Self { content, style, - align: Alignment::Start, + align: TextAlign::Left, break_after: false, no_break: false, } } pub const fn centered(mut self) -> Self { - self.align = Alignment::Center; + self.align = TextAlign::Center; + self + } + + pub const fn blockaligned(mut self) -> Self { + self.align = TextAlign::Block; self } diff --git a/core/embed/rust/src/ui/model_tt/component/fido.rs b/core/embed/rust/src/ui/model_tt/component/fido.rs index baedabab8..39e5ac307 100644 --- a/core/embed/rust/src/ui/model_tt/component/fido.rs +++ b/core/embed/rust/src/ui/model_tt/component/fido.rs @@ -1,7 +1,7 @@ use crate::ui::{ - component::{Child, Component, Event, EventCtx, Image, Label}, + component::{Child, Component, Event, EventCtx, Image, Label, text::TextAlign}, display, - geometry::{Alignment, Insets, Rect}, + geometry::{Insets, Rect}, model_tt::component::{ fido_icons::get_fido_icon_data, swipe::{Swipe, SwipeDirection}, @@ -60,8 +60,8 @@ where page_swipe.allow_left = scrollbar.has_next_page(); Self { - app_name: Label::new(app_name, Alignment::Center, theme::TEXT_BOLD), - account_name: Label::new("".into(), Alignment::Center, theme::TEXT_BOLD), + app_name: Label::new(app_name, TextAlign::Center, theme::TEXT_BOLD), + account_name: Label::new("".into(), TextAlign::Center, theme::TEXT_BOLD), page_swipe, icon: Child::new(Image::new(icon_data)), get_account, diff --git a/core/embed/rust/src/ui/model_tt/component/frame.rs b/core/embed/rust/src/ui/model_tt/component/frame.rs index 4102ee78e..74e2d0fc0 100644 --- a/core/embed/rust/src/ui/model_tt/component/frame.rs +++ b/core/embed/rust/src/ui/model_tt/component/frame.rs @@ -1,8 +1,8 @@ use super::theme; use crate::ui::{ - component::{label::Label, text::TextStyle, Child, Component, Event, EventCtx}, + component::{label::Label, text::{TextAlign, TextStyle}, Child, Component, Event, EventCtx}, display::{self, Color, Font}, - geometry::{Alignment, Insets, Offset, Rect}, + geometry::{Insets, Offset, Rect}, util::icon_text_center, }; @@ -17,7 +17,7 @@ where T: Component, U: AsRef, { - pub fn new(style: TextStyle, alignment: Alignment, title: U, content: T) -> Self { + pub fn new(style: TextStyle, alignment: TextAlign, title: U, content: T) -> Self { Self { title: Child::new(Label::new(title, alignment, style)), border: theme::borders_scroll(), @@ -26,15 +26,15 @@ where } pub fn left_aligned(style: TextStyle, title: U, content: T) -> Self { - Self::new(style, Alignment::Start, title, content) + Self::new(style, TextAlign::Left, title, content) } pub fn right_aligned(style: TextStyle, title: U, content: T) -> Self { - Self::new(style, Alignment::End, title, content) + Self::new(style, TextAlign::Right, title, content) } pub fn centered(style: TextStyle, title: U, content: T) -> Self { - Self::new(style, Alignment::Center, title, content) + Self::new(style, TextAlign::Center, title, content) } pub fn with_border(mut self, border: Insets) -> Self { diff --git a/core/embed/rust/src/ui/model_tt/layout.rs b/core/embed/rust/src/ui/model_tt/layout.rs index f00ebdae9..a956e621b 100644 --- a/core/embed/rust/src/ui/model_tt/layout.rs +++ b/core/embed/rust/src/ui/model_tt/layout.rs @@ -339,10 +339,10 @@ extern "C" fn new_confirm_action(n_args: usize, args: *const Obj, kwargs: *mut M if !reverse { paragraphs .add(Paragraph::new(&theme::TEXT_BOLD, action)) - .add(Paragraph::new(&theme::TEXT_NORMAL, description)); + .add(Paragraph::new(&theme::TEXT_NORMAL, description).blockaligned()); } else { paragraphs - .add(Paragraph::new(&theme::TEXT_NORMAL, description)) + .add(Paragraph::new(&theme::TEXT_NORMAL, description).blockaligned()) .add(Paragraph::new(&theme::TEXT_BOLD, action)); } paragraphs.into_paragraphs() @@ -473,9 +473,9 @@ extern "C" fn new_confirm_reset_device(n_args: usize, args: *const Obj, kwargs: let url: StrBuffer = "https://trezor.io/tos".into(); let paragraphs = Paragraphs::new([ - Paragraph::new(&theme::TEXT_BOLD, prompt), - Paragraph::new(&theme::TEXT_NORMAL, description), - Paragraph::new(&theme::TEXT_BOLD, url), + Paragraph::new(&theme::TEXT_BOLD, prompt).blockaligned(), + Paragraph::new(&theme::TEXT_NORMAL, description).blockaligned(), + Paragraph::new(&theme::TEXT_BOLD, url).blockaligned(), ]); let buttons = Button::cancel_confirm_text(None, "CONTINUE"); @@ -609,9 +609,9 @@ extern "C" fn new_confirm_modify_fee(n_args: usize, args: *const Obj, kwargs: *m }; let paragraphs = Paragraphs::new([ - Paragraph::new(&theme::TEXT_NORMAL, description.into()), + Paragraph::new(&theme::TEXT_NORMAL, description.into()).blockaligned(), Paragraph::new(&theme::TEXT_MONO, change), - Paragraph::new(&theme::TEXT_NORMAL, "\nTransaction fee:".into()), + Paragraph::new(&theme::TEXT_NORMAL, "\nTransaction fee:".into()).blockaligned(), Paragraph::new(&theme::TEXT_MONO, total_fee_new), ]); @@ -794,7 +794,7 @@ extern "C" fn new_show_simple(n_args: usize, args: *const Obj, kwargs: *mut Map) theme::label_title(), t, Dialog::new( - Paragraphs::new([Paragraph::new(&theme::TEXT_NORMAL, description)]), + Paragraphs::new([Paragraph::new(&theme::TEXT_NORMAL, description).blockaligned()]), theme::button_bar(Button::with_text(button).map(|msg| { (matches!(msg, ButtonMsg::Clicked)).then(|| CancelConfirmMsg::Confirmed) })), @@ -805,7 +805,7 @@ extern "C" fn new_show_simple(n_args: usize, args: *const Obj, kwargs: *mut Map) LayoutObj::new(Border::new( theme::borders(), Dialog::new( - Paragraphs::new([Paragraph::new(&theme::TEXT_NORMAL, description)]), + Paragraphs::new([Paragraph::new(&theme::TEXT_NORMAL, description).blockaligned()]), theme::button_bar(Button::with_text(button).map(|msg| { (matches!(msg, ButtonMsg::Clicked)).then(|| CancelConfirmMsg::Confirmed) })), @@ -961,7 +961,7 @@ extern "C" fn new_select_word(n_args: usize, args: *const Obj, kwargs: *mut Map) let words_iterable: Obj = kwargs.get(Qstr::MP_QSTR_words)?; let words: [StrBuffer; 3] = iter_into_array(words_iterable)?; - let paragraphs = Paragraphs::new([Paragraph::new(&theme::TEXT_NORMAL, description)]); + let paragraphs = Paragraphs::new([Paragraph::new(&theme::TEXT_NORMAL, description).blockaligned()]); let buttons = Button::select_word(words); let obj = LayoutObj::new(Frame::left_aligned( @@ -1184,7 +1184,7 @@ extern "C" fn new_show_remaining_shares(n_args: usize, args: *const Obj, kwargs: let [title, description]: [StrBuffer; 2] = iter_into_array(page)?; paragraphs .add(Paragraph::new(&theme::TEXT_BOLD, title)) - .add(Paragraph::new(&theme::TEXT_NORMAL, description).break_after()); + .add(Paragraph::new(&theme::TEXT_NORMAL, description).blockaligned().break_after()); } let obj = LayoutObj::new(Frame::left_aligned( diff --git a/core/embed/rust/src/ui/model_tt/theme.rs b/core/embed/rust/src/ui/model_tt/theme.rs index 0c5f33376..6636084bf 100644 --- a/core/embed/rust/src/ui/model_tt/theme.rs +++ b/core/embed/rust/src/ui/model_tt/theme.rs @@ -382,6 +382,10 @@ pub const TEXT_BOLD: TextStyle = TextStyle::new(Font::BOLD, FG, BG, GREY_LIGHT, pub const TEXT_MONO: TextStyle = TextStyle::new(Font::MONO, FG, BG, GREY_LIGHT, GREY_LIGHT) .with_line_breaking(LineBreaking::BreakWordsNoHyphen) .with_page_breaking(PageBreaking::Cut); +pub const TEXT_MONO_GROUPS: TextStyle = TextStyle::new(Font::MONO, FG, BG, GREY_LIGHT, GREY_LIGHT) + .with_line_breaking(LineBreaking::BreakAtWhitespace) + .with_page_breaking(PageBreaking::Cut) + .with_space_adjust(-5); /// Convert Python-side numeric id to a `TextStyle`. pub fn textstyle_number(num: i32) -> &'static TextStyle { diff --git a/core/src/apps/bitcoin/get_address.py b/core/src/apps/bitcoin/get_address.py index 3feeeceac..035877d0d 100644 --- a/core/src/apps/bitcoin/get_address.py +++ b/core/src/apps/bitcoin/get_address.py @@ -110,6 +110,7 @@ async def get_address( title=title, multisig_index=multisig_index, xpubs=_get_xpubs(coin, multisig_xpub_magic, pubnodes), + group_by=4, ) else: title = address_n_to_str(address_n) @@ -119,6 +120,7 @@ async def get_address( address_qr=address, case_sensitive=address_case_sensitive, title=title, + group_by=4, ) return Address(address=address, mac=mac) diff --git a/core/src/trezor/ui/layouts/tt_v2/__init__.py b/core/src/trezor/ui/layouts/tt_v2/__init__.py index 887e972ba..0052a43cf 100644 --- a/core/src/trezor/ui/layouts/tt_v2/__init__.py +++ b/core/src/trezor/ui/layouts/tt_v2/__init__.py @@ -356,6 +356,11 @@ async def show_xpub(ctx: GenericContext, xpub: str, title: str) -> None: ) +def _address_group_by(address, group_by): + if group_by < 2: + return address + return " ".join(address[i*group_by:i*group_by+group_by] for i in range(1 + len(address) // group_by)) + async def show_address( ctx: GenericContext, address: str, @@ -368,7 +373,9 @@ async def show_address( xpubs: Sequence[str] = (), address_extra: str | None = None, title_qr: str | None = None, + group_by: int = 0, ) -> None: + #address = _address_group_by(address, group_by) is_multisig = len(xpubs) > 0 while True: result = await interact(