perf(eckhart): compute less in render functions
What changed, and why it matters
This commit is a straightforward performance optimization for the user interface on Trezor's newer 'Eckhart' layout. It pre-calculates and stores values like battery percentage text and page counter positions so they don't have to be recomputed every time the screen is redrawn. It also fixes a minor visual alignment issue with a small connection indicator dot. There is no indication this change fixes a security vulnerability.
No security action required; review as normal UI performance refactor.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The patch caches rendering computations in three UI components: FuelGauge (battery state of charge text and icon/color selection), PageCounter (current/max page strings, text positions, and colors), and the connected indicator (simplifies inner bar placement). The changes move work from render() into event/update paths and initialization, reducing per-frame allocations/formatting. The connected.rs change replaces a snapped centered rectangle with an explicit top-left point and removes radius/fg styling on the inner bar, described as fixing misalignment.
Changed components
core/embed/rust/src/ui/layout_eckhart/component/fuel_gauge.rscore/embed/rust/src/ui/layout_eckhart/cshape/connected.rscore/embed/rust/src/ui/layout_eckhart/firmware/hint.rsInspect captured patch +101 / −89
diff --git a/core/embed/rust/src/ui/layout_eckhart/component/fuel_gauge.rs b/core/embed/rust/src/ui/layout_eckhart/component/fuel_gauge.rs
index c5bbe929..35589eee 100644
--- a/core/embed/rust/src/ui/layout_eckhart/component/fuel_gauge.rs
+++ b/core/embed/rust/src/ui/layout_eckhart/component/fuel_gauge.rs
@@ -38,8 +38,13 @@ pub struct FuelGauge {
charging_state: ChargingState,
/// State of charge (0-100) [%]
soc: Option<u8>,
+ /// Cached formatted SOC text
+ soc_text: ShortString,
/// Font used for the soc percentage
font: Font,
+ /// Cached indication (icon, icon color, text color) based on the last known
+ /// charging state and soc.
+ battery_indication: (Icon, Color, Color),
}
#[derive(Clone)]
@@ -76,6 +81,9 @@ impl FuelGauge {
pub fn update_pm_state(&mut self) {
self.soc = Some(power_manager::soc());
self.charging_state = power_manager::charging_state();
+ self.soc_text = uformat!("{} %", self.soc.unwrap_or(0));
+ self.battery_indication =
+ self.battery_indication(self.charging_state, self.soc.unwrap_or(0));
}
pub fn should_be_shown(&self) -> bool {
@@ -91,40 +99,16 @@ impl FuelGauge {
/// Returns the total rendered width of the fuel gauge content.
pub fn content_width(&self) -> i16 {
- let icon_w = self.icon_width();
+ let icon_width = self.battery_indication.0.toif.width();
match self.mode {
- FuelGaugeMode::AlwaysIconOnly | FuelGaugeMode::ChargingIconOnly => icon_w,
- _ => {
- let soc_fmt = self.soc_text();
- icon_w + ICON_PERCENT_GAP + self.font.text_width(&soc_fmt)
- }
- }
- }
-
- const fn icon_width(&self) -> i16 {
- match self.charging_state {
- ChargingState::Charging => ICON_BATTERY_ZAP.toif.width(),
- ChargingState::Discharging | ChargingState::Idle => ICON_BATTERY_FULL.toif.width(),
+ FuelGaugeMode::AlwaysIconOnly | FuelGaugeMode::ChargingIconOnly => icon_width,
+ _ => icon_width + ICON_PERCENT_GAP + self.font.text_width(&self.soc_text),
}
}
- fn soc_text(&self) -> ShortString {
- if self.soc.is_none() {
- uformat!("?")
- } else {
- uformat!("{} %", self.soc.unwrap_or(0))
- }
- }
-
- fn render_icon<'s>(
- &self,
- area: Rect,
- icon: Icon,
- color: Color,
- target: &mut impl Renderer<'s>,
- ) {
- shape::ToifImage::new(area.left_center(), icon.toif)
- .with_fg(color)
+ fn render_icon<'s>(&self, area: Rect, target: &mut impl Renderer<'s>) {
+ shape::ToifImage::new(area.left_center(), self.battery_indication.0.toif)
+ .with_fg(self.battery_indication.1)
.with_align(Alignment2D::CENTER_LEFT)
.render(target);
}
@@ -139,7 +123,9 @@ impl FuelGauge {
mode,
charging_state: ChargingState::Idle,
soc: None,
+ soc_text: ShortString::new(),
font,
+ battery_indication: (ICON_BATTERY_EMPTY, GREY_LIGHT, GREY_LIGHT),
}
}
@@ -208,14 +194,13 @@ impl Component for FuelGauge {
}
}
#[cfg(feature = "micropython")]
- Event::Timer(_) => match &mut self.mode {
- FuelGaugeMode::OnChargingChange(timer) => {
+ Event::Timer(_) => {
+ if let FuelGaugeMode::OnChargingChange(timer) = &mut self.mode {
if timer.expire(event) {
ctx.request_paint();
}
}
- _ => {}
- },
+ }
_ => {}
}
@@ -223,10 +208,8 @@ impl Component for FuelGauge {
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
- let soc = self.soc.unwrap_or(0);
- let (icon, color_icon, color_text) = self.battery_indication(self.charging_state, soc);
- let soc_percent_fmt = self.soc_text();
- let text_width = self.font.text_width(&soc_percent_fmt);
+ let (icon, color_icon, color_text) = self.battery_indication;
+ let text_width = self.font.text_width(&self.soc_text);
let text_height = self.font.text_height();
let icon_width = icon.toif.width();
let icon_height = icon.toif.height();
@@ -243,25 +226,21 @@ impl Component for FuelGauge {
match self.mode {
FuelGaugeMode::AlwaysIconOnly => {
- self.render_icon(area, icon, color_icon, target);
+ self.render_icon(area, target);
}
FuelGaugeMode::ChargingIconOnly => {
if matches!(self.charging_state, ChargingState::Charging) {
- self.render_icon(area, icon, color_icon, target);
+ self.render_icon(area, target);
}
}
- _ => {
+ FuelGaugeMode::AlwaysFull | FuelGaugeMode::OnChargingChange(..) => {
// both icon and percentage
- self.render_icon(area, icon, color_icon, target);
- let text_y_coord = self.font.vert_center(area.y0, area.y1, &soc_percent_fmt);
- shape::Text::new(
- Point::new(area.x1, text_y_coord),
- &soc_percent_fmt,
- self.font,
- )
- .with_fg(color_text)
- .with_align(Alignment::End)
- .render(target);
+ self.render_icon(area, target);
+ let text_y_coord = self.font.vert_center(area.y0, area.y1, &self.soc_text);
+ shape::Text::new(Point::new(area.x1, text_y_coord), &self.soc_text, self.font)
+ .with_fg(color_text)
+ .with_align(Alignment::End)
+ .render(target);
}
}
}
diff --git a/core/embed/rust/src/ui/layout_eckhart/cshape/connected.rs b/core/embed/rust/src/ui/layout_eckhart/cshape/connected.rs
index e7d9fbba..aa2cfe45 100644
--- a/core/embed/rust/src/ui/layout_eckhart/cshape/connected.rs
+++ b/core/embed/rust/src/ui/layout_eckhart/cshape/connected.rs
@@ -19,13 +19,13 @@ pub fn render_connected_indicator<'s>(point: Point, target: &mut impl Renderer<'
.with_fg(INDICATOR_OUTER_COLOR)
.with_bg(INDICATOR_OUTER_COLOR)
.render(target);
- shape::Bar::new(Rect::snap(
- point,
+
+ const HALF: i16 = INDICATOR_INNER_SIZE / 2;
+ let inner_top_left = Point::new(point.x - HALF, point.y - HALF);
+ shape::Bar::new(Rect::from_top_left_and_size(
+ inner_top_left,
Offset::uniform(INDICATOR_INNER_SIZE),
- Alignment2D::CENTER,
))
- .with_fg(INDICATOR_INNER_COLOR)
.with_bg(INDICATOR_INNER_COLOR)
- .with_radius(1)
.render(target);
}
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 4aba5396..2a7b8b4d 100644
--- a/core/embed/rust/src/ui/layout_eckhart/firmware/hint.rs
+++ b/core/embed/rust/src/ui/layout_eckhart/firmware/hint.rs
@@ -129,13 +129,19 @@ impl<'a> Component for Hint<'a> {
let bounds = bounds.inset(insets);
- if let HintContent::Instruction(instruction) = &mut self.content {
- let text_area = match instruction.icon {
- Some(_) => bounds.split_left(instruction.icon_width()).1,
- None => bounds,
- };
- instruction.label.place(text_area);
- }
+ match &mut self.content {
+ HintContent::Instruction(instruction) => {
+ let text_area = match instruction.icon {
+ Some(_) => bounds.split_left(instruction.icon_width()).1,
+ None => bounds,
+ };
+ instruction.label.place(text_area);
+ }
+ HintContent::PageCounter(page_counter) => {
+ page_counter.place(bounds);
+ }
+ };
+
self.content_area = bounds;
self.content_area
}
@@ -239,62 +245,89 @@ impl<'a> Instruction<'a> {
/// as: '1 / 20'.
#[derive(Clone)]
struct PageCounter {
+ area: Rect,
pager: Pager,
+ string_curr: ShortString,
+ string_max: ShortString,
+ base_num_curr: Point,
+ base_foreslash: Point,
+ base_num_max: Point,
+ color_num: Color,
+ color_icon: Color,
}
impl PageCounter {
/// margins from the edges of the screen [px]
const INSETS: Insets = Insets::new(16, 24, 14, 12);
+ const FONT: Font = fonts::FONT_SATOSHI_REGULAR_22;
fn new() -> Self {
- Self {
+ let mut s = Self {
pager: Pager::single_page(),
- }
+ area: Rect::zero(),
+ string_curr: ShortString::new(),
+ string_max: ShortString::new(),
+ base_num_curr: Point::zero(),
+ base_foreslash: Point::zero(),
+ base_num_max: Point::zero(),
+ color_num: theme::GREY,
+ color_icon: theme::GREY_DARK,
+ };
+ s.recompute();
+ s
+ }
+
+ fn place(&mut self, area: Rect) -> Rect {
+ self.area = area;
+ self.recompute();
+ self.area
}
fn update(&mut self, pager: Pager) {
- self.pager = pager
+ self.pager = pager;
+ self.recompute();
}
-}
-impl PageCounter {
- fn render<'s>(&'s self, target: &mut impl Renderer<'s>, area: Rect) {
- let font = fonts::FONT_SATOSHI_REGULAR_22;
- let (color_num, color_icon) = if self.pager.is_last() {
+ fn recompute(&mut self) {
+ (self.color_num, self.color_icon) = if self.pager.is_last() {
(theme::GREEN_LIGHT, theme::GREEN)
} else {
(theme::GREY, theme::GREY_DARK)
};
- let string_curr = uformat!("{}", self.pager.current() + 1);
- let string_max = uformat!("{}", self.pager.total());
+ self.string_curr = uformat!("{}", self.pager.current() + 1);
+ self.string_max = uformat!("{}", self.pager.total());
- // 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 offset_x = 4; // spacing between foreslash and numbers
+ let width_num_curr = Self::FONT.text_width(&self.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 width_num_max = Self::FONT.text_width(&self.string_max);
+ let width_total = width_num_curr + width_foreslash + width_num_max + 2 * offset_x;
- let counter_area = area.inset(Self::INSETS);
+ let counter_area = self.area.inset(Self::INSETS);
let counter_start_x = counter_area.bottom_left().x;
- let counter_y = font.vert_center(counter_area.y0, counter_area.y1, "0");
+ let counter_y = Self::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, font)
+ self.base_num_curr = Point::new(counter_start_x, counter_y);
+ self.base_foreslash = Point::new(counter_start_x + width_num_curr + offset_x, counter_y);
+ self.base_num_max = Point::new(counter_end_x, counter_y);
+ }
+}
+
+impl PageCounter {
+ fn render<'s>(&'s self, target: &mut impl Renderer<'s>, area: Rect) {
+ Text::new(self.base_num_curr, &self.string_curr, Self::FONT)
.with_align(Alignment::Start)
- .with_fg(color_num)
+ .with_fg(self.color_num)
.render(target);
- shape::ToifImage::new(base_foreslash, theme::ICON_FORESLASH.toif)
+ shape::ToifImage::new(self.base_foreslash, theme::ICON_FORESLASH.toif)
.with_align(Alignment2D::BOTTOM_LEFT)
- .with_fg(color_icon)
+ .with_fg(self.color_icon)
.render(target);
- Text::new(base_num_max, &string_max, font)
+ Text::new(self.base_num_max, &self.string_max, Self::FONT)
.with_align(Alignment::End)
- .with_fg(color_num)
+ .with_fg(self.color_num)
.render(target);
}
}
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.