mirror of
https://github.com/trezor/trezor-firmware.git
synced 2025-01-10 07:20:56 +00:00
WIP: feat(lincoln): hint component
This commit is contained in:
parent
1255c3ae2d
commit
0f89fa9bdf
210
core/embed/rust/src/ui/model_lincoln/component/hint.rs
Normal file
210
core/embed/rust/src/ui/model_lincoln/component/hint.rs
Normal 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());
|
||||||
|
}
|
||||||
|
}
|
@ -1,12 +1,14 @@
|
|||||||
mod button;
|
mod button;
|
||||||
mod error;
|
mod error;
|
||||||
mod header;
|
mod header;
|
||||||
|
mod hint;
|
||||||
mod result;
|
mod result;
|
||||||
mod welcome_screen;
|
mod welcome_screen;
|
||||||
|
|
||||||
pub use button::{ButtonStyle, ButtonStyleSheet};
|
pub use button::{ButtonStyle, ButtonStyleSheet};
|
||||||
pub use error::ErrorScreen;
|
pub use error::ErrorScreen;
|
||||||
pub use header::Header;
|
pub use header::Header;
|
||||||
|
pub use hint::Hint;
|
||||||
pub use result::{ResultFooter, ResultScreen, ResultStyle};
|
pub use result::{ResultFooter, ResultScreen, ResultStyle};
|
||||||
pub use welcome_screen::WelcomeScreen;
|
pub use welcome_screen::WelcomeScreen;
|
||||||
|
|
||||||
|
@ -40,6 +40,7 @@ pub const FATAL_ERROR_HIGHLIGHT_COLOR: Color = Color::rgb(0xFF, 0x41, 0x41);
|
|||||||
|
|
||||||
// UI icons (white color).
|
// UI icons (white color).
|
||||||
// TODO: icons
|
// 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_ASTERISK, "model_lincoln/res/asterisk20.toif");
|
||||||
include_icon!(ICON_CHECKMARK, "model_lincoln/res/checkmark24.toif");
|
include_icon!(ICON_CHECKMARK, "model_lincoln/res/checkmark24.toif");
|
||||||
include_icon!(ICON_CHEVRON_DOWN, "model_lincoln/res/chevron_down24.toif");
|
include_icon!(ICON_CHEVRON_DOWN, "model_lincoln/res/chevron_down24.toif");
|
||||||
|
@ -15,7 +15,8 @@ use crate::{
|
|||||||
/// Component showing a task instruction, e.g. "Swipe up", and an optional
|
/// Component showing a task instruction, e.g. "Swipe up", and an optional
|
||||||
/// content consisting of one of these:
|
/// content consisting of one of these:
|
||||||
/// - a task description e.g. "Confirm transaction", or
|
/// - 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
|
/// A host of this component is responsible of providing the exact area
|
||||||
/// considering also the spacing. The height must be 18px (only instruction) or
|
/// considering also the spacing. The height must be 18px (only instruction) or
|
||||||
/// 37px (instruction and description/position).
|
/// 37px (instruction and description/position).
|
||||||
@ -313,7 +314,6 @@ impl<'a> FooterContent<'a> {
|
|||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
struct PageCounter {
|
struct PageCounter {
|
||||||
pub instruction: TString<'static>,
|
pub instruction: TString<'static>,
|
||||||
font: Font,
|
|
||||||
page_curr: u8,
|
page_curr: u8,
|
||||||
page_max: u8,
|
page_max: u8,
|
||||||
}
|
}
|
||||||
@ -324,7 +324,6 @@ impl PageCounter {
|
|||||||
instruction,
|
instruction,
|
||||||
page_curr: 0,
|
page_curr: 0,
|
||||||
page_max: 0,
|
page_max: 0,
|
||||||
font: Font::SUB,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -344,6 +343,7 @@ impl PageCounter {
|
|||||||
|
|
||||||
impl PageCounter {
|
impl PageCounter {
|
||||||
fn render<'s>(&'s self, target: &mut impl Renderer<'s>, area: Rect) {
|
fn render<'s>(&'s self, target: &mut impl Renderer<'s>, area: Rect) {
|
||||||
|
let font = Font::SUB;
|
||||||
let color = if self.is_last_page() {
|
let color = if self.is_last_page() {
|
||||||
theme::GREEN_LIGHT
|
theme::GREEN_LIGHT
|
||||||
} else {
|
} else {
|
||||||
@ -355,14 +355,14 @@ impl PageCounter {
|
|||||||
|
|
||||||
// center the whole counter "x / yz"
|
// center the whole counter "x / yz"
|
||||||
let offset_x = Offset::x(4); // spacing between foreslash and numbers
|
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_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 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 counter_area = area.split_top(Footer::HEIGHT_SIMPLE).0;
|
||||||
let center_x = counter_area.center().x;
|
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_start_x = center_x - width_total / 2;
|
||||||
let counter_end_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_num_curr = Point::new(counter_start_x, counter_y);
|
||||||
@ -372,7 +372,7 @@ impl PageCounter {
|
|||||||
Text::new(base_num_curr, &string_curr)
|
Text::new(base_num_curr, &string_curr)
|
||||||
.with_align(Alignment::Start)
|
.with_align(Alignment::Start)
|
||||||
.with_fg(color)
|
.with_fg(color)
|
||||||
.with_font(self.font)
|
.with_font(font)
|
||||||
.render(target);
|
.render(target);
|
||||||
shape::ToifImage::new(base_foreslash, theme::ICON_FORESLASH.toif)
|
shape::ToifImage::new(base_foreslash, theme::ICON_FORESLASH.toif)
|
||||||
.with_align(Alignment2D::BOTTOM_LEFT)
|
.with_align(Alignment2D::BOTTOM_LEFT)
|
||||||
@ -381,7 +381,7 @@ impl PageCounter {
|
|||||||
Text::new(base_num_max, &string_max)
|
Text::new(base_num_max, &string_max)
|
||||||
.with_align(Alignment::End)
|
.with_align(Alignment::End)
|
||||||
.with_fg(color)
|
.with_fg(color)
|
||||||
.with_font(self.font)
|
.with_font(font)
|
||||||
.render(target);
|
.render(target);
|
||||||
|
|
||||||
FooterContent::render_instruction(target, area, &self.instruction);
|
FooterContent::render_instruction(target, area, &self.instruction);
|
||||||
|
Loading…
Reference in New Issue
Block a user