chore(core/eckhart): add connection icon to the Button
What changed, and why it matters
This commit is a routine user-interface change for the Trezor hardware wallet. It adds a small connection-status icon and a new 'Disconnected' label to buttons that represent Bluetooth Low Energy (BLE) paired devices. There is no security-relevant behavior change in the code.
No security action required; this is a normal UI feature commit.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The patch extends the Eckhart layout Button component to support an optional icon inside TextAndSubtext buttons, adds a new builder new_connection_item() for BLE device entries, introduces a square icon asset, and registers the new translation key words__disconnected. Rendering logic is adjusted to reserve 34 px width for the icon and to use the available width when splitting/wrapping text. No cryptographic, authentication, communication, or memory-safety logic is modified.
Changed components
core/embed/rust/src/ui/layout_eckhart/component/button.rscore/embed/rust/src/ui/layout_eckhart/theme/mod.rscore/embed/rust/src/ui/layout_eckhart/res/square.pngcore/embed/rust/src/ui/layout_eckhart/res/square.toifcore/embed/rust/src/translations/generated/translated_string.rscore/embed/rust/librust_qstr.hcore/mocks/trezortranslate_keys.pyicore/translations/en.jsoncore/translations/order.jsoncore/translations/signatures.jsonInspect captured patch +90 / −13
diff --git a/core/embed/rust/librust_qstr.h b/core/embed/rust/librust_qstr.h
index 45b5266b..5c8987c8 100644
--- a/core/embed/rust/librust_qstr.h
+++ b/core/embed/rust/librust_qstr.h
@@ -938,6 +938,7 @@ static void _librust_qstrs(void) {
MP_QSTR_words__device;
MP_QSTR_words__disabled;
MP_QSTR_words__disconnect;
+ MP_QSTR_words__disconnected;
MP_QSTR_words__enabled;
MP_QSTR_words__error;
MP_QSTR_words__fee;
diff --git a/core/embed/rust/src/translations/generated/translated_string.rs b/core/embed/rust/src/translations/generated/translated_string.rs
index 8b9c68d3..20d92b73 100644
--- a/core/embed/rust/src/translations/generated/translated_string.rs
+++ b/core/embed/rust/src/translations/generated/translated_string.rs
@@ -1511,6 +1511,7 @@ pub enum TranslatedString {
words__wipe = 1120, // "Wipe"
lockscreen__unlock = 1121, // "Unlock"
recovery__start_entering = 1122, // "Start entering"
+ words__disconnected = 1123, // "Disconnected"
}
impl TranslatedString {
@@ -3348,6 +3349,7 @@ impl TranslatedString {
(Self::words__wipe, "Wipe"),
(Self::lockscreen__unlock, "Unlock"),
(Self::recovery__start_entering, "Start entering"),
+ (Self::words__disconnected, "Disconnected"),
];
#[cfg(feature = "micropython")]
@@ -4796,6 +4798,7 @@ impl TranslatedString {
(Qstr::MP_QSTR_words__device, Self::words__device),
(Qstr::MP_QSTR_words__disabled, Self::words__disabled),
(Qstr::MP_QSTR_words__disconnect, Self::words__disconnect),
+ (Qstr::MP_QSTR_words__disconnected, Self::words__disconnected),
(Qstr::MP_QSTR_words__enabled, Self::words__enabled),
(Qstr::MP_QSTR_words__error, Self::words__error),
(Qstr::MP_QSTR_words__fee, Self::words__fee),
diff --git a/core/embed/rust/src/ui/layout_eckhart/component/button.rs b/core/embed/rust/src/ui/layout_eckhart/component/button.rs
index d04bb1b4..fa63c2ad 100644
--- a/core/embed/rust/src/ui/layout_eckhart/component/button.rs
+++ b/core/embed/rust/src/ui/layout_eckhart/component/button.rs
@@ -1,5 +1,8 @@
+#[cfg(feature = "translations")]
+use crate::translations::TR;
#[cfg(feature = "haptic")]
use crate::trezorhal::haptic::{play, HapticEffect};
+
use crate::{
strutil::TString,
time::{Duration, ShortDuration},
@@ -48,6 +51,7 @@ impl Button {
const MENU_ITEM_RADIUS: u8 = 12;
const MENU_ITEM_ALIGNMENT: Alignment = Alignment::Start;
pub const MENU_ITEM_CONTENT_OFFSET: Offset = Offset::x(12);
+ const CONN_ICON_WIDTH: i16 = 34;
#[cfg(feature = "micropython")]
const DEFAULT_STYLESHEET: ButtonStyleSheet = theme::firmware::button_default();
@@ -93,7 +97,42 @@ impl Button {
subtext: TString<'static>,
subtext_style: &'static TextStyle,
) -> Self {
- Self::with_text_and_subtext(text, subtext, subtext_style)
+ Self::with_text_and_subtext(text, subtext, subtext_style, None)
+ .with_text_align(Self::MENU_ITEM_ALIGNMENT)
+ .with_content_offset(Self::MENU_ITEM_CONTENT_OFFSET)
+ .styled(stylesheet)
+ .with_radius(Self::MENU_ITEM_RADIUS)
+ }
+
+ #[cfg(feature = "micropython")]
+ pub fn new_connection_item(
+ text: TString<'static>,
+ stylesheet: ButtonStyleSheet,
+ subtext: Option<TString<'static>>,
+ connected: bool,
+ ) -> Self {
+ let (icon, subtext_style) = if connected {
+ (
+ (theme::ICON_SQUARE, theme::GREEN_LIGHT),
+ &theme::TEXT_MENU_ITEM_SUBTITLE_GREEN,
+ )
+ } else {
+ (
+ (theme::ICON_SQUARE, theme::GREY_DARK),
+ &theme::TEXT_MENU_ITEM_SUBTITLE,
+ )
+ };
+ let subtext = if let Some(subtext) = subtext {
+ subtext
+ } else {
+ if connected {
+ TR::words__connected.into()
+ } else {
+ TR::words__disconnected.into()
+ }
+ };
+
+ Self::with_text_and_subtext(text, subtext, subtext_style, Some(icon))
.with_text_align(Self::MENU_ITEM_ALIGNMENT)
.with_content_offset(Self::MENU_ITEM_CONTENT_OFFSET)
.styled(stylesheet)
@@ -112,11 +151,13 @@ impl Button {
text: TString<'static>,
subtext: TString<'static>,
subtext_style: &'static TextStyle,
+ icon: Option<(Icon, Color)>,
) -> Self {
Self::new(ButtonContent::TextAndSubtext {
text,
subtext,
subtext_style,
+ icon,
})
}
@@ -289,7 +330,17 @@ impl Button {
text.map(|t| self.text_height(t, *single_line, width))
}
ButtonContent::Icon(icon) => icon.toif.height(),
- ButtonContent::TextAndSubtext { text, .. } => {
+ ButtonContent::TextAndSubtext {
+ text,
+ subtext: _,
+ subtext_style: _,
+ icon,
+ } => {
+ let width = if icon.is_some() {
+ width - Self::CONN_ICON_WIDTH
+ } else {
+ width
+ };
text.map(|t| self.text_height(t, false, width) + self.baseline_subtext_height())
}
#[cfg(feature = "micropython")]
@@ -415,14 +466,14 @@ impl Button {
text,
subtext,
subtext_style,
+ icon,
} => {
let text_baseline_height = self.baseline_text_height();
+ let available_width = self.area.width()
+ - 2 * self.content_offset.x
+ - icon.map_or(0, |_| Self::CONN_ICON_WIDTH);
let single_line_text = text.map(|t| {
- let (t1, t2) = split_two_lines(
- t,
- stylesheet.font,
- self.area.width() - 2 * self.content_offset.x,
- );
+ let (t1, t2) = split_two_lines(t, stylesheet.font, available_width);
if t1.is_empty() || t2.is_empty() {
show_text(
t,
@@ -444,8 +495,10 @@ impl Button {
subtext.map(|subtext| {
#[cfg(feature = "ui_debug")]
- if subtext_style.text_font.text_width(subtext) > self.area.width() {
- fatal_error!(&uformat!(len: 128, "Subtext too long: '{}'", subtext));
+ {
+ if subtext_style.text_font.text_width(subtext) > available_width {
+ fatal_error!(&uformat!(len: 128, "Subtext too long: '{}'", subtext));
+ }
}
shape::Text::new(
render_origin(if single_line_text {
@@ -465,6 +518,19 @@ impl Button {
.with_alpha(alpha)
.render(target);
});
+
+ if let Some((icon, icon_color)) = icon {
+ shape::ToifImage::new(
+ self.area
+ .right_center()
+ .ofs(Offset::x(Self::CONN_ICON_WIDTH / 2).neg())
+ .ofs(self.content_offset.neg()),
+ icon.toif,
+ )
+ .with_align(Alignment2D::CENTER)
+ .with_fg(*icon_color)
+ .render(target);
+ }
}
ButtonContent::Icon(icon) => {
shape::ToifImage::new(self.area.center() + self.content_offset, icon.toif)
@@ -662,6 +728,7 @@ pub enum ButtonContent {
text: TString<'static>,
subtext: TString<'static>,
subtext_style: &'static TextStyle,
+ icon: Option<(Icon, Color)>,
},
Icon(Icon),
#[cfg(feature = "micropython")]
diff --git a/core/embed/rust/src/ui/layout_eckhart/res/square.png b/core/embed/rust/src/ui/layout_eckhart/res/square.png
new file mode 100644
index 00000000..55af05f3
Binary files /dev/null and b/core/embed/rust/src/ui/layout_eckhart/res/square.png differ
diff --git a/core/embed/rust/src/ui/layout_eckhart/res/square.toif b/core/embed/rust/src/ui/layout_eckhart/res/square.toif
new file mode 100644
index 00000000..98d4f584
Binary files /dev/null and b/core/embed/rust/src/ui/layout_eckhart/res/square.toif differ
diff --git a/core/embed/rust/src/ui/layout_eckhart/theme/mod.rs b/core/embed/rust/src/ui/layout_eckhart/theme/mod.rs
index d34e7fd3..89758dd6 100644
--- a/core/embed/rust/src/ui/layout_eckhart/theme/mod.rs
+++ b/core/embed/rust/src/ui/layout_eckhart/theme/mod.rs
@@ -168,6 +168,9 @@ include_icon!(ICON_EUROPE, "layout_eckhart/res/europe.toif");
include_icon!(ICON_RCM, "layout_eckhart/res/rcm.toif");
include_icon!(ICON_FCC, "layout_eckhart/res/fcc.toif");
+// Square icon for BLE connection items
+include_icon!(ICON_SQUARE, "layout_eckhart/res/square.toif");
+
// Common text styles and button styles must use fonts accessible from both
// bootloader and firmware
diff --git a/core/mocks/trezortranslate_keys.pyi b/core/mocks/trezortranslate_keys.pyi
index eadbc871..b42b4b1f 100644
--- a/core/mocks/trezortranslate_keys.pyi
+++ b/core/mocks/trezortranslate_keys.pyi
@@ -1028,6 +1028,7 @@ class TR:
words__device: str = "Device"
words__disabled: str = "Disabled"
words__disconnect: str = "Disconnect"
+ words__disconnected: str = "Disconnected"
words__enabled: str = "Enabled"
words__error: str = "Error"
words__fee: str = "Fee"
diff --git a/core/translations/en.json b/core/translations/en.json
index 22f4fbbf..c6a1813d 100644
--- a/core/translations/en.json
+++ b/core/translations/en.json
@@ -1250,6 +1250,7 @@
"words__device": "Device",
"words__disabled": "Disabled",
"words__disconnect": "Disconnect",
+ "words__disconnected": "Disconnected",
"words__enabled": "Enabled",
"words__error": "Error",
"words__fee": "Fee",
diff --git a/core/translations/order.json b/core/translations/order.json
index dffbbabc..6b38b8c0 100644
--- a/core/translations/order.json
+++ b/core/translations/order.json
@@ -1121,5 +1121,6 @@
"1119": "words__set",
"1120": "words__wipe",
"1121": "lockscreen__unlock",
- "1122": "recovery__start_entering"
+ "1122": "recovery__start_entering",
+ "1123": "words__disconnected"
}
diff --git a/core/translations/signatures.json b/core/translations/signatures.json
index 817ae5d9..33fdff4a 100644
--- a/core/translations/signatures.json
+++ b/core/translations/signatures.json
@@ -1,8 +1,8 @@
{
"current": {
- "merkle_root": "ec617a229738c6408469d3570ec6f78206731997b71c2286fe845f76222a8823",
- "datetime": "2025-08-26T14:25:08.789484+00:00",
- "commit": "2131905f505fa633fce27c85a653933addc513be"
+ "merkle_root": "f26e3c5785b28115e11f3c6b42cb2dfcd3e3f806932a77d707edc645f4ae0b6f",
+ "datetime": "2025-08-26T18:37:58.384080+00:00",
+ "commit": "3ec9f04e4aa87e3784cea4e25ff69c986d2b3b62"
},
"history": [
{
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.