1
0
mirror of https://github.com/trezor/trezor-firmware.git synced 2025-05-31 21:28:46 +00:00

refactor(core/rust/ui): paragraphs breaking

Also fix corner case.

[no changelog]
This commit is contained in:
Martin Milata 2022-01-14 18:53:22 +01:00
parent b5da6dc911
commit 10650af1fa
3 changed files with 107 additions and 77 deletions

View File

@ -230,14 +230,16 @@ impl TextLayout {
pub fn measure_ops_height(self, ops: &mut dyn Iterator<Item = Op>) -> i32 { pub fn measure_ops_height(self, ops: &mut dyn Iterator<Item = Op>) -> i32 {
match self.layout_ops(ops, &mut self.initial_cursor(), &mut TextNoOp) { match self.layout_ops(ops, &mut self.initial_cursor(), &mut TextNoOp) {
LayoutFit::Fitting { size, .. } => size.y, LayoutFit::Fitting { size, .. } => size.y,
LayoutFit::OutOfBounds { .. } => self.bounds.height(), LayoutFit::OutOfBounds { processed_chars: 0 } => 0,
_ => self.bounds.height(),
} }
} }
pub fn measure_text_height(self, text: &[u8]) -> i32 { pub fn measure_text_height(self, text: &[u8]) -> i32 {
match self.layout_text(text, &mut self.initial_cursor(), &mut TextNoOp) { match self.layout_text(text, &mut self.initial_cursor(), &mut TextNoOp) {
LayoutFit::Fitting { size, .. } => size.y, LayoutFit::Fitting { size, .. } => size.y,
LayoutFit::OutOfBounds { .. } => self.bounds.height(), LayoutFit::OutOfBounds { processed_chars: 0 } => 0,
_ => self.bounds.height(),
} }
} }
} }

View File

@ -3,7 +3,7 @@ use heapless::Vec;
use crate::ui::{ use crate::ui::{
component::{Component, Event, EventCtx, Never, Paginate}, component::{Component, Event, EventCtx, Never, Paginate},
display::Font, display::Font,
geometry::{Dimensions, LinearLayout, Offset, Rect}, geometry::{Dimensions, Insets, LinearLayout, Offset, Rect},
}; };
use super::layout::{DefaultTextTheme, LayoutFit, TextLayout, TextNoOp, TextRenderer}; use super::layout::{DefaultTextTheme, LayoutFit, TextLayout, TextNoOp, TextRenderer};
@ -15,8 +15,8 @@ pub struct Paragraphs<T> {
area: Rect, area: Rect,
list: Vec<Paragraph<T>, MAX_PARAGRAPHS>, list: Vec<Paragraph<T>, MAX_PARAGRAPHS>,
layout: LinearLayout, layout: LinearLayout,
para_offset: usize, offset: PageOffset,
char_offset: usize, visible: usize,
} }
impl<T> Paragraphs<T> impl<T> Paragraphs<T>
@ -30,8 +30,8 @@ where
layout: LinearLayout::vertical() layout: LinearLayout::vertical()
.align_at_center() .align_at_center()
.with_spacing(DEFAULT_SPACING), .with_spacing(DEFAULT_SPACING),
para_offset: 0, offset: PageOffset::default(),
char_offset: 0, visible: 0,
} }
} }
@ -63,11 +63,38 @@ where
self self
} }
fn break_pages<'a>(&'a mut self) -> PageBreakIterator<'a, T> { /// Update bounding boxes of paragraphs on the current page. First determine
/// the number of visible paragraphs and their sizes. These are then
/// arranged according to the layout.
fn change_offset(&mut self, offset: PageOffset) {
self.offset = offset;
self.visible = 0;
let mut char_offset = offset.chr;
let mut remaining_area = self.area;
for paragraph in self.list.iter_mut().skip(offset.par) {
paragraph.set_area(remaining_area);
let height = paragraph
.layout
.measure_text_height(&paragraph.content.as_ref()[char_offset..]);
if height == 0 {
break;
}
let (used, free) = remaining_area.split_top(height);
paragraph.set_area(used);
remaining_area = free;
self.visible += 1;
char_offset = 0;
}
let visible_paragraphs = &mut self.list[offset.par..offset.par + self.visible];
self.layout.arrange(self.area, visible_paragraphs);
}
fn break_pages<'a>(&'a self) -> PageBreakIterator<'a, T> {
PageBreakIterator { PageBreakIterator {
paragraphs: self, paragraphs: self,
para_offset: 0, current: None,
char_offset: 0,
} }
} }
} }
@ -83,16 +110,13 @@ where
} }
fn paint(&mut self) { fn paint(&mut self) {
let mut char_offset = self.char_offset; let mut char_offset = self.offset.chr;
for paragraph in self.list.iter().skip(self.para_offset) { for paragraph in self.list.iter().skip(self.offset.par).take(self.visible) {
let fit = paragraph.layout.layout_text( paragraph.layout.layout_text(
&paragraph.content.as_ref()[char_offset..], &paragraph.content.as_ref()[char_offset..],
&mut paragraph.layout.initial_cursor(), &mut paragraph.layout.initial_cursor(),
&mut TextRenderer, &mut TextRenderer,
); );
if matches!(fit, LayoutFit::OutOfBounds { .. }) {
break;
}
char_offset = 0; char_offset = 0;
} }
} }
@ -120,6 +144,13 @@ where
} }
t.close(); t.close();
} }
fn bounds(&self, sink: &dyn Fn(Rect)) {
sink(self.area);
for paragraph in self.list.iter().skip(self.offset.par).take(self.visible) {
paragraph.bounds(sink);
}
}
} }
pub struct Paragraph<T> { pub struct Paragraph<T> {
@ -149,75 +180,81 @@ where
} }
} }
struct PageBreakIterator<'a, T> { #[derive(Clone, Copy, Default, PartialEq, Eq)]
paragraphs: &'a mut Paragraphs<T>, struct PageOffset {
para_offset: usize, /// Index of paragraph.
char_offset: usize, par: usize,
/// Index of character in the paragraph.
chr: usize,
} }
/// Yields indices to beginnings of successive pages. As a side effect it struct PageBreakIterator<'a, T> {
/// updates the bounding box of each paragraph on the page. Because a paragraph /// Reference to paragraph vector.
/// can be rendered on multiple pages, such bounding boxes are only valid for paragraphs: &'a Paragraphs<T>,
/// paragraphs processed in the last call to `next`.
/// /// Current offset, or `None` before first `next()` call.
/// The boxes are simply stacked below each other and may be further arranged current: Option<PageOffset>,
/// before drawing. }
/// Yields indices to beginnings of successive pages. First value is always
/// `PageOffset { 0, 0 }` even if the paragraph vector is empty.
impl<'a, T> Iterator for PageBreakIterator<'a, T> impl<'a, T> Iterator for PageBreakIterator<'a, T>
where where
T: AsRef<[u8]>, T: AsRef<[u8]>,
{ {
/// Paragraph index, character index, number of paragraphs shown. /// `PageOffset` denotes the first paragraph that is rendered and a
type Item = (usize, usize, usize); /// character offset in that paragraph.
type Item = PageOffset;
fn next(&mut self) -> Option<Self::Item> { fn next(&mut self) -> Option<Self::Item> {
if self.para_offset >= self.paragraphs.list.len() { let first = self.current.is_none();
return None; let current = self.current.get_or_insert_with(PageOffset::default);
if first {
return self.current;
} }
let old_para_offset = self.para_offset; let mut remaining_area = self.paragraphs.area;
let old_char_offset = self.char_offset; let mut progress = false;
let mut area = self.paragraphs.area;
for paragraph in self.paragraphs.list.iter_mut().skip(self.para_offset) { for paragraph in self.paragraphs.list.iter().skip(current.par) {
loop { loop {
paragraph.set_area(area); let mut temp_layout = paragraph.layout;
let fit = paragraph.layout.layout_text( temp_layout.bounds = remaining_area;
&paragraph.content.as_ref()[self.char_offset..],
&mut paragraph.layout.initial_cursor(), let fit = temp_layout.layout_text(
&paragraph.content.as_ref()[current.chr..],
&mut temp_layout.initial_cursor(),
&mut TextNoOp, &mut TextNoOp,
); );
match fit { match fit {
LayoutFit::Fitting { size, .. } => { LayoutFit::Fitting { size, .. } => {
// Text fits, update the bounding box. // Text fits, update remaining area.
let (used, free) = area.split_top(size.y); remaining_area = remaining_area.inset(Insets::top(size.y));
paragraph.set_area(used);
// Continue with next paragraph in remaining space. // Continue with start of next paragraph.
area = free; current.par += 1;
self.char_offset = 0; current.chr = 0;
self.para_offset += 1; progress = true;
break; break;
} }
LayoutFit::OutOfBounds { processed_chars } => { LayoutFit::OutOfBounds { processed_chars } => {
// Text does not fit, assume whatever fits takes the entire remaining area. // Text does not fit, assume whatever fits takes the entire remaining area.
self.char_offset += processed_chars; current.chr += processed_chars;
let visible = if processed_chars > 0 { if processed_chars == 0 && !progress {
self.para_offset - old_para_offset + 1 // Nothing fits yet page is empty: terminate iterator to avoid looping
} else { // forever.
self.para_offset - old_para_offset return None;
}; }
// Return pointer to start of page. // Return current offset.
return Some((old_para_offset, old_char_offset, visible)); return self.current;
} }
} }
} }
} }
// Last page. // Last page.
Some(( None
old_para_offset,
old_char_offset,
self.para_offset - old_para_offset,
))
} }
} }
@ -227,29 +264,19 @@ where
{ {
fn page_count(&mut self) -> usize { fn page_count(&mut self) -> usize {
// There's always at least one page. // There's always at least one page.
let page_count = self.break_pages().count().max(1); self.break_pages().count().max(1)
// Reset to first page.
self.change_page(0);
page_count
} }
fn change_page(&mut self, to_page: usize) { fn change_page(&mut self, to_page: usize) {
if let Some((para_offset, char_offset, para_visible)) = if let Some(offset) = self.break_pages().skip(to_page).next() {
self.break_pages().skip(to_page).next() self.change_offset(offset)
{
// Set offsets used by `paint`.
self.para_offset = para_offset;
self.char_offset = char_offset;
// Arrange visible paragraphs.
let visible = &mut self.list[para_offset..para_offset + para_visible];
self.layout.arrange(self.area, visible);
} else { } else {
// Should not happen, set index past last paragraph to render empty page. // Should not happen, set index past last paragraph to render empty page.
self.para_offset = self.list.len(); self.offset = PageOffset {
self.char_offset = 0; par: self.list.len(),
chr: 0,
};
self.visible = 0;
} }
} }
} }

View File

@ -62,6 +62,7 @@ where
// Reduce area to make space for scrollbar if it doesn't fit. // Reduce area to make space for scrollbar if it doesn't fit.
content.set_area(layout.content); content.set_area(layout.content);
} }
content.change_page(0);
content content
} }