From df7af9734ddb7a91762d86952810e1173ec79b1f Mon Sep 17 00:00:00 2001 From: grdddj Date: Fri, 31 Mar 2023 15:14:29 +0200 Subject: [PATCH] TR-rust: necessary changes for TR UI --- core/embed/rust/src/micropython/buffer.rs | 2 +- core/embed/rust/src/trace.rs | 15 ++++ core/embed/rust/src/trezorhal/random.rs | 38 +++++++++ core/embed/rust/src/ui/component/base.rs | 4 + core/embed/rust/src/ui/component/marquee.rs | 4 + core/embed/rust/src/ui/component/paginated.rs | 63 -------------- .../rust/src/ui/component/text/layout.rs | 4 +- .../rust/src/ui/component/text/paragraphs.rs | 11 +++ core/embed/rust/src/ui/debug.rs | 30 +++++++ core/embed/rust/src/ui/display/mod.rs | 85 +++++++++++++++++-- core/embed/rust/src/ui/event.rs | 6 ++ core/embed/rust/src/ui/geometry.rs | 53 ++++++++++++ core/embed/rust/src/ui/layout/util.rs | 13 ++- core/embed/rust/src/ui/macros.rs | 12 +++ core/embed/rust/src/ui/util.rs | 7 ++ 15 files changed, 271 insertions(+), 76 deletions(-) diff --git a/core/embed/rust/src/micropython/buffer.rs b/core/embed/rust/src/micropython/buffer.rs index 59ee9f19d2..fd1cadb7cc 100644 --- a/core/embed/rust/src/micropython/buffer.rs +++ b/core/embed/rust/src/micropython/buffer.rs @@ -20,7 +20,7 @@ use super::ffi; /// The `off` field represents offset from the `ptr` and allows us to do /// substring slices while keeping the head pointer as required by GC. #[repr(C)] -#[derive(Clone)] +#[derive(Debug, Clone, Copy)] pub struct StrBuffer { ptr: *const u8, len: u16, diff --git a/core/embed/rust/src/trace.rs b/core/embed/rust/src/trace.rs index 50baedf5ef..3980f9edad 100644 --- a/core/embed/rust/src/trace.rs +++ b/core/embed/rust/src/trace.rs @@ -1,5 +1,8 @@ use heapless::String; +#[cfg(feature = "model_tr")] +use crate::ui::model_tr::component::ButtonPos; + /// Visitor passed into `Trace` types. pub trait Tracer { fn int(&mut self, i: i64); @@ -27,6 +30,18 @@ pub const EMPTY_BTN: &str = "---"; /// Value that can describe own structure and data using the `Tracer` interface. pub trait Trace { fn trace(&self, t: &mut dyn Tracer); + /// Describes what happens when a certain button is triggered. + #[cfg(feature = "model_tr")] + fn get_btn_action(&self, _pos: ButtonPos) -> String<25> { + "Default".into() + } + /// Report actions for all three buttons in easy-to-parse format. + #[cfg(feature = "model_tr")] + fn report_btn_actions(&self, t: &mut dyn Tracer) { + t.kw_pair("left_action", &self.get_btn_action(ButtonPos::Left)); + t.kw_pair("middle_action", &self.get_btn_action(ButtonPos::Middle)); + t.kw_pair("right_action", &self.get_btn_action(ButtonPos::Right)); + } } impl Trace for &[u8] { diff --git a/core/embed/rust/src/trezorhal/random.rs b/core/embed/rust/src/trezorhal/random.rs index 8f64b07734..4f346564cd 100644 --- a/core/embed/rust/src/trezorhal/random.rs +++ b/core/embed/rust/src/trezorhal/random.rs @@ -9,3 +9,41 @@ pub fn shuffle(slice: &mut [T]) { slice.swap(i, j); } } + +/// Returns a random number in the range [min, max]. +pub fn uniform_between(min: u32, max: u32) -> u32 { + assert!(max > min); + uniform(max - min + 1) + min +} + +/// Returns a random number in the range [min, max] except one `except` number. +pub fn uniform_between_except(min: u32, max: u32, except: u32) -> u32 { + // Generate uniform_between as long as it is not except + loop { + let rand = uniform_between(min, max); + if rand != except { + return rand; + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn uniform_between_test() { + for _ in 0..10 { + assert!((10..=11).contains(&uniform_between(10, 11))); + assert!((10..=12).contains(&uniform_between(10, 12))); + assert!((256..=512).contains(&uniform_between(256, 512))); + } + } + + #[test] + fn uniform_between_except_test() { + for _ in 0..10 { + assert!(uniform_between_except(10, 12, 11) != 11); + } + } +} diff --git a/core/embed/rust/src/ui/component/base.rs b/core/embed/rust/src/ui/component/base.rs index 1dc0b66f70..244a14aed1 100644 --- a/core/embed/rust/src/ui/component/base.rs +++ b/core/embed/rust/src/ui/component/base.rs @@ -85,6 +85,10 @@ impl Child { self.component } + pub fn inner_mut(&mut self) -> &mut T { + &mut self.component + } + /// Access inner component mutably, track whether a paint call has been /// requested, and propagate the flag upwards the component tree. pub fn mutate(&mut self, ctx: &mut EventCtx, component_func: F) -> U diff --git a/core/embed/rust/src/ui/component/marquee.rs b/core/embed/rust/src/ui/component/marquee.rs index 93b2eb6f9d..dc35a9763c 100644 --- a/core/embed/rust/src/ui/component/marquee.rs +++ b/core/embed/rust/src/ui/component/marquee.rs @@ -54,6 +54,10 @@ where } } + pub fn set_text(&mut self, text: T) { + self.text = text; + } + pub fn start(&mut self, ctx: &mut EventCtx, now: Instant) { // Not starting if animations are disabled. if animation_disabled() { diff --git a/core/embed/rust/src/ui/component/paginated.rs b/core/embed/rust/src/ui/component/paginated.rs index db0a516e08..3bdbe2dabd 100644 --- a/core/embed/rust/src/ui/component/paginated.rs +++ b/core/embed/rust/src/ui/component/paginated.rs @@ -1,8 +1,3 @@ -use crate::ui::component::{ - text::layout::{LayoutFit, TextNoOp}, - FormattedText, -}; - pub enum AuxPageMsg { /// Page component was instantiated with BACK button on every page and it /// was pressed. @@ -34,61 +29,3 @@ pub trait Paginate { /// Navigate to the given page. fn change_page(&mut self, active_page: usize); } - -impl Paginate for FormattedText -where - F: AsRef, - T: AsRef, -{ - fn page_count(&mut self) -> usize { - let mut page_count = 1; // There's always at least one page. - let mut char_offset = 0; - - loop { - let fit = self.layout_content(&mut TextNoOp); - match fit { - LayoutFit::Fitting { .. } => { - break; // TODO: We should consider if there's more content - // to render. - } - LayoutFit::OutOfBounds { - processed_chars, .. - } => { - page_count += 1; - char_offset += processed_chars; - self.set_char_offset(char_offset); - } - } - } - - // Reset the char offset back to the beginning. - self.set_char_offset(0); - - page_count - } - - fn change_page(&mut self, to_page: usize) { - let mut active_page = 0; - let mut char_offset = 0; - - // Make sure we're starting from the beginning. - self.set_char_offset(char_offset); - - while active_page < to_page { - let fit = self.layout_content(&mut TextNoOp); - match fit { - LayoutFit::Fitting { .. } => { - break; // TODO: We should consider if there's more content - // to render. - } - LayoutFit::OutOfBounds { - processed_chars, .. - } => { - active_page += 1; - char_offset += processed_chars; - self.set_char_offset(char_offset); - } - } - } - } -} diff --git a/core/embed/rust/src/ui/component/text/layout.rs b/core/embed/rust/src/ui/component/text/layout.rs index 5afd456e93..b2d410c0ed 100644 --- a/core/embed/rust/src/ui/component/text/layout.rs +++ b/core/embed/rust/src/ui/component/text/layout.rs @@ -354,7 +354,7 @@ impl TextLayout { } /// Overall height of the content, including paddings. - fn layout_height(&self, init_cursor: Point, end_cursor: Point) -> i16 { + pub fn layout_height(&self, init_cursor: Point, end_cursor: Point) -> i16 { self.padding_top + self.style.text_font.text_height() + (end_cursor.y - init_cursor.y) @@ -562,7 +562,7 @@ pub struct Span { } impl Span { - fn fit_horizontally( + pub fn fit_horizontally( text: &str, max_width: i16, text_font: impl GlyphMetrics, diff --git a/core/embed/rust/src/ui/component/text/paragraphs.rs b/core/embed/rust/src/ui/component/text/paragraphs.rs index 145bde1e08..8df077301f 100644 --- a/core/embed/rust/src/ui/component/text/paragraphs.rs +++ b/core/embed/rust/src/ui/component/text/paragraphs.rs @@ -625,6 +625,17 @@ where } } +impl Paginate for Checklist +where + T: ParagraphSource, +{ + fn page_count(&mut self) -> usize { + 1 + } + + fn change_page(&mut self, _to_page: usize) {} +} + #[cfg(feature = "ui_debug")] impl crate::trace::Trace for Checklist { fn trace(&self, t: &mut dyn crate::trace::Tracer) { diff --git a/core/embed/rust/src/ui/debug.rs b/core/embed/rust/src/ui/debug.rs index e69efd1c7e..ebcb97a40c 100644 --- a/core/embed/rust/src/ui/debug.rs +++ b/core/embed/rust/src/ui/debug.rs @@ -16,6 +16,9 @@ use super::{ }; use crate::{micropython::buffer::StrBuffer, time::Duration}; +#[cfg(feature = "model_tr")] +use super::model_tr::component::ButtonDetails; + // NOTE: not defining a common trait, like // Debug {fn print(&self);}, so that the trait does // not need to be imported when using the @@ -97,6 +100,33 @@ impl Font { } } +#[cfg(feature = "model_tr")] +impl ButtonDetails { + pub fn print(&self) { + let text: String<20> = if let Some(text) = self.text { + text.as_ref().into() + } else { + "None".into() + }; + let force_width: String<20> = if let Some(force_width) = self.force_width { + inttostr!(force_width).into() + } else { + "None".into() + }; + println!( + "ButtonDetails:: ", + "text: ", + text.as_ref(), + ", with_outline: ", + booltostr!(self.with_outline), + ", with_arms: ", + booltostr!(self.with_arms), + ", force_width: ", + force_width.as_ref() + ); + } +} + impl Offset { pub fn print(&self) { println!( diff --git a/core/embed/rust/src/ui/display/mod.rs b/core/embed/rust/src/ui/display/mod.rs index 15017bf10c..d90a81f0c8 100644 --- a/core/embed/rust/src/ui/display/mod.rs +++ b/core/embed/rust/src/ui/display/mod.rs @@ -5,6 +5,8 @@ pub mod loader; pub mod tjpgd; pub mod toif; +use heapless::String; + use super::{ constant, geometry::{Offset, Point, Rect}, @@ -126,16 +128,16 @@ pub fn rect_fill_corners(r: Rect, fg_color: Color) { } #[derive(Copy, Clone, PartialEq, Eq)] -pub struct TextOverlay<'a> { +pub struct TextOverlay { area: Rect, - text: &'a str, + text: T, font: Font, max_height: i16, baseline: i16, } -impl<'a> TextOverlay<'a> { - pub fn new(text: &'a str, font: Font) -> Self { +impl> TextOverlay { + pub fn new(text: T, font: Font) -> Self { let area = Rect::zero(); Self { @@ -147,8 +149,17 @@ impl<'a> TextOverlay<'a> { } } + pub fn set_text(&mut self, text: T) { + self.text = text; + } + + pub fn get_text(&self) -> &T { + &self.text + } + + // baseline relative to the underlying render area pub fn place(&mut self, baseline: Point) { - let text_width = self.font.text_width(self.text); + let text_width = self.font.text_width(self.text.as_ref()); let text_height = self.font.text_height(); let text_area_start = baseline + Offset::new(-(text_width / 2), -text_height); @@ -167,7 +178,12 @@ impl<'a> TextOverlay<'a> { let p_rel = Point::new(p.x - self.area.x0, p.y - self.area.y0); - for g in self.text.bytes().filter_map(|c| self.font.get_glyph(c)) { + for g in self + .text + .as_ref() + .bytes() + .filter_map(|c| self.font.get_glyph(c)) + { let top = self.max_height - self.baseline - g.bearing_y; let char_area = Rect::new( Point::new(tot_adv + g.bearing_x, top), @@ -756,9 +772,9 @@ fn rect_rounded2_get_pixel( /// Optionally draws a text inside the rectangle and adjusts its color to match /// the fill. The coordinates of the text are specified in the TextOverlay /// struct. -pub fn bar_with_text_and_fill( +pub fn bar_with_text_and_fill>( area: Rect, - overlay: Option, + overlay: Option<&TextOverlay>, fg_color: Color, bg_color: Color, fill_from: i16, @@ -836,6 +852,59 @@ pub fn paint_point(point: &Point, color: Color) { display::bar(point.x, point.y, 1, 1, color.into()); } +/// Draws longer multiline texts inside an area. +/// Does not add any characters on the line boundaries. +/// +/// If it fits, returns the rest of the area. +/// If it does not fit, returns `None`. +pub fn text_multiline( + area: Rect, + text: &str, + font: Font, + fg_color: Color, + bg_color: Color, +) -> Option { + let line_height = font.line_height(); + let characters_overall = text.chars().count(); + let mut taken_from_top = 0; + let mut characters_drawn = 0; + 'lines: loop { + let baseline = area.top_left() + Offset::y(line_height + taken_from_top); + if !area.contains(baseline) { + // The whole area was consumed. + return None; + } + let mut line_text: String<50> = String::new(); + 'characters: loop { + if let Some(character) = text.chars().nth(characters_drawn) { + characters_drawn += 1; + if character == '\n' { + // The line is forced to end. + break 'characters; + } + unwrap!(line_text.push(character)); + } else { + // No more characters to draw. + break 'characters; + } + if font.text_width(&line_text) > area.width() { + // Cannot fit on the line anymore. + line_text.pop(); + characters_drawn -= 1; + break 'characters; + } + } + text_left(baseline, &line_text, font, fg_color, bg_color); + taken_from_top += line_height; + if characters_drawn == characters_overall { + // No more lines to draw. + break 'lines; + } + } + // Some of the area was unused and is free to draw some further text. + Some(area.split_top(taken_from_top).1) +} + /// Display text left-aligned to a certain Point pub fn text_left(baseline: Point, text: &str, font: Font, fg_color: Color, bg_color: Color) { display::text( diff --git a/core/embed/rust/src/ui/event.rs b/core/embed/rust/src/ui/event.rs index 3c273f9969..fd1f555543 100644 --- a/core/embed/rust/src/ui/event.rs +++ b/core/embed/rust/src/ui/event.rs @@ -9,8 +9,14 @@ pub enum PhysicalButton { #[derive(Copy, Clone, PartialEq, Eq)] pub enum ButtonEvent { + /// Button pressed down. + /// ▼ * | * ▼ ButtonPressed(PhysicalButton), + /// Button released up. + /// ▲ * | * ▲ ButtonReleased(PhysicalButton), + HoldStarted, + HoldEnded, } impl ButtonEvent { diff --git a/core/embed/rust/src/ui/geometry.rs b/core/embed/rust/src/ui/geometry.rs index 9627f597e2..ecf1569046 100644 --- a/core/embed/rust/src/ui/geometry.rs +++ b/core/embed/rust/src/ui/geometry.rs @@ -239,6 +239,21 @@ impl Rect { } } + pub const fn from_top_right_and_size(p0: Point, size: Offset) -> Self { + let top_left = Point::new(p0.x - size.x, p0.y); + Self::from_top_left_and_size(top_left, size) + } + + pub const fn from_bottom_left_and_size(p0: Point, size: Offset) -> Self { + let top_left = Point::new(p0.x, p0.y - size.y); + Self::from_top_left_and_size(top_left, size) + } + + pub const fn from_bottom_right_and_size(p0: Point, size: Offset) -> Self { + let top_left = Point::new(p0.x - size.x, p0.y - size.y); + Self::from_top_left_and_size(top_left, size) + } + pub const fn from_center_and_size(p: Point, size: Offset) -> Self { let x0 = p.x - size.x / 2; let y0 = p.y - size.y / 2; @@ -304,6 +319,14 @@ impl Rect { self.bottom_left().center(self.bottom_right()) } + pub const fn left_center(&self) -> Point { + self.bottom_left().center(self.top_left()) + } + + pub const fn right_center(&self) -> Point { + self.bottom_right().center(self.top_right()) + } + /// Whether a `Point` is inside the `Rect`. pub const fn contains(&self, point: Point) -> bool { point.x >= self.x0 && point.x < self.x1 && point.y >= self.y0 && point.y < self.y1 @@ -364,6 +387,26 @@ impl Rect { } } + /// Make the `Rect` wider to the left side. + pub const fn extend_left(&self, width: i16) -> Self { + Self { + x0: self.x0 - width, + y0: self.y0, + x1: self.x1, + y1: self.y1, + } + } + + /// Make the `Rect` wider to the right side. + pub const fn extend_right(&self, width: i16) -> Self { + Self { + x0: self.x0, + y0: self.y0, + x1: self.x1 + width, + y1: self.y1, + } + } + /// Split `Rect` into top and bottom, given the top one's `height`. pub const fn split_top(self, height: i16) -> (Self, Self) { let height = clamp(height, 0, self.height()); @@ -404,6 +447,16 @@ impl Rect { self.split_left(self.width() - width) } + /// Split `Rect` into left, center and right, given the center one's + /// `width`. Center element is symmetric, left and right have the same + /// size. + pub const fn split_center(self, width: i16) -> (Self, Self, Self) { + let left_right_width = (self.width() - width) / 2; + let (left, center_right) = self.split_left(left_right_width); + let (center, right) = center_right.split_left(width); + (left, center, right) + } + pub const fn clamp(self, limit: Rect) -> Self { Self { x0: max(self.x0, limit.x0), diff --git a/core/embed/rust/src/ui/layout/util.rs b/core/embed/rust/src/ui/layout/util.rs index 71c68582ea..5ab009486d 100644 --- a/core/embed/rust/src/ui/layout/util.rs +++ b/core/embed/rust/src/ui/layout/util.rs @@ -40,6 +40,16 @@ pub fn iter_into_objs(iterable: Obj) -> Result<[Obj; N], Error> } pub fn iter_into_array(iterable: Obj) -> Result<[T; N], Error> +where + T: TryFrom, +{ + let err = Error::ValueError(cstr!("Invalid iterable length")); + let vec: Vec = iter_into_vec(iterable)?; + // Returns error if array.len() != N + vec.into_array().map_err(|_| err) +} + +pub fn iter_into_vec(iterable: Obj) -> Result, Error> where T: TryFrom, { @@ -49,8 +59,7 @@ where for item in Iter::try_from_obj_with_buf(iterable, &mut iter_buf)? { vec.push(item.try_into()?).map_err(|_| err)?; } - // Returns error if array.len() != N - vec.into_array().map_err(|_| err) + Ok(vec) } /// Maximum number of characters that can be displayed on screen at once. Used diff --git a/core/embed/rust/src/ui/macros.rs b/core/embed/rust/src/ui/macros.rs index 033157fa81..954e2943f6 100644 --- a/core/embed/rust/src/ui/macros.rs +++ b/core/embed/rust/src/ui/macros.rs @@ -24,3 +24,15 @@ macro_rules! inttostr { heapless::String::<10>::from($int).as_str() }}; } + +#[allow(unused_macros)] // Mostly for debugging purposes. +/// Transforms bool into string slice. For example for printing. +macro_rules! booltostr { + ($bool:expr) => {{ + if $bool { + "true" + } else { + "false" + } + }}; +} diff --git a/core/embed/rust/src/ui/util.rs b/core/embed/rust/src/ui/util.rs index 23bb5ed8f9..f0c9988647 100644 --- a/core/embed/rust/src/ui/util.rs +++ b/core/embed/rust/src/ui/util.rs @@ -142,6 +142,13 @@ pub fn icon_text_center( ); } +/// Convert char to a String of chosen length. +pub fn char_to_string(ch: char) -> String { + let mut s = String::new(); + unwrap!(s.push(ch)); + s +} + /// Returns text to be fit on one line of a given length. /// When the text is too long to fit, it is truncated with ellipsis /// on the left side.