1
0
mirror of https://github.com/trezor/trezor-firmware.git synced 2025-07-31 02:48:44 +00:00

TR-rust: necessary changes for TR UI

This commit is contained in:
grdddj 2023-03-31 15:14:29 +02:00
parent da64b21078
commit df7af9734d
15 changed files with 271 additions and 76 deletions

View File

@ -20,7 +20,7 @@ use super::ffi;
/// The `off` field represents offset from the `ptr` and allows us to do /// The `off` field represents offset from the `ptr` and allows us to do
/// substring slices while keeping the head pointer as required by GC. /// substring slices while keeping the head pointer as required by GC.
#[repr(C)] #[repr(C)]
#[derive(Clone)] #[derive(Debug, Clone, Copy)]
pub struct StrBuffer { pub struct StrBuffer {
ptr: *const u8, ptr: *const u8,
len: u16, len: u16,

View File

@ -1,5 +1,8 @@
use heapless::String; use heapless::String;
#[cfg(feature = "model_tr")]
use crate::ui::model_tr::component::ButtonPos;
/// Visitor passed into `Trace` types. /// Visitor passed into `Trace` types.
pub trait Tracer { pub trait Tracer {
fn int(&mut self, i: i64); 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. /// Value that can describe own structure and data using the `Tracer` interface.
pub trait Trace { pub trait Trace {
fn trace(&self, t: &mut dyn Tracer); 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] { impl Trace for &[u8] {

View File

@ -9,3 +9,41 @@ pub fn shuffle<T>(slice: &mut [T]) {
slice.swap(i, j); 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);
}
}
}

View File

@ -85,6 +85,10 @@ impl<T> Child<T> {
self.component self.component
} }
pub fn inner_mut(&mut self) -> &mut T {
&mut self.component
}
/// Access inner component mutably, track whether a paint call has been /// Access inner component mutably, track whether a paint call has been
/// requested, and propagate the flag upwards the component tree. /// requested, and propagate the flag upwards the component tree.
pub fn mutate<F, U>(&mut self, ctx: &mut EventCtx, component_func: F) -> U pub fn mutate<F, U>(&mut self, ctx: &mut EventCtx, component_func: F) -> U

View File

@ -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) { pub fn start(&mut self, ctx: &mut EventCtx, now: Instant) {
// Not starting if animations are disabled. // Not starting if animations are disabled.
if animation_disabled() { if animation_disabled() {

View File

@ -1,8 +1,3 @@
use crate::ui::component::{
text::layout::{LayoutFit, TextNoOp},
FormattedText,
};
pub enum AuxPageMsg { pub enum AuxPageMsg {
/// Page component was instantiated with BACK button on every page and it /// Page component was instantiated with BACK button on every page and it
/// was pressed. /// was pressed.
@ -34,61 +29,3 @@ pub trait Paginate {
/// Navigate to the given page. /// Navigate to the given page.
fn change_page(&mut self, active_page: usize); fn change_page(&mut self, active_page: usize);
} }
impl<F, T> Paginate for FormattedText<F, T>
where
F: AsRef<str>,
T: AsRef<str>,
{
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);
}
}
}
}
}

View File

@ -354,7 +354,7 @@ impl TextLayout {
} }
/// Overall height of the content, including paddings. /// 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.padding_top
+ self.style.text_font.text_height() + self.style.text_font.text_height()
+ (end_cursor.y - init_cursor.y) + (end_cursor.y - init_cursor.y)
@ -562,7 +562,7 @@ pub struct Span {
} }
impl Span { impl Span {
fn fit_horizontally( pub fn fit_horizontally(
text: &str, text: &str,
max_width: i16, max_width: i16,
text_font: impl GlyphMetrics, text_font: impl GlyphMetrics,

View File

@ -625,6 +625,17 @@ where
} }
} }
impl<T> Paginate for Checklist<T>
where
T: ParagraphSource,
{
fn page_count(&mut self) -> usize {
1
}
fn change_page(&mut self, _to_page: usize) {}
}
#[cfg(feature = "ui_debug")] #[cfg(feature = "ui_debug")]
impl<T: ParagraphSource> crate::trace::Trace for Checklist<T> { impl<T: ParagraphSource> crate::trace::Trace for Checklist<T> {
fn trace(&self, t: &mut dyn crate::trace::Tracer) { fn trace(&self, t: &mut dyn crate::trace::Tracer) {

View File

@ -16,6 +16,9 @@ use super::{
}; };
use crate::{micropython::buffer::StrBuffer, time::Duration}; use crate::{micropython::buffer::StrBuffer, time::Duration};
#[cfg(feature = "model_tr")]
use super::model_tr::component::ButtonDetails;
// NOTE: not defining a common trait, like // NOTE: not defining a common trait, like
// Debug {fn print(&self);}, so that the trait does // Debug {fn print(&self);}, so that the trait does
// not need to be imported when using the // 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 { impl Offset {
pub fn print(&self) { pub fn print(&self) {
println!( println!(

View File

@ -5,6 +5,8 @@ pub mod loader;
pub mod tjpgd; pub mod tjpgd;
pub mod toif; pub mod toif;
use heapless::String;
use super::{ use super::{
constant, constant,
geometry::{Offset, Point, Rect}, geometry::{Offset, Point, Rect},
@ -126,16 +128,16 @@ pub fn rect_fill_corners(r: Rect, fg_color: Color) {
} }
#[derive(Copy, Clone, PartialEq, Eq)] #[derive(Copy, Clone, PartialEq, Eq)]
pub struct TextOverlay<'a> { pub struct TextOverlay<T> {
area: Rect, area: Rect,
text: &'a str, text: T,
font: Font, font: Font,
max_height: i16, max_height: i16,
baseline: i16, baseline: i16,
} }
impl<'a> TextOverlay<'a> { impl<T: AsRef<str>> TextOverlay<T> {
pub fn new(text: &'a str, font: Font) -> Self { pub fn new(text: T, font: Font) -> Self {
let area = Rect::zero(); let area = Rect::zero();
Self { 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) { 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_height = self.font.text_height();
let text_area_start = baseline + Offset::new(-(text_width / 2), -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); 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 top = self.max_height - self.baseline - g.bearing_y;
let char_area = Rect::new( let char_area = Rect::new(
Point::new(tot_adv + g.bearing_x, top), 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 /// 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 /// the fill. The coordinates of the text are specified in the TextOverlay
/// struct. /// struct.
pub fn bar_with_text_and_fill( pub fn bar_with_text_and_fill<T: AsRef<str>>(
area: Rect, area: Rect,
overlay: Option<TextOverlay>, overlay: Option<&TextOverlay<T>>,
fg_color: Color, fg_color: Color,
bg_color: Color, bg_color: Color,
fill_from: i16, 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()); 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<Rect> {
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 /// Display text left-aligned to a certain Point
pub fn text_left(baseline: Point, text: &str, font: Font, fg_color: Color, bg_color: Color) { pub fn text_left(baseline: Point, text: &str, font: Font, fg_color: Color, bg_color: Color) {
display::text( display::text(

View File

@ -9,8 +9,14 @@ pub enum PhysicalButton {
#[derive(Copy, Clone, PartialEq, Eq)] #[derive(Copy, Clone, PartialEq, Eq)]
pub enum ButtonEvent { pub enum ButtonEvent {
/// Button pressed down.
/// ▼ * | * ▼
ButtonPressed(PhysicalButton), ButtonPressed(PhysicalButton),
/// Button released up.
/// ▲ * | * ▲
ButtonReleased(PhysicalButton), ButtonReleased(PhysicalButton),
HoldStarted,
HoldEnded,
} }
impl ButtonEvent { impl ButtonEvent {

View File

@ -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 { pub const fn from_center_and_size(p: Point, size: Offset) -> Self {
let x0 = p.x - size.x / 2; let x0 = p.x - size.x / 2;
let y0 = p.y - size.y / 2; let y0 = p.y - size.y / 2;
@ -304,6 +319,14 @@ impl Rect {
self.bottom_left().center(self.bottom_right()) 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`. /// Whether a `Point` is inside the `Rect`.
pub const fn contains(&self, point: Point) -> bool { pub const fn contains(&self, point: Point) -> bool {
point.x >= self.x0 && point.x < self.x1 && point.y >= self.y0 && point.y < self.y1 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`. /// Split `Rect` into top and bottom, given the top one's `height`.
pub const fn split_top(self, height: i16) -> (Self, Self) { pub const fn split_top(self, height: i16) -> (Self, Self) {
let height = clamp(height, 0, self.height()); let height = clamp(height, 0, self.height());
@ -404,6 +447,16 @@ impl Rect {
self.split_left(self.width() - width) 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 { pub const fn clamp(self, limit: Rect) -> Self {
Self { Self {
x0: max(self.x0, limit.x0), x0: max(self.x0, limit.x0),

View File

@ -40,6 +40,16 @@ pub fn iter_into_objs<const N: usize>(iterable: Obj) -> Result<[Obj; N], Error>
} }
pub fn iter_into_array<T, const N: usize>(iterable: Obj) -> Result<[T; N], Error> pub fn iter_into_array<T, const N: usize>(iterable: Obj) -> Result<[T; N], Error>
where
T: TryFrom<Obj, Error = Error>,
{
let err = Error::ValueError(cstr!("Invalid iterable length"));
let vec: Vec<T, N> = iter_into_vec(iterable)?;
// Returns error if array.len() != N
vec.into_array().map_err(|_| err)
}
pub fn iter_into_vec<T, const N: usize>(iterable: Obj) -> Result<Vec<T, N>, Error>
where where
T: TryFrom<Obj, Error = Error>, T: TryFrom<Obj, Error = Error>,
{ {
@ -49,8 +59,7 @@ where
for item in Iter::try_from_obj_with_buf(iterable, &mut iter_buf)? { for item in Iter::try_from_obj_with_buf(iterable, &mut iter_buf)? {
vec.push(item.try_into()?).map_err(|_| err)?; vec.push(item.try_into()?).map_err(|_| err)?;
} }
// Returns error if array.len() != N Ok(vec)
vec.into_array().map_err(|_| err)
} }
/// Maximum number of characters that can be displayed on screen at once. Used /// Maximum number of characters that can be displayed on screen at once. Used

View File

@ -24,3 +24,15 @@ macro_rules! inttostr {
heapless::String::<10>::from($int).as_str() 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"
}
}};
}

View File

@ -142,6 +142,13 @@ pub fn icon_text_center(
); );
} }
/// Convert char to a String of chosen length.
pub fn char_to_string<const L: usize>(ch: char) -> String<L> {
let mut s = String::new();
unwrap!(s.push(ch));
s
}
/// Returns text to be fit on one line of a given length. /// 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 /// When the text is too long to fit, it is truncated with ellipsis
/// on the left side. /// on the left side.