feat: title and subtitle on property pages
What changed, and why it matters
This commit is a routine user-interface feature addition. It lets certain 'property' display screens show an optional subtitle and gives callers a way to override the page title. There is no security-relevant change in the diff.
No security action required. Treat as normal UI feature code review.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The change extends the show_properties UI API across Trezor firmware layout implementations (bolt, caesar, delizia, eckhart) to accept an optional subtitle parameter. Only the eckhart layout actually renders the subtitle; other layouts ignore it with _subtitle. Python layout wrappers (create_details) gain optional title and subtitle arguments that are forwarded to the Rust API. The commit is purely presentational and contains no memory-safety, input-validation, cryptographic, or access-control changes.
Changed components
core/embed/rust/src/ui/api/firmware_micropython.rscore/embed/rust/src/ui/layout_bolt/ui_firmware.rscore/embed/rust/src/ui/layout_caesar/ui_firmware.rscore/embed/rust/src/ui/layout_delizia/ui_firmware.rscore/embed/rust/src/ui/layout_eckhart/theme/firmware.rscore/embed/rust/src/ui/layout_eckhart/ui_firmware.rscore/embed/rust/src/ui/ui_firmware.rscore/mocks/generated/trezorui_api.pyicore/src/trezor/ui/layouts/delizia/__init__.pycore/src/trezor/ui/layouts/eckhart/__init__.pyInspect captured patch +62 / −19
diff --git a/core/embed/rust/src/ui/api/firmware_micropython.rs b/core/embed/rust/src/ui/api/firmware_micropython.rs
index 13ccfb7a..207c7f8c 100644
--- a/core/embed/rust/src/ui/api/firmware_micropython.rs
+++ b/core/embed/rust/src/ui/api/firmware_micropython.rs
@@ -373,9 +373,13 @@ extern "C" fn new_confirm_properties(n_args: usize, args: *const Obj, kwargs: *m
extern "C" fn new_show_properties(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
let block = move |_args: &[Obj], kwargs: &Map| {
let title: TString = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?;
+ let subtitle: Option<TString> = kwargs
+ .get(Qstr::MP_QSTR_subtitle)
+ .and_then(Obj::try_into_option)
+ .unwrap_or(None);
let value: Obj = kwargs.get(Qstr::MP_QSTR_value)?;
- let layout = ModelUI::show_properties(title, value)?;
+ let layout = ModelUI::show_properties(title, subtitle, value)?;
Ok(LayoutObj::new_root(layout)?.into())
};
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
@@ -2095,6 +2099,7 @@ pub static mp_module_trezorui_api: Module = obj_module! {
/// *,
/// title: str,
/// value: Sequence[PropertyType] | str,
+ /// subtitle: str | None = None,
/// ) -> LayoutObj[None]:
/// """Show a list of key-value pairs, or a monospace string."""
Qstr::MP_QSTR_show_properties => obj_fn_kw!(0, new_show_properties).as_obj(),
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 d5952b3e..b4bca947 100644
--- a/core/embed/rust/src/ui/layout_bolt/ui_firmware.rs
+++ b/core/embed/rust/src/ui/layout_bolt/ui_firmware.rs
@@ -1156,6 +1156,7 @@ impl FirmwareUI for UIBolt {
fn show_properties(
_title: TString<'static>,
+ _subtitle: Option<TString<'static>>,
_value: Obj,
) -> Result<impl LayoutMaybeTrace, Error> {
Err::<RootComponent<Empty, ModelUI>, Error>(Error::NotImplementedError)
diff --git a/core/embed/rust/src/ui/layout_caesar/ui_firmware.rs b/core/embed/rust/src/ui/layout_caesar/ui_firmware.rs
index 94a803cf..44288a07 100644
--- a/core/embed/rust/src/ui/layout_caesar/ui_firmware.rs
+++ b/core/embed/rust/src/ui/layout_caesar/ui_firmware.rs
@@ -1297,6 +1297,7 @@ impl FirmwareUI for UICaesar {
fn show_properties(
title: TString<'static>,
+ _subtitle: Option<TString<'static>>,
value: Obj,
) -> Result<impl LayoutMaybeTrace, Error> {
let mut paragraphs = ParagraphVecLong::new();
diff --git a/core/embed/rust/src/ui/layout_delizia/ui_firmware.rs b/core/embed/rust/src/ui/layout_delizia/ui_firmware.rs
index 66f3ad1d..11fe9247 100644
--- a/core/embed/rust/src/ui/layout_delizia/ui_firmware.rs
+++ b/core/embed/rust/src/ui/layout_delizia/ui_firmware.rs
@@ -1194,6 +1194,7 @@ impl FirmwareUI for UIDelizia {
fn show_properties(
title: TString<'static>,
+ _subtitle: Option<TString<'static>>,
value: Obj,
) -> Result<impl LayoutMaybeTrace, Error> {
if Obj::is_str(value) {
diff --git a/core/embed/rust/src/ui/layout_eckhart/theme/firmware.rs b/core/embed/rust/src/ui/layout_eckhart/theme/firmware.rs
index 84786d92..2832505e 100644
--- a/core/embed/rust/src/ui/layout_eckhart/theme/firmware.rs
+++ b/core/embed/rust/src/ui/layout_eckhart/theme/firmware.rs
@@ -17,6 +17,7 @@ use super::{
// props settings
pub const PROP_INNER_SPACING: i16 = 12; // [px]
pub const PROPS_SPACING: i16 = 16; // [px]
+pub const PROPS_SPACING_EXTRA: i16 = 20; // [px]
pub const PROPS_KEY_FONT: TextStyle = TEXT_SMALL_LIGHT;
pub const PROPS_VALUE_FONT: TextStyle = TEXT_MONO_LIGHT;
pub const PROPS_VALUE_MONO_FONT: TextStyle = TEXT_MONO_LIGHT;
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 c5933165..5ebbada9 100644
--- a/core/embed/rust/src/ui/layout_eckhart/ui_firmware.rs
+++ b/core/embed/rust/src/ui/layout_eckhart/ui_firmware.rs
@@ -1499,6 +1499,7 @@ impl FirmwareUI for UIEckhart {
fn show_properties(
title: TString<'static>,
+ subtitle: Option<TString<'static>>,
value: Obj,
) -> Result<impl LayoutMaybeTrace, Error> {
let mut vec = ParagraphVecShort::new();
@@ -1506,27 +1507,47 @@ impl FirmwareUI for UIEckhart {
let text: TString = value.try_into()?;
unwrap!(vec.push(Paragraph::new(&theme::TEXT_MONO_ADDRESS_CHUNKS, text)));
} else {
+ let mut first_item_is_address: Option<bool> = None;
for property in IterBuf::new().try_iterate(value)? {
let [header, text, _is_data]: [Obj; 3] = util::iter_into_array(property)?;
+
let header = header
.try_into_option::<TString>()?
.unwrap_or_else(TString::empty);
- let text = text
- .try_into_option::<TString>()?
- .unwrap_or_else(TString::empty);
-
- unwrap!(vec.push(Paragraph::new(&theme::TEXT_SMALL, header)));
- let mut value_paragraph = Paragraph::new(
- if header.is_empty() {
- &theme::TEXT_MONO_ADDRESS_CHUNKS
+ if first_item_is_address.is_none() {
+ // TODO: should be based on the first item's "property type" (when we have it)
+ first_item_is_address = Some(header.is_empty());
+ }
+ let mut header_paragraph = Paragraph::new(
+ if subtitle.is_none() {
+ &theme::TEXT_SMALL
} else {
- &theme::TEXT_MONO_LIGHT
+ // subtitle is already quite prominent
+ &theme::TEXT_SMALL_LIGHT
},
- text,
- );
- if header.is_empty() {
- value_paragraph = value_paragraph.with_bottom_padding(20);
+ header,
+ )
+ .no_break();
+ if !first_item_is_address.unwrap_or(false) {
+ // normal spacing between property keys and values
+ // unless the first property is an address,
+ // in which case less space looks better
+ header_paragraph =
+ header_paragraph.with_bottom_padding(theme::PROP_INNER_SPACING);
}
+ unwrap!(vec.push(header_paragraph));
+
+ let text = text
+ .try_into_option::<TString>()?
+ .unwrap_or_else(TString::empty);
+ // TODO: should be based on the "property type"
+ let value_paragraph = if header.is_empty() {
+ Paragraph::new(&theme::TEXT_MONO_ADDRESS_CHUNKS, text)
+ .with_bottom_padding(theme::PROPS_SPACING_EXTRA)
+ } else {
+ Paragraph::new(&theme::TEXT_MONO_LIGHT, text)
+ .with_bottom_padding(theme::PROPS_SPACING)
+ };
unwrap!(vec.push(value_paragraph));
}
};
@@ -1535,7 +1556,8 @@ impl FirmwareUI for UIEckhart {
vec.into_paragraphs()
.with_placement(LinearPlacement::vertical()),
)
- .with_header(Header::new(title).with_close_button());
+ .with_header(Header::new(title).with_close_button())
+ .with_subtitle(subtitle.unwrap_or(TString::empty()));
let obj = RootComponent::new(screen);
Ok(obj)
diff --git a/core/embed/rust/src/ui/ui_firmware.rs b/core/embed/rust/src/ui/ui_firmware.rs
index 24dc96ff..4cad8bca 100644
--- a/core/embed/rust/src/ui/ui_firmware.rs
+++ b/core/embed/rust/src/ui/ui_firmware.rs
@@ -444,6 +444,7 @@ pub trait FirmwareUI {
fn show_properties(
_title: TString<'static>,
+ _subtitle: Option<TString<'static>>,
_value: Obj,
) -> Result<impl LayoutMaybeTrace, Error>;
diff --git a/core/mocks/generated/trezorui_api.pyi b/core/mocks/generated/trezorui_api.pyi
index 2dd6f080..dae78aa6 100644
--- a/core/mocks/generated/trezorui_api.pyi
+++ b/core/mocks/generated/trezorui_api.pyi
@@ -762,6 +762,7 @@ def show_properties(
*,
title: str,
value: Sequence[PropertyType] | str,
+ subtitle: str | None = None,
) -> LayoutObj[None]:
"""Show a list of key-value pairs, or a monospace string."""
diff --git a/core/src/trezor/ui/layouts/delizia/__init__.py b/core/src/trezor/ui/layouts/delizia/__init__.py
index 8055ad1b..1b6e56f8 100644
--- a/core/src/trezor/ui/layouts/delizia/__init__.py
+++ b/core/src/trezor/ui/layouts/delizia/__init__.py
@@ -1808,9 +1808,11 @@ def tutorial(br_code: ButtonRequestType = BR_CODE_OTHER) -> Awaitable[None]:
)
-def create_details(name: str, value: list[PropertyType] | str) -> Details:
+def create_details(
+ name: str, value: list[PropertyType] | str, title: str | None = None
+) -> Details:
from trezor.ui.layouts.menu import Details
return Details.from_layout(
- name, lambda: trezorui_api.show_properties(title=name, value=value)
+ name, lambda: trezorui_api.show_properties(title=(title or name), value=value)
)
diff --git a/core/src/trezor/ui/layouts/eckhart/__init__.py b/core/src/trezor/ui/layouts/eckhart/__init__.py
index 3db1c364..d225d905 100644
--- a/core/src/trezor/ui/layouts/eckhart/__init__.py
+++ b/core/src/trezor/ui/layouts/eckhart/__init__.py
@@ -1844,9 +1844,17 @@ def tutorial(br_code: ButtonRequestType = BR_CODE_OTHER) -> Awaitable[None]:
)
-def create_details(name: str, value: list[PropertyType] | str) -> Details:
+def create_details(
+ name: str,
+ value: list[PropertyType] | str,
+ title: str | None = None,
+ subtitle: str | None = None,
+) -> Details:
from trezor.ui.layouts.menu import Details
return Details.from_layout(
- name, lambda: trezorui_api.show_properties(title=name, value=value)
+ name,
+ lambda: trezorui_api.show_properties(
+ title=(title or name), subtitle=subtitle, value=value
+ ),
)
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.