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/op.rs

291 lines
9.6 KiB

use crate::{
strutil::TString,
ui::{
display::{Color, Font},
geometry::{Alignment, Offset, Rect},
util::ResultExt,
},
};
use super::{
layout::{Chunks, LayoutFit, LayoutSink, TextLayout},
LineBreaking, TextStyle,
};
use heapless::Vec;
// So that there is only one implementation, and not multiple generic ones
// as would be via `const N: usize` generics.
const MAX_OPS: usize = 20;
/// To account for operations that are not made of characters
/// but need to be accounted for somehow.
/// Number of processed characters will be increased by this
/// to account for the operation.
const PROCESSED_CHARS_ONE: usize = 1;
#[derive(Clone)]
/// Extension of TextLayout, allowing for Op-based operations
pub struct OpTextLayout<'a> {
pub layout: TextLayout,
ops: Vec<Op<'a>, MAX_OPS>,
}
impl<'a> OpTextLayout<'a> {
pub fn new(style: TextStyle) -> Self {
Self {
layout: TextLayout::new(style),
ops: Vec::new(),
}
}
pub fn is_empty(&self) -> bool {
self.ops.len() == 0
}
pub fn place(&mut self, bounds: Rect) -> Rect {
self.layout.bounds = bounds;
bounds
}
/// Send the layout's content into a sink.
pub fn layout_content(&mut self, skip_bytes: usize, sink: &mut dyn LayoutSink) -> LayoutFit {
self.layout_ops(skip_bytes, Offset::zero(), sink)
}
/// Perform some operations defined on `Op` for a list of those `Op`s
/// - e.g. changing the color, changing the font or rendering the text.
pub fn layout_ops(
&mut self,
skip_bytes: usize,
offset: Offset,
sink: &mut dyn LayoutSink,
) -> LayoutFit {
// TODO: make sure it is called when we have the current font (not sooner)
let cursor = &mut (self.layout.initial_cursor() + offset);
let init_cursor = *cursor;
let mut total_processed_chars = 0;
// Do something when it was not skipped
for op in Self::filter_skipped_ops(self.ops.iter(), skip_bytes) {
match op {
// Changing color
Op::Color(color) => {
self.layout.style.text_color = color;
}
// Changing font
Op::Font(font) => {
self.layout.style.text_font = font;
}
// Changing line/text alignment
Op::Alignment(line_alignment) => {
self.layout.align = line_alignment;
}
// Changing line breaking
Op::LineBreaking(line_breaking) => {
self.layout.style.line_breaking = line_breaking;
}
// Moving the cursor
Op::CursorOffset(offset) => {
cursor.x += offset.x;
cursor.y += offset.y;
}
Op::Chunkify(chunks) => {
self.layout.style.chunks = chunks;
}
Op::LineSpacing(line_spacing) => {
self.layout.style.line_spacing = line_spacing;
}
// Moving to the next page
Op::NextPage => {
// Pretending that nothing more fits on current page to force
// continuing on the next one
total_processed_chars += PROCESSED_CHARS_ONE;
return LayoutFit::OutOfBounds {
processed_chars: total_processed_chars,
height: self.layout.layout_height(init_cursor, *cursor),
};
}
// Drawing text
Op::Text(text, continued) => {
// Try to fit text on the current page and if they do not fit,
// return the appropriate OutOfBounds message
// Inserting the ellipsis at the very beginning of the text if needed
// (just for incomplete texts that were separated)
self.layout.continues_from_prev_page = continued;
let fit = text.map(|t| self.layout.layout_text(t, cursor, sink));
match fit {
LayoutFit::Fitting {
processed_chars, ..
} => {
total_processed_chars += processed_chars;
}
LayoutFit::OutOfBounds {
processed_chars, ..
} => {
total_processed_chars += processed_chars;
return LayoutFit::OutOfBounds {
processed_chars: total_processed_chars,
height: self.layout.layout_height(init_cursor, *cursor),
};
}
}
}
}
}
LayoutFit::Fitting {
processed_chars: total_processed_chars,
height: self.layout.layout_height(init_cursor, *cursor),
}
}
/// Gets rid of all action-Ops that are before the `skip_bytes` threshold.
/// (Not removing the style changes, e.g. Font or Color, because they need
/// to be correctly set for future Text operations.)
fn filter_skipped_ops<'b, I>(
ops_iter: I,
skip_bytes: usize,
) -> impl Iterator<Item = Op<'a>> + 'b
where
I: Iterator<Item = &'b Op<'a>> + 'b,
'a: 'b,
{
let mut skipped = 0;
ops_iter.filter_map(move |op| {
match op {
Op::Text(text, _continued) if skipped < skip_bytes => {
let skip_text_bytes_if_fits_partially = skip_bytes - skipped;
skipped = skipped.saturating_add(text.map(str::len));
if skipped > skip_bytes {
// Fits partially
// Skipping some bytes at the beginning, leaving rest
// Signifying that the text continues from previous page
Some(Op::Text(
text.skip_prefix(skip_text_bytes_if_fits_partially),
true,
))
} else {
// Does not fit at all
None
}
}
Op::NextPage if skipped < skip_bytes => {
skipped = skipped.saturating_add(PROCESSED_CHARS_ONE);
None
}
Op::CursorOffset(_) if skipped < skip_bytes => {
// Skip any offsets
None
}
op_to_pass_through => Some(op_to_pass_through.clone()),
}
})
}
}
// Op-adding operations
impl<'a> OpTextLayout<'a> {
pub fn with_new_item(mut self, item: Op<'a>) -> Self {
self.ops
.push(item)
.assert_if_debugging_ui("Could not push to self.ops - increase MAX_OPS.");
self
}
pub fn text(self, text: TString<'a>) -> Self {
self.with_new_item(Op::Text(text, false))
}
pub fn newline(self) -> Self {
self.text("\n".into())
}
pub fn newline_half(self) -> Self {
self.text("\r".into())
}
pub fn next_page(self) -> Self {
self.with_new_item(Op::NextPage)
}
pub fn font(self, font: Font) -> Self {
self.with_new_item(Op::Font(font))
}
pub fn offset(self, offset: Offset) -> Self {
self.with_new_item(Op::CursorOffset(offset))
}
pub fn alignment(self, alignment: Alignment) -> Self {
self.with_new_item(Op::Alignment(alignment))
}
pub fn line_breaking(self, line_breaking: LineBreaking) -> Self {
self.with_new_item(Op::LineBreaking(line_breaking))
}
pub fn chunks(self, chunks: Option<Chunks>) -> Self {
self.with_new_item(Op::Chunkify(chunks))
}
pub fn line_spacing(self, spacing: i16) -> Self {
self.with_new_item(Op::LineSpacing(spacing))
}
}
// Op-adding aggregation operations
impl<'a> OpTextLayout<'a> {
pub fn text_normal(self, text: impl Into<TString<'a>>) -> Self {
self.font(Font::NORMAL).text(text.into())
}
pub fn text_mono(self, text: impl Into<TString<'a>>) -> Self {
self.font(Font::MONO).text(text.into())
}
pub fn text_bold(self, text: impl Into<TString<'a>>) -> Self {
self.font(Font::BOLD).text(text.into())
}
pub fn text_demibold(self, text: impl Into<TString<'a>>) -> Self {
self.font(Font::DEMIBOLD).text(text.into())
}
pub fn chunkify_text(self, chunks: Option<(Chunks, i16)>) -> Self {
if let Some(chunks) = chunks {
self.chunks(Some(chunks.0)).line_spacing(chunks.1)
} else {
self.chunks(None).line_spacing(0)
}
}
}
#[derive(Clone)]
pub enum Op<'a> {
/// Render text with current color and font.
/// Bool signifies whether this is a split Text Op continued from previous
/// page. If true, a leading ellipsis will be rendered.
Text(TString<'a>, bool),
/// Set current text color.
Color(Color),
/// Set currently used font.
Font(Font),
/// Set currently used line alignment.
Alignment(Alignment),
/// Set currently used line breaking algorithm.
LineBreaking(LineBreaking),
/// Move the current cursor by specified Offset.
CursorOffset(Offset),
/// Force continuing on the next page.
NextPage,
/// Render the following text in a chunkified way. None will disable that.
Chunkify(Option<Chunks>),
/// Change the line vertical line spacing.
LineSpacing(i16),
}