Skip to content

Commit

Permalink
feat(core/ui): add PageCounter to T3T1 Footer
Browse files Browse the repository at this point in the history
PageCounter sub-component is used within Footer to indicate progress in
screens, rendered e.g. as "1 / 20" for the first word in wallet backup.
  • Loading branch information
obrusvit committed Jun 13, 2024
1 parent 79d2483 commit e9f8577
Show file tree
Hide file tree
Showing 6 changed files with 144 additions and 29 deletions.
128 changes: 118 additions & 10 deletions core/embed/rust/src/ui/model_mercury/component/footer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,32 @@ use crate::{
strutil::TString,
ui::{
component::{text::TextStyle, Component, Event, EventCtx, Never, SwipeDirection},
display::Color,
display::{Color, Font},
event::SwipeEvent,
geometry::{Alignment, Offset, Rect},
geometry::{Alignment, Alignment2D, Offset, Point, Rect},
lerp::Lerp,
model_mercury::theme,
shape,
shape::{Renderer, Text},
},
};

/// Component showing a task instruction (e.g. "Swipe up") and optionally task
/// description (e.g. "Confirm transaction") to a user. 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 (both description and
/// instruction). The content and style of both description and instruction is
/// configurable separatedly.
use heapless::String;

/// Component showing a task instruction, e.g. "Swipe up", and optionally 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 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). The content and style of both
/// is configurable separatedly.
#[derive(Clone)]
pub struct Footer<'a> {
area: Rect,
text_instruction: TString<'a>,
text_description: Option<TString<'a>>,
counter: Option<PageCounter>,
style_instruction: &'static TextStyle,
style_description: &'static TextStyle,
swipe_allow_up: bool,
Expand All @@ -42,6 +47,7 @@ impl<'a> Footer<'a> {
area: Rect::zero(),
text_instruction: instruction.into(),
text_description: None,
counter: None,
style_instruction: &theme::TEXT_SUB_GREY,
style_description: &theme::TEXT_SUB_GREY_LIGHT,
swipe_allow_down: false,
Expand All @@ -58,16 +64,37 @@ impl<'a> Footer<'a> {
}
}

pub fn with_page_counter(self, max_pages: u8) -> Self {
Self {
counter: Some(PageCounter::new(max_pages)),
..self
}
}

pub fn update_instruction<T: Into<TString<'a>>>(&mut self, ctx: &mut EventCtx, s: T) {
self.text_instruction = s.into();
ctx.request_paint();
}

pub fn update_description<T: Into<TString<'a>>>(&mut self, ctx: &mut EventCtx, s: T) {
if self.text_description.is_none() {
#[cfg(feature = "ui_debug")]
panic!("footer does not have description")
}
self.text_description = Some(s.into());
ctx.request_paint();
}

pub fn update_page_counter(&mut self, ctx: &mut EventCtx, n: u8) {
if self.counter.is_none() {
#[cfg(feature = "ui_debug")]
panic!("footer does not have counter")
} else {
self.counter.as_mut().unwrap().update_current_page(n);
ctx.request_paint();
}
}

pub fn update_instruction_style(&mut self, ctx: &mut EventCtx, style: &'static TextStyle) {
self.style_instruction = style;
ctx.request_paint();
Expand All @@ -79,7 +106,7 @@ impl<'a> Footer<'a> {
}

pub fn height(&self) -> i16 {
if self.text_description.is_some() {
if self.text_description.is_some() || self.counter.is_some() {
Footer::HEIGHT_DEFAULT
} else {
Footer::HEIGHT_SIMPLE
Expand All @@ -106,8 +133,13 @@ impl<'a> Component for Footer<'a> {
fn place(&mut self, bounds: Rect) -> Rect {
let h = bounds.height();
assert!(h == Footer::HEIGHT_SIMPLE || h == Footer::HEIGHT_DEFAULT);
if let Some(counter) = &mut self.counter {
if h == Footer::HEIGHT_DEFAULT {
counter.place(bounds.split_top(Footer::HEIGHT_SIMPLE).0);
}
}
self.area = bounds;
bounds
self.area
}

fn event(&mut self, _ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
Expand Down Expand Up @@ -181,6 +213,8 @@ impl<'a> Component for Footer<'a> {
.with_align(Alignment::Center)
.render(target);
});
} else if let Some(counter) = &self.counter {
counter.render(target);
}
}

Expand Down Expand Up @@ -224,3 +258,77 @@ impl crate::trace::Trace for Footer<'_> {
t.string("instruction", self.text_instruction);
}
}

/// Helper component used within Footer instead of description for counting
/// pages of content, rendered e.g. as: '1 / 20'.
#[derive(Clone)]
struct PageCounter {
area: Rect,
page_curr: u8,
page_max: u8,
font: Font,
}

impl PageCounter {
fn new(page_max: u8) -> Self {
Self {
area: Rect::zero(),
page_curr: 1,
page_max,
font: Font::SUB,
}
}

fn update_current_page(&mut self, new_value: u8) {
self.page_curr = new_value;
}
}

impl Component for PageCounter {
type Msg = Never;

fn place(&mut self, bounds: Rect) -> Rect {
self.area = bounds;
self.area
}

fn event(&mut self, _ctx: &mut EventCtx, _event: Event) -> Option<Self::Msg> {
None
}

fn paint(&mut self) {
todo!("remove when ui-t3t1 done")
}

fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
let color = if self.page_curr == self.page_max {
theme::GREEN_LIGHT
} else {
theme::GREY_LIGHT
};

let foreslash_offset = Offset::x(10); // half of foreslash icon (6px) + spacing (4px)
let num_base_y = self.font.vert_center(self.area.y0, self.area.y1, "1");
let base_num = Point::new(self.area.center().x, num_base_y);
let base_num_curr = base_num - foreslash_offset;
let base_num_max = base_num + foreslash_offset;

let string_curr = build_string!(10, inttostr!(self.page_curr));
let string_max = build_string!(10, inttostr!(self.page_max));

Text::new(base_num_curr, &string_curr)
.with_align(Alignment::End)
.with_fg(color)
.with_font(self.font)
.render(target);
shape::ToifImage::new(self.area.center(), theme::ICON_FORESLASH.toif)
.with_align(Alignment2D::CENTER)
.with_fg(color)
.render(target);
Text::new(base_num_max, &string_max)
.with_align(Alignment::Start)
.with_fg(color)
.with_font(self.font)
.render(target);
}
}
12 changes: 12 additions & 0 deletions core/embed/rust/src/ui/model_mercury/component/frame.rs
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,12 @@ where
self
}

#[inline(never)]
pub fn with_footer_counter(mut self, instruction: TString<'static>, max_value: u8) -> Self {
self.footer = Some(Footer::new(instruction).with_page_counter(max_value));
self
}

pub fn with_danger(self) -> Self {
self.button_styled(theme::button_danger())
.title_styled(theme::label_title_danger())
Expand Down Expand Up @@ -195,6 +201,12 @@ where
res
}

pub fn update_footer_counter(&mut self, ctx: &mut EventCtx, new_val: u8) {
if let Some(footer) = &mut self.footer {
footer.update_page_counter(ctx, new_val);
}
}

#[inline(never)]
pub fn with_swipe(mut self, dir: SwipeDirection, settings: SwipeSettings) -> Self {
self.footer = self.footer.map(|f| match dir {
Expand Down
19 changes: 7 additions & 12 deletions core/embed/rust/src/ui/model_mercury/component/share_words.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use crate::{
},
event::SwipeEvent,
geometry::{Alignment, Alignment2D, Insets, Offset, Rect},
model_mercury::component::{Footer, Frame, FrameMsg},
model_mercury::component::{Frame, FrameMsg},
shape,
shape::Renderer,
util,
Expand Down Expand Up @@ -48,14 +48,16 @@ impl<'a> ShareWords<'a> {
} else {
None
};
let n_words = share_words.len();
Self {
area: Rect::zero(),
subtitle,
frame: Frame::left_aligned(title, ShareWordsInner::new(share_words))
.with_swipe(SwipeDirection::Up, SwipeSettings::default())
.with_swipe(SwipeDirection::Down, SwipeSettings::default())
.with_vertical_pages()
.with_subtitle(subtitle),
.with_subtitle(subtitle)
.with_footer_counter(TR::instructions__swipe_up.into(), n_words as u8),
repeated_indices,
}
}
Expand Down Expand Up @@ -83,8 +85,9 @@ impl<'a> Component for ShareWords<'a> {
}

fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
let page_index = self.frame.inner().page_index as usize;
if let Some(repeated_indices) = &self.repeated_indices {
if repeated_indices.contains(&(self.frame.inner().page_index as usize)) {
if repeated_indices.contains(&page_index) {
let updated_subtitle = TString::from_translation(TR::reset__the_word_is_repeated);
self.frame
.update_subtitle(ctx, updated_subtitle, Some(theme::TEXT_SUB_GREEN_LIME));
Expand All @@ -93,6 +96,7 @@ impl<'a> Component for ShareWords<'a> {
.update_subtitle(ctx, self.subtitle, Some(theme::TEXT_SUB_GREY));
}
}
self.frame.update_footer_counter(ctx, page_index as u8 + 1);
self.frame.event(ctx, event)
}

Expand Down Expand Up @@ -136,8 +140,6 @@ struct ShareWordsInner<'a> {
area_word: Rect,
/// `Some` when transition animation is in progress
animation: Option<Animation<f32>>,
/// Footer component for instructions and word counting
footer: Footer<'static>,
progress: i16,
}

Expand All @@ -152,7 +154,6 @@ impl<'a> ShareWordsInner<'a> {
next_index: 0,
area_word: Rect::zero(),
animation: None,
footer: Footer::new(TR::instructions__swipe_up),
progress: 0,
}
}
Expand Down Expand Up @@ -197,9 +198,6 @@ impl<'a> Component for ShareWordsInner<'a> {
Alignment2D::CENTER,
);

self.footer
.place(used_area.split_bottom(Footer::HEIGHT_SIMPLE).1);

self.area
}

Expand Down Expand Up @@ -295,9 +293,6 @@ impl<'a> Component for ShareWordsInner<'a> {
self.render_word(self.page_index, target);
})
};

// footer with instructions
self.footer.render(target);
}

#[cfg(feature = "ui_bounds")]
Expand Down
6 changes: 3 additions & 3 deletions core/embed/rust/src/ui/model_mercury/theme/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ pub const QR_SIDE_MAX: u32 = 140;

// UI icons (white color).

// 12x12
include_icon!(ICON_FORESLASH, "model_mercury/res/foreslash12.toif");

// 20x20
include_icon!(
ICON_BULLET_CHECKMARK,
Expand Down Expand Up @@ -140,9 +143,6 @@ include_icon!(ICON_UP, "model_tt/res/caret-up24.toif");
include_icon!(ICON_DOWN, "model_tt/res/caret-down24.toif");
include_icon!(ICON_CLICK, "model_tt/res/finger24.toif");

include_icon!(ICON_CORNER_CANCEL, "model_tt/res/x32.toif");
include_icon!(ICON_CORNER_INFO, "model_tt/res/info32.toif");

// Homescreen notifications.
include_icon!(ICON_WARN, "model_tt/res/warning16.toif");
include_icon!(ICON_WARNING40, "model_tt/res/warning40.toif");
Expand Down
2 changes: 1 addition & 1 deletion core/embed/rust/src/ui/shape/text.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ pub struct Text<'a> {

impl<'a> Text<'a> {
/// Creates a `shape::Text` structure with a specified
/// text (`str`) and the top-left corner (`pos`).
/// text (`str`) and the bottom-left corner (`pos`).
pub fn new(pos: Point, text: &'a str) -> Self {
Self {
pos,
Expand Down
6 changes: 3 additions & 3 deletions core/translations/signatures.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
{
"current": {
"merkle_root": "b89f1a6620bb714b3a34bc44e54692bba0d283e926c5ba52b99ba3df51290626",
"datetime": "2024-06-10T13:43:09.597756",
"commit": "df051077ea42bcf360bc3afd4366c68c2fac5841"
"merkle_root": "5d31ad78a79697343575fc37083c01a9d4c05e9271a00672cab59a5e7ed50e13",
"datetime": "2024-06-13T07:31:31.919548",
"commit": "40999978c4ab6790bedc0a0623121492de5f6ad8"
},
"history": [
{
Expand Down

0 comments on commit e9f8577

Please sign in to comment.