1
0
mirror of https://github.com/trezor/trezor-firmware.git synced 2025-01-09 23:11:10 +00:00

WIP: feat(lincoln): hint component

This commit is contained in:
obrusvit 2025-01-06 18:39:46 +01:00
parent 1255c3ae2d
commit 0f89fa9bdf
4 changed files with 221 additions and 8 deletions

View File

@ -0,0 +1,210 @@
use crate::{
strutil::TString,
ui::{
component::{text::TextStyle, Component, EventCtx, Never},
display::{Color, Font, Icon},
geometry::{Alignment, Alignment2D, Direction, Offset, Point, Rect},
shape::{self, Renderer, Text},
},
};
use super::theme;
/// Component rendered above ActionBar, showing one of these:
/// - a task instruction/hint, e.g. "Confirm transaction"
/// - a page counter e.g. "1 / 3", meaning the first screen of three total.
#[derive(Clone)]
pub struct Hint<'a> {
area: Rect,
content: HintContent<'a>,
swipe_allow_up: bool,
swipe_allow_down: bool,
progress: i16,
dir: Direction,
}
#[derive(Clone)]
enum HintContent<'a> {
Instruction(Instruction<'a>),
PageCounter(PageCounter),
}
impl<'a> Hint<'a> {
/// height of the single line component [px]
pub const HEIGHT_SINGLE_LINE: i16 = 40;
/// height of the multi line component [px]
pub const HEIGHT_MULTI_LINE: i16 = 60;
fn from_content(content: HintContent<'a>) -> Self {
Self {
area: Rect::zero(),
content,
swipe_allow_down: false,
swipe_allow_up: false,
progress: 0,
dir: Direction::Up,
}
}
pub fn new_instruction<T: Into<TString<'a>>>(
instruction: T,
color: Color,
icon: Option<Icon>,
) -> Self {
let instruction = instruction.into();
let instruction_component = Instruction::new(instruction, color, icon);
Self::from_content(HintContent::Instruction(instruction_component))
}
pub fn new_page_counter() -> Self {
Self::from_content(HintContent::PageCounter(PageCounter::new()))
}
pub fn update_page_counter(&mut self, ctx: &mut EventCtx, current: usize, max: usize) {
match &mut self.content {
HintContent::PageCounter(counter) => {
counter.update_current_page(current, max);
self.swipe_allow_down = counter.is_first_page();
self.swipe_allow_up = counter.is_last_page();
ctx.request_paint();
}
_ => {
#[cfg(feature = "ui_debug")]
panic!("hint component does not have counter")
}
}
}
pub fn height(&self) -> i16 {
self.content.height()
}
pub fn with_swipe(self, swipe_direction: Direction) -> Self {
match swipe_direction {
Direction::Up => Self {
swipe_allow_up: true,
..self
},
Direction::Down => Self {
swipe_allow_down: true,
..self
},
_ => self,
}
}
}
// impl<'a> Component for Hint<'a> {
// type Msg = Never;
// }
impl<'a> HintContent<'a> {
fn height(&self) -> i16 {
if matches!(self, HintContent::PageCounter(_)) {
Hint::HEIGHT_SINGLE_LINE
} else {
// TODO: determine height based on text length
Hint::HEIGHT_MULTI_LINE
}
}
}
// Helper componet used within Hint for instruction/hint rendering.
#[derive(Clone)]
struct Instruction<'a> {
text: TString<'a>,
color: Color,
icon: Option<Icon>,
}
impl<'a> Instruction<'a> {
const STYLE_INSTRUCTION: &'static TextStyle = &theme::TEXT_NORMAL;
fn new(text: TString<'a>, color: Color, icon: Option<Icon>) -> Self {
Self { text, color, icon }
}
}
/// Helper component used within Hint for page count indication, rendered e.g. as: '1 / 20'.
#[derive(Clone)]
struct PageCounter {
page_curr: u8,
page_max: u8,
}
impl PageCounter {
fn new() -> Self {
Self {
page_curr: 0,
page_max: 0,
}
}
fn update_current_page(&mut self, new_value: usize, max: usize) {
self.page_max = max as u8;
self.page_curr = (new_value as u8).clamp(0, self.page_max.saturating_sub(1));
}
fn is_first_page(&self) -> bool {
self.page_curr == 0
}
fn is_last_page(&self) -> bool {
self.page_curr + 1 == self.page_max
}
}
impl PageCounter {
fn render<'s>(&'s self, target: &mut impl Renderer<'s>, area: Rect) {
let font = Font::SUB;
let color = if self.is_last_page() {
theme::GREEN_LIGHT
} else {
theme::GREY
};
let string_curr = uformat!("{}", self.page_curr + 1);
let string_max = uformat!("{}", self.page_max);
// center the whole counter "x / yz"
let offset_x = Offset::x(4); // spacing between foreslash and numbers
let width_num_curr = font.text_width(&string_curr);
let width_foreslash = theme::ICON_FORESLASH.toif.width();
let width_num_max = font.text_width(&string_max);
let width_total = width_num_curr + width_foreslash + width_num_max + 2 * offset_x.x;
let counter_area = area.split_top(Hint::HEIGHT_SINGLE_LINE).0;
let center_x = counter_area.center().x;
let counter_y = font.vert_center(counter_area.y0, counter_area.y1, "0");
let counter_start_x = center_x - width_total / 2;
let counter_end_x = center_x + width_total / 2;
let base_num_curr = Point::new(counter_start_x, counter_y);
let base_foreslash = Point::new(counter_start_x + width_num_curr + offset_x.x, counter_y);
let base_num_max = Point::new(counter_end_x, counter_y);
Text::new(base_num_curr, &string_curr)
.with_align(Alignment::Start)
.with_fg(color)
.with_font(font)
.render(target);
shape::ToifImage::new(base_foreslash, theme::ICON_FORESLASH.toif)
.with_align(Alignment2D::BOTTOM_LEFT)
.with_fg(color)
.render(target);
Text::new(base_num_max, &string_max)
.with_align(Alignment::End)
.with_fg(color)
.with_font(font)
.render(target);
}
}
#[cfg(feature = "ui_debug")]
impl crate::trace::Trace for PageCounter {
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.component("PageCounter");
t.int("page current", self.page_curr.into());
t.int("page max", self.page_max.into());
}
}

View File

@ -1,12 +1,14 @@
mod button;
mod error;
mod header;
mod hint;
mod result;
mod welcome_screen;
pub use button::{ButtonStyle, ButtonStyleSheet};
pub use error::ErrorScreen;
pub use header::Header;
pub use hint::Hint;
pub use result::{ResultFooter, ResultScreen, ResultStyle};
pub use welcome_screen::WelcomeScreen;

View File

@ -40,6 +40,7 @@ pub const FATAL_ERROR_HIGHLIGHT_COLOR: Color = Color::rgb(0xFF, 0x41, 0x41);
// UI icons (white color).
// TODO: icons
include_icon!(ICON_FORESLASH, "model_mercury/res/foreslash12.toif"); // FIXME: mercury icon
include_icon!(ICON_ASTERISK, "model_lincoln/res/asterisk20.toif");
include_icon!(ICON_CHECKMARK, "model_lincoln/res/checkmark24.toif");
include_icon!(ICON_CHEVRON_DOWN, "model_lincoln/res/chevron_down24.toif");

View File

@ -15,7 +15,8 @@ use crate::{
/// Component showing a task instruction, e.g. "Swipe up", and an optional
/// content consisting of one of these:
/// - a task description e.g. "Confirm transaction", or
/// - a page counter e.g. "1 / 3", meaning the first screen of three total.
/// - a page counter e.g. "1 / 3", meaning the first screen of three total, or
/// - a page hint e.g. "Go back" if you are on the last page.
/// A host of this component is responsible of providing the exact area
/// considering also the spacing. The height must be 18px (only instruction) or
/// 37px (instruction and description/position).
@ -313,7 +314,6 @@ impl<'a> FooterContent<'a> {
#[derive(Clone)]
struct PageCounter {
pub instruction: TString<'static>,
font: Font,
page_curr: u8,
page_max: u8,
}
@ -324,7 +324,6 @@ impl PageCounter {
instruction,
page_curr: 0,
page_max: 0,
font: Font::SUB,
}
}
@ -344,6 +343,7 @@ impl PageCounter {
impl PageCounter {
fn render<'s>(&'s self, target: &mut impl Renderer<'s>, area: Rect) {
let font = Font::SUB;
let color = if self.is_last_page() {
theme::GREEN_LIGHT
} else {
@ -355,14 +355,14 @@ impl PageCounter {
// center the whole counter "x / yz"
let offset_x = Offset::x(4); // spacing between foreslash and numbers
let width_num_curr = self.font.text_width(&string_curr);
let width_num_curr = font.text_width(&string_curr);
let width_foreslash = theme::ICON_FORESLASH.toif.width();
let width_num_max = self.font.text_width(&string_max);
let width_num_max = font.text_width(&string_max);
let width_total = width_num_curr + width_foreslash + width_num_max + 2 * offset_x.x;
let counter_area = area.split_top(Footer::HEIGHT_SIMPLE).0;
let center_x = counter_area.center().x;
let counter_y = self.font.vert_center(counter_area.y0, counter_area.y1, "0");
let counter_y = font.vert_center(counter_area.y0, counter_area.y1, "0");
let counter_start_x = center_x - width_total / 2;
let counter_end_x = center_x + width_total / 2;
let base_num_curr = Point::new(counter_start_x, counter_y);
@ -372,7 +372,7 @@ impl PageCounter {
Text::new(base_num_curr, &string_curr)
.with_align(Alignment::Start)
.with_fg(color)
.with_font(self.font)
.with_font(font)
.render(target);
shape::ToifImage::new(base_foreslash, theme::ICON_FORESLASH.toif)
.with_align(Alignment2D::BOTTOM_LEFT)
@ -381,7 +381,7 @@ impl PageCounter {
Text::new(base_num_max, &string_max)
.with_align(Alignment::End)
.with_fg(color)
.with_font(self.font)
.with_font(font)
.render(target);
FooterContent::render_instruction(target, area, &self.instruction);