refactor(core/eckhart): fuse passphrase and label keyboards to one
What changed, and why it matters
This commit is a code cleanup that merges two very similar on-screen keyboards in the Trezor firmware into a single shared component. It does not add new features, change security behavior, or fix a bug. The passphrase keyboard and the label keyboard now share one generic 'StringKeyboard' implementation, with separate input logic for passphrases and labels. Test helpers were updated to recognize the new component name.
No security action required. Treat as a normal UI refactor; standard code review and regression testing are sufficient.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The refactor introduces a generic StringKeyboard
Changed components
core/embed/rust/src/ui/layout_eckhart/firmware/keyboard/string.rscore/embed/rust/src/ui/layout_eckhart/firmware/keyboard/passphrase.rscore/embed/rust/src/ui/layout_eckhart/firmware/keyboard/label.rscore/embed/rust/src/ui/layout_eckhart/firmware/keyboard/mod.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_passphrase.rscore/embed/rust/src/ui/layout_eckhart/ui_firmware.rspython/src/trezorlib/debuglink.pytests/click_tests/test_autolock.pytests/click_tests/test_passphrase_bde.pyInspect captured patch +619 / −812
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 7a38257c..29952c4d 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, StringKeyboard, StringKeyboardMsg, TextScreen,
- TextScreenMsg, ValueInput, ValueInputScreen, ValueInputScreenMsg,
+ SelectWordScreen, SetBrightnessScreen, StringInput, StringKeyboard, StringKeyboardMsg,
+ TextScreen, TextScreenMsg, ValueInput, ValueInputScreen, ValueInputScreenMsg,
};
impl ComponentMsgObj for PinKeyboard<'_> {
@@ -30,10 +30,10 @@ impl ComponentMsgObj for PinKeyboard<'_> {
}
}
-impl ComponentMsgObj for StringKeyboard {
+impl<I: StringInput> ComponentMsgObj for StringKeyboard<I> {
fn msg_try_into_obj(&self, msg: Self::Msg) -> Result<Obj, Error> {
match msg {
- StringKeyboardMsg::Confirmed => self.string().try_into(),
+ StringKeyboardMsg::Confirmed(content) => content.as_str().try_into(),
StringKeyboardMsg::Cancelled => Ok(CANCELLED.as_obj()),
}
}
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
new file mode 100644
index 00000000..747ec8ad
--- /dev/null
+++ b/core/embed/rust/src/ui/layout_eckhart/firmware/keyboard/label.rs
@@ -0,0 +1,346 @@
+use crate::{
+ strutil::TString,
+ ui::{
+ component::{
+ text::{
+ common::TextBox,
+ layout::{LayoutFit, LineBreaking},
+ TextStyle,
+ },
+ Component, Event, EventCtx, TextLayout,
+ },
+ event::TouchEvent,
+ geometry::{Alignment, Insets, Offset, Rect},
+ shape::{Bar, Renderer, Text},
+ util::long_line_content_with_ellipsis,
+ },
+};
+
+use super::super::{
+ constant::SCREEN,
+ keyboard::{
+ common::{
+ render_pending_marker, MultiTapKeyboard, KEYBOARD_INPUT_INSETS, KEYBOARD_INPUT_RADIUS,
+ SHOWN_INSETS,
+ },
+ keypad::{ButtonState, KeypadState},
+ },
+ theme, StringInput, StringInputMsg,
+};
+
+#[derive(PartialEq, Debug, Copy, Clone)]
+#[cfg_attr(feature = "ui_debug", derive(ufmt::derive::uDebug))]
+enum LabelDisplayStyle {
+ /// 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 struct LabelInput {
+ area: Rect,
+ textbox: TextBox,
+ display_style: LabelDisplayStyle,
+ shown_area: Rect,
+ max_len: usize,
+ multi_tap: MultiTapKeyboard,
+ allow_cancel: bool,
+ allow_empty: bool,
+}
+
+impl LabelInput {
+ const STYLE: TextStyle =
+ theme::TEXT_REGULAR.with_line_breaking(LineBreaking::BreakWordsNoHyphen);
+ const SHOWN_TOUCH_OUTSET: Insets = Insets::bottom(200);
+ const ONE_LINE_INSETS: Insets = Insets::new(
+ KEYBOARD_INPUT_INSETS.top,
+ 0,
+ KEYBOARD_INPUT_INSETS.bottom,
+ KEYBOARD_INPUT_INSETS.left,
+ );
+
+ pub fn new(
+ max_len: usize,
+ prefill: Option<TString<'static>>,
+ allow_cancel: bool,
+ allow_empty: bool,
+ ) -> 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: LabelDisplayStyle::OneLine,
+ shown_area: Rect::zero(),
+ max_len,
+ multi_tap: MultiTapKeyboard::new(),
+ allow_cancel,
+ allow_empty,
+ }
+ }
+
+ 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::STYLE)
+ .with_align(Alignment::Start)
+ .with_bounds(shown_area.inset(SHOWN_INSETS))
+ .fit_text(self.content())
+ {
+ shown_area = shown_area.outset(Insets::bottom(Self::STYLE.text_font.line_height()));
+ }
+
+ self.shown_area = shown_area;
+ }
+
+ fn render_complete<'s>(&self, target: &mut impl Renderer<'s>) {
+ // Make sure the entire label should be shown
+ debug_assert_eq!(self.display_style, LabelDisplayStyle::Complete);
+
+ Bar::new(self.shown_area)
+ .with_bg(theme::GREY_SUPER_DARK)
+ .with_radius(KEYBOARD_INPUT_RADIUS)
+ .render(target);
+
+ TextLayout::new(Self::STYLE)
+ .with_bounds(self.shown_area.inset(SHOWN_INSETS))
+ .with_align(Alignment::Start)
+ .render_text(self.content(), target, true);
+ }
+
+ fn render_one_line<'s>(&self, target: &mut impl Renderer<'s>) {
+ debug_assert_ne!(self.display_style, LabelDisplayStyle::Complete);
+
+ let area: Rect = self.area.inset(Self::ONE_LINE_INSETS);
+
+ // 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.content(),
+ "...",
+ Self::STYLE.text_font,
+ available_area_width,
+ );
+
+ let cursor = area.left_center().ofs(Offset::new(
+ 8,
+ Self::STYLE.text_font.visible_text_height("1") / 2,
+ ));
+
+ Text::new(cursor, &text_to_display, Self::STYLE.text_font)
+ .with_fg(Self::STYLE.text_color)
+ .render(target);
+
+ // Paint the pending marker.
+ if self.display_style == LabelDisplayStyle::OneLineWithMarker {
+ render_pending_marker(
+ target,
+ cursor,
+ &text_to_display,
+ Self::STYLE.text_font,
+ Self::STYLE.text_color,
+ );
+ }
+ }
+}
+
+impl StringInput for LabelInput {
+ fn on_key_click(&mut self, ctx: &mut EventCtx, idx: usize, text: TString<'static>) {
+ let edit = text.map(|c| self.multi_tap.click_key(ctx, idx, c));
+ self.textbox.apply(ctx, edit);
+ if text.len() == 1 {
+ // If the key has just one character, it is immediately applied
+ self.display_style = LabelDisplayStyle::OneLine;
+ } else {
+ // multi tap timer is running, the marker should be shown
+ self.display_style = LabelDisplayStyle::OneLineWithMarker;
+ }
+ }
+
+ fn on_erase(&mut self, ctx: &mut EventCtx, long_erase: bool) {
+ self.multi_tap.clear_pending_state(ctx);
+ if long_erase {
+ self.textbox.clear(ctx);
+ } else {
+ self.textbox.delete_last(ctx);
+ }
+ self.display_style = LabelDisplayStyle::OneLine;
+ }
+
+ fn get_keypad_state(&self) -> KeypadState {
+ if self.display_style == LabelDisplayStyle::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,
+ }
+ } else if self.is_full() {
+ // Disable all except of confirm, erase and the pending key if there is some
+ let override_key = self
+ .multi_tap
+ .pending_key()
+ .map(|k| (k, ButtonState::Enabled));
+
+ KeypadState {
+ back: ButtonState::Hidden,
+ erase: ButtonState::Enabled,
+ cancel: ButtonState::Hidden,
+ confirm: ButtonState::Enabled,
+ keys: ButtonState::Disabled,
+ override_key,
+ }
+ } else if self.is_empty() {
+ // Disable all except of confirm and erase buttons
+ KeypadState {
+ back: ButtonState::Hidden,
+ erase: ButtonState::Hidden,
+ cancel: if self.allow_cancel {
+ ButtonState::Enabled
+ } else {
+ 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,
+ }
+ }
+ }
+
+ fn on_page_change(&mut self, ctx: &mut EventCtx) {
+ // Clear the pending state.
+ self.multi_tap.clear_pending_state(ctx);
+ self.display_style = LabelDisplayStyle::OneLine;
+ }
+
+ fn content(&self) -> &str {
+ self.textbox.content()
+ }
+
+ fn is_full(&self) -> bool {
+ self.textbox.len() >= self.max_len
+ }
+
+ fn might_overlap_keypad(&self) -> bool {
+ self.display_style == LabelDisplayStyle::Complete
+ }
+}
+
+impl Component for LabelInput {
+ 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.is_empty() {
+ return None;
+ }
+
+ // Extend the passphrase area downward to allow touch input without the finger
+ // covering the passphrase
+ let extended_shown_area = self
+ .shown_area
+ .outset(Self::SHOWN_TOUCH_OUTSET)
+ .clamp(SCREEN);
+
+ match event {
+ Event::Timer(_) if self.multi_tap.timeout_event(event) => {
+ self.multi_tap.clear_pending_state(ctx);
+ self.display_style = LabelDisplayStyle::OneLine;
+ // Return update message to disable keypad when the passphrase reached the max
+ // length
+ if self.is_full() {
+ return Some(StringInputMsg::UpdateKeypad);
+ }
+ return None;
+ }
+ // Return update message to to disable keypad if the touch is detected inside the
+ // touchable area
+ Event::Touch(TouchEvent::TouchStart(pos)) if self.area.contains(pos) => {
+ self.multi_tap.clear_pending_state(ctx);
+ // Show the entire label on the touch start
+ self.display_style = LabelDisplayStyle::Complete;
+ self.update_shown_area();
+ return Some(StringInputMsg::UpdateKeypad);
+ }
+ // Return update message to to re-enable keypad if the touch end is detected inside the
+ // touchable area
+ Event::Touch(TouchEvent::TouchEnd(pos))
+ if extended_shown_area.contains(pos)
+ && self.display_style == LabelDisplayStyle::Complete =>
+ {
+ self.multi_tap.clear_pending_state(ctx);
+ self.display_style = LabelDisplayStyle::OneLine;
+ return Some(StringInputMsg::UpdateKeypad);
+ }
+ // Return update message to to re-enable keypad if the touch moves out of the visible
+ // area
+ Event::Touch(TouchEvent::TouchMove(pos))
+ if !extended_shown_area.contains(pos)
+ && self.display_style == LabelDisplayStyle::Complete =>
+ {
+ self.multi_tap.clear_pending_state(ctx);
+ self.display_style = LabelDisplayStyle::OneLine;
+ return Some(StringInputMsg::UpdateKeypad);
+ }
+ _ => {}
+ };
+ None
+ }
+
+ fn render<'s>(&self, target: &mut impl Renderer<'s>) {
+ // Don't render if the input is empty
+ if self.is_empty() {
+ return;
+ }
+
+ match self.display_style {
+ LabelDisplayStyle::Complete => self.render_complete(target),
+ _ => self.render_one_line(target),
+ }
+ }
+}
+
+#[cfg(feature = "ui_debug")]
+impl crate::trace::Trace for LabelInput {
+ fn trace(&self, t: &mut dyn crate::trace::Tracer) {
+ t.component("LabelInput");
+ t.string("content", self.content().into());
+ let display_style = uformat!("{:?}", self.display_style);
+ t.string("display_style", display_style.as_str().into());
+ t.bool("allow_empty", self.allow_empty);
+ t.bool("allow_cancel", self.allow_cancel);
+ }
+}
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 e298c5b8..1d651969 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,4 +1,5 @@
pub mod bip39;
+pub mod label;
pub mod mnemonic;
pub mod passphrase;
pub mod pin;
diff --git a/core/embed/rust/src/ui/layout_eckhart/firmware/keyboard/passphrase.rs b/core/embed/rust/src/ui/layout_eckhart/firmware/keyboard/passphrase.rs
index 3a28657f..c48056aa 100644
--- a/core/embed/rust/src/ui/layout_eckhart/firmware/keyboard/passphrase.rs
+++ b/core/embed/rust/src/ui/layout_eckhart/firmware/keyboard/passphrase.rs
@@ -1,427 +1,72 @@
use crate::{
- strutil::{ShortString, TString},
+ strutil::TString,
time::Duration,
ui::{
component::{
- swipe_detect::SwipeConfig,
text::{
common::TextBox,
layout::{LayoutFit, LineBreaking},
TextStyle,
},
- Component, Event, EventCtx, Label, Swipe, TextLayout, Timer,
+ Component, Event, EventCtx, TextLayout, Timer,
},
display::Icon,
event::TouchEvent,
- flow::Swipable,
- geometry::{Alignment, Alignment2D, Direction, Insets, Offset, Rect},
+ geometry::{Alignment, Alignment2D, Insets, Offset, Rect},
shape::{Bar, Renderer, Text, ToifImage},
- util::{DisplayStyle, Pager},
+ util::DisplayStyle,
},
};
use super::super::{
- super::component::{Button, ButtonContent, ButtonMsg, ButtonStyleSheet},
constant::SCREEN,
keyboard::{
common::{
- render_pending_marker, KeyboardLayout, MultiTapKeyboard, FADING_ICON_COLORS,
- FADING_ICON_COUNT, INPUT_TOUCH_HEIGHT, KEYBOARD_INPUT_INSETS, KEYBOARD_INPUT_RADIUS,
- KEYBOARD_PROMPT_INSETS, KEYPAD_VISIBLE_HEIGHT, SHOWN_INSETS,
+ render_pending_marker, MultiTapKeyboard, FADING_ICON_COLORS, FADING_ICON_COUNT,
+ KEYBOARD_INPUT_INSETS, KEYBOARD_INPUT_RADIUS, SHOWN_INSETS,
},
- keypad::{ButtonState, Keypad, KeypadButton, KeypadMsg, KeypadState},
+ keypad::{ButtonState, KeypadState},
},
- theme,
+ theme, StringInput, StringInputMsg,
};
-pub enum PassphraseKeyboardMsg {
- Confirmed(ShortString),
- Cancelled,
-}
-
-pub struct PassphraseKeyboard {
- page_swipe: Swipe,
- input: PassphraseInput,
- input_prompt: Label<'static>,
- keypad: Keypad,
- next_btn: Button,
- active_layout: KeyboardLayout,
- swipe_config: SwipeConfig,
- multi_tap: MultiTapKeyboard,
- max_len: usize,
-}
-
-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 MAX_SHOWN_LEN: usize = 13; // max number of icons per line
-const LAST_DIGIT_TIMEOUT: Duration = Duration::from_secs(1);
-
-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 PassphraseKeyboard {
- pub fn new(prompt: TString<'static>, max_len: usize) -> 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);
-
- Self {
- page_swipe: Swipe::horizontal(),
- input: PassphraseInput::new(max_len),
- 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,
- }
- }
-
- 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::LastOnly;
- // the character has been added, show it for a bit and then hide it
- self.input.last_char_timer.start(ctx, LAST_DIGIT_TIMEOUT);
- }
- // 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 passphrase 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::Shown => {
- // 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.passphrase().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: ButtonState::Enabled,
- 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 passphrase(&self) -> &str {
- self.input.textbox.content()
- }
-}
-
-impl Component for PassphraseKeyboard {
- type Msg = PassphraseKeyboardMsg;
-
- 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_PROMPT_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.last_char_timer.start(ctx, LAST_DIGIT_TIMEOUT);
- self.input.display_style = DisplayStyle::LastOnly;
- // Disable keypad when the passphrase reached the max length
- if self.passphrase().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(PassphraseInputMsg::TouchStart) => {
- self.multi_tap.clear_pending_state(ctx);
- // Disable keypad.
- self.update_keypad_state(ctx);
- return None;
- }
- Some(PassphraseInputMsg::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 and the last
- // digit timer should be started
- self.input.display_style = DisplayStyle::LastOnly;
- self.input.last_char_timer.start(ctx, LAST_DIGIT_TIMEOUT);
- } else {
- // multi tap timer is runnig, the last digit timer should be stopped
- self.input.last_char_timer.stop();
- self.input.display_style = DisplayStyle::LastWithMarker;
- }
- 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::Hidden;
- 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::Hidden;
- self.update_keypad_state(ctx);
- return None;
- }
- Some(KeypadMsg::Cancel) => {
- return Some(PassphraseKeyboardMsg::Cancelled);
- }
- Some(KeypadMsg::Confirm) => {
- return Some(PassphraseKeyboardMsg::Confirmed(unwrap!(
- ShortString::try_from(self.passphrase())
- )));
- }
- _ => {}
- }
-
- None
- }
-
- fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
- let empty = self.passphrase().is_empty();
-
- // Render prompt when the pin is empty
- if empty {
- self.input_prompt.render(target);
- }
-
- // When the entire passphrase is shown, the input area might overlap the keypad
- // so it has to be render later
- match self.input.display_style {
- DisplayStyle::Shown => {
- 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 PassphraseInputMsg {
- TouchStart,
- TouchEnd,
-}
-
-struct PassphraseInput {
+pub struct PassphraseInput {
area: Rect,
textbox: TextBox,
display_style: DisplayStyle,
last_char_timer: Timer,
shown_area: Rect,
+ max_len: usize,
+ multi_tap: MultiTapKeyboard,
+ allow_cancel: bool,
+ allow_empty: bool,
}
impl PassphraseInput {
const TWITCH: i16 = 4;
const 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;
+ const MAX_SHOWN_LEN: usize = 13; // max number of icons per line
+ const LAST_DIGIT_TIMEOUT: Duration = Duration::from_secs(1);
- fn new(max_len: usize) -> Self {
+ pub fn new(max_len: usize, allow_cancel: bool, allow_empty: bool) -> Self {
Self {
area: Rect::zero(),
textbox: TextBox::empty(max_len),
display_style: DisplayStyle::Hidden,
last_char_timer: Timer::new(),
shown_area: Rect::zero(),
+ max_len,
+ multi_tap: MultiTapKeyboard::new(),
+ allow_cancel,
+ allow_empty,
}
}
- fn passphrase(&self) -> &str {
- self.textbox.content()
- }
-
fn update_shown_area(&mut self) {
// The area where the passphrase is shown
let mut shown_area = Rect::from_top_left_and_size(
@@ -434,7 +79,7 @@ impl PassphraseInput {
while let LayoutFit::OutOfBounds { .. } = TextLayout::new(Self::STYLE)
.with_align(Alignment::Start)
.with_bounds(shown_area.inset(SHOWN_INSETS))
- .fit_text(self.passphrase())
+ .fit_text(self.content())
{
shown_area = shown_area.outset(Insets::bottom(Self::STYLE.text_font.line_height()));
}
@@ -454,14 +99,14 @@ impl PassphraseInput {
TextLayout::new(Self::STYLE)
.with_bounds(self.shown_area.inset(SHOWN_INSETS))
.with_align(Alignment::Start)
- .render_text(self.passphrase(), target, true);
+ .render_text(self.content(), target, true);
}
fn render_hidden<'s>(&self, target: &mut impl Renderer<'s>) {
debug_assert_ne!(self.display_style, DisplayStyle::Shown);
let hidden_area: Rect = self.area.inset(KEYBOARD_INPUT_INSETS);
- let pp_len = self.passphrase().len();
+ let pp_len = self.content().len();
let last_char = self.display_style != DisplayStyle::Hidden;
let mut cursor = hidden_area.left_center().ofs(Offset::x(12));
@@ -471,7 +116,7 @@ impl PassphraseInput {
return;
}
// Number of visible icons + characters
- let visible_len = pp_len.min(MAX_SHOWN_LEN);
+ let visible_len = pp_len.min(Self::MAX_SHOWN_LEN);
// Number of visible icons
let visible_icons = visible_len - last_char as usize;
@@ -507,7 +152,7 @@ impl PassphraseInput {
if last_char {
// This should not fail because pp_len > 0
- let last = &self.passphrase()[(pp_len - 1)..pp_len];
+ let last = &self.content()[(pp_len - 1)..pp_len];
// Adapt x and y positions for the character
cursor.y += Self::STYLE.text_font.visible_text_height("1") / 2;
@@ -532,8 +177,113 @@ impl PassphraseInput {
}
}
+impl StringInput for PassphraseInput {
+ fn on_key_click(&mut self, ctx: &mut EventCtx, idx: usize, text: TString<'static>) {
+ let edit = text.map(|c| self.multi_tap.click_key(ctx, idx, c));
+ self.textbox.apply(ctx, edit);
+ if text.len() == 1 {
+ // If the key has just one character, it is immediately applied and the last
+ // digit timer should be started
+ self.display_style = DisplayStyle::LastOnly;
+ self.last_char_timer.start(ctx, Self::LAST_DIGIT_TIMEOUT);
+ } else {
+ // multi tap timer is runnig, the last digit timer should be stopped
+ self.last_char_timer.stop();
+ self.display_style = DisplayStyle::LastWithMarker;
+ }
+ }
+
+ fn on_erase(&mut self, ctx: &mut EventCtx, long_erase: bool) {
+ self.multi_tap.clear_pending_state(ctx);
+ if long_erase {
+ self.textbox.clear(ctx);
+ } else {
+ self.textbox.delete_last(ctx);
+ }
+ self.display_style = DisplayStyle::Hidden;
+ }
+
+ fn get_keypad_state(&self) -> KeypadState {
+ if self.display_style == DisplayStyle::Shown {
+ // Disable the entire active keypad
+ KeypadState {
+ back: ButtonState::Hidden,
+ erase: ButtonState::Disabled,
+ cancel: ButtonState::Hidden,
+ confirm: ButtonState::Disabled,
+ keys: ButtonState::Disabled,
+ override_key: None,
+ }
+ } else if self.is_full() {
+ // Disable all except of confirm, erase and the pending key if there is some
+ let override_key = self
+ .multi_tap
+ .pending_key()
+ .map(|k| (k, ButtonState::Enabled));
+
+ KeypadState {
+ back: ButtonState::Hidden,
+ erase: ButtonState::Enabled,
+ cancel: ButtonState::Hidden,
+ confirm: ButtonState::Enabled,
+ keys: ButtonState::Disabled,
+ override_key,
+ }
+ } else if self.is_empty() {
+ // Disable all except of confirm and erase buttons
+ KeypadState {
+ back: ButtonState::Hidden,
+ erase: ButtonState::Hidden,
+ cancel: if self.allow_cancel {
+ ButtonState::Enabled
+ } else {
+ 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,
+ }
+ }
+ }
+
+ fn on_page_change(&mut self, ctx: &mut EventCtx) {
+ if self.multi_tap.pending_key().is_some() {
+ // Clear the pending state.
+ self.multi_tap.clear_pending_state(ctx);
+ self.display_style = DisplayStyle::LastOnly;
+ // the character has been added, show it for a bit and then hide it
+ self.last_char_timer.start(ctx, Self::LAST_DIGIT_TIMEOUT);
+ }
+ }
+
+ fn content(&self) -> &str {
+ self.textbox.content()
+ }
+
+ fn is_full(&self) -> bool {
+ self.textbox.len() >= self.max_len
+ }
+
+ fn might_overlap_keypad(&self) -> bool {
+ self.display_style == DisplayStyle::Shown
+ }
+}
+
impl Component for PassphraseInput {
- type Msg = PassphraseInputMsg;
+ type Msg = StringInputMsg;
fn place(&mut self, bounds: Rect) -> Rect {
self.area = bounds;
@@ -542,7 +292,7 @@ impl Component for PassphraseInput {
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() {
+ if self.is_empty() {
return None;
}
@@ -554,30 +304,43 @@ impl Component for PassphraseInput {
.clamp(SCREEN);
match event {
+ Event::Timer(_) if self.multi_tap.timeout_event(event) => {
+ self.multi_tap.clear_pending_state(ctx);
+ self.last_char_timer.start(ctx, Self::LAST_DIGIT_TIMEOUT);
+ self.display_style = DisplayStyle::LastOnly;
+ // Disable keypad when the passphrase reached the max length
+ if self.is_full() {
+ return Some(StringInputMsg::UpdateKeypad);
+ }
+ return None;
+ }
// Return touch start if the touch is detected inside the touchable area
Event::Touch(TouchEvent::TouchStart(pos)) if self.area.contains(pos) => {
+ self.multi_tap.clear_pending_state(ctx);
// Stop the last char timer
self.last_char_timer.stop();
// Show the entire passphrase on the touch start
self.display_style = DisplayStyle::Shown;
self.update_shown_area();
- return Some(PassphraseInputMsg::TouchStart);
+ return Some(StringInputMsg::UpdateKeypad);
}
// 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::Shown =>
{
+ self.multi_tap.clear_pending_state(ctx);
self.display_style = DisplayStyle::Hidden;
- return Some(PassphraseInputMsg::TouchEnd);
+ return Some(StringInputMsg::UpdateKeypad);
}
// 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::Shown =>
{
+ self.multi_tap.clear_pending_state(ctx);
self.display_style = DisplayStyle::Hidden;
- return Some(PassphraseInputMsg::TouchEnd);
+ return Some(StringInputMsg::UpdateKeypad);
}
// Timeout for showing the last char.
Event::Timer(_) if self.last_char_timer.expire(event) => {
@@ -589,35 +352,27 @@ impl Component for PassphraseInput {
None
}
- fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
- if !self.passphrase().is_empty() {
- match self.display_style {
- DisplayStyle::Shown => self.render_shown(target),
- _ => self.render_hidden(target),
- }
+ fn render<'s>(&self, target: &mut impl Renderer<'s>) {
+ // Don't render if the input is empty
+ if self.is_empty() {
+ return;
}
- }
-}
-#[cfg(feature = "micropython")]
-impl Swipable for PassphraseKeyboard {
- fn get_swipe_config(&self) -> SwipeConfig {
- self.swipe_config
- }
-
- fn get_pager(&self) -> Pager {
- Pager::single_page()
+ match self.display_style {
+ DisplayStyle::Shown => self.render_shown(target),
+ _ => self.render_hidden(target),
+ }
}
}
#[cfg(feature = "ui_debug")]
-impl crate::trace::Trace for PassphraseKeyboard {
+impl crate::trace::Trace for PassphraseInput {
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("PassphraseKeyboard");
- t.string("passphrase", self.passphrase().into());
+ t.component("PassphraseInput");
+ t.string("content", self.content().into());
+ let display_style = uformat!("{:?}", self.display_style);
t.string("display_style", display_style.as_str().into());
- t.string("active_layout", active_layout.as_str().into());
+ t.bool("allow_empty", self.allow_empty);
+ t.bool("allow_cancel", self.allow_cancel);
}
}
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
index 153db27f..f183b1c8 100644
--- a/core/embed/rust/src/ui/layout_eckhart/firmware/keyboard/string.rs
+++ b/core/embed/rust/src/ui/layout_eckhart/firmware/keyboard/string.rs
@@ -1,21 +1,11 @@
use crate::{
- strutil::TString,
+ 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,
+ component::{swipe_detect::SwipeConfig, Component, Event, EventCtx, Label, Swipe},
flow::Swipable,
- geometry::{Alignment, Direction, Insets, Offset, Rect},
- shape::{Bar, Renderer, Text},
- util::{long_line_content_with_ellipsis, Pager},
+ geometry::{Alignment, Direction, Insets, Rect},
+ shape::Renderer,
+ util::Pager,
},
};
@@ -24,41 +14,26 @@ use super::super::{
constant::SCREEN,
keyboard::{
common::{
- render_pending_marker, KeyboardLayout, MultiTapKeyboard, INPUT_TOUCH_HEIGHT,
- KEYBOARD_INPUT_INSETS, KEYBOARD_INPUT_RADIUS, KEYPAD_VISIBLE_HEIGHT,
+ KeyboardLayout, INPUT_TOUCH_HEIGHT, KEYBOARD_PROMPT_INSETS, KEYPAD_VISIBLE_HEIGHT,
},
- keypad::{ButtonState, Keypad, KeypadButton, KeypadMsg, KeypadState},
+ keypad::{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,
+ Confirmed(ShortString),
Cancelled,
}
-pub struct StringKeyboard {
+pub struct StringKeyboard<I: StringInput> {
page_swipe: Swipe,
- input: StringInput,
+ input: I,
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;
@@ -76,13 +51,8 @@ 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 {
+impl<I: StringInput> StringKeyboard<I> {
+ pub fn new(prompt: TString<'static>, input: I) -> Self {
let active_layout = KeyboardLayout::LettersLower;
let layout: &[&str; KEY_COUNT] = &KEYBOARD[active_layout as usize];
let keypad_content: [ButtonContent; KEY_COUNT] =
@@ -94,22 +64,15 @@ impl StringKeyboard {
.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,
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,
}
}
@@ -144,14 +107,11 @@ impl StringKeyboard {
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);
+ // Update input state.
+ self.input.on_page_change(ctx);
}
fn replace_keys_contents(&mut self) {
@@ -168,80 +128,14 @@ impl StringKeyboard {
}
}
- /// Update the keypad state based on the current string and input state
+ /// Update the keypad state based on the current passphrase 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()
+ self.keypad.set_state(self.input.get_keypad_state(), ctx);
}
}
-impl Component for StringKeyboard {
+impl<I: StringInput> Component for StringKeyboard<I> {
type Msg = StringKeyboardMsg;
fn place(&mut self, bounds: Rect) -> Rect {
@@ -263,7 +157,7 @@ impl Component for StringKeyboard {
self.input.place(input_area);
self.input_prompt
- .place(top_area.inset(KEYBOARD_INPUT_INSETS));
+ .place(top_area.inset(KEYBOARD_PROMPT_INSETS));
self.keypad.place(keypad_area);
self.next_btn.place(next_btn_area);
@@ -271,84 +165,47 @@ impl Component for StringKeyboard {
}
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;
- }
-
- _ => {}
+ if let Event::Attach(_) = event {
+ 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;
- }
- _ => {}
+ if let Some(StringInputMsg::UpdateKeypad) = self.input.event(ctx, event) {
+ 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(swipe @ (Direction::Left | Direction::Right)) =
+ self.page_swipe.event(ctx, event)
+ {
+ // 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);
+ return None;
}
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.input.on_key_click(ctx, idx, text);
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.input.on_erase(ctx, false);
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.input.on_erase(ctx, true);
self.update_keypad_state(ctx);
return None;
}
@@ -356,7 +213,9 @@ impl Component for StringKeyboard {
return Some(StringKeyboardMsg::Cancelled);
}
Some(KeypadMsg::Confirm) => {
- return Some(StringKeyboardMsg::Confirmed);
+ return Some(StringKeyboardMsg::Confirmed(unwrap!(
+ ShortString::try_from(self.input.content())
+ )));
}
_ => {}
}
@@ -365,240 +224,75 @@ impl Component for StringKeyboard {
}
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 {
+ if self.input.is_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 => {
+ // When the input area might overlap the keypad, it has to be rendered as the
+ // second
+ if self.input.might_overlap_keypad() {
+ self.keypad.render(target);
+ self.input.render(target);
+ } else {
+ // When the next button is pressed, it overlaps 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);
- 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);
+#[cfg(feature = "micropython")]
+impl<I: StringInput> Swipable for StringKeyboard<I> {
+ fn get_swipe_config(&self) -> SwipeConfig {
+ self.swipe_config
}
- 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,
- );
- }
+ fn get_pager(&self) -> Pager {
+ Pager::single_page()
}
}
-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),
- }
- }
- }
+#[derive(PartialEq, Debug, Copy, Clone)]
+#[cfg_attr(feature = "ui_debug", derive(ufmt::derive::uDebug))]
+pub enum StringInputMsg {
+ UpdateKeypad,
}
-#[cfg(feature = "micropython")]
-impl Swipable for StringKeyboard {
- fn get_swipe_config(&self) -> SwipeConfig {
- self.swipe_config
+pub trait StringInput: Component<Msg = StringInputMsg> {
+ // Actions
+ fn on_page_change(&mut self, ctx: &mut EventCtx);
+ fn get_keypad_state(&self) -> KeypadState;
+ fn on_key_click(&mut self, ctx: &mut EventCtx, idx: usize, text: TString<'static>);
+ fn on_erase(&mut self, ctx: &mut EventCtx, long_erase: bool);
+
+ /// Basic input info.
+ fn content(&self) -> &str;
+ fn is_empty(&self) -> bool {
+ self.content().is_empty()
}
+ fn is_full(&self) -> bool;
- fn get_pager(&self) -> Pager {
- Pager::single_page()
- }
+ // The information needed for the component render order
+ fn might_overlap_keypad(&self) -> bool;
}
#[cfg(feature = "ui_debug")]
-impl crate::trace::Trace for StringKeyboard {
+impl<I> crate::trace::Trace for StringKeyboard<I>
+where
+ I: StringInput + crate::trace::Trace,
+{
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());
+ t.child("input", &self.input);
}
}
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 aa6d614b..3432db83 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,12 @@ pub use hold_to_confirm::HoldToConfirmAnim;
pub use homescreen::{check_homescreen_format, Homescreen, HomescreenMsg};
pub use keyboard::{
bip39::Bip39Input,
+ label::LabelInput,
mnemonic::{MnemonicInput, MnemonicKeyboard, MnemonicKeyboardMsg},
- passphrase::{PassphraseKeyboard, PassphraseKeyboardMsg},
+ passphrase::PassphraseInput,
pin::{PinKeyboard, PinKeyboardMsg},
slip39::Slip39Input,
- string::{StringKeyboard, StringKeyboardMsg},
+ string::{StringInput, StringInputMsg, StringKeyboard, StringKeyboardMsg},
word_count_screen::{SelectWordCountMsg, SelectWordCountScreen},
};
pub use progress_screen::ProgressScreen;
diff --git a/core/embed/rust/src/ui/layout_eckhart/flow/request_passphrase.rs b/core/embed/rust/src/ui/layout_eckhart/flow/request_passphrase.rs
index 22d4f1a9..0fd40fb1 100644
--- a/core/embed/rust/src/ui/layout_eckhart/flow/request_passphrase.rs
+++ b/core/embed/rust/src/ui/layout_eckhart/flow/request_passphrase.rs
@@ -18,7 +18,8 @@ use crate::{
use super::super::{
component::Button,
firmware::{
- ActionBar, Header, PassphraseKeyboard, PassphraseKeyboardMsg, TextScreen, TextScreenMsg,
+ ActionBar, Header, PassphraseInput, StringKeyboard, StringKeyboardMsg, TextScreen,
+ TextScreenMsg,
},
theme,
};
@@ -80,9 +81,10 @@ pub fn new_request_passphrase(
_ => Some(FlowMsg::Cancelled),
});
- let content_keypad = PassphraseKeyboard::new(prompt, max_len).map(|msg| match msg {
- PassphraseKeyboardMsg::Confirmed(s) => Some(FlowMsg::Text(s)),
- PassphraseKeyboardMsg::Cancelled => Some(FlowMsg::Cancelled),
+ let input = PassphraseInput::new(max_len, false, true);
+ let content_keypad = StringKeyboard::new(prompt, input).map(|msg| match msg {
+ StringKeyboardMsg::Confirmed(s) => Some(FlowMsg::Text(s)),
+ StringKeyboardMsg::Cancelled => Some(FlowMsg::Cancelled),
});
let mut res = SwipeFlow::new(&RequestPassphrase::Keypad)?;
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 0febfe38..20f41625 100644
--- a/core/embed/rust/src/ui/layout_eckhart/ui_firmware.rs
+++ b/core/embed/rust/src/ui/layout_eckhart/ui_firmware.rs
@@ -38,7 +38,7 @@ use super::{
component::Button,
firmware::{
ActionBar, Bip39Input, ConfirmHomescreen, DeviceMenuScreen, DurationInput, Header,
- HeaderMsg, Hint, Homescreen, MnemonicKeyboard, PinKeyboard, ProgressScreen,
+ HeaderMsg, Hint, Homescreen, LabelInput, MnemonicKeyboard, PinKeyboard, ProgressScreen,
SelectWordCountScreen, SelectWordScreen, SetBrightnessScreen, ShortMenuVec, Slip39Input,
StringKeyboard, TextScreen, TextScreenMsg, ValueInputScreen, VerticalMenu,
VerticalMenuScreen, VerticalMenuScreenMsg,
@@ -982,7 +982,8 @@ impl FirmwareUI for UIEckhart {
allow_empty: bool,
prefill: Option<TString<'static>>,
) -> Result<impl LayoutMaybeTrace, Error> {
- let layout = RootComponent::new(StringKeyboard::new(prompt, max_len, allow_empty, prefill));
+ let input = LabelInput::new(max_len, prefill, true, allow_empty);
+ let layout = RootComponent::new(StringKeyboard::new(prompt, input));
Ok(layout)
}
diff --git a/python/src/trezorlib/debuglink.py b/python/src/trezorlib/debuglink.py
index 24a81db2..974b0fdc 100644
--- a/python/src/trezorlib/debuglink.py
+++ b/python/src/trezorlib/debuglink.py
@@ -385,6 +385,7 @@ class LayoutContent(UnstructuredJSONReader):
assert (
"PinKeyboard" in self.all_components()
or "PassphraseKeyboard" in self.all_components()
+ or "StringKeyboard" in self.all_components()
)
style_str = self.find_unique_value_by_key(
"display_style", default="", only_type=str
@@ -396,8 +397,14 @@ class LayoutContent(UnstructuredJSONReader):
def passphrase(self) -> str:
"""Get passphrase from the layout."""
- assert "PassphraseKeyboard" in self.all_components()
- return self.find_unique_value_by_key("passphrase", default="", only_type=str)
+ if "StringKeyboard" in self.all_components():
+ return self.find_unique_value_by_key("content", default="", only_type=str)
+ elif "PassphraseKeyboard" in self.all_components():
+ return self.find_unique_value_by_key(
+ "passphrase", default="", only_type=str
+ )
+ else:
+ raise ValueError("No passphrase component in layout")
def page_count(self) -> int:
"""Get number of pages for the layout."""
diff --git a/tests/click_tests/test_autolock.py b/tests/click_tests/test_autolock.py
index 05114190..ef738ea7 100644
--- a/tests/click_tests/test_autolock.py
+++ b/tests/click_tests/test_autolock.py
@@ -233,7 +233,7 @@ def test_autolock_passphrase_keyboard(device_handler: "BackgroundDeviceHandler")
debug = device_handler.debuglink()
device_handler.get_session(passphrase=PASSPHRASE_ON_DEVICE) # type: ignore
- debug.synchronize_at("PassphraseKeyboard")
+ debug.synchronize_at(["PassphraseKeyboard", "StringKeyboard"])
if debug.layout_type is LayoutType.Caesar:
# Going into the selected character category
@@ -279,7 +279,7 @@ def test_autolock_interrupts_passphrase(device_handler: "BackgroundDeviceHandler
# get address (derive_seed)
device_handler.get_session(passphrase=PASSPHRASE_ON_DEVICE)
- debug.synchronize_at("PassphraseKeyboard")
+ debug.synchronize_at(["PassphraseKeyboard", "StringKeyboard"])
if debug.layout_type is LayoutType.Caesar:
# Going into the selected character category
diff --git a/tests/click_tests/test_passphrase_bde.py b/tests/click_tests/test_passphrase_bde.py
index 0dbda6c5..d477f15f 100644
--- a/tests/click_tests/test_passphrase_bde.py
+++ b/tests/click_tests/test_passphrase_bde.py
@@ -92,7 +92,7 @@ def prepare_passphrase_dialogue(
) -> Generator["DebugLink", None, None]:
debug = device_handler.debuglink()
device_handler.get_session(passphrase=PASSPHRASE_ON_DEVICE)
- debug.synchronize_at("PassphraseKeyboard")
+ debug.synchronize_at(["PassphraseKeyboard", "StringKeyboard"])
# Resetting the category as it could have been changed by previous tests
global KEYBOARD_CATEGORY
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.