11use crate :: {
2- strutil:: TString ,
2+ strutil:: { ShortString , TString } ,
3+ time:: Duration ,
34 ui:: {
45 component:: {
5- base:: ComponentExt , text:: common:: TextBox , Child , Component , Event , EventCtx , Never ,
6- Paginate ,
6+ base:: ComponentExt ,
7+ text:: {
8+ common:: TextBox ,
9+ layout:: { LayoutFit , LineBreaking } ,
10+ TextStyle ,
11+ } ,
12+ Child , Component , Event , EventCtx , Never , Pad , Paginate , TextLayout , Timer ,
713 } ,
814 display,
9- geometry:: { Grid , Offset , Rect } ,
10- shape:: { self , Renderer } ,
11- util:: { long_line_content_with_ellipsis, Pager } ,
15+ event:: TouchEvent ,
16+ geometry:: { Alignment , Grid , Insets , Offset , Rect } ,
17+ shape:: { Bar , Renderer , Text } ,
18+ util:: { DisplayStyle , Pager } ,
1219 } ,
1320} ;
1421
1522use super :: super :: {
23+ super :: constant:: SCREEN ,
1624 button:: { Button , ButtonContent , ButtonMsg } ,
1725 keyboard:: common:: { render_pending_marker, MultiTapKeyboard } ,
1826 swipe:: { Swipe , SwipeDirection } ,
@@ -134,9 +142,15 @@ impl PassphraseKeyboard {
134142 } ;
135143
136144 self . scrollbar . set_pager ( pager) ;
137- // Clear the pending state.
138- self . input
139- . mutate ( ctx, |ctx, i| i. multi_tap . clear_pending_state ( ctx) ) ;
145+ // Clear the pending state. If there was a pending character, it has been
146+ // committed; show it briefly then hide.
147+ self . input . mutate ( ctx, |ctx, i| {
148+ if i. multi_tap . pending_key ( ) . is_some ( ) {
149+ i. multi_tap . clear_pending_state ( ctx) ;
150+ i. display_style = DisplayStyle :: LastOnly ;
151+ i. last_char_timer . start ( ctx, Input :: LAST_DIGIT_TIMEOUT ) ;
152+ }
153+ } ) ;
140154 // Update buttons.
141155 self . replace_button_content ( ctx, pager. current ( ) . into ( ) ) ;
142156 // Reset backlight to normal level on next paint.
@@ -255,9 +269,12 @@ impl Component for PassphraseKeyboard {
255269 }
256270
257271 fn event ( & mut self , ctx : & mut EventCtx , event : Event ) -> Option < Self :: Msg > {
272+ // Handle multi-tap timeout: commit the pending character and show it briefly
258273 let multitap_timeout = self . input . mutate ( ctx, |ctx, i| {
259274 if i. multi_tap . timeout_event ( event) {
260275 i. multi_tap . clear_pending_state ( ctx) ;
276+ i. display_style = DisplayStyle :: LastOnly ;
277+ i. last_char_timer . start ( ctx, Input :: LAST_DIGIT_TIMEOUT ) ;
261278 true
262279 } else {
263280 false
@@ -266,6 +283,15 @@ impl Component for PassphraseKeyboard {
266283 if multitap_timeout {
267284 return None ;
268285 }
286+
287+ // Handle input touch events (reveal/hide passphrase)
288+ self . input . event ( ctx, event) ;
289+
290+ // When passphrase is shown in full, disable all keypad interaction
291+ if self . input . inner ( ) . display_style == DisplayStyle :: Shown {
292+ return None ;
293+ }
294+
269295 if let Some ( swipe) = self . page_swipe . event ( ctx, event) {
270296 // We have detected a horizontal swipe. Change the keyboard page.
271297 self . on_page_swipe ( ctx, swipe) ;
@@ -286,6 +312,7 @@ impl Component for PassphraseKeyboard {
286312 self . input . mutate ( ctx, |ctx, i| {
287313 i. multi_tap . clear_pending_state ( ctx) ;
288314 i. textbox . delete_last ( ctx) ;
315+ i. display_style = DisplayStyle :: Hidden ;
289316 } ) ;
290317 self . after_edit ( ctx) ;
291318 None
@@ -295,6 +322,7 @@ impl Component for PassphraseKeyboard {
295322 self . input . mutate ( ctx, |ctx, i| {
296323 i. multi_tap . clear_pending_state ( ctx) ;
297324 i. textbox . clear ( ctx) ;
325+ i. display_style = DisplayStyle :: Hidden ;
298326 } ) ;
299327 self . after_edit ( ctx) ;
300328 return None ;
@@ -319,6 +347,15 @@ impl Component for PassphraseKeyboard {
319347 self . input . mutate ( ctx, |ctx, i| {
320348 let edit = text. map ( |c| i. multi_tap . click_key ( ctx, key, c) ) ;
321349 i. textbox . apply ( ctx, edit) ;
350+ if text. len ( ) == 1 {
351+ // Single-char key: immediately applied, show last char briefly
352+ i. display_style = DisplayStyle :: LastOnly ;
353+ i. last_char_timer . start ( ctx, Input :: LAST_DIGIT_TIMEOUT ) ;
354+ } else {
355+ // Multi-tap key: pending state, show char with marker
356+ i. last_char_timer . stop ( ) ;
357+ i. display_style = DisplayStyle :: LastWithMarker ;
358+ }
322359 } ) ;
323360 self . after_edit ( ctx) ;
324361 return None ;
@@ -328,16 +365,21 @@ impl Component for PassphraseKeyboard {
328365 }
329366
330367 fn render < ' s > ( & ' s self , target : & mut impl Renderer < ' s > ) {
331- self . input . render ( target) ;
332- self . scrollbar . render ( target) ;
333- self . confirm . render ( target) ;
334- self . back . render ( target) ;
335- for btn in & self . keys {
336- btn. render ( target) ;
337- }
338- if self . fade . take ( ) {
339- // Note that this is blocking and takes some time.
340- display:: fade_backlight ( theme:: backlight:: get_backlight_normal ( ) ) ;
368+ if self . input . inner ( ) . display_style == DisplayStyle :: Shown {
369+ // When passphrase is revealed, render only the shown overlay
370+ self . input . render ( target) ;
371+ } else {
372+ self . input . render ( target) ;
373+ self . scrollbar . render ( target) ;
374+ self . confirm . render ( target) ;
375+ self . back . render ( target) ;
376+ for btn in & self . keys {
377+ btn. render ( target) ;
378+ }
379+ if self . fade . take ( ) {
380+ // Note that this is blocking and takes some time.
381+ display:: fade_backlight ( theme:: backlight:: get_backlight_normal ( ) ) ;
382+ }
341383 }
342384 }
343385}
@@ -346,14 +388,129 @@ struct Input {
346388 area : Rect ,
347389 textbox : TextBox ,
348390 multi_tap : MultiTapKeyboard ,
391+ display_style : DisplayStyle ,
392+ last_char_timer : Timer ,
393+ pad : Pad ,
394+ shown_area : Rect ,
349395}
350396
351397impl Input {
398+ const TWITCH : i16 = 4 ;
399+ const LAST_DIGIT_TIMEOUT : Duration = Duration :: from_secs ( 1 ) ;
400+ const STYLE : TextStyle =
401+ theme:: label_keyboard ( ) . with_line_breaking ( LineBreaking :: BreakWordsNoHyphen ) ;
402+ const SHOWN_INSETS : Insets = Insets :: new ( 8 , 10 , 8 , 10 ) ;
403+ const SHOWN_TOUCH_OUTSET : Insets = Insets :: bottom ( 80 ) ;
404+
352405 fn new ( max_len : usize ) -> Self {
353406 Self {
354407 area : Rect :: zero ( ) ,
355408 textbox : TextBox :: empty ( max_len) ,
356409 multi_tap : MultiTapKeyboard :: new ( ) ,
410+ display_style : DisplayStyle :: Hidden ,
411+ last_char_timer : Timer :: new ( ) ,
412+ pad : Pad :: with_background ( theme:: BG ) ,
413+ shown_area : Rect :: zero ( ) ,
414+ }
415+ }
416+
417+ fn update_shown_area ( & mut self ) {
418+ let line_height = Self :: STYLE . text_font . line_height ( ) ;
419+
420+ // Start with full screen width, positioned at the input area top
421+ let initial_height = line_height + Self :: SHOWN_INSETS . top + Self :: SHOWN_INSETS . bottom ;
422+ let mut shown_area = SCREEN . inset ( Self :: SHOWN_INSETS ) . with_height ( initial_height) ;
423+
424+ // Extend the shown area until the text fits
425+ while let LayoutFit :: OutOfBounds { .. } = TextLayout :: new ( Self :: STYLE )
426+ . with_align ( Alignment :: Start )
427+ . with_bounds ( shown_area. inset ( Self :: SHOWN_INSETS ) )
428+ . fit_text ( self . textbox . content ( ) )
429+ {
430+ shown_area = shown_area. outset ( Insets :: bottom ( line_height) ) ;
431+ }
432+
433+ self . shown_area = shown_area;
434+ }
435+
436+ fn render_shown < ' s > ( & self , target : & mut impl Renderer < ' s > ) {
437+ debug_assert_eq ! ( self . display_style, DisplayStyle :: Shown ) ;
438+
439+ Bar :: new ( self . shown_area )
440+ . with_bg ( theme:: GREY_DARK )
441+ . with_radius ( theme:: RADIUS as i16 )
442+ . render ( target) ;
443+
444+ TextLayout :: new ( Self :: STYLE )
445+ . with_bounds ( self . shown_area . inset ( Self :: SHOWN_INSETS ) )
446+ . with_align ( Alignment :: Start )
447+ . render_text ( self . textbox . content ( ) , target, true ) ;
448+ }
449+
450+ fn render_hidden < ' s > ( & self , target : & mut impl Renderer < ' s > ) {
451+ debug_assert_ne ! ( self . display_style, DisplayStyle :: Shown ) ;
452+
453+ let style = theme:: label_keyboard ( ) ;
454+ let pp_len = self . textbox . count ( ) ;
455+ if pp_len == 0 {
456+ return ;
457+ }
458+
459+ let last_char_visible = self . display_style == DisplayStyle :: LastOnly
460+ || self . display_style == DisplayStyle :: LastWithMarker ;
461+
462+ // Compute how many characters fit in the available width. Account for the
463+ // pending marker, which draws itself one pixel longer than the last char.
464+ let available_width = self . area . width ( ) - 1 ;
465+ let asterisk_width = style. text_font . char_width ( '*' ) . max ( 1 ) ;
466+ let max_visible = ( available_width / asterisk_width) . max ( 1 ) as usize ;
467+ let visible_count = pp_len. min ( max_visible) ;
468+ let asterisk_count = visible_count. saturating_sub ( last_char_visible as usize ) ;
469+
470+ // Build asterisks string
471+ let mut asterisks = ShortString :: new ( ) ;
472+ for _ in 0 ..asterisk_count {
473+ let _ = asterisks. push ( '*' ) ;
474+ }
475+
476+ let mut text_baseline = self . area . top_left ( ) + Offset :: y ( style. text_font . text_height ( ) )
477+ - Offset :: y ( style. text_font . text_baseline ( ) ) ;
478+
479+ // Twitch when overflowed (alternates so user sees feedback when typing past
480+ // what fits)
481+ if pp_len > max_visible && pp_len % 2 == 1 {
482+ text_baseline. x += Self :: TWITCH ;
483+ }
484+
485+ // Render asterisks in GREY_LIGHT
486+ if !asterisks. is_empty ( ) {
487+ Text :: new ( text_baseline, & asterisks, style. text_font )
488+ . with_align ( Alignment :: Start )
489+ . with_fg ( theme:: GREY_LIGHT )
490+ . render ( target) ;
491+ }
492+
493+ // Render the visible last character in the regular text color
494+ if last_char_visible {
495+ if let Some ( last) = self . textbox . last_char_str ( ) {
496+ let last_baseline =
497+ text_baseline + Offset :: x ( style. text_font . text_width ( & asterisks) ) ;
498+
499+ Text :: new ( last_baseline, last, style. text_font )
500+ . with_align ( Alignment :: Start )
501+ . with_fg ( style. text_color )
502+ . render ( target) ;
503+
504+ if self . display_style == DisplayStyle :: LastWithMarker {
505+ render_pending_marker (
506+ target,
507+ last_baseline,
508+ last,
509+ style. text_font ,
510+ style. text_color ,
511+ ) ;
512+ }
513+ }
357514 }
358515 }
359516}
@@ -362,44 +519,64 @@ impl Component for Input {
362519 type Msg = Never ;
363520
364521 fn place ( & mut self , bounds : Rect ) -> Rect {
522+ self . pad . place ( bounds) ;
365523 self . area = bounds;
366524 self . area
367525 }
368526
369- fn event ( & mut self , _ctx : & mut EventCtx , _event : Event ) -> Option < Self :: Msg > {
527+ fn event ( & mut self , ctx : & mut EventCtx , event : Event ) -> Option < Self :: Msg > {
528+ if self . textbox . is_empty ( ) {
529+ return None ;
530+ }
531+
532+ let extended_shown_area = self
533+ . shown_area
534+ . outset ( Self :: SHOWN_TOUCH_OUTSET )
535+ . clamp ( SCREEN ) ;
536+
537+ match event {
538+ Event :: Touch ( TouchEvent :: TouchStart ( pos) ) if self . area . contains ( pos) => {
539+ self . multi_tap . clear_pending_state ( ctx) ;
540+ self . last_char_timer . stop ( ) ;
541+ self . display_style = DisplayStyle :: Shown ;
542+ self . update_shown_area ( ) ;
543+ self . pad . clear ( ) ;
544+ ctx. request_paint ( ) ;
545+ }
546+ Event :: Touch ( TouchEvent :: TouchEnd ( _) ) if self . display_style == DisplayStyle :: Shown => {
547+ self . display_style = DisplayStyle :: Hidden ;
548+ self . pad . clear ( ) ;
549+ ctx. request_paint ( ) ;
550+ }
551+ Event :: Touch ( TouchEvent :: TouchMove ( pos) )
552+ if !extended_shown_area. contains ( pos)
553+ && self . display_style == DisplayStyle :: Shown =>
554+ {
555+ self . display_style = DisplayStyle :: Hidden ;
556+ self . pad . clear ( ) ;
557+ ctx. request_paint ( ) ;
558+ }
559+ // Timeout for showing the last char
560+ Event :: Timer ( _) if self . last_char_timer . expire ( event) => {
561+ self . display_style = DisplayStyle :: Hidden ;
562+ self . request_complete_repaint ( ctx) ;
563+ ctx. request_paint ( ) ;
564+ }
565+ _ => { }
566+ }
370567 None
371568 }
372569
373570 fn render < ' s > ( & ' s self , target : & mut impl Renderer < ' s > ) {
374- let style = theme :: label_keyboard ( ) ;
571+ self . pad . render ( target ) ;
375572
376- let text_baseline = self . area . top_left ( ) + Offset :: y ( style. text_font . text_height ( ) )
377- - Offset :: y ( style. text_font . text_baseline ( ) ) ;
378-
379- let text = self . textbox . content ( ) ;
380-
381- shape:: Bar :: new ( self . area ) . with_bg ( theme:: BG ) . render ( target) ;
382-
383- // Find out how much text can fit into the textbox.
384- // Accounting for the pending marker, which draws itself one pixel longer than
385- // the last character
386- let available_area_width = self . area . width ( ) - 1 ;
387- let text_to_display =
388- long_line_content_with_ellipsis ( text, "..." , style. text_font , available_area_width) ;
389-
390- shape:: Text :: new ( text_baseline, & text_to_display, style. text_font )
391- . with_fg ( style. text_color )
392- . render ( target) ;
573+ if self . textbox . is_empty ( ) {
574+ return ;
575+ }
393576
394- // Paint the pending marker.
395- if self . multi_tap . pending_key ( ) . is_some ( ) {
396- render_pending_marker (
397- target,
398- text_baseline,
399- & text_to_display,
400- style. text_font ,
401- style. text_color ,
402- ) ;
577+ match self . display_style {
578+ DisplayStyle :: Shown => self . render_shown ( target) ,
579+ _ => self . render_hidden ( target) ,
403580 }
404581 }
405582}
@@ -410,8 +587,10 @@ impl crate::trace::Trace for PassphraseKeyboard {
410587 let page = self . scrollbar . pager ( ) . current ( ) ;
411588 debug_assert ! ( page < PAGE_COUNT as u16 ) ;
412589 let active_layout = uformat ! ( "{:?}" , KeyboardLayout :: from_page_unchecked( page. into( ) ) ) ;
590+ let display_style = uformat ! ( "{:?}" , self . input. inner( ) . display_style) ;
413591 t. component ( "PassphraseKeyboard" ) ;
414592 t. string ( "active_layout" , active_layout. as_str ( ) . into ( ) ) ;
415593 t. string ( "passphrase" , self . passphrase ( ) . into ( ) ) ;
594+ t. string ( "display_style" , display_style. as_str ( ) . into ( ) ) ;
416595 }
417596}
0 commit comments