chore(core/eckhart): revision of string input FwUI function
What changed, and why it matters
This commit rewrites the on-screen keyboard used to type text into the Trezor Safe 5 (Eckhart layout). It renames the component, removes a separate confirmation screen for empty labels, lets users submit an empty string directly, and allows cancelling even when nothing has been typed. The change is described as a UI/UX cleanup, not a security fix. There is no direct evidence in the commit that it patches an exploitable vulnerability, but removing an extra confirmation step for empty device names slightly changes the safety surface around user input.
Treat as a routine UI refactor. Review whether any downstream callers rely on the removed empty-string confirmation screen for safety-critical input (e.g., passphrase, seed labels, or device names). If the device-name field is the only consumer, the risk is low; if the same keyboard is reused for security-sensitive strings, consider re-enabling an explicit empty-value confirmation. No immediate patching is indicated by the diff alone.
Security signals we found
UI flow simplification removes an explicit empty-string confirmation screen
Cancel action enabled while input is empty
Empty string submission enabled for device-name entry
No changelog entry and no security framing in commit message
No references to CVEs, advisories, or external reports
Evidence from the diff
The patch refactors LabelKeyboard into StringKeyboard in the layout_eckhart firmware UI. It deletes the request_string flow (flow/request_string.rs) and instead returns the keyboard component directly via RootComponent. Behavioural changes include: (1) StringKeyboardMsg::Confirmed no longer carries the typed string; the caller reads it via ComponentMsgObj and string(). (2) When the input textbox is empty, the Cancel button is now enabled (ButtonState::Enabled) instead of hidden. (3) The allow_empty flag now enables the Confirm button on an empty input without showing a separate ‘continue with empty label’ confirmation page. (4) device_menu.py now calls request_string(..., allow_empty=True, ...) for the device-name prompt. The diff is large because it is mostly a rename and deletion of the flow file; the actual logic changes are small and UI-facing.
Changed components
core/embed/rust/src/ui/layout_eckhart/firmware/keyboard/string.rscore/embed/rust/src/ui/layout_eckhart/firmware/keyboard/label.rscore/embed/rust/src/ui/layout_eckhart/firmware/mod.rscore/embed/rust/src/ui/layout_eckhart/component_msg_obj.rscore/embed/rust/src/ui/layout_eckhart/flow/request_string.rscore/embed/rust/src/ui/layout_eckhart/flow/mod.rscore/embed/rust/src/ui/layout_eckhart/ui_firmware.rscore/src/apps/homescreen/device_menu.pyInspect captured patch +622 / −774
diff --git a/core/embed/rust/src/ui/layout_eckhart/component_msg_obj.rs b/core/embed/rust/src/ui/layout_eckhart/component_msg_obj.rs
index 00b3e142..7d603e8d 100644
--- a/core/embed/rust/src/ui/layout_eckhart/component_msg_obj.rs
+++ b/core/embed/rust/src/ui/layout_eckhart/component_msg_obj.rs
@@ -17,8 +17,8 @@ use super::firmware::{
AllowedTextContent, ConfirmHomescreen, ConfirmHomescreenMsg, DeviceMenuMsg, DeviceMenuScreen,
Homescreen, HomescreenMsg, MnemonicInput, MnemonicKeyboard, MnemonicKeyboardMsg, PinKeyboard,
PinKeyboardMsg, ProgressScreen, SelectWordCountMsg, SelectWordCountScreen, SelectWordMsg,
- SelectWordScreen, SetBrightnessScreen, TextScreen, TextScreenMsg, ValueInput, ValueInputScreen,
- ValueInputScreenMsg,
+ SelectWordScreen, SetBrightnessScreen, StringKeyboard, StringKeyboardMsg, TextScreen,
+ TextScreenMsg, ValueInput, ValueInputScreen, ValueInputScreenMsg,
};
impl ComponentMsgObj for PinKeyboard<'_> {
@@ -30,6 +30,15 @@ impl ComponentMsgObj for PinKeyboard<'_> {
}
}
+impl ComponentMsgObj for StringKeyboard {
+ fn msg_try_into_obj(&self, msg: Self::Msg) -> Result<Obj, Error> {
+ match msg {
+ StringKeyboardMsg::Confirmed => self.string().try_into(),
+ StringKeyboardMsg::Cancelled => Ok(CANCELLED.as_obj()),
+ }
+ }
+}
+
impl<T> ComponentMsgObj for MnemonicKeyboard<T>
where
T: MnemonicInput,
diff --git a/core/embed/rust/src/ui/layout_eckhart/firmware/keyboard/label.rs b/core/embed/rust/src/ui/layout_eckhart/firmware/keyboard/label.rs
deleted file mode 100644
index 3bf18a00..00000000
--- a/core/embed/rust/src/ui/layout_eckhart/firmware/keyboard/label.rs
+++ /dev/null
@@ -1,668 +0,0 @@
-use crate::{
- strutil::{ShortString, TString},
- ui::{
- component::{
- swipe_detect::SwipeConfig,
- text::{
- common::TextBox,
- layout::{LayoutFit, LineBreaking},
- TextStyle,
- },
- Component, Event, EventCtx, Label, Swipe, TextLayout,
- },
- display::Icon,
- event::TouchEvent,
- flow::Swipable,
- geometry::{Alignment, Direction, Insets, Offset, Rect},
- shape::{Bar, Renderer, Text},
- util::{long_line_content_with_ellipsis, Pager},
- },
-};
-
-use super::super::{
- super::component::{Button, ButtonContent, ButtonMsg, ButtonStyleSheet},
- constant::SCREEN,
- keyboard::{
- common::{
- render_pending_marker, KeyboardLayout, MultiTapKeyboard, INPUT_TOUCH_HEIGHT,
- KEYBOARD_INPUT_INSETS, KEYBOARD_INPUT_RADIUS, KEYPAD_VISIBLE_HEIGHT,
- },
- keypad::{ButtonState, Keypad, KeypadButton, KeypadMsg, KeypadState},
- },
- theme,
-};
-
-#[derive(PartialEq, Debug, Copy, Clone)]
-#[cfg_attr(feature = "ui_debug", derive(ufmt::derive::uDebug))]
-enum DisplayStyle {
- /// A part that fits on one line
- OneLine,
- /// One line with the last pending character.
- OneLineWithMarker,
- /// The complete string is shown in the input area.
- Complete,
-}
-
-pub enum LabelKeyboardMsg {
- Confirmed(ShortString),
- Cancelled,
-}
-
-pub struct LabelKeyboard {
- page_swipe: Swipe,
- input: LabelInput,
- input_prompt: Label<'static>,
- keypad: Keypad,
- next_btn: Button,
- active_layout: KeyboardLayout,
- swipe_config: SwipeConfig,
- multi_tap: MultiTapKeyboard,
- max_len: usize,
- allow_empty: bool,
-}
-
-const PAGE_COUNT: usize = 4;
-const KEY_COUNT: usize = 10;
-#[rustfmt::skip]
-const KEYBOARD: [[&str; KEY_COUNT]; PAGE_COUNT] = [
- ["abc", "def", "ghi", "jkl", "mno", "pq", "rst", "uvw", "xyz", " *#"],
- ["ABC", "DEF", "GHI", "JKL", "MNO", "PQ", "RST", "UVW", "XYZ", " *#"],
- ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"],
- ["_<>", ".:@", "/|\\", "!()", "+%&", "-[]", "?{}", ",'`", ";\"~", "$^="],
- ];
-
-const NEXT_BTN_WIDTH: i16 = 103;
-const NEXT_BTN_PADDING: i16 = 14;
-const NEXT_BTN_INSETS: Insets =
- Insets::new(NEXT_BTN_PADDING, NEXT_BTN_PADDING, 0, NEXT_BTN_PADDING);
-
-impl LabelKeyboard {
- pub fn new(
- prompt: TString<'static>,
- max_len: usize,
- allow_empty: bool,
- prefill: Option<TString<'static>>,
- ) -> Self {
- let active_layout = KeyboardLayout::LettersLower;
- let layout: &[&str; KEY_COUNT] = &KEYBOARD[active_layout as usize];
- let keypad_content: [ButtonContent; KEY_COUNT] =
- core::array::from_fn(|idx| Self::key_content(layout[idx]));
-
- let next_btn = Button::new(active_layout.next().into())
- .styled(theme::button_keyboard_next())
- .with_radius(12)
- .with_text_align(Alignment::Center)
- .with_expanded_touch_area(NEXT_BTN_INSETS);
-
- if let Some(prefill) = prefill {
- debug_assert!(prefill.len() <= max_len);
- }
-
- Self {
- page_swipe: Swipe::horizontal(),
- input: LabelInput::new(max_len, prefill),
- input_prompt: Label::left_aligned(prompt, theme::firmware::TEXT_SMALL)
- .vertically_centered(),
- next_btn,
- keypad: Keypad::new_shown().with_keys_content(&keypad_content),
- active_layout,
- swipe_config: SwipeConfig::new(),
- multi_tap: MultiTapKeyboard::new(),
- max_len,
- allow_empty,
- }
- }
-
- fn key_text(content: &ButtonContent) -> Option<TString<'static>> {
- match content {
- ButtonContent::Text { text, .. } => Some(*text),
- ButtonContent::Icon(theme::ICON_SPECIAL_CHARS) => Some(" *#".into()),
- ButtonContent::Icon(_) => Some(" ".into()),
- _ => None,
- }
- }
-
- fn key_content(text: &'static str) -> ButtonContent {
- match text {
- " *#" => ButtonContent::Icon(theme::ICON_SPECIAL_CHARS),
- t => ButtonContent::single_line_text(t.into()),
- }
- }
-
- fn key_style(layout: KeyboardLayout) -> ButtonStyleSheet {
- if layout == KeyboardLayout::Numeric {
- theme::button_keyboard_numeric()
- } else {
- theme::button_keyboard()
- }
- }
-
- fn on_page_change(&mut self, ctx: &mut EventCtx, swipe: Direction) {
- // Change the keyboard layout.
- self.active_layout = match swipe {
- Direction::Left => self.active_layout.next(),
- Direction::Right => self.active_layout.prev(),
- _ => self.active_layout,
- };
- if self.multi_tap.pending_key().is_some() {
- // Clear the pending state.
- self.multi_tap.clear_pending_state(ctx);
- self.input.display_style = DisplayStyle::OneLine;
- }
- // Update keys.
- self.replace_keys_contents();
- self.update_keypad_state(ctx);
- }
-
- fn replace_keys_contents(&mut self) {
- self.next_btn.set_content(self.active_layout.next().into());
- let layout = self.active_layout as usize;
- let styles = Self::key_style(self.active_layout);
-
- for idx in 0..KEY_COUNT {
- let text = KEYBOARD[layout][idx];
- let content = Self::key_content(text);
- self.keypad.set_key_content(idx, content);
- self.keypad
- .set_button_stylesheet(KeypadButton::Key(idx), styles);
- }
- }
-
- /// Update the keypad state based on the current label and input state
- /// Can be used only when no key is pressed
- fn update_keypad_state(&mut self, ctx: &mut EventCtx) {
- let keypad_state = match self.input.display_style {
- DisplayStyle::Complete => {
- // Disable the entire active keypad
- KeypadState {
- back: ButtonState::Hidden,
- erase: ButtonState::Disabled,
- cancel: ButtonState::Hidden,
- confirm: ButtonState::Disabled,
- keys: ButtonState::Disabled,
- override_key: None,
- }
- }
- _ => {
- if self.label().len() == self.max_len {
- if let Some(pending_key) = self.multi_tap.pending_key() {
- // Disable all except of confirm, erase and the pending key
- KeypadState {
- back: ButtonState::Hidden,
- erase: ButtonState::Enabled,
- cancel: ButtonState::Hidden,
- confirm: ButtonState::Enabled,
- keys: ButtonState::Disabled,
- override_key: Some((pending_key, ButtonState::Enabled)),
- }
- } else {
- // Disable all except of confirm and erase buttons
- KeypadState {
- back: ButtonState::Hidden,
- erase: ButtonState::Enabled,
- cancel: ButtonState::Hidden,
- confirm: ButtonState::Enabled,
- keys: ButtonState::Disabled,
- override_key: None,
- }
- }
- } else if self.input.textbox.is_empty() {
- // Disable all except of confirm and erase buttons
- KeypadState {
- back: ButtonState::Hidden,
- erase: ButtonState::Hidden,
- cancel: ButtonState::Hidden,
- confirm: if self.allow_empty {
- ButtonState::Enabled
- } else {
- ButtonState::Disabled
- },
- keys: ButtonState::Enabled,
- override_key: None,
- }
- } else {
- KeypadState {
- back: ButtonState::Hidden,
- erase: ButtonState::Enabled,
- cancel: ButtonState::Hidden,
- confirm: ButtonState::Enabled,
- keys: ButtonState::Enabled,
- override_key: None,
- }
- }
- }
- };
-
- self.keypad.set_state(keypad_state, ctx);
- }
-
- pub fn label(&self) -> &str {
- self.input.textbox.content()
- }
-}
-
-impl Component for LabelKeyboard {
- type Msg = LabelKeyboardMsg;
-
- fn place(&mut self, bounds: Rect) -> Rect {
- // assert full screen
- debug_assert_eq!(bounds.height(), SCREEN.height());
- debug_assert_eq!(bounds.width(), SCREEN.width());
-
- // Enable swiping over the entire screen.
- self.page_swipe.place(bounds);
-
- // Keypad and input areas are overlapped
- let (_, keypad_area) = bounds.split_bottom(KEYPAD_VISIBLE_HEIGHT);
- let (top_area, _) = bounds.split_top(INPUT_TOUCH_HEIGHT);
-
- let (input_area, next_btn_area) =
- top_area.split_right(NEXT_BTN_WIDTH + 2 * NEXT_BTN_PADDING);
-
- let next_btn_area = next_btn_area.inset(NEXT_BTN_INSETS);
-
- self.input.place(input_area);
- self.input_prompt
- .place(top_area.inset(KEYBOARD_INPUT_INSETS));
- self.keypad.place(keypad_area);
- self.next_btn.place(next_btn_area);
-
- bounds
- }
-
- fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
- match event {
- Event::Attach(_) => {
- // Update the keypad state in the first event
- self.update_keypad_state(ctx);
- }
- Event::Timer(_) if self.multi_tap.timeout_event(event) => {
- self.multi_tap.clear_pending_state(ctx);
- self.input.display_style = DisplayStyle::OneLine;
- // Disable keypad when the label reached the max length
- if self.label().len() == self.max_len {
- self.update_keypad_state(ctx);
- }
- return None;
- }
-
- _ => {}
- }
-
- // Input event has to be handled before the swipe so that swipe in the input
- // area is not processed
- match self.input.event(ctx, event) {
- Some(LabelInputMsg::TouchStart) => {
- self.multi_tap.clear_pending_state(ctx);
- // Disable keypad.
- self.update_keypad_state(ctx);
- return None;
- }
- Some(LabelInputMsg::TouchEnd) => {
- // Enable keypad.
- self.update_keypad_state(ctx);
- return None;
- }
- _ => {}
- }
-
- // Swipe event has to be handled before the individual button events
- if let Some(swipe) = self.page_swipe.event(ctx, event) {
- match swipe {
- Direction::Left | Direction::Right => {
- // We have detected a horizontal swipe. Change the keyboard page.
- self.on_page_change(ctx, swipe);
- return None;
- }
- _ => {}
- }
- }
-
- if let Some(ButtonMsg::Clicked) = self.next_btn.event(ctx, event) {
- self.on_page_change(ctx, Direction::Left);
- }
-
- match self.keypad.event(ctx, event) {
- Some(KeypadMsg::Key(idx)) => {
- if let Some(text) = Self::key_text(self.keypad.get_key_content(idx)) {
- let edit = text.map(|c| self.multi_tap.click_key(ctx, idx, c));
- self.input.textbox.apply(ctx, edit);
- if text.len() == 1 {
- // If the key has just one character, it is immediately applied
- self.input.display_style = DisplayStyle::OneLine;
- } else {
- // multi tap timer is running, the last digit timer should be stopped
- self.input.display_style = DisplayStyle::OneLineWithMarker;
- }
- self.update_keypad_state(ctx);
- }
- return None;
- }
- Some(KeypadMsg::EraseShort) => {
- self.multi_tap.clear_pending_state(ctx);
- self.input.textbox.delete_last(ctx);
- self.input.display_style = DisplayStyle::OneLine;
- self.update_keypad_state(ctx);
- return None;
- }
- Some(KeypadMsg::EraseLong) => {
- self.multi_tap.clear_pending_state(ctx);
- self.input.textbox.clear(ctx);
- self.input.display_style = DisplayStyle::OneLine;
- self.update_keypad_state(ctx);
- return None;
- }
- Some(KeypadMsg::Cancel) => {
- return Some(LabelKeyboardMsg::Cancelled);
- }
- Some(KeypadMsg::Confirm) => {
- return Some(LabelKeyboardMsg::Confirmed(unwrap!(ShortString::try_from(
- self.label()
- ))));
- }
- _ => {}
- }
-
- None
- }
-
- fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
- let empty = self.label().is_empty();
-
- // Render prompt when the pin is empty
- if empty {
- self.input_prompt.render(target);
- }
-
- // When the entire label is shown, the input area might overlap the keypad
- // so it has to be render later
- match self.input.display_style {
- DisplayStyle::Complete => {
- self.keypad.render(target);
- self.input.render(target);
- }
- _ => {
- // When the next button is shown, the input area might overlap the keypad so it
- // has to be render later
- self.input.render(target);
-
- if self.next_btn.is_pressed() {
- self.keypad.render(target);
- self.next_btn.render(target);
- } else {
- self.next_btn.render(target);
- self.keypad.render(target);
- }
- }
- }
- }
-}
-
-#[derive(PartialEq, Debug, Copy, Clone)]
-#[cfg_attr(feature = "ui_debug", derive(ufmt::derive::uDebug))]
-pub enum LabelInputMsg {
- TouchStart,
- TouchEnd,
-}
-
-struct LabelInput {
- area: Rect,
- textbox: TextBox,
- display_style: DisplayStyle,
- shown_area: Rect,
-}
-
-impl LabelInput {
- const TWITCH: i16 = 4;
- const SHOWN_INSETS: Insets = Insets::new(12, 24, 12, 24);
- const SHOWN_STYLE: TextStyle =
- theme::TEXT_REGULAR.with_line_breaking(LineBreaking::BreakWordsNoHyphen);
- const SHOWN_TOUCH_OUTSET: Insets = Insets::bottom(200);
- const ICON: Icon = theme::ICON_DASH_VERTICAL;
- const ICON_WIDTH: i16 = Self::ICON.toif.width();
- const ICON_SPACE: i16 = 12;
-
- fn new(max_len: usize, prefill: Option<TString<'static>>) -> Self {
- let textbox = if let Some(prefill) = prefill {
- prefill.map(|s| TextBox::new(s, max_len))
- } else {
- TextBox::empty(max_len)
- };
-
- Self {
- area: Rect::zero(),
- textbox,
- display_style: DisplayStyle::OneLine,
- shown_area: Rect::zero(),
- }
- }
-
- fn label(&self) -> &str {
- self.textbox.content()
- }
-
- fn update_shown_area(&mut self) {
- // The area where the label is shown
- let mut shown_area = Rect::from_top_left_and_size(
- self.area.top_left(),
- Offset::new(SCREEN.width(), self.area.height()),
- )
- .inset(KEYBOARD_INPUT_INSETS);
-
- // Extend the shown area until the text fits
- while let LayoutFit::OutOfBounds { .. } = TextLayout::new(Self::SHOWN_STYLE)
- .with_align(Alignment::Start)
- .with_bounds(shown_area.inset(Self::SHOWN_INSETS))
- .fit_text(self.label())
- {
- shown_area =
- shown_area.outset(Insets::bottom(Self::SHOWN_STYLE.text_font.line_height()));
- }
-
- self.shown_area = shown_area;
- }
-
- fn render_complete<'s>(&self, target: &mut impl Renderer<'s>) {
- // Make sure the pin should be shown
- debug_assert_eq!(self.display_style, DisplayStyle::Complete);
-
- Bar::new(self.shown_area)
- .with_bg(theme::GREY_SUPER_DARK)
- .with_radius(KEYBOARD_INPUT_RADIUS)
- .render(target);
-
- TextLayout::new(Self::SHOWN_STYLE)
- .with_bounds(self.shown_area.inset(Self::SHOWN_INSETS))
- .with_align(Alignment::Start)
- .render_text(self.label(), target, true);
- }
-
- fn render_one_line<'s>(&self, target: &mut impl Renderer<'s>) {
- debug_assert_ne!(self.display_style, DisplayStyle::Complete);
-
- let insets = Insets::new(
- KEYBOARD_INPUT_INSETS.top,
- 0,
- KEYBOARD_INPUT_INSETS.bottom,
- KEYBOARD_INPUT_INSETS.left,
- );
-
- let area: Rect = self.area.inset(insets);
- let style = theme::TEXT_REGULAR;
-
- // Find out how much text can fit into the textbox.
- // Accounting for the pending marker, which draws itself one pixel longer than
- // the last character
- let available_area_width = area.width() - 1;
- let text_to_display = long_line_content_with_ellipsis(
- self.label(),
- "...",
- style.text_font,
- available_area_width,
- );
-
- let cursor = area
- .left_center()
- .ofs(Offset::new(8, style.text_font.text_max_height() / 2 - 4));
-
- Text::new(cursor, &text_to_display, style.text_font)
- .with_fg(style.text_color)
- .render(target);
-
- // Paint the pending marker.
- if self.display_style == DisplayStyle::OneLineWithMarker {
- render_pending_marker(
- target,
- cursor,
- &text_to_display,
- style.text_font,
- style.text_color,
- );
- }
- // let pp_len = self.label().len();
- // let last_char = self.display_style != DisplayStyle::Hidden;
-
- // // Render only when there are characters
- // if pp_len == 0 {
- // return;
- // }
- // // Number of visible icons + characters
- // let visible_len = pp_len.min(MAX_SHOWN_LEN);
- // // Number of visible icons
- // let visible_icons = visible_len - last_char as usize;
-
- // // Jiggle when overflowed.
- // if pp_len > visible_len && pp_len % 2 == 0 && self.display_style !=
- // DisplayStyle::Shown { cursor.x += Self::TWITCH;
- // }
-
- // let mut char_idx = 0;
-
- // // Greyed out overflowing icons
- // for (i, &fg_color) in FADING_ICON_COLORS.iter().enumerate() {
- // if pp_len > visible_len + (FADING_ICON_COUNT - 1 - i) {
- // ToifImage::new(cursor, Self::ICON.toif)
- // .with_align(Alignment2D::TOP_LEFT)
- // .with_fg(fg_color)
- // .render(target);
- // cursor.x += Self::ICON_SPACE + Self::ICON_WIDTH;
- // char_idx += 1;
- // }
- // }
-
- // if visible_icons > 0 {
- // // Classical dot(s)
- // for _ in char_idx..visible_icons {
- // ToifImage::new(cursor, Self::ICON.toif)
- // .with_align(Alignment2D::TOP_LEFT)
- // .with_fg(style.text_color)
- // .render(target);
- // cursor.x += Self::ICON_SPACE + Self::ICON_WIDTH;
- // }
- // }
-
- // if last_char {
- // // This should not fail because pp_len > 0
- // let last = &self.label()[(pp_len - 1)..pp_len];
-
- // // Adapt x and y positions for the character
- // cursor.y = hidden_area.left_center().y +
- // style.text_font.text_max_height() / 2; cursor.x -=
- // Self::ICON_WIDTH;
-
- // // Paint the last character
- // Text::new(cursor, last, style.text_font)
- // .with_align(Alignment::Start)
- // .with_fg(style.text_color)
- // .render(target);
-
- // // Paint the pending marker.
- // if self.display_style == DisplayStyle::LastWithMarker {
- // render_pending_marker(target, cursor, last, style.text_font,
- // style.text_color); }
- // }
- }
-}
-
-impl Component for LabelInput {
- type Msg = LabelInputMsg;
-
- fn place(&mut self, bounds: Rect) -> Rect {
- self.area = bounds;
- bounds
- }
-
- fn event(&mut self, _ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
- // No touch events are handled when the textbox is empty
- if self.textbox.is_empty() {
- return None;
- }
-
- // Extend the label area downward to allow touch input without the finger
- // covering the label
- let extended_shown_area = self
- .shown_area
- .outset(Self::SHOWN_TOUCH_OUTSET)
- .clamp(SCREEN);
-
- match event {
- // Return touch start if the touch is detected inside the touchable area
- Event::Touch(TouchEvent::TouchStart(pos)) if self.area.contains(pos) => {
- // Show the entire label on the touch start
- self.display_style = DisplayStyle::Complete;
- self.update_shown_area();
- return Some(LabelInputMsg::TouchStart);
- }
- // Return touch end if the touch end is detected inside the visible area
- Event::Touch(TouchEvent::TouchEnd(pos))
- if extended_shown_area.contains(pos)
- && self.display_style == DisplayStyle::Complete =>
- {
- self.display_style = DisplayStyle::OneLine;
- return Some(LabelInputMsg::TouchEnd);
- }
- // Return touch end if the touch moves out of the visible area
- Event::Touch(TouchEvent::TouchMove(pos))
- if !extended_shown_area.contains(pos)
- && self.display_style == DisplayStyle::Complete =>
- {
- self.display_style = DisplayStyle::OneLine;
- return Some(LabelInputMsg::TouchEnd);
- }
- _ => {}
- };
- None
- }
-
- fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
- if !self.label().is_empty() {
- match self.display_style {
- DisplayStyle::Complete => self.render_complete(target),
- _ => self.render_one_line(target),
- }
- }
- }
-}
-
-#[cfg(feature = "micropython")]
-impl Swipable for LabelKeyboard {
- fn get_swipe_config(&self) -> SwipeConfig {
- self.swipe_config
- }
-
- fn get_pager(&self) -> Pager {
- Pager::single_page()
- }
-}
-
-#[cfg(feature = "ui_debug")]
-impl crate::trace::Trace for LabelKeyboard {
- fn trace(&self, t: &mut dyn crate::trace::Tracer) {
- let display_style = uformat!("{:?}", self.input.display_style);
- let active_layout = uformat!("{:?}", self.active_layout);
- t.component("LabelKeyboard");
- t.string("label", self.label().into());
- t.string("display_style", display_style.as_str().into());
- t.string("active_layout", active_layout.as_str().into());
- }
-}
diff --git a/core/embed/rust/src/ui/layout_eckhart/firmware/keyboard/mod.rs b/core/embed/rust/src/ui/layout_eckhart/firmware/keyboard/mod.rs
index 756b6621..e298c5b8 100644
--- a/core/embed/rust/src/ui/layout_eckhart/firmware/keyboard/mod.rs
+++ b/core/embed/rust/src/ui/layout_eckhart/firmware/keyboard/mod.rs
@@ -1,9 +1,9 @@
pub mod bip39;
-pub mod label;
pub mod mnemonic;
pub mod passphrase;
pub mod pin;
pub mod slip39;
+pub mod string;
pub mod word_count_screen;
mod common;
diff --git a/core/embed/rust/src/ui/layout_eckhart/firmware/keyboard/string.rs b/core/embed/rust/src/ui/layout_eckhart/firmware/keyboard/string.rs
new file mode 100644
index 00000000..153db27f
--- /dev/null
+++ b/core/embed/rust/src/ui/layout_eckhart/firmware/keyboard/string.rs
@@ -0,0 +1,604 @@
+use crate::{
+ strutil::TString,
+ ui::{
+ component::{
+ swipe_detect::SwipeConfig,
+ text::{
+ common::TextBox,
+ layout::{LayoutFit, LineBreaking},
+ TextStyle,
+ },
+ Component, Event, EventCtx, Label, Swipe, TextLayout,
+ },
+ display::Icon,
+ event::TouchEvent,
+ flow::Swipable,
+ geometry::{Alignment, Direction, Insets, Offset, Rect},
+ shape::{Bar, Renderer, Text},
+ util::{long_line_content_with_ellipsis, Pager},
+ },
+};
+
+use super::super::{
+ super::component::{Button, ButtonContent, ButtonMsg, ButtonStyleSheet},
+ constant::SCREEN,
+ keyboard::{
+ common::{
+ render_pending_marker, KeyboardLayout, MultiTapKeyboard, INPUT_TOUCH_HEIGHT,
+ KEYBOARD_INPUT_INSETS, KEYBOARD_INPUT_RADIUS, KEYPAD_VISIBLE_HEIGHT,
+ },
+ keypad::{ButtonState, Keypad, KeypadButton, KeypadMsg, KeypadState},
+ },
+ theme,
+};
+
+#[derive(PartialEq, Debug, Copy, Clone)]
+#[cfg_attr(feature = "ui_debug", derive(ufmt::derive::uDebug))]
+enum DisplayStyle {
+ /// A part that fits on one line
+ OneLine,
+ /// One line with the last pending character.
+ OneLineWithMarker,
+ /// The complete string is shown in the input area.
+ Complete,
+}
+
+pub enum StringKeyboardMsg {
+ Confirmed,
+ Cancelled,
+}
+
+pub struct StringKeyboard {
+ page_swipe: Swipe,
+ input: StringInput,
+ input_prompt: Label<'static>,
+ keypad: Keypad,
+ next_btn: Button,
+ active_layout: KeyboardLayout,
+ swipe_config: SwipeConfig,
+ multi_tap: MultiTapKeyboard,
+ max_len: usize,
+ allow_empty: bool,
+}
+
+const PAGE_COUNT: usize = 4;
+const KEY_COUNT: usize = 10;
+#[rustfmt::skip]
+const KEYBOARD: [[&str; KEY_COUNT]; PAGE_COUNT] = [
+ ["abc", "def", "ghi", "jkl", "mno", "pq", "rst", "uvw", "xyz", " *#"],
+ ["ABC", "DEF", "GHI", "JKL", "MNO", "PQ", "RST", "UVW", "XYZ", " *#"],
+ ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"],
+ ["_<>", ".:@", "/|\\", "!()", "+%&", "-[]", "?{}", ",'`", ";\"~", "$^="],
+ ];
+
+const NEXT_BTN_WIDTH: i16 = 103;
+const NEXT_BTN_PADDING: i16 = 14;
+const NEXT_BTN_INSETS: Insets =
+ Insets::new(NEXT_BTN_PADDING, NEXT_BTN_PADDING, 0, NEXT_BTN_PADDING);
+
+impl StringKeyboard {
+ pub fn new(
+ prompt: TString<'static>,
+ max_len: usize,
+ allow_empty: bool,
+ prefill: Option<TString<'static>>,
+ ) -> Self {
+ let active_layout = KeyboardLayout::LettersLower;
+ let layout: &[&str; KEY_COUNT] = &KEYBOARD[active_layout as usize];
+ let keypad_content: [ButtonContent; KEY_COUNT] =
+ core::array::from_fn(|idx| Self::key_content(layout[idx]));
+
+ let next_btn = Button::new(active_layout.next().into())
+ .styled(theme::button_keyboard_next())
+ .with_radius(12)
+ .with_text_align(Alignment::Center)
+ .with_expanded_touch_area(NEXT_BTN_INSETS);
+
+ if let Some(prefill) = prefill {
+ debug_assert!(prefill.len() <= max_len);
+ }
+
+ Self {
+ page_swipe: Swipe::horizontal(),
+ input: StringInput::new(max_len, prefill),
+ input_prompt: Label::left_aligned(prompt, theme::firmware::TEXT_SMALL)
+ .vertically_centered(),
+ next_btn,
+ keypad: Keypad::new_shown().with_keys_content(&keypad_content),
+ active_layout,
+ swipe_config: SwipeConfig::new(),
+ multi_tap: MultiTapKeyboard::new(),
+ max_len,
+ allow_empty,
+ }
+ }
+
+ fn key_text(content: &ButtonContent) -> Option<TString<'static>> {
+ match content {
+ ButtonContent::Text { text, .. } => Some(*text),
+ ButtonContent::Icon(theme::ICON_SPECIAL_CHARS) => Some(" *#".into()),
+ ButtonContent::Icon(_) => Some(" ".into()),
+ _ => None,
+ }
+ }
+
+ fn key_content(text: &'static str) -> ButtonContent {
+ match text {
+ " *#" => ButtonContent::Icon(theme::ICON_SPECIAL_CHARS),
+ t => ButtonContent::single_line_text(t.into()),
+ }
+ }
+
+ fn key_style(layout: KeyboardLayout) -> ButtonStyleSheet {
+ if layout == KeyboardLayout::Numeric {
+ theme::button_keyboard_numeric()
+ } else {
+ theme::button_keyboard()
+ }
+ }
+
+ fn on_page_change(&mut self, ctx: &mut EventCtx, swipe: Direction) {
+ // Change the keyboard layout.
+ self.active_layout = match swipe {
+ Direction::Left => self.active_layout.next(),
+ Direction::Right => self.active_layout.prev(),
+ _ => self.active_layout,
+ };
+ if self.multi_tap.pending_key().is_some() {
+ // Clear the pending state.
+ self.multi_tap.clear_pending_state(ctx);
+ self.input.display_style = DisplayStyle::OneLine;
+ }
+ // Update keys.
+ self.replace_keys_contents();
+ self.update_keypad_state(ctx);
+ }
+
+ fn replace_keys_contents(&mut self) {
+ self.next_btn.set_content(self.active_layout.next().into());
+ let layout = self.active_layout as usize;
+ let styles = Self::key_style(self.active_layout);
+
+ for idx in 0..KEY_COUNT {
+ let text = KEYBOARD[layout][idx];
+ let content = Self::key_content(text);
+ self.keypad.set_key_content(idx, content);
+ self.keypad
+ .set_button_stylesheet(KeypadButton::Key(idx), styles);
+ }
+ }
+
+ /// Update the keypad state based on the current string and input state
+ /// Can be used only when no key is pressed
+ fn update_keypad_state(&mut self, ctx: &mut EventCtx) {
+ let keypad_state = match self.input.display_style {
+ DisplayStyle::Complete => {
+ // Disable the entire active keypad
+ KeypadState {
+ back: ButtonState::Hidden,
+ erase: ButtonState::Disabled,
+ cancel: ButtonState::Hidden,
+ confirm: ButtonState::Disabled,
+ keys: ButtonState::Disabled,
+ override_key: None,
+ }
+ }
+ _ => {
+ if self.string().len() == self.max_len {
+ if let Some(pending_key) = self.multi_tap.pending_key() {
+ // Disable all except of confirm, erase and the pending key
+ KeypadState {
+ back: ButtonState::Hidden,
+ erase: ButtonState::Enabled,
+ cancel: ButtonState::Hidden,
+ confirm: ButtonState::Enabled,
+ keys: ButtonState::Disabled,
+ override_key: Some((pending_key, ButtonState::Enabled)),
+ }
+ } else {
+ // Disable all except of confirm and erase buttons
+ KeypadState {
+ back: ButtonState::Hidden,
+ erase: ButtonState::Enabled,
+ cancel: ButtonState::Hidden,
+ confirm: ButtonState::Enabled,
+ keys: ButtonState::Disabled,
+ override_key: None,
+ }
+ }
+ } else if self.input.textbox.is_empty() {
+ // Disable all except of confirm and erase buttons
+ KeypadState {
+ back: ButtonState::Hidden,
+ erase: ButtonState::Hidden,
+ cancel: ButtonState::Enabled,
+ confirm: if self.allow_empty {
+ ButtonState::Enabled
+ } else {
+ ButtonState::Disabled
+ },
+ keys: ButtonState::Enabled,
+ override_key: None,
+ }
+ } else {
+ KeypadState {
+ back: ButtonState::Hidden,
+ erase: ButtonState::Enabled,
+ cancel: ButtonState::Hidden,
+ confirm: ButtonState::Enabled,
+ keys: ButtonState::Enabled,
+ override_key: None,
+ }
+ }
+ }
+ };
+
+ self.keypad.set_state(keypad_state, ctx);
+ }
+
+ pub fn string(&self) -> &str {
+ self.input.textbox.content()
+ }
+}
+
+impl Component for StringKeyboard {
+ type Msg = StringKeyboardMsg;
+
+ fn place(&mut self, bounds: Rect) -> Rect {
+ // assert full screen
+ debug_assert_eq!(bounds.height(), SCREEN.height());
+ debug_assert_eq!(bounds.width(), SCREEN.width());
+
+ // Enable swiping over the entire screen.
+ self.page_swipe.place(bounds);
+
+ // Keypad and input areas are overlapped
+ let (_, keypad_area) = bounds.split_bottom(KEYPAD_VISIBLE_HEIGHT);
+ let (top_area, _) = bounds.split_top(INPUT_TOUCH_HEIGHT);
+
+ let (input_area, next_btn_area) =
+ top_area.split_right(NEXT_BTN_WIDTH + 2 * NEXT_BTN_PADDING);
+
+ let next_btn_area = next_btn_area.inset(NEXT_BTN_INSETS);
+
+ self.input.place(input_area);
+ self.input_prompt
+ .place(top_area.inset(KEYBOARD_INPUT_INSETS));
+ self.keypad.place(keypad_area);
+ self.next_btn.place(next_btn_area);
+
+ bounds
+ }
+
+ fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
+ match event {
+ Event::Attach(_) => {
+ // Update the keypad state in the first event
+ self.update_keypad_state(ctx);
+ }
+ Event::Timer(_) if self.multi_tap.timeout_event(event) => {
+ self.multi_tap.clear_pending_state(ctx);
+ self.input.display_style = DisplayStyle::OneLine;
+ // Disable keypad when the string reached the max length
+ if self.string().len() == self.max_len {
+ self.update_keypad_state(ctx);
+ }
+ return None;
+ }
+
+ _ => {}
+ }
+
+ // Input event has to be handled before the swipe so that swipe in the input
+ // area is not processed
+ match self.input.event(ctx, event) {
+ Some(StringInputMsg::TouchStart) => {
+ self.multi_tap.clear_pending_state(ctx);
+ // Disable keypad.
+ self.update_keypad_state(ctx);
+ return None;
+ }
+ Some(StringInputMsg::TouchEnd) => {
+ // Enable keypad.
+ self.update_keypad_state(ctx);
+ return None;
+ }
+ _ => {}
+ }
+
+ // Swipe event has to be handled before the individual button events
+ if let Some(swipe) = self.page_swipe.event(ctx, event) {
+ match swipe {
+ Direction::Left | Direction::Right => {
+ // We have detected a horizontal swipe. Change the keyboard page.
+ self.on_page_change(ctx, swipe);
+ return None;
+ }
+ _ => {}
+ }
+ }
+
+ if let Some(ButtonMsg::Clicked) = self.next_btn.event(ctx, event) {
+ self.on_page_change(ctx, Direction::Left);
+ }
+
+ match self.keypad.event(ctx, event) {
+ Some(KeypadMsg::Key(idx)) => {
+ if let Some(text) = Self::key_text(self.keypad.get_key_content(idx)) {
+ let edit = text.map(|c| self.multi_tap.click_key(ctx, idx, c));
+ self.input.textbox.apply(ctx, edit);
+ if text.len() == 1 {
+ // If the key has just one character, it is immediately applied
+ self.input.display_style = DisplayStyle::OneLine;
+ } else {
+ // multi tap timer is running, the last digit timer should be stopped
+ self.input.display_style = DisplayStyle::OneLineWithMarker;
+ }
+ self.update_keypad_state(ctx);
+ }
+ return None;
+ }
+ Some(KeypadMsg::EraseShort) => {
+ self.multi_tap.clear_pending_state(ctx);
+ self.input.textbox.delete_last(ctx);
+ self.input.display_style = DisplayStyle::OneLine;
+ self.update_keypad_state(ctx);
+ return None;
+ }
+ Some(KeypadMsg::EraseLong) => {
+ self.multi_tap.clear_pending_state(ctx);
+ self.input.textbox.clear(ctx);
+ self.input.display_style = DisplayStyle::OneLine;
+ self.update_keypad_state(ctx);
+ return None;
+ }
+ Some(KeypadMsg::Cancel) => {
+ return Some(StringKeyboardMsg::Cancelled);
+ }
+ Some(KeypadMsg::Confirm) => {
+ return Some(StringKeyboardMsg::Confirmed);
+ }
+ _ => {}
+ }
+
+ None
+ }
+
+ fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
+ let empty = self.string().is_empty();
+
+ // Render prompt when the pin is empty
+ if empty {
+ self.input_prompt.render(target);
+ }
+
+ // When the entire string is shown, the input area might overlap the keypad
+ // so it has to be render later
+ match self.input.display_style {
+ DisplayStyle::Complete => {
+ self.keypad.render(target);
+ self.input.render(target);
+ }
+ _ => {
+ // When the next button is shown, the input area might overlap the keypad so it
+ // has to be render later
+ self.input.render(target);
+
+ if self.next_btn.is_pressed() {
+ self.keypad.render(target);
+ self.next_btn.render(target);
+ } else {
+ self.next_btn.render(target);
+ self.keypad.render(target);
+ }
+ }
+ }
+ }
+}
+
+#[derive(PartialEq, Debug, Copy, Clone)]
+#[cfg_attr(feature = "ui_debug", derive(ufmt::derive::uDebug))]
+pub enum StringInputMsg {
+ TouchStart,
+ TouchEnd,
+}
+
+struct StringInput {
+ area: Rect,
+ textbox: TextBox,
+ display_style: DisplayStyle,
+ shown_area: Rect,
+}
+
+impl StringInput {
+ const TWITCH: i16 = 4;
+ const SHOWN_INSETS: Insets = Insets::new(12, 24, 12, 24);
+ const SHOWN_STYLE: TextStyle =
+ theme::TEXT_REGULAR.with_line_breaking(LineBreaking::BreakWordsNoHyphen);
+ const SHOWN_TOUCH_OUTSET: Insets = Insets::bottom(200);
+ const ICON: Icon = theme::ICON_DASH_VERTICAL;
+ const ICON_WIDTH: i16 = Self::ICON.toif.width();
+ const ICON_SPACE: i16 = 12;
+
+ fn new(max_len: usize, prefill: Option<TString<'static>>) -> Self {
+ let textbox = if let Some(prefill) = prefill {
+ prefill.map(|s| TextBox::new(s, max_len))
+ } else {
+ TextBox::empty(max_len)
+ };
+
+ Self {
+ area: Rect::zero(),
+ textbox,
+ display_style: DisplayStyle::OneLine,
+ shown_area: Rect::zero(),
+ }
+ }
+
+ fn string(&self) -> &str {
+ self.textbox.content()
+ }
+
+ fn update_shown_area(&mut self) {
+ // The area where the string is shown
+ let mut shown_area = Rect::from_top_left_and_size(
+ self.area.top_left(),
+ Offset::new(SCREEN.width(), self.area.height()),
+ )
+ .inset(KEYBOARD_INPUT_INSETS);
+
+ // Extend the shown area until the text fits
+ while let LayoutFit::OutOfBounds { .. } = TextLayout::new(Self::SHOWN_STYLE)
+ .with_align(Alignment::Start)
+ .with_bounds(shown_area.inset(Self::SHOWN_INSETS))
+ .fit_text(self.string())
+ {
+ shown_area =
+ shown_area.outset(Insets::bottom(Self::SHOWN_STYLE.text_font.line_height()));
+ }
+
+ self.shown_area = shown_area;
+ }
+
+ fn render_complete<'s>(&self, target: &mut impl Renderer<'s>) {
+ // Make sure the pin should be shown
+ debug_assert_eq!(self.display_style, DisplayStyle::Complete);
+
+ Bar::new(self.shown_area)
+ .with_bg(theme::GREY_SUPER_DARK)
+ .with_radius(KEYBOARD_INPUT_RADIUS)
+ .render(target);
+
+ TextLayout::new(Self::SHOWN_STYLE)
+ .with_bounds(self.shown_area.inset(Self::SHOWN_INSETS))
+ .with_align(Alignment::Start)
+ .render_text(self.string(), target, true);
+ }
+
+ fn render_one_line<'s>(&self, target: &mut impl Renderer<'s>) {
+ debug_assert_ne!(self.display_style, DisplayStyle::Complete);
+
+ let insets = Insets::new(
+ KEYBOARD_INPUT_INSETS.top,
+ 0,
+ KEYBOARD_INPUT_INSETS.bottom,
+ KEYBOARD_INPUT_INSETS.left,
+ );
+
+ let area: Rect = self.area.inset(insets);
+ let style = theme::TEXT_REGULAR;
+
+ // Find out how much text can fit into the textbox.
+ // Accounting for the pending marker, which draws itself one pixel longer than
+ // the last character
+ let available_area_width = area.width() - 1;
+ let text_to_display = long_line_content_with_ellipsis(
+ self.string(),
+ "...",
+ style.text_font,
+ available_area_width,
+ );
+
+ let cursor = area
+ .left_center()
+ .ofs(Offset::new(8, style.text_font.text_max_height() / 2 - 4));
+
+ Text::new(cursor, &text_to_display, style.text_font)
+ .with_fg(style.text_color)
+ .render(target);
+
+ // Paint the pending marker.
+ if self.display_style == DisplayStyle::OneLineWithMarker {
+ render_pending_marker(
+ target,
+ cursor,
+ &text_to_display,
+ style.text_font,
+ style.text_color,
+ );
+ }
+ }
+}
+
+impl Component for StringInput {
+ type Msg = StringInputMsg;
+
+ fn place(&mut self, bounds: Rect) -> Rect {
+ self.area = bounds;
+ bounds
+ }
+
+ fn event(&mut self, _ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
+ // No touch events are handled when the textbox is empty
+ if self.textbox.is_empty() {
+ return None;
+ }
+
+ // Extend the string area downward to allow touch input without the finger
+ // covering the string
+ let extended_shown_area = self
+ .shown_area
+ .outset(Self::SHOWN_TOUCH_OUTSET)
+ .clamp(SCREEN);
+
+ match event {
+ // Return touch start if the touch is detected inside the touchable area
+ Event::Touch(TouchEvent::TouchStart(pos)) if self.area.contains(pos) => {
+ // Show the entire string on the touch start
+ self.display_style = DisplayStyle::Complete;
+ self.update_shown_area();
+ return Some(StringInputMsg::TouchStart);
+ }
+ // Return touch end if the touch end is detected inside the visible area
+ Event::Touch(TouchEvent::TouchEnd(pos))
+ if extended_shown_area.contains(pos)
+ && self.display_style == DisplayStyle::Complete =>
+ {
+ self.display_style = DisplayStyle::OneLine;
+ return Some(StringInputMsg::TouchEnd);
+ }
+ // Return touch end if the touch moves out of the visible area
+ Event::Touch(TouchEvent::TouchMove(pos))
+ if !extended_shown_area.contains(pos)
+ && self.display_style == DisplayStyle::Complete =>
+ {
+ self.display_style = DisplayStyle::OneLine;
+ return Some(StringInputMsg::TouchEnd);
+ }
+ _ => {}
+ };
+ None
+ }
+
+ fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
+ if !self.string().is_empty() {
+ match self.display_style {
+ DisplayStyle::Complete => self.render_complete(target),
+ _ => self.render_one_line(target),
+ }
+ }
+ }
+}
+
+#[cfg(feature = "micropython")]
+impl Swipable for StringKeyboard {
+ fn get_swipe_config(&self) -> SwipeConfig {
+ self.swipe_config
+ }
+
+ fn get_pager(&self) -> Pager {
+ Pager::single_page()
+ }
+}
+
+#[cfg(feature = "ui_debug")]
+impl crate::trace::Trace for StringKeyboard {
+ fn trace(&self, t: &mut dyn crate::trace::Tracer) {
+ let display_style = uformat!("{:?}", self.input.display_style);
+ let active_layout = uformat!("{:?}", self.active_layout);
+ t.component("StringKeyboard");
+ t.string("string", self.string().into());
+ t.string("display_style", display_style.as_str().into());
+ t.string("active_layout", active_layout.as_str().into());
+ }
+}
diff --git a/core/embed/rust/src/ui/layout_eckhart/firmware/mod.rs b/core/embed/rust/src/ui/layout_eckhart/firmware/mod.rs
index 779c7288..1e93c1fc 100644
--- a/core/embed/rust/src/ui/layout_eckhart/firmware/mod.rs
+++ b/core/embed/rust/src/ui/layout_eckhart/firmware/mod.rs
@@ -33,11 +33,11 @@ pub use hold_to_confirm::HoldToConfirmAnim;
pub use homescreen::{check_homescreen_format, Homescreen, HomescreenMsg};
pub use keyboard::{
bip39::Bip39Input,
- label::{LabelKeyboard, LabelKeyboardMsg},
mnemonic::{MnemonicInput, MnemonicKeyboard, MnemonicKeyboardMsg},
passphrase::{PassphraseKeyboard, PassphraseKeyboardMsg},
pin::{PinKeyboard, PinKeyboardMsg},
slip39::Slip39Input,
+ string::{StringKeyboard, StringKeyboardMsg},
word_count_screen::{SelectWordCountMsg, SelectWordCountScreen},
};
pub use progress_screen::ProgressScreen;
diff --git a/core/embed/rust/src/ui/layout_eckhart/flow/mod.rs b/core/embed/rust/src/ui/layout_eckhart/flow/mod.rs
index 1101f2fc..cc289543 100644
--- a/core/embed/rust/src/ui/layout_eckhart/flow/mod.rs
+++ b/core/embed/rust/src/ui/layout_eckhart/flow/mod.rs
@@ -12,7 +12,6 @@ pub mod prompt_backup;
pub mod receive;
pub mod request_number;
pub mod request_passphrase;
-pub mod request_string;
pub mod show_danger;
pub mod show_share_words;
pub mod show_thp_pairing_code;
@@ -33,7 +32,6 @@ pub use prompt_backup::PromptBackup;
pub use receive::Receive;
pub use request_number::new_request_number;
pub use request_passphrase::RequestPassphrase;
-pub use request_string::RequestString;
pub use show_danger::ShowDanger;
pub use show_share_words::new_show_share_words_flow;
pub use show_thp_pairing_code::new_show_thp_pairing_code;
diff --git a/core/embed/rust/src/ui/layout_eckhart/flow/request_string.rs b/core/embed/rust/src/ui/layout_eckhart/flow/request_string.rs
deleted file mode 100644
index 8bdcc245..00000000
--- a/core/embed/rust/src/ui/layout_eckhart/flow/request_string.rs
+++ /dev/null
@@ -1,95 +0,0 @@
-use crate::{
- error,
- strutil::{ShortString, TString},
- translations::TR,
- ui::{
- component::{
- text::paragraphs::{Paragraph, ParagraphSource},
- ComponentExt,
- },
- flow::{
- base::{Decision, DecisionBuilder as _},
- FlowController, FlowMsg, SwipeFlow,
- },
- geometry::{Direction, LinearPlacement},
- },
-};
-
-use super::super::{
- component::Button,
- firmware::{ActionBar, Header, LabelKeyboard, LabelKeyboardMsg, TextScreen, TextScreenMsg},
- theme,
-};
-
-#[derive(Copy, Clone, PartialEq, Eq)]
-pub enum RequestString {
- Keypad,
- ConfirmEmpty,
-}
-
-impl FlowController for RequestString {
- #[inline]
- fn index(&'static self) -> usize {
- *self as usize
- }
-
- fn handle_swipe(&'static self, _direction: Direction) -> Decision {
- self.do_nothing()
- }
-
- fn handle_event(&'static self, msg: FlowMsg) -> Decision {
- match (self, msg) {
- (Self::Keypad, FlowMsg::Text(s)) => {
- if s.is_empty() {
- Self::ConfirmEmpty.goto()
- } else {
- self.return_msg(FlowMsg::Text(s))
- }
- }
- (Self::Keypad, FlowMsg::Cancelled) => self.return_msg(FlowMsg::Cancelled),
- (Self::ConfirmEmpty, FlowMsg::Cancelled) => Self::Keypad.goto(),
- (Self::ConfirmEmpty, FlowMsg::Confirmed) => {
- self.return_msg(FlowMsg::Text(ShortString::new()))
- }
- _ => self.do_nothing(),
- }
- }
-}
-
-pub fn new_request_string(
- prompt: TString<'static>,
- max_len: usize,
- allow_empty: bool,
- prefill: Option<TString<'static>>,
-) -> Result<SwipeFlow, error::Error> {
- let content_confirm_empty = TextScreen::new(
- Paragraph::new(
- &theme::TEXT_REGULAR,
- TR::device_name__continue_with_empty_label,
- )
- .into_paragraphs()
- .with_placement(LinearPlacement::vertical()),
- )
- .with_header(Header::new(prompt))
- .with_action_bar(ActionBar::new_double(
- Button::with_icon(theme::ICON_CHEVRON_LEFT),
- Button::with_text(TR::buttons__confirm.into()),
- ))
- .with_page_limit(1)
- .map(|msg| match msg {
- TextScreenMsg::Cancelled => Some(FlowMsg::Cancelled),
- TextScreenMsg::Confirmed => Some(FlowMsg::Confirmed),
- _ => Some(FlowMsg::Cancelled),
- });
-
- let content_keypad =
- LabelKeyboard::new(prompt, max_len, allow_empty, prefill).map(|msg| match msg {
- LabelKeyboardMsg::Confirmed(s) => Some(FlowMsg::Text(s)),
- LabelKeyboardMsg::Cancelled => Some(FlowMsg::Cancelled),
- });
-
- let mut res = SwipeFlow::new(&RequestString::Keypad)?;
- res.add_page(&RequestString::Keypad, content_keypad)?
- .add_page(&RequestString::ConfirmEmpty, content_confirm_empty)?;
- Ok(res)
-}
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 5c3aec15..18a7e846 100644
--- a/core/embed/rust/src/ui/layout_eckhart/ui_firmware.rs
+++ b/core/embed/rust/src/ui/layout_eckhart/ui_firmware.rs
@@ -39,8 +39,8 @@ use super::{
ActionBar, Bip39Input, ConfirmHomescreen, DeviceMenuScreen, DurationInput, Header,
HeaderMsg, Hint, Homescreen, MnemonicKeyboard, PinKeyboard, ProgressScreen,
SelectWordCountScreen, SelectWordScreen, SetBrightnessScreen, ShortMenuVec, Slip39Input,
- TextScreen, TextScreenMsg, ValueInputScreen, VerticalMenu, VerticalMenuScreen,
- VerticalMenuScreenMsg,
+ StringKeyboard, TextScreen, TextScreenMsg, ValueInputScreen, VerticalMenu,
+ VerticalMenuScreen, VerticalMenuScreenMsg,
},
flow, fonts,
theme::{
@@ -975,8 +975,8 @@ impl FirmwareUI for UIEckhart {
allow_empty: bool,
prefill: Option<TString<'static>>,
) -> Result<impl LayoutMaybeTrace, Error> {
- let flow = flow::request_string::new_request_string(prompt, max_len, allow_empty, prefill)?;
- Ok(flow)
+ let layout = RootComponent::new(StringKeyboard::new(prompt, max_len, allow_empty, prefill));
+ Ok(layout)
}
fn select_menu(
diff --git a/core/src/apps/homescreen/device_menu.py b/core/src/apps/homescreen/device_menu.py
index cd732001..a4070790 100644
--- a/core/src/apps/homescreen/device_menu.py
+++ b/core/src/apps/homescreen/device_menu.py
@@ -175,7 +175,7 @@ async def handle_device_menu() -> None:
trezorui_api.request_string(
prompt=TR.device_name__enter,
max_len=storage_device.LABEL_MAXLENGTH,
- allow_empty=False,
+ allow_empty=True,
prefill=storage_device.get_label(),
),
"device_name",
Why this scored 20/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.