1
0
mirror of https://github.com/trezor/trezor-firmware.git synced 2025-07-29 18:08:19 +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
/// 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,

View File

@ -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] {

View File

@ -9,3 +9,41 @@ pub fn shuffle<T>(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);
}
}
}

View File

@ -85,6 +85,10 @@ impl<T> Child<T> {
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<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) {
// Not starting if animations are disabled.
if animation_disabled() {

View File

@ -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<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.
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,

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")]
impl<T: ParagraphSource> crate::trace::Trace for Checklist<T> {
fn trace(&self, t: &mut dyn crate::trace::Tracer) {

View File

@ -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!(

View File

@ -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<T> {
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<T: AsRef<str>> TextOverlay<T> {
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<T: AsRef<str>>(
area: Rect,
overlay: Option<TextOverlay>,
overlay: Option<&TextOverlay<T>>,
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<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
pub fn text_left(baseline: Point, text: &str, font: Font, fg_color: Color, bg_color: Color) {
display::text(

View File

@ -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 {

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 {
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),

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>
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
T: TryFrom<Obj, Error = Error>,
{
@ -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

View File

@ -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"
}
}};
}

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.
/// When the text is too long to fit, it is truncated with ellipsis
/// on the left side.