fix(core/delizia): passphrase prompt marquee
What changed, and why it matters
This commit is a user-interface polish fix for the Trezor hardware wallet's passphrase entry screen. It replaces a static text label with a scrolling 'marquee' so that longer prompts fit on the small device screen. There is no security-relevant change here.
No security action needed; treat as a normal UI/UX fix.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The change swaps the input_prompt field in PassphraseKeyboard from a Label to a Marquee component, adds logic to start/reset the marquee when the input is empty, and adjusts the placement rectangle so the prompt text baseline aligns with the previous label and does not overlap the confirm-empty button. The second file is only updated UI test fixture hashes reflecting the new rendering.
Changed components
core/embed/rust/src/ui/layout_delizia/component/keyboard/passphrase.rstests/ui_tests/fixtures.jsonInspect captured patch +77 / −28
### core/embed/rust/src/ui/layout_delizia/component/keyboard/passphrase.rs
@@ -1,7 +1,7 @@
use core::cell::Cell;
use num_traits::ToPrimitive;
-use sys::time::Duration;
+use sys::time::{Duration, Instant};
use super::super::super::component::button::{Button, ButtonContent, ButtonMsg};
use super::super::super::component::keyboard::common::{render_pending_marker, MultiTapKeyboard};
@@ -15,11 +15,11 @@ use crate::ui::component::text::common::TextBox;
use crate::ui::component::text::layout::{LayoutFit, LineBreaking};
use crate::ui::component::text::TextStyle;
use crate::ui::component::{
- Component, Event, EventCtx, Label, Maybe, Never, Pad, Swipe, TextLayout, Timer,
+ Component, Event, EventCtx, Marquee, Maybe, Never, Pad, Swipe, TextLayout, Timer,
};
use crate::ui::display;
use crate::ui::event::TouchEvent;
-use crate::ui::geometry::{Alignment, Alignment2D, Direction, Grid, Insets, Rect};
+use crate::ui::geometry::{Alignment, Alignment2D, Direction, Grid, Insets, Offset, Point, Rect};
use crate::ui::shape::{Bar, Renderer, Text, ToifImage};
use crate::ui::util::{DisplayStyle, Pager};
@@ -75,7 +75,7 @@ impl From<KeyboardLayout> for ButtonContent {
pub struct PassphraseKeyboard {
page_swipe: Swipe,
input: Input,
- input_prompt: Label<'static>,
+ input_prompt: Marquee,
erase_btn: Maybe<Button>,
cancel_btn: Maybe<Button>,
confirm_btn: Maybe<Button>,
@@ -137,7 +137,7 @@ impl PassphraseKeyboard {
Self {
page_swipe: Swipe::horizontal(),
input: Input::new(max_len),
- input_prompt: Label::left_aligned(prompt, theme::label_keyboard()),
+ input_prompt: Self::prompt_marquee(prompt, theme::label_keyboard()),
erase_btn,
cancel_btn,
confirm_btn,
@@ -223,6 +223,14 @@ impl PassphraseKeyboard {
self.cancel_btn.show_if(ctx, is_empty);
self.cancel_btn.inner_mut().enable_if(ctx, is_empty);
+ if is_empty {
+ // The prompt becomes visible again; reset and restart the marquee
+ // so it scrolls from the beginning.
+ self.input_prompt.reset();
+ self.input_prompt.start(ctx, Instant::now());
+ self.input_prompt.request_complete_repaint(ctx);
+ }
+
self.update_input_btns_state(ctx);
self.input.request_complete_repaint(ctx);
}
@@ -266,6 +274,31 @@ impl PassphraseKeyboard {
pub fn passphrase(&self) -> &str {
self.input.textbox.content()
}
+
+ fn prompt_marquee(text: TString<'static>, style: TextStyle) -> Marquee {
+ Marquee::new(
+ text,
+ style.text_font,
+ style.text_color,
+ style.background_color,
+ )
+ }
+
+ /// Area of a `Marquee` replacing a top-aligned `Label` in `strip`:
+ /// `Marquee` renders the text baseline at `text_height - 1` below the top
+ /// of its area while `Label` places it at `text_max_height -
+ /// text_baseline`, so the area is shifted to make the baselines match.
+ /// The bottom of the area covers the descent so that descenders are not
+ /// clipped.
+ fn prompt_marquee_area(strip: Rect) -> Rect {
+ let font = theme::label_keyboard().text_font;
+ let baseline_y = strip.y0 + font.text_max_height() - font.text_baseline();
+ let top = baseline_y - (font.text_height() - 1);
+ Rect::from_top_left_and_size(
+ Point::new(strip.x0, top),
+ Offset::new(strip.width(), baseline_y + font.text_baseline() - top),
+ )
+ }
}
impl Component for PassphraseKeyboard {
@@ -280,12 +313,14 @@ impl Component for PassphraseKeyboard {
bounds.split_bottom(4 * theme::PASSPHRASE_BUTTON_HEIGHT + 3 * theme::BUTTON_SPACING);
self.keypad_area = keypad_area;
let (input_area, confirm_btn_area) = top_area.split_right(CONFIRM_BTN_WIDTH);
- let confirm_empty_btn_area = confirm_btn_area
- .split_right(CONFIRM_EMPTY_BTN_WIDTH + CONFIRM_EMPTY_BTN_MARGIN_RIGHT)
- .1;
+ // The prompt is shown only while the input is empty, i.e. when the
+ // small confirm-empty button is shown instead of the wider confirm
+ // button, so it may extend up to the confirm-empty button's area.
+ let (prompt_area, confirm_empty_btn_area) =
+ top_area.split_right(CONFIRM_EMPTY_BTN_WIDTH + CONFIRM_EMPTY_BTN_MARGIN_RIGHT);
- let top_area = top_area.inset(INPUT_INSETS);
let input_area = input_area.inset(INPUT_INSETS);
+ let prompt_area = prompt_area.inset(INPUT_INSETS);
let confirm_btn_area = confirm_btn_area.inset(CONFIRM_BTN_INSETS);
let confirm_empty_btn_area = confirm_empty_btn_area.inset(CONFIRM_EMPTY_BTN_INSETS);
@@ -295,7 +330,10 @@ impl Component for PassphraseKeyboard {
self.page_swipe.place(bounds);
self.input.place(input_area);
- self.input_prompt.place(top_area);
+ // The prompt is clipped to the prompt area so that it does not
+ // overlap the confirm-empty button; it scrolls if it does not fit.
+ self.input_prompt
+ .place(Self::prompt_marquee_area(prompt_area));
// control buttons
self.confirm_btn.place(confirm_btn_area);
@@ -322,6 +360,17 @@ impl Component for PassphraseKeyboard {
}
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
+ // Start and drive the prompt marquee only while it is visible, i.e.
+ // while the input textbox is empty (matching render).
+ if self.input.textbox.is_empty() {
+ if let Event::Attach(_) = event {
+ self.input_prompt.reset();
+ self.input_prompt.start(ctx, Instant::now());
+ } else {
+ self.input_prompt.event(ctx, event);
+ }
+ }
+
// Handle multi-tap timeout: commit the pending character
if self.input.multi_tap.timeout_event(event) {
self.input.multi_tap.clear_pending_state(ctx);
### tests/ui_tests/fixtures.json
@@ -29150,30 +29150,30 @@
"T3T1_fr_test_tutorial_delizia.py::test_tutorial_menu_repeat": "bd7f438e9480a8aef0e2825ff12e33bb894a8c967918a04c4ff189ecfb6f37ff",
"T3T1_id_test_autolock.py::test_autolock_does_not_interrupt_preauthorized": "463a54eab1fd2d77fb05f3848dfe84b0e5b2f723772a977e45b6b50130cbb67d",
"T3T1_id_test_autolock.py::test_autolock_does_not_interrupt_signing": "12c825691f61f032a434180aa32f7d0a4ddc9c3cd2df1600a0f77fcb8a83ce70",
-"T3T1_id_test_autolock.py::test_autolock_interrupts_passphrase": "4cfb5440edd0d14a0a3312d384828ed513be8c97b0d6536d53123f33c7bc1eb8",
+"T3T1_id_test_autolock.py::test_autolock_interrupts_passphrase": "290b4357fd8f9b05ca324084a5fab3692eff3f0ab63294fda48ba4983b6fadbe",
"T3T1_id_test_autolock.py::test_autolock_interrupts_signing": "27c3765c84fcc0a561e6e0193552617672a821179f4c2e46cda7b5635da6c89c",
-"T3T1_id_test_autolock.py::test_autolock_passphrase_keyboard": "62667e895820dec4bc5cbf871717ddadfc0280c878e6602b36798d53822c1e0e",
+"T3T1_id_test_autolock.py::test_autolock_passphrase_keyboard": "088f9a6d898d28a5f46bfb2a5fbaa73bd3f6d8934e9158543956b9a4f4fbf38b",
"T3T1_id_test_autolock.py::test_dryrun_enter_word_slowly": "d52a4343a93f1b55100be517e4e1d4d16561d56f7f7d408ee2232f6c2b9a2631",
"T3T1_id_test_autolock.py::test_dryrun_locks_at_number_of_words": "c93002fc2f87bc0eb5c60fe8f37eaac399e01d26b9c03b2f022f3365c641c0fd",
"T3T1_id_test_autolock.py::test_dryrun_locks_at_word_entry": "6545e69e22ff842dc2e9034c98c8b9ed6f847dead715c11c96c8ae3ec14bd1cd",
"T3T1_id_test_backup_slip39_custom.py::test_backup_slip39_custom[1of1]": "0e20c1f4829c0900b77059b904bccecdde23be97f0a8e21a804ed49d5fe92908",
"T3T1_id_test_backup_slip39_custom.py::test_backup_slip39_custom[2of3]": "68e5451edd5972178a71e7229b019b69aba3e43bff74137fa9c21635f86a2869",
"T3T1_id_test_backup_slip39_custom.py::test_backup_slip39_custom[5of5]": "427a7240060376ab4fa13de0477b12a9f75cb72a4577101ad3289b498b2cc8d2",
"T3T1_id_test_lock.py::test_hold_to_lock": "24491bcf7fb280165c836b5444a39840861a4064431141e9f7199d8efa1689a4",
-"T3T1_id_test_passphrase_bde.py::test_cycle_through_last_character": "3107cad1d214c01c62a12e68f801531c8902e6a5d1bd6e54f38610764f3a496e",
-"T3T1_id_test_passphrase_bde.py::test_passphrase_click_same_button_many_times": "40e8b951051735adaaa8b26038c4085727ac23c97686084c08a108f0aba584e8",
-"T3T1_id_test_passphrase_bde.py::test_passphrase_delete": "e887f9783b23aca0f7298d8460c3ea062604c26afa3b79ff38a95deeadb4baeb",
-"T3T1_id_test_passphrase_bde.py::test_passphrase_delete_all": "ecadd512c075078f6a43061885c24bdc025658828fe3152a4631dfd24e52ff6e",
-"T3T1_id_test_passphrase_bde.py::test_passphrase_dollar_sign_deletion": "0e757962d7d06550abccde661754264a1f51f61b414dfa7aec905a3ef69ae933",
-"T3T1_id_test_passphrase_bde.py::test_passphrase_input[random_25]": "b8fa298961c0c9a962df4470e61350abfee1a0a5d973bf774e6acf565bd25325",
-"T3T1_id_test_passphrase_bde.py::test_passphrase_input[short]": "aa0db67b27fd5ba9144cf4393c148fb34945ef890b5189a4c7f160f706cf61fb",
-"T3T1_id_test_passphrase_bde.py::test_passphrase_input[with_space]": "47316dff19abd33d9ba7b5c412f4bb22fc9b9cf4b20c9a3632f49f85c2be652d",
-"T3T1_id_test_passphrase_bde.py::test_passphrase_input[www_limit]": "d85db952798ba6adf610621de62db9867c7075a7ff9adef573a76b891c898d54",
-"T3T1_id_test_passphrase_bde.py::test_passphrase_input[www_shorter]": "9d0a1da1fabef77d1098cc1b59c58ef7a38ed82f457b5833da2dc8088386dc67",
-"T3T1_id_test_passphrase_bde.py::test_passphrase_input_over_limit": "bed6b93d5efc4cba3eac4f60080ebf6de7ce05e1bd1854d461bd2c21034dacc6",
-"T3T1_id_test_passphrase_bde.py::test_passphrase_long_spaces_deletion": "258f12ce6d3cf084fe73ca2fc10e8e6ffdfe8326394eee5b060a921f241f1ca2",
-"T3T1_id_test_passphrase_bde.py::test_passphrase_loop_all_characters": "e9af2e7f8ca987a7f604ef14f278b2aca6bbe0dcc31b59c302d3b94d9ca901fb",
-"T3T1_id_test_passphrase_bde.py::test_passphrase_prompt_disappears": "d335adfc92656cabde176a88b978ae09afda45c113f8880937dfb423ac689d76",
+"T3T1_id_test_passphrase_bde.py::test_cycle_through_last_character": "a6a98451a488e7b545909146fcd750ae365a42689956d97765041f3ce510aa44",
+"T3T1_id_test_passphrase_bde.py::test_passphrase_click_same_button_many_times": "d080e053747fb07ca4645c1b55d18b725c2c5a6e68a93d841dc210366b991a65",
+"T3T1_id_test_passphrase_bde.py::test_passphrase_delete": "629bfa6e62fa27e9bac9a08d422328a6a0685606fce3d5cc8ac23d12e4860d24",
+"T3T1_id_test_passphrase_bde.py::test_passphrase_delete_all": "3c148a07e1633fda035f40a348b67c4187912eab8a6d28d71bdcb0a7f02cdecc",
+"T3T1_id_test_passphrase_bde.py::test_passphrase_dollar_sign_deletion": "6cbf9f355416fa1cf5f208205f7c30611d117938457a6e8c4e82aa515c2d172d",
+"T3T1_id_test_passphrase_bde.py::test_passphrase_input[random_25]": "b0f8ede27c1dd9a6d559bedba03dfa144451443b2eab1e408a10de770dc61d9d",
+"T3T1_id_test_passphrase_bde.py::test_passphrase_input[short]": "220c7c9c22dbed0ad8861c3579db90833a790df366ceb9de69c3bfa123f1f9d8",
+"T3T1_id_test_passphrase_bde.py::test_passphrase_input[with_space]": "5d196849730a1691e64e6ddec6b17267db8e7346ab8a29eb09f73d6c5f5ab408",
+"T3T1_id_test_passphrase_bde.py::test_passphrase_input[www_limit]": "13f07670bb90a48c01c0a454bae6a2c962c957c45af7d23a18711053669048fa",
+"T3T1_id_test_passphrase_bde.py::test_passphrase_input[www_shorter]": "9d51d041eafb1404942ebbd7cc4671c27920eafc96c3febca34400ed968f3592",
+"T3T1_id_test_passphrase_bde.py::test_passphrase_input_over_limit": "963220f1d333e9495af1a67c813f8315c3966da67556a248fe3faf5602569750",
+"T3T1_id_test_passphrase_bde.py::test_passphrase_long_spaces_deletion": "0cb3f9dc3200ec0386513d0b44b492209373f2a154408e22d334147db22b6896",
+"T3T1_id_test_passphrase_bde.py::test_passphrase_loop_all_characters": "4d9189ab73302e9c92281a14edd8128efb5c6b4ace874a065dd241aebdebdc7f",
+"T3T1_id_test_passphrase_bde.py::test_passphrase_prompt_disappears": "9e560194a7ae79d26f650e2187dab3833b1f6a09eed2a3616ea3ebc29f021213",
"T3T1_id_test_pin.py::test_last_digit_timeout": "3f9c41f3c18a4689981aaa9c4a69b7abb17843dc992c057f96cc94eb81153a66",
"T3T1_id_test_pin.py::test_long_press_digit": "5ccd0d9663190506b44a09c94397f0c42cff1ccb5a3a26689f5c04d60b3d5385",
"T3T1_id_test_pin.py::test_pin_cancel": "73579f1a2e35f338680bbb1ed4b78f6ff41f8a989b41bae099a66f1487cf0685",
@@ -40885,10 +40885,10 @@
"T3T1_id_test_session_id_and_passphrase.py::test_multiple_passphrases": "4556dac072c3796270cf742dd51636e272d8c6ab5468b7a25edd99be4b4c7022",
"T3T1_id_test_session_id_and_passphrase.py::test_multiple_sessions": "138001b8df08578e71972625e63e47c6eba747b4886cd91f1397a14cdd9bd8fb",
"T3T1_id_test_session_id_and_passphrase.py::test_passphrase_ack_mismatch": "138001b8df08578e71972625e63e47c6eba747b4886cd91f1397a14cdd9bd8fb",
-"T3T1_id_test_session_id_and_passphrase.py::test_passphrase_always_on_device": "f3a93af1f7842d0fed3f5635202730300000824e6f03b7879c70df8b240388f9",
+"T3T1_id_test_session_id_and_passphrase.py::test_passphrase_always_on_device": "f73c04bc06388797012b0d9bef3d8459599158f092a0b4db6eb68985e864c694",
"T3T1_id_test_session_id_and_passphrase.py::test_passphrase_length": "0e4e0ecdbcd343020fc9a934581e99a013df529232f37a7711047c8b2d4b56ef",
"T3T1_id_test_session_id_and_passphrase.py::test_passphrase_missing": "138001b8df08578e71972625e63e47c6eba747b4886cd91f1397a14cdd9bd8fb",
-"T3T1_id_test_session_id_and_passphrase.py::test_passphrase_on_device": "9fd0ad10cb1fa7e047e9acc51d17993ed5d284ea0d81e3a296a913c739727365",
+"T3T1_id_test_session_id_and_passphrase.py::test_passphrase_on_device": "3db7ebade45b1255f506548d0ea3656e0c5e34441a59a39e1db90a454b4a8a75",
"T3T1_id_test_session_id_and_passphrase.py::test_session_enable_passphrase": "d49af298ecf97a657a7ee789a35004c2ef17b62a8a5c6bc971842d8fd6b1b738",
"T3T1_id_test_session_id_and_passphrase.py::test_session_with_passphrase": "efc13eb7f1f902e76f7a52d7215e0a736d929986264ed1effd289596be866020",
"T3T1_id_tezos-test_getaddress.py::test_tezos_get_address[m-44h-1729h-0h-tz1Kef7BSg6fo75jk37WkKRYSnJ-80986d6e": "dc2c0bc35a78fb790cf28e68ba99cb1b879d37d985a2ced719518eaeb15dce49",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.