From 10650af1faa7c72e79d4680b5a33eb4412ec3fee Mon Sep 17 00:00:00 2001 From: Martin Milata Date: Fri, 14 Jan 2022 18:53:22 +0100 Subject: [PATCH] refactor(core/rust/ui): paragraphs breaking Also fix corner case. [no changelog] --- .../rust/src/ui/component/text/layout.rs | 6 +- .../rust/src/ui/component/text/paragraphs.rs | 175 ++++++++++-------- .../rust/src/ui/model_tt/component/page.rs | 1 + 3 files changed, 106 insertions(+), 76 deletions(-) diff --git a/core/embed/rust/src/ui/component/text/layout.rs b/core/embed/rust/src/ui/component/text/layout.rs index 6ba971d70..913b71a8d 100644 --- a/core/embed/rust/src/ui/component/text/layout.rs +++ b/core/embed/rust/src/ui/component/text/layout.rs @@ -230,14 +230,16 @@ impl TextLayout { pub fn measure_ops_height(self, ops: &mut dyn Iterator) -> i32 { match self.layout_ops(ops, &mut self.initial_cursor(), &mut TextNoOp) { 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 { match self.layout_text(text, &mut self.initial_cursor(), &mut TextNoOp) { LayoutFit::Fitting { size, .. } => size.y, - LayoutFit::OutOfBounds { .. } => self.bounds.height(), + LayoutFit::OutOfBounds { processed_chars: 0 } => 0, + _ => self.bounds.height(), } } } diff --git a/core/embed/rust/src/ui/component/text/paragraphs.rs b/core/embed/rust/src/ui/component/text/paragraphs.rs index 6fe1f2d6a..410593af2 100644 --- a/core/embed/rust/src/ui/component/text/paragraphs.rs +++ b/core/embed/rust/src/ui/component/text/paragraphs.rs @@ -3,7 +3,7 @@ use heapless::Vec; use crate::ui::{ component::{Component, Event, EventCtx, Never, Paginate}, display::Font, - geometry::{Dimensions, LinearLayout, Offset, Rect}, + geometry::{Dimensions, Insets, LinearLayout, Offset, Rect}, }; use super::layout::{DefaultTextTheme, LayoutFit, TextLayout, TextNoOp, TextRenderer}; @@ -15,8 +15,8 @@ pub struct Paragraphs { area: Rect, list: Vec, MAX_PARAGRAPHS>, layout: LinearLayout, - para_offset: usize, - char_offset: usize, + offset: PageOffset, + visible: usize, } impl Paragraphs @@ -30,8 +30,8 @@ where layout: LinearLayout::vertical() .align_at_center() .with_spacing(DEFAULT_SPACING), - para_offset: 0, - char_offset: 0, + offset: PageOffset::default(), + visible: 0, } } @@ -63,11 +63,38 @@ where 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(¶graph.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 { paragraphs: self, - para_offset: 0, - char_offset: 0, + current: None, } } } @@ -83,16 +110,13 @@ where } fn paint(&mut self) { - let mut char_offset = self.char_offset; - for paragraph in self.list.iter().skip(self.para_offset) { - let fit = paragraph.layout.layout_text( + let mut char_offset = self.offset.chr; + for paragraph in self.list.iter().skip(self.offset.par).take(self.visible) { + paragraph.layout.layout_text( ¶graph.content.as_ref()[char_offset..], &mut paragraph.layout.initial_cursor(), &mut TextRenderer, ); - if matches!(fit, LayoutFit::OutOfBounds { .. }) { - break; - } char_offset = 0; } } @@ -120,6 +144,13 @@ where } 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 { @@ -149,75 +180,81 @@ where } } +#[derive(Clone, Copy, Default, PartialEq, Eq)] +struct PageOffset { + /// Index of paragraph. + par: usize, + + /// Index of character in the paragraph. + chr: usize, +} + struct PageBreakIterator<'a, T> { - paragraphs: &'a mut Paragraphs, - para_offset: usize, - char_offset: usize, + /// Reference to paragraph vector. + paragraphs: &'a Paragraphs, + + /// Current offset, or `None` before first `next()` call. + current: Option, } -/// Yields indices to beginnings of successive pages. As a side effect it -/// updates the bounding box of each paragraph on the page. Because a paragraph -/// can be rendered on multiple pages, such bounding boxes are only valid for -/// paragraphs processed in the last call to `next`. -/// -/// The boxes are simply stacked below each other and may be further arranged -/// 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> where T: AsRef<[u8]>, { - /// Paragraph index, character index, number of paragraphs shown. - type Item = (usize, usize, usize); + /// `PageOffset` denotes the first paragraph that is rendered and a + /// character offset in that paragraph. + type Item = PageOffset; fn next(&mut self) -> Option { - if self.para_offset >= self.paragraphs.list.len() { - return None; + let first = self.current.is_none(); + let current = self.current.get_or_insert_with(PageOffset::default); + if first { + return self.current; } - let old_para_offset = self.para_offset; - let old_char_offset = self.char_offset; - let mut area = self.paragraphs.area; + let mut remaining_area = self.paragraphs.area; + let mut progress = false; - for paragraph in self.paragraphs.list.iter_mut().skip(self.para_offset) { + for paragraph in self.paragraphs.list.iter().skip(current.par) { loop { - paragraph.set_area(area); - let fit = paragraph.layout.layout_text( - ¶graph.content.as_ref()[self.char_offset..], - &mut paragraph.layout.initial_cursor(), + let mut temp_layout = paragraph.layout; + temp_layout.bounds = remaining_area; + + let fit = temp_layout.layout_text( + ¶graph.content.as_ref()[current.chr..], + &mut temp_layout.initial_cursor(), &mut TextNoOp, ); match fit { LayoutFit::Fitting { size, .. } => { - // Text fits, update the bounding box. - let (used, free) = area.split_top(size.y); - paragraph.set_area(used); - // Continue with next paragraph in remaining space. - area = free; - self.char_offset = 0; - self.para_offset += 1; + // Text fits, update remaining area. + remaining_area = remaining_area.inset(Insets::top(size.y)); + + // Continue with start of next paragraph. + current.par += 1; + current.chr = 0; + progress = true; break; } LayoutFit::OutOfBounds { processed_chars } => { // Text does not fit, assume whatever fits takes the entire remaining area. - self.char_offset += processed_chars; - let visible = if processed_chars > 0 { - self.para_offset - old_para_offset + 1 - } else { - self.para_offset - old_para_offset - }; - // Return pointer to start of page. - return Some((old_para_offset, old_char_offset, visible)); + current.chr += processed_chars; + if processed_chars == 0 && !progress { + // Nothing fits yet page is empty: terminate iterator to avoid looping + // forever. + return None; + } + // Return current offset. + return self.current; } } } } // Last page. - Some(( - old_para_offset, - old_char_offset, - self.para_offset - old_para_offset, - )) + None } } @@ -227,29 +264,19 @@ where { fn page_count(&mut self) -> usize { // There's always at least one page. - let page_count = self.break_pages().count().max(1); - - // Reset to first page. - self.change_page(0); - - page_count + self.break_pages().count().max(1) } fn change_page(&mut self, to_page: usize) { - if let Some((para_offset, char_offset, para_visible)) = - self.break_pages().skip(to_page).next() - { - // 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); + if let Some(offset) = self.break_pages().skip(to_page).next() { + self.change_offset(offset) } else { // Should not happen, set index past last paragraph to render empty page. - self.para_offset = self.list.len(); - self.char_offset = 0; + self.offset = PageOffset { + par: self.list.len(), + chr: 0, + }; + self.visible = 0; } } } diff --git a/core/embed/rust/src/ui/model_tt/component/page.rs b/core/embed/rust/src/ui/model_tt/component/page.rs index d7f8387fc..0ac163077 100644 --- a/core/embed/rust/src/ui/model_tt/component/page.rs +++ b/core/embed/rust/src/ui/model_tt/component/page.rs @@ -62,6 +62,7 @@ where // Reduce area to make space for scrollbar if it doesn't fit. content.set_area(layout.content); } + content.change_page(0); content }