mirror of
https://github.com/trezor/trezor-firmware.git
synced 2024-11-17 21:22:10 +00:00
feat(core/ui): page break icons
Co-authored-by: grdddj <jiri.musil06@seznam.cz> [no changelog]
This commit is contained in:
parent
60aa2e7292
commit
1b94a7cb7b
BIN
core/assets/page-next.png
Normal file
BIN
core/assets/page-next.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 146 B |
BIN
core/assets/page-prev.png
Normal file
BIN
core/assets/page-prev.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 296 B |
@ -160,6 +160,7 @@ fn select_line_breaks(
|
||||
|
||||
pub trait GlyphMetrics {
|
||||
fn char_width(&self, ch: char) -> i16;
|
||||
fn text_width(&self, text: &str) -> i16;
|
||||
fn line_height(&self) -> i16;
|
||||
}
|
||||
|
||||
@ -168,6 +169,10 @@ impl GlyphMetrics for Font {
|
||||
Font::char_width(*self, ch)
|
||||
}
|
||||
|
||||
fn text_width(&self, text: &str) -> i16 {
|
||||
Font::text_width(*self, text)
|
||||
}
|
||||
|
||||
fn line_height(&self) -> i16 {
|
||||
Font::line_height(*self)
|
||||
}
|
||||
@ -217,6 +222,10 @@ mod tests {
|
||||
fn line_height(&self) -> i16 {
|
||||
self.height
|
||||
}
|
||||
|
||||
fn text_width(&self, text: &str) -> i16 {
|
||||
self.width * text.len() as i16
|
||||
}
|
||||
}
|
||||
|
||||
fn break_text(s: &str, w: i16) -> Vec<Span> {
|
||||
|
@ -1,10 +1,12 @@
|
||||
use super::iter::GlyphMetrics;
|
||||
use crate::ui::{
|
||||
display,
|
||||
display::{Color, Font},
|
||||
geometry::{Alignment, Dimensions, Offset, Point, Rect},
|
||||
display::{toif::Icon, Color, Font},
|
||||
geometry::{Alignment, Dimensions, Offset, Point, Rect, BOTTOM_LEFT},
|
||||
};
|
||||
|
||||
const ELLIPSIS: &str = "...";
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub enum LineBreaking {
|
||||
/// Break line only at whitespace, if possible. If we don't find any
|
||||
@ -24,6 +26,9 @@ pub enum PageBreaking {
|
||||
/// Before stopping at the bottom-right edge, insert ellipsis to signify
|
||||
/// more content is available, but only if no hyphen has been inserted yet.
|
||||
CutAndInsertEllipsis,
|
||||
/// Same as `CutAndInsertEllipsis` but also insert an ellipsis/icon at the
|
||||
/// beginning of the next page.
|
||||
CutAndInsertEllipsisBoth,
|
||||
}
|
||||
|
||||
/// Visual instructions for laying out a formatted block of text.
|
||||
@ -43,6 +48,9 @@ pub struct TextLayout {
|
||||
pub style: TextStyle,
|
||||
/// Horizontal alignment.
|
||||
pub align: Alignment,
|
||||
|
||||
/// Whether to draw "..." (or an icon) at the beginning.
|
||||
pub continues_from_prev_page: bool,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
@ -58,6 +66,10 @@ pub struct TextStyle {
|
||||
pub hyphen_color: Color,
|
||||
/// Foreground color used for drawing the ellipsis.
|
||||
pub ellipsis_color: Color,
|
||||
/// Optional icon shown as ellipsis.
|
||||
pub ellipsis_icon: Option<Icon>,
|
||||
/// Optional icon to signal content continues from previous page.
|
||||
pub prev_page_ellipsis_icon: Option<Icon>,
|
||||
|
||||
/// Specifies which line-breaking strategy to use.
|
||||
pub line_breaking: LineBreaking,
|
||||
@ -79,6 +91,8 @@ impl TextStyle {
|
||||
background_color,
|
||||
hyphen_color,
|
||||
ellipsis_color,
|
||||
ellipsis_icon: None,
|
||||
prev_page_ellipsis_icon: None,
|
||||
line_breaking: LineBreaking::BreakAtWhitespace,
|
||||
page_breaking: PageBreaking::CutAndInsertEllipsis,
|
||||
}
|
||||
@ -93,6 +107,34 @@ impl TextStyle {
|
||||
self.page_breaking = page_breaking;
|
||||
self
|
||||
}
|
||||
|
||||
/// Adding optional icon shown instead of "..." ellipsis.
|
||||
pub const fn with_ellipsis_icon(mut self, icon: Icon) -> Self {
|
||||
self.ellipsis_icon = Some(icon);
|
||||
self
|
||||
}
|
||||
|
||||
/// Adding optional icon signalling content continues from previous page.
|
||||
pub const fn with_prev_page_icon(mut self, icon: Icon) -> Self {
|
||||
self.prev_page_ellipsis_icon = Some(icon);
|
||||
self
|
||||
}
|
||||
|
||||
fn ellipsis_width(&self) -> i16 {
|
||||
if let Some(icon) = self.ellipsis_icon {
|
||||
icon.toif.width()
|
||||
} else {
|
||||
self.text_font.text_width(ELLIPSIS)
|
||||
}
|
||||
}
|
||||
|
||||
fn prev_page_ellipsis_width(&self) -> i16 {
|
||||
if let Some(icon) = self.prev_page_ellipsis_icon {
|
||||
icon.toif.width()
|
||||
} else {
|
||||
self.text_font.text_width(ELLIPSIS)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TextLayout {
|
||||
@ -105,6 +147,7 @@ impl TextLayout {
|
||||
padding_bottom: 0,
|
||||
style,
|
||||
align: Alignment::Start,
|
||||
continues_from_prev_page: false,
|
||||
}
|
||||
}
|
||||
|
||||
@ -192,13 +235,31 @@ impl TextLayout {
|
||||
};
|
||||
}
|
||||
|
||||
// Draw the arrow icon if we are in the middle of a string
|
||||
if matches!(
|
||||
self.style.page_breaking,
|
||||
PageBreaking::CutAndInsertEllipsisBoth
|
||||
) && self.continues_from_prev_page
|
||||
{
|
||||
sink.prev_page_ellipsis(*cursor, self);
|
||||
cursor.x += self.style.prev_page_ellipsis_width();
|
||||
}
|
||||
|
||||
while !remaining_text.is_empty() {
|
||||
let is_last_line = cursor.y + self.style.text_font.line_height() > self.bottom_y();
|
||||
let line_ending_space = if is_last_line {
|
||||
self.style.ellipsis_width()
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
let remaining_width = self.bounds.x1 - cursor.x;
|
||||
let span = Span::fit_horizontally(
|
||||
remaining_text,
|
||||
remaining_width,
|
||||
self.style.text_font,
|
||||
self.style.line_breaking,
|
||||
line_ending_space,
|
||||
);
|
||||
|
||||
cursor.x += match self.align {
|
||||
@ -228,9 +289,11 @@ impl TextLayout {
|
||||
if !remaining_text.is_empty() {
|
||||
// Append ellipsis to indicate more content is available, but only if we
|
||||
// haven't already appended a hyphen.
|
||||
let should_append_ellipsis =
|
||||
matches!(self.style.page_breaking, PageBreaking::CutAndInsertEllipsis)
|
||||
&& !span.insert_hyphen_before_line_break;
|
||||
let should_append_ellipsis = matches!(
|
||||
self.style.page_breaking,
|
||||
PageBreaking::CutAndInsertEllipsis
|
||||
| PageBreaking::CutAndInsertEllipsisBoth
|
||||
) && !span.insert_hyphen_before_line_break;
|
||||
if should_append_ellipsis {
|
||||
sink.ellipsis(*cursor, self);
|
||||
}
|
||||
@ -270,6 +333,10 @@ impl TextLayout {
|
||||
+ (end_cursor.y - init_cursor.y)
|
||||
+ self.padding_bottom
|
||||
}
|
||||
|
||||
fn bottom_y(&self) -> i16 {
|
||||
(self.bounds.y1 - self.padding_bottom).max(self.bounds.y0)
|
||||
}
|
||||
}
|
||||
|
||||
impl Dimensions for TextLayout {
|
||||
@ -303,6 +370,7 @@ pub trait LayoutSink {
|
||||
fn text(&mut self, _cursor: Point, _layout: &TextLayout, _text: &str) {}
|
||||
fn hyphen(&mut self, _cursor: Point, _layout: &TextLayout) {}
|
||||
fn ellipsis(&mut self, _cursor: Point, _layout: &TextLayout) {}
|
||||
fn prev_page_ellipsis(&mut self, _cursor: Point, _layout: &TextLayout) {}
|
||||
fn line_break(&mut self, _cursor: Point) {}
|
||||
fn out_of_bounds(&mut self) {}
|
||||
}
|
||||
@ -335,13 +403,41 @@ impl LayoutSink for TextRenderer {
|
||||
}
|
||||
|
||||
fn ellipsis(&mut self, cursor: Point, layout: &TextLayout) {
|
||||
display::text(
|
||||
cursor,
|
||||
"...",
|
||||
layout.style.text_font,
|
||||
layout.style.ellipsis_color,
|
||||
layout.style.background_color,
|
||||
);
|
||||
if let Some(icon) = layout.style.ellipsis_icon {
|
||||
icon.draw(
|
||||
cursor,
|
||||
BOTTOM_LEFT,
|
||||
layout.style.ellipsis_color,
|
||||
layout.style.background_color,
|
||||
);
|
||||
} else {
|
||||
display::text(
|
||||
cursor,
|
||||
ELLIPSIS,
|
||||
layout.style.text_font,
|
||||
layout.style.ellipsis_color,
|
||||
layout.style.background_color,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn prev_page_ellipsis(&mut self, cursor: Point, layout: &TextLayout) {
|
||||
if let Some(icon) = layout.style.prev_page_ellipsis_icon {
|
||||
icon.draw(
|
||||
cursor,
|
||||
BOTTOM_LEFT,
|
||||
layout.style.ellipsis_color,
|
||||
layout.style.background_color,
|
||||
);
|
||||
} else {
|
||||
display::text(
|
||||
cursor,
|
||||
ELLIPSIS,
|
||||
layout.style.text_font,
|
||||
layout.style.ellipsis_color,
|
||||
layout.style.background_color,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -366,6 +462,10 @@ pub mod trace {
|
||||
self.0.string("...");
|
||||
}
|
||||
|
||||
fn prev_page_ellipsis(&mut self, _cursor: Point, _layout: &TextLayout) {
|
||||
self.0.string("...");
|
||||
}
|
||||
|
||||
fn line_break(&mut self, _cursor: Point) {
|
||||
self.0.string("\n");
|
||||
}
|
||||
@ -425,6 +525,7 @@ impl Span {
|
||||
max_width: i16,
|
||||
text_font: impl GlyphMetrics,
|
||||
breaking: LineBreaking,
|
||||
line_ending_space: i16,
|
||||
) -> Self {
|
||||
const ASCII_LF: char = '\n';
|
||||
const ASCII_CR: char = '\r';
|
||||
@ -435,12 +536,31 @@ impl Span {
|
||||
ch == ASCII_SPACE || ch == ASCII_LF || ch == ASCII_CR
|
||||
}
|
||||
|
||||
let use_hyphens = !matches!(breaking, LineBreaking::BreakWordsNoHyphen);
|
||||
let hyphen_width = if use_hyphens {
|
||||
// Checking if the trimmed text fits the line - the whitespace is not being
|
||||
// drawn, so we don't need to account for it.
|
||||
let fits_completely = text_font.text_width(text.trim_end()) <= max_width;
|
||||
let mut use_hyphens = !matches!(breaking, LineBreaking::BreakWordsNoHyphen);
|
||||
|
||||
// How much space we need to left unused at the end of the line
|
||||
// (e.g. for the line-ending hyphen or page-ending ellipsis).
|
||||
// Differs for incomplete and complete words (incomplete need
|
||||
// to account for a possible hyphen).
|
||||
let incomplete_word_end_width = if fits_completely {
|
||||
use_hyphens = false;
|
||||
0
|
||||
} else if line_ending_space > 0 {
|
||||
use_hyphens = false;
|
||||
line_ending_space
|
||||
} else if use_hyphens {
|
||||
text_font.char_width(ASCII_HYPHEN)
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let complete_word_end_width = if fits_completely {
|
||||
0
|
||||
} else {
|
||||
line_ending_space
|
||||
};
|
||||
|
||||
// The span we return in case the line has to break. We mutate it in the
|
||||
// possible break points, and its initial value is returned in case no text
|
||||
@ -463,7 +583,7 @@ impl Span {
|
||||
let char_width = text_font.char_width(ch);
|
||||
|
||||
// Consider if we could be breaking the line at this position.
|
||||
if is_whitespace(ch) {
|
||||
if is_whitespace(ch) && span_width + complete_word_end_width <= max_width {
|
||||
// Break before the whitespace, without hyphen.
|
||||
line.length = i;
|
||||
line.advance.x = span_width;
|
||||
@ -483,7 +603,8 @@ impl Span {
|
||||
// Return the last breakpoint.
|
||||
return line;
|
||||
} else {
|
||||
let have_space_for_break = span_width + char_width + hyphen_width <= max_width;
|
||||
let have_space_for_break =
|
||||
span_width + char_width + incomplete_word_end_width <= max_width;
|
||||
let can_break_word =
|
||||
!matches!(breaking, LineBreaking::BreakAtWhitespace) || !found_any_whitespace;
|
||||
if have_space_for_break && can_break_word {
|
||||
@ -528,6 +649,10 @@ mod tests {
|
||||
fn line_height(&self) -> i16 {
|
||||
self.height
|
||||
}
|
||||
|
||||
fn text_width(&self, text: &str) -> i16 {
|
||||
self.width * text.len() as i16
|
||||
}
|
||||
}
|
||||
|
||||
const FIXED_FONT: Fixed = Fixed {
|
||||
@ -591,6 +716,7 @@ mod tests {
|
||||
max_width,
|
||||
FIXED_FONT,
|
||||
LineBreaking::BreakAtWhitespace,
|
||||
0,
|
||||
);
|
||||
spans.push((
|
||||
&remaining_text[..span.length],
|
||||
|
@ -392,6 +392,9 @@ impl PageOffset {
|
||||
|
||||
// Find out the dimensions of the paragraph at given char offset.
|
||||
let mut layout = paragraph.layout(area);
|
||||
if self.chr > 0 {
|
||||
layout.continues_from_prev_page = true;
|
||||
}
|
||||
let fit = layout.fit_text(paragraph.content.as_ref());
|
||||
let (used, remaining_area) = area.split_top(fit.height());
|
||||
layout.bounds = used;
|
||||
|
@ -114,9 +114,12 @@ pub struct Icon {
|
||||
}
|
||||
|
||||
impl Icon {
|
||||
pub fn new(data: &'static [u8]) -> Self {
|
||||
let toif = unwrap!(Toif::new(data));
|
||||
assert!(toif.format() == ToifFormat::GrayScaleEH);
|
||||
pub const fn new(data: &'static [u8]) -> Self {
|
||||
let toif = match Toif::new(data) {
|
||||
Some(t) => t,
|
||||
None => panic!("Invalid image."),
|
||||
};
|
||||
assert!(matches!(toif.format(), ToifFormat::GrayScaleEH));
|
||||
Self { toif }
|
||||
}
|
||||
|
||||
|
@ -497,8 +497,8 @@ mod tests {
|
||||
);
|
||||
page.place(SCREEN);
|
||||
|
||||
let expected1 = "<SwipePage active_page:0 page_count:2 content:<Paragraphs This is somewhat long\nparagraph that goes on\nand on and on and on\nand on and will definitely\nnot fit on just a single\nscreen. You have to\nswipe a bit to see all the\ntext it contains I guess....\n> buttons:<FixedHeightBar inner:<Button text:NO > > >";
|
||||
let expected2 = "<SwipePage active_page:1 page_count:2 content:<Paragraphs There's just so much\nletters in it.\n> buttons:<FixedHeightBar inner:<Button text:NO > > >";
|
||||
let expected1 = "<SwipePage active_page:0 page_count:2 content:<Paragraphs This is somewhat long\nparagraph that goes on\nand on and on and on\nand on and will definitely\nnot fit on just a single\nscreen. You have to\nswipe a bit to see all the\ntext it contains I...\n> buttons:<FixedHeightBar inner:<Button text:NO > > >";
|
||||
let expected2 = "<SwipePage active_page:1 page_count:2 content:<Paragraphs guess. There's just so\nmuch letters in it.\n> buttons:<FixedHeightBar inner:<Button text:NO > > >";
|
||||
|
||||
assert_eq!(trace(&page), expected1);
|
||||
swipe_down(&mut page);
|
||||
@ -533,9 +533,9 @@ mod tests {
|
||||
);
|
||||
page.place(SCREEN);
|
||||
|
||||
let expected1 = "<SwipePage active_page:0 page_count:3 content:<Paragraphs This paragraph is using a\nbold font. It doesn't\nneed to be all that long.\nAnd this one is\nusing MONO. Mono\nspace is nice fo\nr numbers, they\n> buttons:<FixedHeightBar inner:<Button text:IDK > > >";
|
||||
let expected2 = "<SwipePage active_page:1 page_count:3 content:<Paragraphs have the same wi\ndth and can be s\ncanned quickly.\nEven if they spa\nn several pages\nor something.\nLet's add another one...\n> buttons:<FixedHeightBar inner:<Button text:IDK > > >";
|
||||
let expected3 = "<SwipePage active_page:2 page_count:3 content:<Paragraphs for a good measure. This\none should overflow all\nthe way to the third\npage with a bit of luck.\n> buttons:<FixedHeightBar inner:<Button text:IDK > > >";
|
||||
let expected1 = "<SwipePage active_page:0 page_count:3 content:<Paragraphs This paragraph is using a\nbold font. It doesn't\nneed to be all that long.\nAnd this one is\nusing MONO. Mono\nspace is nice fo\nr numbers,...\n> buttons:<FixedHeightBar inner:<Button text:IDK > > >";
|
||||
let expected2 = "<SwipePage active_page:1 page_count:3 content:<Paragraphs ...they have th\ne same width and\ncan be scanned q\nuickly. Even if\nthey span severa\nl pages or somet\nhing.\n> buttons:<FixedHeightBar inner:<Button text:IDK > > >";
|
||||
let expected3 = "<SwipePage active_page:2 page_count:3 content:<Paragraphs Let's add another one\nfor a good measure. This\none should overflow all\nthe way to the third\npage with a bit of luck.\n> buttons:<FixedHeightBar inner:<Button text:IDK > > >";
|
||||
|
||||
assert_eq!(trace(&page), expected1);
|
||||
swipe_down(&mut page);
|
||||
|
BIN
core/embed/rust/src/ui/model_tt/res/page-next.toif
Normal file
BIN
core/embed/rust/src/ui/model_tt/res/page-next.toif
Normal file
Binary file not shown.
BIN
core/embed/rust/src/ui/model_tt/res/page-prev.toif
Normal file
BIN
core/embed/rust/src/ui/model_tt/res/page-prev.toif
Normal file
Binary file not shown.
@ -5,7 +5,7 @@ use crate::{
|
||||
text::{formatted::FormattedFonts, LineBreaking, PageBreaking, TextStyle},
|
||||
FixedHeightBar,
|
||||
},
|
||||
display::{Color, Font},
|
||||
display::{Color, Font, Icon},
|
||||
geometry::Insets,
|
||||
},
|
||||
};
|
||||
@ -68,6 +68,8 @@ pub const ICON_LOCK: &[u8] = include_res!("model_tt/res/lock.toif");
|
||||
pub const ICON_LOGO: &[u8] = include_res!("model_tt/res/logo.toif");
|
||||
pub const ICON_SUCCESS_SMALL: &[u8] = include_res!("model_tt/res/success_bld.toif");
|
||||
pub const ICON_WARN_SMALL: &[u8] = include_res!("model_tt/res/warn_bld.toif");
|
||||
pub const ICON_PAGE_NEXT: &[u8] = include_res!("model_tt/res/page-next.toif");
|
||||
pub const ICON_PAGE_PREV: &[u8] = include_res!("model_tt/res/page-prev.toif");
|
||||
|
||||
// Large, three-color icons.
|
||||
pub const WARN_COLOR: Color = YELLOW;
|
||||
@ -389,7 +391,9 @@ pub const TEXT_DEMIBOLD: TextStyle = TextStyle::new(Font::DEMIBOLD, FG, BG, GREY
|
||||
pub const TEXT_BOLD: TextStyle = TextStyle::new(Font::BOLD, FG, BG, GREY_LIGHT, 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);
|
||||
.with_page_breaking(PageBreaking::CutAndInsertEllipsisBoth)
|
||||
.with_ellipsis_icon(Icon::new(ICON_PAGE_NEXT))
|
||||
.with_prev_page_icon(Icon::new(ICON_PAGE_PREV));
|
||||
|
||||
/// Convert Python-side numeric id to a `TextStyle`.
|
||||
pub fn textstyle_number(num: i32) -> &'static TextStyle {
|
||||
|
Loading…
Reference in New Issue
Block a user