What changed, and why it matters
This commit is a user-interface layout tweak for the Trezor hardware wallet's Bolt design. It widens the 'Continue' button and replaces some text buttons with icons in certain confirmation dialogs. There is no security-relevant change visible in the code diff.
No security action required. Treat as a normal UI/UX improvement.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The patch refactors modal button construction in the Bolt UI layout. It introduces a ModalButtons enum to consolidate how button configurations (no buttons, timeout-only, single text button, cancel+confirm icons, cancel icon + text confirm) are passed to new_show_modal. In number_input.rs, the button grid changes from 1x2 to 1x3 so the info button becomes a small corner icon and the Continue button spans two cells, making it wider. These are cosmetic/UX changes; no cryptographic, authorization, or input-validation logic is modified.
Changed components
core/embed/rust/src/ui/layout_bolt/component/number_input.rscore/embed/rust/src/ui/layout_bolt/ui_firmware.rsInspect captured patch +103 / −79
diff --git a/core/embed/rust/src/ui/layout_bolt/component/number_input.rs b/core/embed/rust/src/ui/layout_bolt/component/number_input.rs
index ec4f1d2d..e33a51ee 100644
--- a/core/embed/rust/src/ui/layout_bolt/component/number_input.rs
+++ b/core/embed/rust/src/ui/layout_bolt/component/number_input.rs
@@ -6,7 +6,7 @@ use crate::translations::TR;
use crate::ui::component::base::ComponentExt;
use crate::ui::component::text::paragraphs::{Paragraph, Paragraphs};
use crate::ui::component::{Child, Component, Event, EventCtx, Pad};
-use crate::ui::geometry::{Alignment, Grid, Insets, Offset, Rect};
+use crate::ui::geometry::{Alignment, Grid, GridCellSpan, Insets, Offset, Rect};
use crate::ui::shape::{self, Renderer};
#[cfg_attr(feature = "debug", derive(ufmt::derive::uDebug))]
@@ -40,7 +40,7 @@ where
input: NumberInput::new(min, max, init_value).into_child(),
paragraphs: Paragraphs::new(Paragraph::new(&theme::TEXT_NORMAL, text)).into_child(),
paragraphs_pad: Pad::with_background(theme::BG),
- info_button: Button::with_text(TR::buttons__info.into()).into_child(),
+ info_button: Button::with_icon(theme::ICON_CORNER_INFO).into_child(),
confirm_button: Button::with_text(TR::buttons__continue.into())
.styled(theme::button_confirm())
.into_child(),
@@ -81,12 +81,15 @@ where
theme::CONTENT_BORDER,
));
- let grid = Grid::new(button_area, 1, 2).with_spacing(theme::KEYBOARD_SPACING);
+ let grid = Grid::new(button_area, 1, 3).with_spacing(theme::KEYBOARD_SPACING);
self.input.place(input_area);
self.paragraphs.place(content_area);
self.paragraphs_pad.place(content_area);
self.info_button.place(grid.row_col(0, 0));
- self.confirm_button.place(grid.row_col(0, 1));
+ self.confirm_button.place(grid.cells(GridCellSpan {
+ from: (0, 1),
+ to: (0, 2),
+ }));
bounds
}
diff --git a/core/embed/rust/src/ui/layout_bolt/ui_firmware.rs b/core/embed/rust/src/ui/layout_bolt/ui_firmware.rs
index 6ef06917..01dd0f65 100644
--- a/core/embed/rust/src/ui/layout_bolt/ui_firmware.rs
+++ b/core/embed/rust/src/ui/layout_bolt/ui_firmware.rs
@@ -880,9 +880,7 @@ impl FirmwareUI for UIBolt {
Some(title),
TString::empty(),
description,
- (!button.is_empty()).then_some(button),
- allow_cancel,
- time_ms,
+ ModalButtons::new(button, allow_cancel, time_ms),
icon,
theme::button_default(),
)
@@ -995,13 +993,13 @@ impl FirmwareUI for UIBolt {
if external_menu {
return Err(Error::NotImplementedError);
}
- let button_text = match (button, time_ms) {
+ let buttons = match (button, time_ms) {
// either button or timeout must be set
(None, 0) => return Err(Error::NotImplementedError),
- (None, _) => None,
+ (None, _) => ModalButtons::NoButtonsTimeout(time_ms),
// disabled buttons are not supported on Bolt
(Some((_, false)), _) => return Err(Error::NotImplementedError),
- (Some((text, true)), _) => Some(text),
+ (Some((text, true)), _) => ModalButtons::ConfirmText(text),
};
let icon = BlendedImage::new(
@@ -1015,9 +1013,7 @@ impl FirmwareUI for UIBolt {
Some(title),
TString::empty(),
description,
- button_text,
- false,
- time_ms,
+ buttons,
icon,
theme::button_info(),
)
@@ -1253,9 +1249,7 @@ impl FirmwareUI for UIBolt {
Some(title),
TString::empty(),
description,
- (!button.is_empty()).then_some(button),
- allow_cancel,
- time_ms,
+ ModalButtons::new(button, allow_cancel, time_ms),
icon,
theme::button_confirm(),
)
@@ -1280,13 +1274,12 @@ impl FirmwareUI for UIBolt {
// Disallow showing "dangerous" warning with no header.
return Err(Error::ValueError(c"Non-empty title is required"));
}
+
new_show_modal(
title,
value,
description,
- (!button.is_empty()).then_some(button),
- allow_cancel,
- 0,
+ ModalButtons::new(button, allow_cancel, 0).force_icons(),
icon,
theme::button_reset(),
)
@@ -1301,74 +1294,102 @@ impl FirmwareUI for UIBolt {
}
}
+enum ModalButtons {
+ /// Will be closed from the outside or layout will be discarded after first
+ /// paint.
+ NoButtons,
+ /// No buttons, will close after timeout.
+ NoButtonsTimeout(u32),
+ /// Single button with text.
+ ConfirmText(TString<'static>),
+ /// Two buttons with an icon.
+ CancelAndConfirm,
+ /// Cancel button has icon, confirm has text.
+ CancelAndText(TString<'static>),
+}
+
+impl ModalButtons {
+ fn new(button: TString<'static>, allow_cancel: bool, time_ms: u32) -> Self {
+ if button.is_empty() && time_ms == 0 {
+ Self::NoButtons
+ } else if button.is_empty() && time_ms > 0 {
+ Self::NoButtonsTimeout(time_ms)
+ } else if allow_cancel {
+ Self::CancelAndText(button)
+ } else {
+ Self::ConfirmText(button)
+ }
+ }
+
+ fn force_icons(self) -> Self {
+ match self {
+ Self::CancelAndText(_) => Self::CancelAndConfirm,
+ x => x,
+ }
+ }
+}
+
#[allow(clippy::too_many_arguments)]
fn new_show_modal(
title: Option<TString<'static>>,
value: TString<'static>,
description: TString<'static>,
- button: Option<TString<'static>>,
- allow_cancel: bool,
- time_ms: u32,
+ buttons: ModalButtons,
icon: BlendedImage,
button_style: ButtonStyleSheet,
) -> Result<Gc<LayoutObj>, Error> {
- let obj = match button {
- None => {
- if time_ms == 0 {
- // No buttons and no timer, used when we only want to draw the dialog once and
- // then throw away the layout object.
- LayoutObj::new(
- IconDialog::new(icon, title, Empty)
- .with_value(value)
- .with_description(description),
- )?
- } else {
- // Timeout, no buttons.
- LayoutObj::new(
- IconDialog::new(
- icon,
- title,
- Timeout::new(time_ms).map(|_| Some(CancelConfirmMsg::Confirmed)),
- )
- .with_value(value)
- .with_description(description),
- )?
- }
- }
- Some(button) => {
- if allow_cancel {
- // Two buttons.
- LayoutObj::new(
- IconDialog::new(
- icon,
- title,
- Button::cancel_confirm(
- Button::with_icon(theme::ICON_CANCEL),
- Button::with_text(button).styled(button_style),
- false,
- ),
- )
- .with_value(value)
- .with_description(description),
- )?
- } else {
- // Single button.
- LayoutObj::new(
- IconDialog::new(
- icon,
- title,
- theme::button_bar(Button::with_text(button).styled(button_style).map(
- |msg| {
- (matches!(msg, ButtonMsg::Clicked))
- .then(|| CancelConfirmMsg::Confirmed)
- },
- )),
- )
- .with_value(value)
- .with_description(description),
- )?
- }
- }
+ let obj = match buttons {
+ ModalButtons::NoButtons => LayoutObj::new(
+ IconDialog::new(icon, title, Empty)
+ .with_value(value)
+ .with_description(description),
+ )?,
+ ModalButtons::NoButtonsTimeout(time_ms) => LayoutObj::new(
+ IconDialog::new(
+ icon,
+ title,
+ Timeout::new(time_ms).map(|_| Some(CancelConfirmMsg::Confirmed)),
+ )
+ .with_value(value)
+ .with_description(description),
+ )?,
+ ModalButtons::CancelAndConfirm => LayoutObj::new(
+ IconDialog::new(
+ icon,
+ title,
+ Button::cancel_confirm(
+ Button::with_icon(theme::ICON_CANCEL),
+ Button::with_icon(theme::ICON_CONFIRM).styled(button_style),
+ false,
+ ),
+ )
+ .with_value(value)
+ .with_description(description),
+ )?,
+ ModalButtons::CancelAndText(text) => LayoutObj::new(
+ IconDialog::new(
+ icon,
+ title,
+ Button::cancel_confirm(
+ Button::with_icon(theme::ICON_CANCEL),
+ Button::with_text(text).styled(button_style),
+ false,
+ ),
+ )
+ .with_value(value)
+ .with_description(description),
+ )?,
+ ModalButtons::ConfirmText(text) => LayoutObj::new(
+ IconDialog::new(
+ icon,
+ title,
+ theme::button_bar(Button::with_text(text).styled(button_style).map(|msg| {
+ (matches!(msg, ButtonMsg::Clicked)).then(|| CancelConfirmMsg::Confirmed)
+ })),
+ )
+ .with_value(value)
+ .with_description(description),
+ )?,
};
Ok(obj)
}
Why this scored 15/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.