mirror of
https://github.com/trezor/trezor-firmware.git
synced 2025-01-24 22:31:35 +00:00
feat(lincoln): hint component
This commit is contained in:
parent
8d753ed96a
commit
04dfd31aa0
299
core/embed/rust/src/ui/model_lincoln/component/hint.rs
Normal file
299
core/embed/rust/src/ui/model_lincoln/component/hint.rs
Normal file
@ -0,0 +1,299 @@
|
||||
use crate::{
|
||||
strutil::TString,
|
||||
ui::{
|
||||
component::{text::TextStyle, Component, Event, EventCtx, Never},
|
||||
display::{Color, Font, Icon},
|
||||
event::SwipeEvent,
|
||||
geometry::{Alignment, Alignment2D, Direction, Offset, Point, Rect},
|
||||
lerp::Lerp,
|
||||
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;
|
||||
|
||||
fn place(&mut self, bounds: Rect) -> Rect {
|
||||
debug_assert!(bounds.height() == self.content.height());
|
||||
self.area = bounds;
|
||||
self.area
|
||||
}
|
||||
|
||||
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||
match event {
|
||||
Event::Attach(_) => {
|
||||
self.progress = 0;
|
||||
}
|
||||
Event::Swipe(SwipeEvent::Move(dir, progress)) => match dir {
|
||||
Direction::Up if self.swipe_allow_up => {
|
||||
self.progress = progress;
|
||||
self.dir = dir;
|
||||
}
|
||||
Direction::Down if self.swipe_allow_down => {
|
||||
self.progress = progress;
|
||||
self.dir = dir;
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
_ => {}
|
||||
};
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||
let progress = self.progress as f32 / 1000.0;
|
||||
|
||||
let shift = pareen::constant(0.0).seq_ease_out(
|
||||
0.0,
|
||||
easer::functions::Cubic,
|
||||
1.0,
|
||||
pareen::constant(1.0),
|
||||
);
|
||||
|
||||
let offset = i16::lerp(0, 20, shift.eval(progress));
|
||||
|
||||
let mask = u8::lerp(0, 255, shift.eval(progress));
|
||||
|
||||
let offset = match self.dir {
|
||||
Direction::Up => Offset::y(-offset),
|
||||
Direction::Down => Offset::y(3 * offset),
|
||||
_ => Offset::zero(),
|
||||
};
|
||||
|
||||
target.with_origin(offset, &|target| {
|
||||
self.content.render(self.area, target);
|
||||
shape::Bar::new(self.area)
|
||||
.with_alpha(mask)
|
||||
.with_fg(Color::black())
|
||||
.with_bg(Color::black())
|
||||
.render(target);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
fn render<'s>(&'s self, area: Rect, target: &mut impl Renderer<'s>)
|
||||
where
|
||||
's: 'a,
|
||||
{
|
||||
match self {
|
||||
HintContent::Instruction(instruction) => instruction.render(target, area),
|
||||
HintContent::PageCounter(page_counter) => page_counter.render(target, area),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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 }
|
||||
}
|
||||
|
||||
fn render<'s>(&'s self, target: &mut impl Renderer<'s>, area: Rect) {
|
||||
const ICON_OFFSET: Offset = Offset::x(24);
|
||||
let text_baseline = if let Some(icon) = self.icon {
|
||||
let offset_x = Offset::x(icon.toif.width() + ICON_OFFSET.x);
|
||||
shape::ToifImage::new(area.top_left(), icon.toif)
|
||||
.with_align(Alignment2D::TOP_LEFT)
|
||||
.with_fg(self.color)
|
||||
.render(target);
|
||||
area.bottom_left() + offset_x
|
||||
} else {
|
||||
area.bottom_left()
|
||||
};
|
||||
self.text.map(|t| {
|
||||
Text::new(text_baseline, t)
|
||||
.with_font(Self::STYLE_INSTRUCTION.text_font)
|
||||
.with_fg(self.color)
|
||||
.with_align(Alignment::Start)
|
||||
.render(target)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// 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);
|
||||
|
||||
// the counter is left aligned
|
||||
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;
|
||||
let counter_start_x = counter_area.bottom_left().x;
|
||||
let counter_y = font.vert_center(counter_area.y0, counter_area.y1, "0");
|
||||
let counter_end_x = counter_start_x + width_total;
|
||||
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 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;
|
||||
|
||||
|
@ -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);
|
||||
|
Loading…
Reference in New Issue
Block a user