From 0f89fa9bdf856ee78152ef7ece675b3f930edd33 Mon Sep 17 00:00:00 2001 From: obrusvit Date: Mon, 6 Jan 2025 18:39:46 +0100 Subject: [PATCH] WIP: feat(lincoln): hint component --- .../src/ui/model_lincoln/component/hint.rs | 210 ++++++++++++++++++ .../src/ui/model_lincoln/component/mod.rs | 2 + .../rust/src/ui/model_lincoln/theme/mod.rs | 1 + .../src/ui/model_mercury/component/footer.rs | 16 +- 4 files changed, 221 insertions(+), 8 deletions(-) create mode 100644 core/embed/rust/src/ui/model_lincoln/component/hint.rs diff --git a/core/embed/rust/src/ui/model_lincoln/component/hint.rs b/core/embed/rust/src/ui/model_lincoln/component/hint.rs new file mode 100644 index 0000000000..02c6ce5c61 --- /dev/null +++ b/core/embed/rust/src/ui/model_lincoln/component/hint.rs @@ -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>>( + instruction: T, + color: Color, + icon: Option, + ) -> 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, +} + +impl<'a> Instruction<'a> { + const STYLE_INSTRUCTION: &'static TextStyle = &theme::TEXT_NORMAL; + + fn new(text: TString<'a>, color: Color, icon: Option) -> 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()); + } +} diff --git a/core/embed/rust/src/ui/model_lincoln/component/mod.rs b/core/embed/rust/src/ui/model_lincoln/component/mod.rs index 25c8950463..aa6e7195f4 100644 --- a/core/embed/rust/src/ui/model_lincoln/component/mod.rs +++ b/core/embed/rust/src/ui/model_lincoln/component/mod.rs @@ -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; diff --git a/core/embed/rust/src/ui/model_lincoln/theme/mod.rs b/core/embed/rust/src/ui/model_lincoln/theme/mod.rs index d2ef89e431..2d988a84d4 100644 --- a/core/embed/rust/src/ui/model_lincoln/theme/mod.rs +++ b/core/embed/rust/src/ui/model_lincoln/theme/mod.rs @@ -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"); diff --git a/core/embed/rust/src/ui/model_mercury/component/footer.rs b/core/embed/rust/src/ui/model_mercury/component/footer.rs index d9a97a2202..06b7737dad 100644 --- a/core/embed/rust/src/ui/model_mercury/component/footer.rs +++ b/core/embed/rust/src/ui/model_mercury/component/footer.rs @@ -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);