refactor(core): remove builder for page counter
What changed, and why it matters
This is a small internal code cleanup in the user-interface code for the Trezor hardware wallet's newer 'Eckhart' layout. It removes a special 'pagination hint' builder flag and instead lets callers pass an ordinary page-counter hint component. The visible behavior is meant to stay the same: a page counter appears only when content spans more than one page. There is no indication this change fixes a security bug or introduces a vulnerability.
No security action required. Treat as a normal UI refactor; review for visual/layout regressions during QA if desired.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The commit refactors TextScreen in core/embed/rust/src/ui/layout_eckhart/firmware/text_screen.rs. Previously TextScreen carried a boolean pagination_hint and lazily created a Hint::new_page_counter() during placement if the content had more than one page. The patch removes that boolean and the with_pagination_hint() builder method. Callers now supply the hint explicitly via with_hint(Hint::new_page_counter()). A new helper place_content_with_hint() handles layout: if the hint is a page counter it places content first, checks self.content.pager().total(), and only reserves space for the counter when there is more than one page; otherwise it drops the hint. Non-page-counter hints are always placed at the bottom. The logic is functionally equivalent for the existing call sites (confirm_output, receive, ui_firmware).
Changed components
core/embed/rust/src/ui/layout_eckhart/firmware/text_screen.rscore/embed/rust/src/ui/layout_eckhart/firmware/hint.rscore/embed/rust/src/ui/layout_eckhart/flow/confirm_output.rscore/embed/rust/src/ui/layout_eckhart/flow/receive.rscore/embed/rust/src/ui/layout_eckhart/ui_firmware.rsInspect captured patch +48 / −42
diff --git a/core/embed/rust/src/ui/layout_eckhart/firmware/hint.rs b/core/embed/rust/src/ui/layout_eckhart/firmware/hint.rs
index 7b7a3a16..4aba5396 100644
--- a/core/embed/rust/src/ui/layout_eckhart/firmware/hint.rs
+++ b/core/embed/rust/src/ui/layout_eckhart/firmware/hint.rs
@@ -107,6 +107,10 @@ impl<'a> Hint<'a> {
let insets = self.content.insets();
self.content.height() + insets.top + insets.bottom
}
+
+ pub fn is_page_counter(&self) -> bool {
+ matches!(self.content, HintContent::PageCounter(_))
+ }
}
impl<'a> Component for Hint<'a> {
diff --git a/core/embed/rust/src/ui/layout_eckhart/firmware/text_screen.rs b/core/embed/rust/src/ui/layout_eckhart/firmware/text_screen.rs
index ac34abd9..bf767e8a 100644
--- a/core/embed/rust/src/ui/layout_eckhart/firmware/text_screen.rs
+++ b/core/embed/rust/src/ui/layout_eckhart/firmware/text_screen.rs
@@ -38,7 +38,6 @@ pub struct TextScreen<T> {
action_bar: Option<ActionBar>,
page_limit: Option<u16>,
background: Option<ScreenBackground>,
- pagination_hint: bool,
// TODO: swipe handling
// TODO: animations
}
@@ -66,7 +65,6 @@ where
action_bar: Some(ActionBar::new_paginate_only()),
page_limit: None,
background: None,
- pagination_hint: false,
}
}
@@ -87,11 +85,6 @@ where
self
}
- pub fn with_pagination_hint(mut self) -> Self {
- self.pagination_hint = true;
- self
- }
-
pub fn with_action_bar(mut self, action_bar: ActionBar) -> Self {
self.action_bar = Some(action_bar);
self
@@ -126,6 +119,45 @@ where
self.content.pager()
}
}
+
+ fn place_content_with_hint(&mut self, bounds: Rect) {
+ let compute_content_area = |area: Rect, has_header: bool| {
+ // Introduce side insets + top padding if the header is not present
+ let mut area = area.inset(SIDE_INSETS);
+ if !has_header {
+ area = area.inset(Insets::top(CONTENT_INSETS_NO_HEADER.top));
+ }
+ area
+ };
+
+ if let Some(hint) = &mut self.hint {
+ if hint.is_page_counter() {
+ let content_area = compute_content_area(bounds, self.header.is_some());
+ self.content.place(content_area);
+ // place page counter only if the content doesn't fit on a
+ // single page
+ if self.content.pager().total() > 1 {
+ let (rest, hint_area) = bounds.split_bottom(hint.height());
+ hint.place(hint_area);
+ let content_area = compute_content_area(rest, self.header.is_some());
+ // re-place content to account for the reduced area
+ self.content.place(content_area);
+ } else {
+ self.hint = None;
+ }
+ } else {
+ // always place non-page-counter hints at the bottom
+ let (content_area, hint_area) = bounds.split_bottom(hint.height());
+ let content_area = compute_content_area(content_area, self.header.is_some());
+ hint.place(hint_area);
+ self.content.place(content_area);
+ }
+ } else {
+ let content_area = compute_content_area(bounds, self.header.is_some());
+ self.content.place(content_area);
+ }
+ self.update_page(0);
+ }
}
impl<T> Component for TextScreen<T>
@@ -174,37 +206,7 @@ where
rest
};
- let rest = if let Some(hint) = &mut self.hint {
- let (rest, hint_area) = rest.split_bottom(hint.height());
- hint.place(hint_area);
- rest
- } else {
- rest
- };
-
- let compute_content_area = |area: Rect, has_header: bool| {
- let mut area = area.inset(SIDE_INSETS);
- if !has_header {
- area = area.inset(Insets::top(CONTENT_INSETS_NO_HEADER.top));
- }
- area
- };
-
- // Introduce side insets + top padding if the header is not present
- let content_area = compute_content_area(rest, self.header.is_some());
- self.content.place(content_area);
- self.update_page(0);
-
- if self.pagination_hint && self.hint.is_none() && self.content.pager().total() > 1 {
- let mut hint = Hint::new_page_counter();
- let (rest, hint_area) = rest.split_bottom(hint.height());
- hint.place(hint_area);
- self.hint = Some(hint);
- let content_area = compute_content_area(rest, self.header.is_some());
- self.content.place(content_area);
- self.update_page(0);
- }
-
+ self.place_content_with_hint(rest);
bounds
}
diff --git a/core/embed/rust/src/ui/layout_eckhart/flow/confirm_output.rs b/core/embed/rust/src/ui/layout_eckhart/flow/confirm_output.rs
index 77f7ea37..dbcdcd28 100644
--- a/core/embed/rust/src/ui/layout_eckhart/flow/confirm_output.rs
+++ b/core/embed/rust/src/ui/layout_eckhart/flow/confirm_output.rs
@@ -23,7 +23,7 @@ use crate::{
use super::super::{
component::Button,
firmware::{
- ActionBar, Header, ShortMenuVec, TextScreen, TextScreenMsg, VerticalMenu,
+ ActionBar, Header, Hint, ShortMenuVec, TextScreen, TextScreenMsg, VerticalMenu,
VerticalMenuScreen, VerticalMenuScreenMsg,
},
flow::util::content_menu_info,
@@ -288,7 +288,7 @@ pub fn new_confirm_output(
))
.with_header(Header::new(title.unwrap_or(TString::empty())).with_menu_button())
.with_subtitle(subtitle.unwrap_or(TString::empty()))
- .with_pagination_hint()
+ .with_hint(Hint::new_page_counter())
.with_action_bar(ActionBar::new_single(Button::with_text(
TR::buttons__continue.into(),
)))
diff --git a/core/embed/rust/src/ui/layout_eckhart/flow/receive.rs b/core/embed/rust/src/ui/layout_eckhart/flow/receive.rs
index b29f600e..e557e640 100644
--- a/core/embed/rust/src/ui/layout_eckhart/flow/receive.rs
+++ b/core/embed/rust/src/ui/layout_eckhart/flow/receive.rs
@@ -129,7 +129,7 @@ pub fn new_receive(
)
.with_header(Header::new(title).with_menu_button())
.with_subtitle(subtitle.unwrap_or(TString::empty()))
- .with_pagination_hint()
+ .with_hint(Hint::new_page_counter())
.with_action_bar(ActionBar::new_single(button));
if let Some(hint) = hint {
address_screen = address_screen.with_hint(Hint::new_warning_caution(hint));
diff --git a/core/embed/rust/src/ui/layout_eckhart/ui_firmware.rs b/core/embed/rust/src/ui/layout_eckhart/ui_firmware.rs
index 85375c96..ff1037ec 100644
--- a/core/embed/rust/src/ui/layout_eckhart/ui_firmware.rs
+++ b/core/embed/rust/src/ui/layout_eckhart/ui_firmware.rs
@@ -516,7 +516,7 @@ impl FirmwareUI for UIEckhart {
.with_subtitle(subtitle.unwrap_or(TString::empty()))
.with_action_bar(action_bar);
if page_counter {
- screen = screen.with_pagination_hint();
+ screen = screen.with_hint(Hint::new_page_counter())
} else if let Some(warning_footer) = warning_footer {
screen = screen.with_hint(Hint::new_warning_caution(warning_footer));
}
Why this scored 13/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.