diff --git a/core/embed/rust/src/lib.rs b/core/embed/rust/src/lib.rs index ec8678390..5f57eab3a 100644 --- a/core/embed/rust/src/lib.rs +++ b/core/embed/rust/src/lib.rs @@ -15,9 +15,10 @@ mod trezorhal; #[cfg(feature = "ui")] #[macro_use] -mod ui; +pub mod ui; mod util; +#[cfg(not(test))] #[cfg(not(feature = "test"))] #[panic_handler] fn panic(_info: &core::panic::PanicInfo) -> ! { diff --git a/core/embed/rust/src/micropython/buffer.rs b/core/embed/rust/src/micropython/buffer.rs index 27d672365..1ad33b75f 100644 --- a/core/embed/rust/src/micropython/buffer.rs +++ b/core/embed/rust/src/micropython/buffer.rs @@ -22,6 +22,12 @@ pub struct Buffer { len: usize, } +impl Buffer { + pub fn empty() -> Self { + Self::from("") + } +} + impl TryFrom for Buffer { type Error = Error; @@ -35,6 +41,12 @@ impl TryFrom for Buffer { } } +impl Default for Buffer { + fn default() -> Self { + Self::empty() + } +} + impl Deref for Buffer { type Target = [u8]; diff --git a/core/embed/rust/src/ui/component/base.rs b/core/embed/rust/src/ui/component/base.rs index aa69f452c..7accad4e7 100644 --- a/core/embed/rust/src/ui/component/base.rs +++ b/core/embed/rust/src/ui/component/base.rs @@ -6,7 +6,10 @@ use heapless::Vec; use crate::ui::model_t1::event::ButtonEvent; #[cfg(feature = "model_tt")] use crate::ui::model_tt::event::TouchEvent; -use crate::{time::Duration, ui::geometry::Rect}; +use crate::{ + time::Duration, + ui::{component::Map, geometry::Rect}, +}; /// Type used by components that do not return any messages. /// @@ -14,12 +17,38 @@ use crate::{time::Duration, ui::geometry::Rect}; pub enum Never {} /// User interface is composed of components that can react to `Event`s through -/// the `event` method and know how to paint themselves to screen through the +/// the `event` method, and know how to paint themselves to screen through the /// `paint` method. Components can emit messages as a reaction to events. pub trait Component { type Msg; + + /// Position the component into some available space, specified by `bounds`. + /// + /// Component should lay itself out, together with all children, and return + /// the total bounding box. This area can, occasionally, be larger than + /// `bounds` (it is a soft-limit), but the component **should never** paint + /// outside of it. + /// + /// No painting should be done in this phase. + fn place(&mut self, bounds: Rect) -> Rect; + + /// React to an outside event. See the `Event` type for possible cases. + /// + /// Component should modify its internal state as a response to the event, + /// and usually call `EventCtx::request_paint` to mark itself for painting. + /// Component can also optionally return a message as a result of the + /// interaction. + /// + /// No painting should be done in this phase. fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option; + + /// Render to screen, based on current internal state. + /// + /// To prevent unnecessary over-draw, dirty state checking is performed in + /// the `Child` wrapper. fn paint(&mut self); + + /// Report current paint bounds of this component. Used for debugging. fn bounds(&self, _sink: &mut dyn FnMut(Rect)) {} } @@ -78,6 +107,10 @@ where { type Msg = T::Msg; + fn place(&mut self, bounds: Rect) -> Rect { + self.component.place(bounds) + } + fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { self.mutate(ctx, |ctx, c| { // Handle the internal invalidation event here, so components don't have to. We @@ -112,7 +145,47 @@ where } } +impl Component for (T, U) +where + T: Component, + U: Component, +{ + type Msg = M; + + fn place(&mut self, bounds: Rect) -> Rect { + self.0.place(bounds).union(self.1.place(bounds)) + } + + fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { + self.0 + .event(ctx, event) + .or_else(|| self.1.event(ctx, event)) + } + + fn paint(&mut self) { + self.0.paint(); + self.1.paint(); + } +} + +#[cfg(feature = "ui_debug")] +impl crate::trace::Trace for (T, U) +where + T: Component, + T: crate::trace::Trace, + U: Component, + U: crate::trace::Trace, +{ + fn trace(&self, d: &mut dyn crate::trace::Tracer) { + d.open("Tuple"); + d.field("0", &self.0); + d.field("1", &self.1); + d.close(); + } +} + pub trait ComponentExt: Sized { + fn map(self, func: F) -> Map; fn into_child(self) -> Child; fn request_complete_repaint(&mut self, ctx: &mut EventCtx); } @@ -121,6 +194,10 @@ impl ComponentExt for T where T: Component, { + fn map(self, func: F) -> Map { + Map::new(self, func) + } + fn into_child(self) -> Child { Child::new(self) } @@ -172,6 +249,7 @@ impl TimerToken { pub struct EventCtx { timers: Vec<(TimerToken, Duration), { Self::MAX_TIMERS }>, next_token: u32, + place_requested: bool, paint_requested: bool, anim_frame_scheduled: bool, } @@ -194,11 +272,26 @@ impl EventCtx { Self { timers: Vec::new(), next_token: Self::STARTING_TIMER_TOKEN, - paint_requested: false, + place_requested: true, // We need to perform a place pass in the beginning. + paint_requested: false, /* We also need to paint, but this is supplemented by + * `Child::marked_for_paint` being true. */ anim_frame_scheduled: false, } } + /// Indicate that position or sizes of components inside the component tree + /// have changed, and we should perform a place pass before next event or + /// paint traversals. + pub fn request_place(&mut self) { + self.place_requested = true; + } + + /// Returns `true` if we should first perform a place traversal before + /// processing events or painting. + pub fn needs_place_before_next_event_or_paint(&self) -> bool { + self.place_requested + } + /// Indicate that the inner state of the component has changed, any screen /// content it has painted before is now invalid, and it should be painted /// again by the nearest `Child` wrapper. @@ -226,6 +319,7 @@ impl EventCtx { } pub fn clear(&mut self) { + self.place_requested = false; self.paint_requested = false; self.anim_frame_scheduled = false; } diff --git a/core/embed/rust/src/ui/component/empty.rs b/core/embed/rust/src/ui/component/empty.rs index f803ce32b..8c352e103 100644 --- a/core/embed/rust/src/ui/component/empty.rs +++ b/core/embed/rust/src/ui/component/empty.rs @@ -1,10 +1,15 @@ use super::{Component, Event, EventCtx, Never}; +use crate::ui::geometry::Rect; pub struct Empty; impl Component for Empty { type Msg = Never; + fn place(&mut self, _bounds: Rect) -> Rect { + Rect::zero() + } + fn event(&mut self, _ctx: &mut EventCtx, _event: Event) -> Option { None } diff --git a/core/embed/rust/src/ui/component/label.rs b/core/embed/rust/src/ui/component/label.rs index 63a52a8f5..f81b645b1 100644 --- a/core/embed/rust/src/ui/component/label.rs +++ b/core/embed/rust/src/ui/component/label.rs @@ -3,7 +3,7 @@ use core::ops::Deref; use crate::ui::{ component::{Component, Event, EventCtx, Never}, display::{self, Color, Font}, - geometry::{Alignment, Point, Rect}, + geometry::{Alignment, Offset, Rect}, }; pub struct LabelStyle { @@ -14,6 +14,7 @@ pub struct LabelStyle { pub struct Label { area: Rect, + align: Alignment, style: LabelStyle, text: T, } @@ -22,45 +23,25 @@ impl Label where T: Deref, { - pub fn new(origin: Point, align: Alignment, text: T, style: LabelStyle) -> Self { - let width = style.font.text_width(&text); - let height = style.font.line_height(); - let area = match align { - // `origin` is the top-left point. - Alignment::Start => Rect { - x0: origin.x, - y0: origin.y, - x1: origin.x + width, - y1: origin.y + height, - }, - // `origin` is the top-centered point. - Alignment::Center => Rect { - x0: origin.x - width / 2, - y0: origin.y, - x1: origin.x + width / 2, - y1: origin.y + height, - }, - // `origin` is the top-right point. - Alignment::End => Rect { - x0: origin.x - width, - y0: origin.y, - x1: origin.x, - y1: origin.y + height, - }, - }; - Self { area, style, text } + pub fn new(text: T, align: Alignment, style: LabelStyle) -> Self { + Self { + area: Rect::zero(), + align, + style, + text, + } } - pub fn left_aligned(origin: Point, text: T, style: LabelStyle) -> Self { - Self::new(origin, Alignment::Start, text, style) + pub fn left_aligned(text: T, style: LabelStyle) -> Self { + Self::new(text, Alignment::Start, style) } - pub fn right_aligned(origin: Point, text: T, style: LabelStyle) -> Self { - Self::new(origin, Alignment::End, text, style) + pub fn right_aligned(text: T, style: LabelStyle) -> Self { + Self::new(text, Alignment::End, style) } - pub fn centered(origin: Point, text: T, style: LabelStyle) -> Self { - Self::new(origin, Alignment::Center, text, style) + pub fn centered(text: T, style: LabelStyle) -> Self { + Self::new(text, Alignment::Center, style) } pub fn text(&self) -> &T { @@ -74,6 +55,35 @@ where { type Msg = Never; + fn place(&mut self, bounds: Rect) -> Rect { + let size = Offset::new( + self.style.font.text_width(&self.text), + self.style.font.line_height(), + ); + self.area = match self.align { + Alignment::Start => Rect::from_top_left_and_size(bounds.top_left(), size), + Alignment::Center => { + let origin = bounds.top_left().center(bounds.top_right()); + Rect { + x0: origin.x - size.x / 2, + y0: origin.y, + x1: origin.x + size.x / 2, + y1: origin.y + size.y, + } + } + Alignment::End => { + let origin = bounds.top_right(); + Rect { + x0: origin.x - size.x, + y0: origin.y, + x1: origin.x, + y1: origin.y + size.y, + } + } + }; + self.area + } + fn event(&mut self, _ctx: &mut EventCtx, _event: Event) -> Option { None } diff --git a/core/embed/rust/src/ui/component/map.rs b/core/embed/rust/src/ui/component/map.rs index 29e34ae53..9e271b8f1 100644 --- a/core/embed/rust/src/ui/component/map.rs +++ b/core/embed/rust/src/ui/component/map.rs @@ -19,6 +19,10 @@ where { type Msg = U; + fn place(&mut self, bounds: Rect) -> Rect { + self.inner.place(bounds) + } + fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { self.inner.event(ctx, event).and_then(&self.func) } diff --git a/core/embed/rust/src/ui/component/maybe.rs b/core/embed/rust/src/ui/component/maybe.rs index 836247463..820fdd4f3 100644 --- a/core/embed/rust/src/ui/component/maybe.rs +++ b/core/embed/rust/src/ui/component/maybe.rs @@ -19,12 +19,12 @@ impl Maybe { } } - pub fn visible(area: Rect, clear: Color, inner: T) -> Self { - Self::new(Pad::with_background(area, clear), inner, true) + pub fn visible(clear: Color, inner: T) -> Self { + Self::new(Pad::with_background(clear), inner, true) } - pub fn hidden(area: Rect, clear: Color, inner: T) -> Self { - Self::new(Pad::with_background(area, clear), inner, false) + pub fn hidden(clear: Color, inner: T) -> Self { + Self::new(Pad::with_background(clear), inner, false) } } @@ -72,6 +72,12 @@ where { type Msg = T::Msg; + fn place(&mut self, bounds: Rect) -> Rect { + let area = self.inner.place(bounds); + self.pad.place(area); + area + } + fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { if self.visible { self.inner.event(ctx, event) diff --git a/core/embed/rust/src/ui/component/mod.rs b/core/embed/rust/src/ui/component/mod.rs index bc28f791e..bb3918f52 100644 --- a/core/embed/rust/src/ui/component/mod.rs +++ b/core/embed/rust/src/ui/component/mod.rs @@ -7,8 +7,9 @@ pub mod map; pub mod maybe; pub mod pad; pub mod paginated; +pub mod painter; +pub mod placed; pub mod text; -pub mod tuple; pub use base::{Child, Component, ComponentExt, Event, EventCtx, Never, TimerToken}; pub use empty::Empty; @@ -17,6 +18,8 @@ pub use map::Map; pub use maybe::Maybe; pub use pad::Pad; pub use paginated::{PageMsg, Paginate}; +pub use painter::Painter; +pub use placed::GridPlaced; pub use text::{ formatted::FormattedText, layout::{LineBreaking, PageBreaking, TextLayout}, diff --git a/core/embed/rust/src/ui/component/pad.rs b/core/embed/rust/src/ui/component/pad.rs index dcbff6d48..b5e80aaa6 100644 --- a/core/embed/rust/src/ui/component/pad.rs +++ b/core/embed/rust/src/ui/component/pad.rs @@ -10,14 +10,18 @@ pub struct Pad { } impl Pad { - pub fn with_background(area: Rect, color: Color) -> Self { + pub fn with_background(color: Color) -> Self { Self { - area, color, + area: Rect::zero(), clear: false, } } + pub fn place(&mut self, area: Rect) { + self.area = area; + } + pub fn clear(&mut self) { self.clear = true; } diff --git a/core/embed/rust/src/ui/component/painter.rs b/core/embed/rust/src/ui/component/painter.rs new file mode 100644 index 000000000..cf821127c --- /dev/null +++ b/core/embed/rust/src/ui/component/painter.rs @@ -0,0 +1,38 @@ +use crate::ui::{ + component::{Component, Event, EventCtx, Never}, + geometry::Rect, +}; + +pub struct Painter { + area: Rect, + func: F, +} + +impl Painter { + pub fn new(func: F) -> Self { + Self { + func, + area: Rect::zero(), + } + } +} + +impl Component for Painter +where + F: FnMut(Rect), +{ + type Msg = Never; + + fn place(&mut self, bounds: Rect) -> Rect { + self.area = bounds; + self.area + } + + fn event(&mut self, _ctx: &mut EventCtx, _event: Event) -> Option { + None + } + + fn paint(&mut self) { + (self.func)(self.area); + } +} diff --git a/core/embed/rust/src/ui/component/placed.rs b/core/embed/rust/src/ui/component/placed.rs new file mode 100644 index 000000000..a80465ff9 --- /dev/null +++ b/core/embed/rust/src/ui/component/placed.rs @@ -0,0 +1,79 @@ +use crate::ui::{ + component::{Component, Event, EventCtx}, + geometry::{Grid, GridCellSpan, Rect}, +}; + +pub struct GridPlaced { + inner: T, + grid: Grid, + cells: GridCellSpan, +} + +impl GridPlaced { + pub fn new(inner: T) -> Self { + Self { + inner, + grid: Grid::new(Rect::zero(), 0, 0), + cells: GridCellSpan { + from: (0, 0), + to: (0, 0), + }, + } + } + + pub fn with_grid(mut self, rows: usize, cols: usize) -> Self { + self.grid.rows = rows; + self.grid.cols = cols; + self + } + + pub fn with_spacing(mut self, spacing: i32) -> Self { + self.grid.spacing = spacing; + self + } + + pub fn with_row_col(mut self, row: usize, col: usize) -> Self { + self.cells.from = (row, col); + self.cells.to = (row, col); + self + } + + pub fn with_from_to(mut self, from: (usize, usize), to: (usize, usize)) -> Self { + self.cells.from = from; + self.cells.to = to; + self + } +} + +impl Component for GridPlaced +where + T: Component, +{ + type Msg = T::Msg; + + fn place(&mut self, bounds: Rect) -> Rect { + self.grid.area = bounds; + self.inner.place(self.grid.cells(self.cells)) + } + + fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { + self.inner.event(ctx, event) + } + + fn paint(&mut self) { + self.inner.paint() + } +} + +#[cfg(feature = "ui_debug")] +impl crate::trace::Trace for GridPlaced +where + T: Component, + T: crate::trace::Trace, +{ + fn trace(&self, d: &mut dyn crate::trace::Tracer) { + d.open("GridPlaced"); + d.field("inner", &self.inner); + d.close(); + } +} diff --git a/core/embed/rust/src/ui/component/text/formatted.rs b/core/embed/rust/src/ui/component/text/formatted.rs index bc7448e51..6944e8e30 100644 --- a/core/embed/rust/src/ui/component/text/formatted.rs +++ b/core/embed/rust/src/ui/component/text/formatted.rs @@ -26,10 +26,10 @@ pub struct FormattedText { } impl FormattedText { - pub fn new(area: Rect, format: F) -> Self { + pub fn new(format: F) -> Self { Self { - layout: TextLayout::new::(area), format, + layout: TextLayout::new::(), args: LinearMap::new(), char_offset: 0, } @@ -113,6 +113,11 @@ where { type Msg = Never; + fn place(&mut self, bounds: Rect) -> Rect { + self.layout.bounds = bounds; + self.layout.bounds + } + fn event(&mut self, _ctx: &mut EventCtx, _event: Event) -> Option { None } @@ -237,32 +242,30 @@ impl<'a> Iterator for Tokenizer<'a> { #[cfg(test)] mod tests { - use std::array::IntoIter; - use super::*; #[test] fn tokenizer_yields_expected_tokens() { - assert!(Tokenizer::new(b"").eq(IntoIter::new([]))); - assert!(Tokenizer::new(b"x").eq(IntoIter::new([Token::Literal(b"x")]))); - assert!(Tokenizer::new(b"x\0y").eq(IntoIter::new([Token::Literal("x\0y".as_bytes())]))); - assert!(Tokenizer::new(b"{").eq(IntoIter::new([]))); - assert!(Tokenizer::new(b"x{").eq(IntoIter::new([Token::Literal(b"x")]))); - assert!(Tokenizer::new(b"x{y").eq(IntoIter::new([Token::Literal(b"x")]))); - assert!(Tokenizer::new(b"{}").eq(IntoIter::new([Token::Argument(b"")]))); - assert!(Tokenizer::new(b"x{}y{").eq(IntoIter::new([ + assert!(Tokenizer::new(b"").eq([])); + assert!(Tokenizer::new(b"x").eq([Token::Literal(b"x")])); + assert!(Tokenizer::new(b"x\0y").eq([Token::Literal("x\0y".as_bytes())])); + assert!(Tokenizer::new(b"{").eq([])); + assert!(Tokenizer::new(b"x{").eq([Token::Literal(b"x")])); + assert!(Tokenizer::new(b"x{y").eq([Token::Literal(b"x")])); + assert!(Tokenizer::new(b"{}").eq([Token::Argument(b"")])); + assert!(Tokenizer::new(b"x{}y{").eq([ Token::Literal(b"x"), Token::Argument(b""), Token::Literal(b"y"), - ]))); - assert!(Tokenizer::new(b"{\0}").eq(IntoIter::new([Token::Argument("\0".as_bytes()),]))); - assert!(Tokenizer::new(b"{{y}").eq(IntoIter::new([Token::Argument(b"{y"),]))); - assert!(Tokenizer::new(b"{{{{xyz").eq(IntoIter::new([]))); - assert!(Tokenizer::new(b"x{}{{}}}}").eq(IntoIter::new([ + ])); + assert!(Tokenizer::new(b"{\0}").eq([Token::Argument("\0".as_bytes()),])); + assert!(Tokenizer::new(b"{{y}").eq([Token::Argument(b"{y"),])); + assert!(Tokenizer::new(b"{{{{xyz").eq([])); + assert!(Tokenizer::new(b"x{}{{}}}}").eq([ Token::Literal(b"x"), Token::Argument(b""), Token::Argument(b"{"), Token::Literal(b"}}}"), - ]))); + ])); } } diff --git a/core/embed/rust/src/ui/component/text/iter.rs b/core/embed/rust/src/ui/component/text/iter.rs new file mode 100644 index 000000000..fd99c5c79 --- /dev/null +++ b/core/embed/rust/src/ui/component/text/iter.rs @@ -0,0 +1,283 @@ +use crate::ui::{ + component::{text::layout::Op, LineBreaking}, + display::Font, + geometry::Offset, +}; +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, + line_height: i32, + available_height: i32, +) -> impl Iterator { + 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 { + 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, + text_font: impl GlyphMetrics, + hyphen_font: impl GlyphMetrics, + breaking: LineBreaking, + available_width: i32, +) -> impl Iterator { + 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 { + self.text_width(&[ch as u8]) + } + + 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 { + break_text_to_spans( + s, + Fixed { + width: 10, + height: 10, + }, + Fixed { + width: 5, + height: 10, + }, + LineBreaking::BreakWordsAndInsertHyphen, + w, + ) + .collect::>() + } + + fn line_breaks(s: &str, w: i32) -> Vec { + select_line_breaks( + s.char_indices(), + Fixed { + width: 10, + height: 10, + }, + Fixed { + width: 5, + height: 10, + }, + LineBreaking::BreakWordsAndInsertHyphen, + w, + ) + .collect::>() + } + + 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, + } + } +} diff --git a/core/embed/rust/src/ui/component/text/layout.rs b/core/embed/rust/src/ui/component/text/layout.rs index 10d54c357..5e8a60009 100644 --- a/core/embed/rust/src/ui/component/text/layout.rs +++ b/core/embed/rust/src/ui/component/text/layout.rs @@ -82,9 +82,11 @@ pub trait DefaultTextTheme { } impl TextLayout { - pub fn new(bounds: Rect) -> Self { + /// Create a new text layout, with empty size and default text parameters + /// filled from `T`. + pub fn new() -> Self { Self { - bounds, + bounds: Rect::zero(), padding_top: 0, padding_bottom: 0, background_color: T::BACKGROUND_COLOR, @@ -103,10 +105,23 @@ impl TextLayout { } } + pub fn with_bounds(mut self, bounds: Rect) -> Self { + self.bounds = bounds; + self + } + pub fn initial_cursor(&self) -> Point { self.bounds.top_left() + Offset::y(self.text_font.text_height() + self.padding_top) } + pub fn fit_text(&self, text: &[u8]) -> LayoutFit { + self.layout_text(text, &mut self.initial_cursor(), &mut TextNoOp) + } + + pub fn render_text(&self, text: &[u8]) { + self.layout_text(text, &mut self.initial_cursor(), &mut TextRenderer); + } + pub fn layout_ops<'o>( mut self, ops: &mut dyn Iterator>, @@ -235,16 +250,6 @@ impl TextLayout { } } - pub fn measure_ops_height(self, ops: &mut dyn Iterator) -> i32 { - self.layout_ops(ops, &mut self.initial_cursor(), &mut TextNoOp) - .height() - } - - pub fn measure_text_height(self, text: &[u8]) -> i32 { - self.layout_text(text, &mut self.initial_cursor(), &mut TextNoOp) - .height() - } - fn layout_height(&self, init_cursor: Point, end_cursor: Point) -> i32 { self.padding_top + self.text_font.text_height() diff --git a/core/embed/rust/src/ui/component/text/mod.rs b/core/embed/rust/src/ui/component/text/mod.rs index a4aed46f4..8e143a897 100644 --- a/core/embed/rust/src/ui/component/text/mod.rs +++ b/core/embed/rust/src/ui/component/text/mod.rs @@ -1,3 +1,4 @@ pub mod formatted; +mod iter; pub mod layout; pub mod paragraphs; diff --git a/core/embed/rust/src/ui/component/text/paragraphs.rs b/core/embed/rust/src/ui/component/text/paragraphs.rs index 86a070cb7..dffacf27c 100644 --- a/core/embed/rust/src/ui/component/text/paragraphs.rs +++ b/core/embed/rust/src/ui/component/text/paragraphs.rs @@ -3,10 +3,10 @@ use heapless::Vec; use crate::ui::{ component::{Component, Event, EventCtx, Never, Paginate}, display::Font, - geometry::{Dimensions, Insets, LinearLayout, Offset, Rect}, + geometry::{Dimensions, Insets, LinearPlacement, Rect}, }; -use super::layout::{DefaultTextTheme, LayoutFit, TextLayout, TextNoOp, TextRenderer}; +use super::layout::{DefaultTextTheme, LayoutFit, TextLayout}; pub const MAX_PARAGRAPHS: usize = 6; /// Maximum space between paragraphs. Actual result may be smaller (even 0) if @@ -22,7 +22,7 @@ pub const PARAGRAPH_BOTTOM_SPACE: i32 = 5; pub struct Paragraphs { area: Rect, list: Vec, MAX_PARAGRAPHS>, - layout: LinearLayout, + placement: LinearPlacement, offset: PageOffset, visible: usize, } @@ -31,11 +31,11 @@ impl Paragraphs where T: AsRef<[u8]>, { - pub fn new(area: Rect) -> Self { + pub fn new() -> Self { Self { - area, + area: Rect::zero(), list: Vec::new(), - layout: LinearLayout::vertical() + placement: LinearPlacement::vertical() .align_at_center() .with_spacing(DEFAULT_SPACING), offset: PageOffset::default(), @@ -43,13 +43,13 @@ where } } - pub fn with_layout(mut self, layout: LinearLayout) -> Self { - self.layout = layout; + pub fn with_placement(mut self, placement: LinearPlacement) -> Self { + self.placement = placement; self } pub fn with_spacing(mut self, spacing: i32) -> Self { - self.layout = self.layout.with_spacing(spacing); + self.placement = self.placement.with_spacing(spacing); self } @@ -63,7 +63,7 @@ where text_font, padding_top: PARAGRAPH_TOP_SPACE, padding_bottom: PARAGRAPH_BOTTOM_SPACE, - ..TextLayout::new::(self.area) + ..TextLayout::new::() }, ); if self.list.push(paragraph).is_err() { @@ -82,26 +82,29 @@ where 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); + for paragraph in &mut self.list[self.offset.par..] { + paragraph.fit(remaining_area); let height = paragraph .layout - .measure_text_height(¶graph.content.as_ref()[char_offset..]); + .fit_text(paragraph.content(char_offset)) + .height(); if height == 0 { break; } let (used, free) = remaining_area.split_top(height); - paragraph.set_area(used); + paragraph.fit(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); + self.placement.arrange( + self.area, + &mut self.list[offset.par..offset.par + self.visible], + ); } - fn break_pages<'a>(&'a self) -> PageBreakIterator<'a, T> { + fn break_pages(&self) -> PageBreakIterator { PageBreakIterator { paragraphs: self, current: None, @@ -115,45 +118,61 @@ where { type Msg = Never; + fn place(&mut self, bounds: Rect) -> Rect { + self.area = bounds; + self.change_offset(self.offset); + self.area + } + fn event(&mut self, _ctx: &mut EventCtx, _event: Event) -> Option { None } fn paint(&mut self) { 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, - ); + for paragraph in &self.list[self.offset.par..self.offset.par + self.visible] { + paragraph.layout.render_text(paragraph.content(char_offset)); char_offset = 0; } } fn bounds(&self, sink: &mut dyn FnMut(Rect)) { sink(self.area); - for paragraph in self.list.iter().skip(self.offset.par).take(self.visible) { + for paragraph in &self.list[self.offset.par..self.offset.par + self.visible] { sink(paragraph.layout.bounds) } } } -impl Dimensions for Paragraphs { - fn get_size(&mut self) -> Offset { - self.area.size() +impl Paginate for Paragraphs +where + T: AsRef<[u8]>, +{ + fn page_count(&mut self) -> usize { + // There's always at least one page. + self.break_pages().count().max(1) } - fn set_area(&mut self, area: Rect) { - self.area = area + fn change_page(&mut self, to_page: usize) { + if let Some(offset) = self.break_pages().nth(to_page) { + self.change_offset(offset) + } else { + // Should not happen, set index past last paragraph to render empty page. + self.offset = PageOffset { + par: self.list.len(), + chr: 0, + }; + self.visible = 0; + } } } #[cfg(feature = "ui_debug")] pub mod trace { - use super::*; use crate::ui::component::text::layout::trace::TraceSink; + use super::*; + impl crate::trace::Trace for Paragraphs where T: AsRef<[u8]>, @@ -163,7 +182,7 @@ pub mod trace { 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..], + paragraph.content(char_offset), &mut paragraph.layout.initial_cursor(), &mut TraceSink(t), ); @@ -187,18 +206,22 @@ where pub fn new(content: T, layout: TextLayout) -> Self { Self { content, layout } } + + pub fn content(&self, char_offset: usize) -> &[u8] { + &self.content.as_ref()[char_offset..] + } } impl Dimensions for Paragraph where T: AsRef<[u8]>, { - fn get_size(&mut self) -> Offset { - self.layout.bounds.size() + fn fit(&mut self, area: Rect) { + self.layout.bounds = area; } - fn set_area(&mut self, area: Rect) { - self.layout.bounds = area; + fn area(&self) -> Rect { + self.layout.bounds } } @@ -240,39 +263,32 @@ where let mut progress = false; for paragraph in self.paragraphs.list.iter().skip(current.par) { - loop { - 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 { height, .. } => { - // Text fits, update remaining area. - remaining_area = remaining_area.inset(Insets::top(height)); - - // 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. - 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; + let fit = paragraph + .layout + .with_bounds(remaining_area) + .fit_text(paragraph.content(current.chr)); + match fit { + LayoutFit::Fitting { height, .. } => { + // Text fits, update remaining area. + remaining_area = remaining_area.inset(Insets::top(height)); + + // Continue with start of next paragraph. + current.par += 1; + current.chr = 0; + progress = true; + } + LayoutFit::OutOfBounds { + processed_chars, .. + } => { + // Text does not fit, assume whatever fits takes the entire remaining area. + 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; } } } @@ -281,26 +297,3 @@ where None } } - -impl Paginate for Paragraphs -where - T: AsRef<[u8]>, -{ - fn page_count(&mut self) -> usize { - // There's always at least one page. - self.break_pages().count().max(1) - } - - fn change_page(&mut self, to_page: usize) { - 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.offset = PageOffset { - par: self.list.len(), - chr: 0, - }; - self.visible = 0; - } - } -} diff --git a/core/embed/rust/src/ui/component/tuple.rs b/core/embed/rust/src/ui/component/tuple.rs deleted file mode 100644 index 1643943f0..000000000 --- a/core/embed/rust/src/ui/component/tuple.rs +++ /dev/null @@ -1,84 +0,0 @@ -use super::{Component, Event, EventCtx}; -use crate::ui::geometry::Rect; - -impl Component for (A, B) -where - A: Component, - B: Component, -{ - type Msg = T; - - fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { - self.0 - .event(ctx, event) - .or_else(|| self.1.event(ctx, event)) - } - - fn paint(&mut self) { - self.0.paint(); - self.1.paint(); - } - - fn bounds(&self, sink: &mut dyn FnMut(Rect)) { - self.0.bounds(sink); - self.1.bounds(sink); - } -} - -impl Component for (A, B, C) -where - A: Component, - B: Component, - C: Component, -{ - type Msg = T; - - fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { - self.0 - .event(ctx, event) - .or_else(|| self.1.event(ctx, event)) - .or_else(|| self.2.event(ctx, event)) - } - - fn paint(&mut self) { - self.0.paint(); - self.1.paint(); - self.2.paint(); - } - - fn bounds(&self, sink: &mut dyn FnMut(Rect)) { - self.0.bounds(sink); - self.1.bounds(sink); - self.2.bounds(sink); - } -} - -#[cfg(feature = "ui_debug")] -impl crate::trace::Trace for (A, B) -where - A: Component + crate::trace::Trace, - B: Component + crate::trace::Trace, -{ - fn trace(&self, t: &mut dyn crate::trace::Tracer) { - t.open("Tuple"); - t.field("0", &self.0); - t.field("1", &self.1); - t.close(); - } -} - -#[cfg(feature = "ui_debug")] -impl crate::trace::Trace for (A, B, C) -where - A: Component + crate::trace::Trace, - B: Component + crate::trace::Trace, - C: Component + crate::trace::Trace, -{ - fn trace(&self, t: &mut dyn crate::trace::Tracer) { - t.open("Tuple"); - t.field("0", &self.0); - t.field("1", &self.1); - t.field("2", &self.2); - t.close(); - } -} diff --git a/core/embed/rust/src/ui/geometry.rs b/core/embed/rust/src/ui/geometry.rs index 6f4aec198..507b9e3c8 100644 --- a/core/embed/rust/src/ui/geometry.rs +++ b/core/embed/rust/src/ui/geometry.rs @@ -148,6 +148,22 @@ impl Rect { } } + pub fn with_top_left(self, p0: Point) -> Self { + Self::from_top_left_and_size(p0, self.size()) + } + + pub fn with_size(self, size: Offset) -> Self { + Self::from_top_left_and_size(self.top_left(), size) + } + + pub fn with_width(self, width: i32) -> Self { + self.with_size(Offset::new(width, self.height())) + } + + pub fn with_height(self, height: i32) -> Self { + self.with_size(Offset::new(self.width(), height)) + } + pub fn width(&self) -> i32 { self.x1 - self.x0 } @@ -399,16 +415,28 @@ impl Grid { pub fn cell(&self, index: usize) -> Rect { self.row_col(index / self.cols, index % self.cols) } + + pub fn cells(&self, cells: GridCellSpan) -> Rect { + let from = self.row_col(cells.from.0, cells.to.1); + let to = self.row_col(cells.to.0, cells.to.1); + from.union(to) + } } #[derive(Copy, Clone)] -pub struct LinearLayout { +pub struct GridCellSpan { + pub from: (usize, usize), + pub to: (usize, usize), +} + +#[derive(Copy, Clone)] +pub struct LinearPlacement { axis: Axis, align: Alignment, spacing: i32, } -impl LinearLayout { +impl LinearPlacement { pub fn horizontal() -> Self { Self { axis: Axis::Horizontal, @@ -445,42 +473,20 @@ impl LinearLayout { self } - fn compute_spacing(&self, area: Rect, count: usize, size_sum: i32) -> (i32, i32) { - let spacing_count = count.saturating_sub(1); - let spacing_sum = spacing_count as i32 * self.spacing; - let naive_size = size_sum + spacing_sum; - let available_space = area.size().axis(self.axis); - - // scale down spacing to fit everything into area - let (total_size, spacing) = if naive_size > available_space { - let scaled_space = (available_space - size_sum) / spacing_count as i32; - // forbid negative spacing - (available_space, scaled_space.max(0)) - } else { - (naive_size, self.spacing) - }; - - let init_cursor = match self.align { - Alignment::Start => 0, - Alignment::Center => available_space / 2 - total_size / 2, - Alignment::End => available_space - total_size, - }; - - (init_cursor, spacing) - } - /// Arranges all `items` by parameters configured in `self` into `area`. - /// Does not change the size of the items (only the position), but it needs - /// to iterate (and ask for the size) twice. + /// Does not change the size of the items (only the position). pub fn arrange(&self, area: Rect, items: &mut [impl Dimensions]) { - let item_sum: i32 = items.iter_mut().map(|i| i.get_size().axis(self.axis)).sum(); - let (mut cursor, spacing) = self.compute_spacing(area, items.len(), item_sum); + let size_sum: i32 = items + .iter_mut() + .map(|i| i.area().size().axis(self.axis)) + .sum(); + let (mut cursor, spacing) = self.compute_spacing(area, items.len(), size_sum); for item in items { - let top_left = area.top_left() + Offset::on_axis(self.axis, cursor); - let size = item.get_size(); - item.set_area(Rect::from_top_left_and_size(top_left, size)); - cursor += size.axis(self.axis); + let item_origin = area.top_left() + Offset::on_axis(self.axis, cursor); + let item_area = item.area().with_top_left(item_origin); + item.fit(item_area); + cursor += item_area.size().axis(self.axis); cursor += spacing; } } @@ -509,10 +515,34 @@ impl LinearLayout { cursor += spacing; } } + + fn compute_spacing(&self, area: Rect, count: usize, size_sum: i32) -> (i32, i32) { + let spacing_count = count.saturating_sub(1); + let spacing_sum = spacing_count as i32 * self.spacing; + let naive_size = size_sum + spacing_sum; + let available_space = area.size().axis(self.axis); + + // scale down spacing to fit everything into area + let (total_size, spacing) = if naive_size > available_space { + let scaled_space = (available_space - size_sum) / spacing_count.max(1) as i32; + // forbid negative spacing + (available_space, scaled_space.max(0)) + } else { + (naive_size, self.spacing) + }; + + let initial_cursor = match self.align { + Alignment::Start => 0, + Alignment::Center => available_space / 2 - total_size / 2, + Alignment::End => available_space - total_size, + }; + + (initial_cursor, spacing) + } } -/// Types that have a size and a position. +/// Types that can place themselves within area specified by `bounds`. pub trait Dimensions { - fn get_size(&mut self) -> Offset; - fn set_area(&mut self, area: Rect); + fn fit(&mut self, bounds: Rect); + fn area(&self) -> Rect; } diff --git a/core/embed/rust/src/ui/layout/obj.rs b/core/embed/rust/src/ui/layout/obj.rs index 7fd6598e6..b542bd456 100644 --- a/core/embed/rust/src/ui/layout/obj.rs +++ b/core/embed/rust/src/ui/layout/obj.rs @@ -55,6 +55,7 @@ mod maybe_trace { impl MaybeTrace for T {} } +use crate::ui::display; /// Stand-in for the optionally-compiled trait `Trace`. /// If UI debugging is enabled, `MaybeTrace` implies `Trace` and is implemented /// for everything that implements Trace. If disabled, `MaybeTrace` is @@ -69,6 +70,7 @@ use maybe_trace::MaybeTrace; /// T>` field. `Component` itself is not object-safe because of `Component::Msg` /// associated type. pub trait ObjComponent: MaybeTrace { + fn obj_place(&mut self, bounds: Rect) -> Rect; fn obj_event(&mut self, ctx: &mut EventCtx, event: Event) -> Result; fn obj_paint(&mut self); fn obj_bounds(&self, sink: &mut dyn FnMut(Rect)); @@ -78,6 +80,10 @@ impl ObjComponent for Child where T: ComponentMsgObj + MaybeTrace, { + fn obj_place(&mut self, bounds: Rect) -> Rect { + self.place(bounds) + } + fn obj_event(&mut self, ctx: &mut EventCtx, event: Event) -> Result { if let Some(msg) = self.event(ctx, event) { self.inner().msg_try_into_obj(msg) @@ -143,6 +149,12 @@ impl LayoutObj { fn obj_event(&self, event: Event) -> Result { let inner = &mut *self.inner.borrow_mut(); + // Place the root component on the screen in case it was previously requested. + if inner.event_ctx.needs_place_before_next_event_or_paint() { + // SAFETY: `inner.root` is unique because of the `inner.borrow_mut()`. + unsafe { Gc::as_mut(&mut inner.root) }.obj_place(display::screen()); + } + // Clear the leftover flags from the previous event pass. inner.event_ctx.clear(); @@ -170,6 +182,13 @@ impl LayoutObj { /// Run a paint pass over the component tree. fn obj_paint_if_requested(&self) { let mut inner = self.inner.borrow_mut(); + + // Place the root component on the screen in case it was previously requested. + if inner.event_ctx.needs_place_before_next_event_or_paint() { + // SAFETY: `inner.root` is unique because of the `inner.borrow_mut()`. + unsafe { Gc::as_mut(&mut inner.root) }.obj_place(display::screen()); + } + // SAFETY: `inner.root` is unique because of the `inner.borrow_mut()`. unsafe { Gc::as_mut(&mut inner.root) }.obj_paint(); } diff --git a/core/embed/rust/src/ui/model_t1/component/button.rs b/core/embed/rust/src/ui/model_t1/component/button.rs index 76fc36246..6d2b523db 100644 --- a/core/embed/rust/src/ui/model_t1/component/button.rs +++ b/core/embed/rust/src/ui/model_t1/component/button.rs @@ -38,34 +38,23 @@ pub struct Button { } impl> Button { - pub fn new( - area: Rect, - pos: ButtonPos, - content: ButtonContent, - styles: ButtonStyleSheet, - ) -> Self { - let (area, baseline) = Self::placement(area, pos, &content, &styles); + pub fn new(pos: ButtonPos, content: ButtonContent, styles: ButtonStyleSheet) -> Self { Self { - area, pos, - baseline, content, styles, + baseline: Point::zero(), + area: Rect::zero(), state: State::Released, } } - pub fn with_text(area: Rect, pos: ButtonPos, text: T, styles: ButtonStyleSheet) -> Self { - Self::new(area, pos, ButtonContent::Text(text), styles) + pub fn with_text(pos: ButtonPos, text: T, styles: ButtonStyleSheet) -> Self { + Self::new(pos, ButtonContent::Text(text), styles) } - pub fn with_icon( - area: Rect, - pos: ButtonPos, - image: &'static [u8], - styles: ButtonStyleSheet, - ) -> Self { - Self::new(area, pos, ButtonContent::Icon(image), styles) + pub fn with_icon(pos: ButtonPos, image: &'static [u8], styles: ButtonStyleSheet) -> Self { + Self::new(pos, ButtonContent::Icon(image), styles) } pub fn content(&self) -> &ButtonContent { @@ -109,9 +98,19 @@ impl> Button { } } -impl> Component for Button { +impl Component for Button +where + T: AsRef<[u8]>, +{ type Msg = ButtonMsg; + fn place(&mut self, bounds: Rect) -> Rect { + let (area, baseline) = Self::placement(bounds, self.pos, &self.content, &self.styles); + self.area = area; + self.baseline = baseline; + self.area + } + fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { match event { Event::Button(ButtonEvent::ButtonPressed(which)) if self.pos.hit(&which) => { diff --git a/core/embed/rust/src/ui/model_t1/component/dialog.rs b/core/embed/rust/src/ui/model_t1/component/dialog.rs index ad265f3c1..706e555a3 100644 --- a/core/embed/rust/src/ui/model_t1/component/dialog.rs +++ b/core/embed/rust/src/ui/model_t1/component/dialog.rs @@ -1,5 +1,5 @@ use super::{ - button::{Button, ButtonMsg::Clicked, ButtonPos}, + button::{Button, ButtonMsg::Clicked}, theme, }; use crate::ui::{ @@ -19,34 +19,36 @@ pub struct Dialog { right_btn: Option>>, } -impl> Dialog { - pub fn new( - area: Rect, - content: impl FnOnce(Rect) -> T, - left: Option Button>, - right: Option Button>, - ) -> Self { - let (content_area, button_area) = Self::areas(area); - let content = Child::new(content(content_area)); - let left_btn = left.map(|f| Child::new(f(button_area, ButtonPos::Left))); - let right_btn = right.map(|f| Child::new(f(button_area, ButtonPos::Right))); +impl Dialog +where + T: Component, + U: AsRef<[u8]>, +{ + pub fn new(content: T, left: Option>, right: Option>) -> Self { Self { - content, - left_btn, - right_btn, + content: Child::new(content), + left_btn: left.map(Child::new), + right_btn: right.map(Child::new), } } - - fn areas(area: Rect) -> (Rect, Rect) { - let button_height = theme::FONT_BOLD.line_height() + 2; - let (content_area, button_area) = area.split_bottom(button_height); - (content_area, button_area) - } } -impl> Component for Dialog { +impl Component for Dialog +where + T: Component, + U: AsRef<[u8]>, +{ type Msg = DialogMsg; + fn place(&mut self, bounds: Rect) -> Rect { + let button_height = theme::FONT_BOLD.line_height() + 2; + let (content_area, button_area) = bounds.split_bottom(button_height); + self.content.place(content_area); + self.left_btn.as_mut().map(|b| b.place(button_area)); + self.right_btn.as_mut().map(|b| b.place(button_area)); + bounds + } + fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { if let Some(msg) = self.content.event(ctx, event) { Some(DialogMsg::Content(msg)) diff --git a/core/embed/rust/src/ui/model_t1/component/frame.rs b/core/embed/rust/src/ui/model_t1/component/frame.rs index 7573cf952..5d056ee15 100644 --- a/core/embed/rust/src/ui/model_t1/component/frame.rs +++ b/core/embed/rust/src/ui/model_t1/component/frame.rs @@ -1,8 +1,8 @@ use super::theme; use crate::ui::{ - component::{Child, Component, ComponentExt, Event, EventCtx}, + component::{Child, Component, Event, EventCtx}, display, - geometry::{Offset, Rect}, + geometry::{Insets, Offset, Rect}, }; pub struct Frame { @@ -11,29 +11,37 @@ pub struct Frame { content: Child, } -impl> Frame { - pub fn new(area: Rect, title: U, content: impl FnOnce(Rect) -> T) -> Self { - let (title_area, content_area) = Self::areas(area); +impl Frame +where + T: Component, + U: AsRef<[u8]>, +{ + pub fn new(title: U, content: T) -> Self { Self { - area: title_area, title, - content: content(content_area).into_child(), + area: Rect::zero(), + content: Child::new(content), } } +} + +impl Component for Frame +where + T: Component, + U: AsRef<[u8]>, +{ + type Msg = T::Msg; - fn areas(area: Rect) -> (Rect, Rect) { - const HEADER_SPACE: i32 = 4; - let header_height = theme::FONT_BOLD.line_height(); + fn place(&mut self, bounds: Rect) -> Rect { + const TITLE_SPACE: i32 = 4; - let (header_area, content_area) = area.split_top(header_height); - let (_space, content_area) = content_area.split_top(HEADER_SPACE); + let (title_area, content_area) = bounds.split_top(theme::FONT_BOLD.line_height()); + let content_area = content_area.inset(Insets::top(TITLE_SPACE)); - (header_area, content_area) + self.area = title_area; + self.content.place(content_area); + bounds } -} - -impl> Component for Frame { - type Msg = T::Msg; fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { self.content.event(ctx, event) diff --git a/core/embed/rust/src/ui/model_t1/component/page.rs b/core/embed/rust/src/ui/model_t1/component/page.rs index c22a15491..cf9598e72 100644 --- a/core/embed/rust/src/ui/model_t1/component/page.rs +++ b/core/embed/rust/src/ui/model_t1/component/page.rs @@ -21,54 +21,18 @@ where T: Paginate, T: Component, { - pub fn new(area: Rect, content: impl FnOnce(Rect) -> T, background: Color) -> Self { - let (content_area, scrollbar_area, button_area) = Self::areas(area); - let mut content = content(content_area); - let pad = Pad::with_background(area, background); - - // Always start at the first page. - let scrollbar = ScrollBar::vertical_right(scrollbar_area, content.page_count(), 0); - - // Create the button controls. - let prev = Button::with_text(button_area, ButtonPos::Left, "BACK", theme::button_cancel()); - let next = Button::with_text( - button_area, - ButtonPos::Right, - "NEXT", - theme::button_default(), - ); - let cancel = Button::with_text( - button_area, - ButtonPos::Left, - "CANCEL", - theme::button_cancel(), - ); - let confirm = Button::with_text( - button_area, - ButtonPos::Right, - "CONFIRM", - theme::button_default(), - ); - + pub fn new(content: T, background: Color) -> Self { Self { content, - scrollbar, - pad, - prev, - next, - cancel, - confirm, + scrollbar: ScrollBar::vertical(), + pad: Pad::with_background(background), + prev: Button::with_text(ButtonPos::Left, "BACK", theme::button_cancel()), + next: Button::with_text(ButtonPos::Right, "NEXT", theme::button_default()), + cancel: Button::with_text(ButtonPos::Left, "CANCEL", theme::button_cancel()), + confirm: Button::with_text(ButtonPos::Right, "CONFIRM", theme::button_default()), } } - fn areas(area: Rect) -> (Rect, Rect, Rect) { - let button_height = theme::FONT_BOLD.line_height() + 2; - let (content_area, button_area) = area.split_bottom(button_height); - let (content_area, scrollbar_area) = content_area.split_right(ScrollBar::WIDTH); - let (content_area, _) = content_area.split_bottom(1); - (content_area, scrollbar_area, button_area) - } - fn change_page(&mut self, ctx: &mut EventCtx, page: usize) { // Change the page in the content, clear the background under it and make sure // it gets completely repainted. @@ -85,6 +49,23 @@ where { type Msg = PageMsg; + fn place(&mut self, bounds: Rect) -> Rect { + let button_height = theme::FONT_BOLD.line_height() + 2; + let (content_area, button_area) = bounds.split_bottom(button_height); + let (content_area, scrollbar_area) = content_area.split_right(ScrollBar::WIDTH); + let content_area = content_area.inset(Insets::top(1)); + self.pad.place(bounds); + self.content.place(content_area); + let page_count = self.content.page_count(); + self.scrollbar.set_count_and_active_page(page_count, 0); + self.scrollbar.place(scrollbar_area); + self.prev.place(button_area); + self.next.place(button_area); + self.cancel.place(button_area); + self.confirm.place(button_area); + bounds + } + fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { if self.scrollbar.has_previous_page() { if let Some(ButtonMsg::Clicked) = self.prev.event(ctx, event) { @@ -156,14 +137,19 @@ impl ScrollBar { pub const DOT_SIZE: Offset = Offset::new(4, 4); pub const DOT_INTERVAL: i32 = 6; - pub fn vertical_right(area: Rect, page_count: usize, active_page: usize) -> Self { + pub fn vertical() -> Self { Self { - area, - page_count, - active_page, + area: Rect::zero(), + page_count: 0, + active_page: 0, } } + pub fn set_count_and_active_page(&mut self, page_count: usize, active_page: usize) { + self.page_count = page_count; + self.active_page = active_page; + } + pub fn has_next_page(&self) -> bool { self.active_page < self.page_count - 1 } @@ -208,6 +194,11 @@ impl ScrollBar { impl Component for ScrollBar { type Msg = Never; + fn place(&mut self, bounds: Rect) -> Rect { + self.area = bounds; + self.area + } + fn event(&mut self, _ctx: &mut EventCtx, _event: Event) -> Option { None } diff --git a/core/embed/rust/src/ui/model_t1/layout.rs b/core/embed/rust/src/ui/model_t1/layout.rs index d1e350754..e7f72bcdd 100644 --- a/core/embed/rust/src/ui/model_t1/layout.rs +++ b/core/embed/rust/src/ui/model_t1/layout.rs @@ -4,8 +4,8 @@ use crate::{ micropython::{buffer::Buffer, map::Map, obj::Obj, qstr::Qstr}, ui::{ component::{text::paragraphs::Paragraphs, FormattedText}, - display, layout::obj::LayoutObj, + model_t1::component::ButtonPos, }, util, }; @@ -40,21 +40,19 @@ extern "C" fn ui_layout_new_confirm_action( }; let left = verb_cancel - .map(|label| |area, pos| Button::with_text(area, pos, label, theme::button_cancel())); - let right = verb - .map(|label| |area, pos| Button::with_text(area, pos, label, theme::button_default())); + .map(|label| Button::with_text(ButtonPos::Left, label, theme::button_cancel())); + let right = + verb.map(|label| Button::with_text(ButtonPos::Right, label, theme::button_default())); - let obj = LayoutObj::new(Frame::new(display::screen(), title, |area| { + let obj = LayoutObj::new(Frame::new( + title, ButtonPage::new( - area, - |area| { - FormattedText::new::(area, format) - .with(b"action", action.unwrap_or("".into())) - .with(b"description", description.unwrap_or("".into())) - }, + FormattedText::new::(format) + .with(b"action", action.unwrap_or_default()) + .with(b"description", description.unwrap_or_default()), theme::BG, - ) - }))?; + ), + ))?; Ok(obj.into()) }; unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } @@ -72,20 +70,18 @@ extern "C" fn ui_layout_new_confirm_text( let description: Option = kwargs.get(Qstr::MP_QSTR_description)?.try_into_option()?; - let obj = LayoutObj::new(Frame::new(display::screen(), title, |area| { + let obj = LayoutObj::new(Frame::new( + title, ButtonPage::new( - area, - |area| { - Paragraphs::new(area) - .add::( - theme::FONT_NORMAL, - description.unwrap_or("".into()), - ) - .add::(theme::FONT_BOLD, data) - }, + Paragraphs::new() + .add::( + theme::FONT_NORMAL, + description.unwrap_or_default(), + ) + .add::(theme::FONT_BOLD, data), theme::BG, - ) - }))?; + ), + ))?; Ok(obj.into()) }; unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } @@ -96,7 +92,11 @@ mod tests { use crate::{ error::Error, trace::Trace, - ui::model_t1::component::{Dialog, DialogMsg}, + ui::{ + component::Component, + display, + model_t1::component::{Dialog, DialogMsg}, + }, }; use super::*; @@ -125,18 +125,23 @@ mod tests { #[test] fn trace_example_layout() { - let layout = Dialog::new( - display::screen(), - |area| { - FormattedText::new::( - area, - "Testing text layout, with some text, and some more text. And {param}", - ) - .with(b"param", b"parameters!") - }, - Some(|area, pos| Button::with_text(area, pos, "Left", theme::button_cancel())), - Some(|area, pos| Button::with_text(area, pos, "Right", theme::button_default())), + let mut layout = Dialog::new( + FormattedText::new::( + "Testing text layout, with some text, and some more text. And {param}", + ) + .with(b"param", b"parameters!"), + Some(Button::with_text( + ButtonPos::Left, + "Left", + theme::button_cancel(), + )), + Some(Button::with_text( + ButtonPos::Right, + "Right", + theme::button_default(), + )), ); + layout.place(display::screen()); assert_eq!( trace(&layout), r#" left: