You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
trezor-firmware/core/embed/rust/src/ui/component/text/iter.rs

280 lines
7.5 KiB

use crate::ui::{component::LineBreaking, display::Font};
use core::iter;
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
struct LineBreak {
/// Index of character **after** the line-break.
offset: usize,
/// Distance from the last line-break of the sequence, in pixels.
width: i32,
style: BreakStyle,
}
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
enum BreakStyle {
Hard,
AtWhitespaceOrWordBoundary,
InsideWord,
}
fn limit_line_breaks(
breaks: impl Iterator<Item = LineBreak>,
line_height: i32,
available_height: i32,
) -> impl Iterator<Item = LineBreak> {
breaks.take(available_height as usize / line_height as usize)
}
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
enum Appendix {
None,
Hyphen,
}
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
struct Span<'a> {
text: &'a str,
append: Appendix,
}
fn break_text_to_spans(
text: &str,
text_font: impl GlyphMetrics,
hyphen_font: impl GlyphMetrics,
breaking: LineBreaking,
available_width: i32,
) -> impl Iterator<Item = Span> {
let mut finished = false;
let mut last_break = LineBreak {
offset: 0,
width: 0,
style: BreakStyle::AtWhitespaceOrWordBoundary,
};
let mut breaks = select_line_breaks(
text.char_indices(),
text_font,
hyphen_font,
breaking,
available_width,
);
iter::from_fn(move || {
if finished {
None
} else if let Some(lb) = breaks.next() {
let start_of_line = last_break.offset;
let end_of_line = lb.offset; // Not inclusive.
last_break = lb;
if let BreakStyle::AtWhitespaceOrWordBoundary = lb.style {
last_break.offset += 1;
}
Some(Span {
text: &text[start_of_line..end_of_line],
append: match lb.style {
BreakStyle::Hard | BreakStyle::AtWhitespaceOrWordBoundary => Appendix::None,
BreakStyle::InsideWord => Appendix::Hyphen,
},
})
} else {
finished = true;
Some(Span {
text: &text[last_break.offset..],
append: Appendix::None,
})
}
})
}
fn select_line_breaks(
chars: impl Iterator<Item = (usize, char)>,
text_font: impl GlyphMetrics,
hyphen_font: impl GlyphMetrics,
breaking: LineBreaking,
available_width: i32,
) -> impl Iterator<Item = LineBreak> {
let hyphen_width = hyphen_font.char_width('-');
let mut proposed = None;
let mut line_width = 0;
let mut found_any_whitespace = false;
chars.filter_map(move |(offset, ch)| {
let char_width = text_font.char_width(ch);
let exceeds_available_width = line_width + char_width > available_width;
let have_space_for_break = line_width + char_width + hyphen_width <= available_width;
let can_break_word =
matches!(breaking, LineBreaking::BreakWordsAndInsertHyphen) || !found_any_whitespace;
let break_line = match ch {
'\n' | '\r' => {
// Immediate hard break.
Some(LineBreak {
offset,
width: line_width,
style: BreakStyle::Hard,
})
}
' ' | '\t' => {
// Whitespace, propose a line-break before this character.
proposed = Some(LineBreak {
offset,
width: line_width,
style: BreakStyle::AtWhitespaceOrWordBoundary,
});
found_any_whitespace = true;
None
}
_ if have_space_for_break && can_break_word => {
// Propose a word-break after this character. In case the next character is
// whitespace, the proposed word break is replaced by a whitespace break.
proposed = Some(LineBreak {
offset: offset + 1,
width: line_width + char_width + hyphen_width,
style: BreakStyle::InsideWord,
});
None
}
_ if exceeds_available_width => {
// Consume the last proposed line-break. In case we don't have anything
// proposed, we hard-break immediately before this character. This only happens
// if the first character of the line doesn't fit.
Some(proposed.unwrap_or(LineBreak {
offset,
width: line_width,
style: BreakStyle::Hard,
}))
}
_ => None,
};
if break_line.is_some() {
// Reset the state.
proposed = None;
line_width = 0;
found_any_whitespace = false;
} else {
// Shift cursor.
line_width += char_width;
}
break_line
})
}
trait GlyphMetrics {
fn char_width(&self, ch: char) -> i32;
fn line_height(&self) -> i32;
}
impl GlyphMetrics for Font {
fn char_width(&self, ch: char) -> i32 {
Font::char_width(*self, ch)
}
fn line_height(&self) -> i32 {
Font::line_height(*self)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_selected_line_breaks() {
assert_eq!(line_breaks("abcd ef", 34), vec![inside_word(2, 25)]);
}
#[test]
fn test_break_text() {
assert_eq!(
break_text("abcd ef", 24),
vec![
Span {
text: "a",
append: Appendix::Hyphen
},
Span {
text: "bcd",
append: Appendix::None
},
Span {
text: "ef",
append: Appendix::None
}
]
)
}
#[derive(Copy, Clone)]
struct Fixed {
width: i32,
height: i32,
}
impl GlyphMetrics for Fixed {
fn char_width(&self, _ch: char) -> i32 {
self.width
}
fn line_height(&self) -> i32 {
self.height
}
}
fn break_text(s: &str, w: i32) -> Vec<Span> {
break_text_to_spans(
s,
Fixed {
width: 10,
height: 10,
},
Fixed {
width: 5,
height: 10,
},
LineBreaking::BreakWordsAndInsertHyphen,
w,
)
.collect::<Vec<_>>()
}
fn line_breaks(s: &str, w: i32) -> Vec<LineBreak> {
select_line_breaks(
s.char_indices(),
Fixed {
width: 10,
height: 10,
},
Fixed {
width: 5,
height: 10,
},
LineBreaking::BreakWordsAndInsertHyphen,
w,
)
.collect::<Vec<_>>()
}
fn hard(offset: usize, width: i32) -> LineBreak {
LineBreak {
offset,
width,
style: BreakStyle::Hard,
}
}
fn whitespace(offset: usize, width: i32) -> LineBreak {
LineBreak {
offset,
width,
style: BreakStyle::AtWhitespaceOrWordBoundary,
}
}
fn inside_word(offset: usize, width: i32) -> LineBreak {
LineBreak {
offset,
width,
style: BreakStyle::InsideWord,
}
}
}