//! `cargo run --example inputs` //! //! Two pages. *Login* shows a plain username field and a secure //! password field with a built-in show / hide-password eye toggle: //! tap the eye to flip the bullets ↔ plaintext rendering — the buffer //! keeps its wipe-on-drop guarantee either way. Tab / Shift+Tab moves //! focus between fields, Enter or the Login button submits. *Shell* //! is a mock terminal: one multiline field holds the whole //! transcript, prompts included, and is declared a terminal, so the //! on-screen keyboard adds its row of Escape, Tab, Ctrl, Alt and //! arrows. Enter runs the line under the last prompt, arrows move //! through the transcript, and a Ctrl combination interrupts the line //! the way a terminal would show it. Run with `LTK_BUILTIN_OSK=1` on a //! desktop to see the keyboard under a compositor that has its own. //! Esc quits from the Login page. //! //! NOTE: ltk is a Wayland layer-shell toolkit. This example requires a running //! Wayland compositor (e.g. sway, labwc, or a full desktop session). use ltk::{ App, ButtonVariant, Element, InputPurpose, Keysym, WidgetId, button, column, flex, notebook, spacer, text, text_edit }; // ── Messages ────────────────────────────────────────────────────────────────── #[ derive( Clone ) ] enum Message { SelectPage( usize ), UsernameChanged( String ), PasswordChanged( String ), TogglePasswordVisibility, Submit, TranscriptChanged( String ), Interrupt( String ), } const LOGIN: usize = 0; const SHELL: usize = 1; const SHELL_FIELD: WidgetId = WidgetId( "inputs/shell" ); const PROMPT: &str = "$ "; // ── App state ───────────────────────────────────────────────────────────────── struct InputsApp { page: usize, username: String, password: String, password_visible: bool, submitted: bool, /// The whole shell transcript, the current prompt line last. transcript: String, focus_request: Option, } impl InputsApp { fn new() -> Self { Self { page: LOGIN, username: String::new(), password: String::new(), password_visible: false, submitted: false, transcript: format!( "ltk mock shell — type a command and press Enter\n{PROMPT}" ), focus_request: None, } } fn login_page( &self ) -> Element { let palette = ltk::theme_palette(); let primary = palette.text_primary; let secondary = palette.text_secondary; let status = if self.submitted { format!( "Submitted as: {}", self.username ) } else { String::from( "Fill in the fields and press Login" ) }; column::() .padding( 32.0 ) .spacing( 16.0 ) .max_width( 380.0 ) .center_y( true ) .push( text( "ltk — text input showcase" ).size( 24.0 ).color( primary ).align_center() ) .push( text( status ).size( 14.0 ).color( secondary ).align_center() ) .push( spacer() ) .push( text_edit::( "Username", &self.username ) .on_change( Message::UsernameChanged ) .on_submit( Message::Submit ), ) .push( text_edit::( "Password", &self.password ) .on_change( Message::PasswordChanged ) .on_submit( Message::Submit ) .password_toggle( self.password_visible, Message::TogglePasswordVisibility ), ) .push( button::( "Login" ) .variant( ButtonVariant::Primary ) .on_press( Message::Submit ), ) .push( spacer() ) .push( text( "Tab = next field · Enter = submit · Esc = quit" ) .size( 12.0 ) .color( secondary ) .align_center(), ) .into() } /// The transcript takes whatever height is left above the keyboard and scrolls to keep the cursor's line in view. fn shell_page( &self ) -> Element { column::() .padding( 16.0 ) .spacing( 0.0 ) .push( flex( text_edit::( "", &self.transcript ) .id( SHELL_FIELD ) .multiline( true ) .purpose( InputPurpose::Terminal ) .on_change( Message::TranscriptChanged ), ) ) .into() } /// A newline typed at the end of the transcript runs the line under the last prompt. fn transcript_changed( &mut self, value: String ) { let Some( body ) = value.strip_suffix( '\n' ) else { self.transcript = value; return }; let line = body.rsplit( '\n' ).next().unwrap_or( "" ); let command = line.strip_prefix( PROMPT ).unwrap_or( line ).trim().to_string(); self.transcript = body.to_string(); match command.as_str() { "" => {} "clear" => self.transcript.clear(), c => { let reply = if let Some( rest ) = c.strip_prefix( "echo " ) { rest.to_string() } else if c == "ls" { "Documents Downloads Music Pictures".to_string() } else if c == "pwd" { "/home/user".to_string() } else { format!( "{c}: command not found" ) }; self.transcript.push( '\n' ); self.transcript.push_str( &reply ); } } self.new_prompt(); } fn new_prompt( &mut self ) { if !self.transcript.is_empty() { self.transcript.push( '\n' ); } self.transcript.push_str( PROMPT ); // Re-focusing the field parks the cursor at the end of the value. self.focus_request = Some( SHELL_FIELD ); } } // ── App trait ───────────────────────────────────────────────────────────────── impl App for InputsApp { type Message = Message; fn app_id( &self ) -> &str { "net.liberux.ltk.example.inputs" } fn save_state( &self ) -> Option> { None } fn restore_state( &mut self, _state: Vec ) {} fn view( &self ) -> Element { notebook::() .page( "Login", self.login_page() ) .page( "Shell", self.shell_page() ) .selected( self.page ) .on_select( Message::SelectPage ) .build() } fn update( &mut self, msg: Message ) { match msg { Message::SelectPage( page ) => self.page = page, Message::UsernameChanged( v ) => self.username = v, Message::PasswordChanged( v ) => self.password = v, Message::TogglePasswordVisibility => self.password_visible = !self.password_visible, Message::Submit => self.submitted = true, Message::TranscriptChanged( v ) => self.transcript_changed( v ), Message::Interrupt( key ) => { self.transcript.push_str( &key ); self.new_prompt(); } } } fn take_focus_request( &mut self ) -> Option { self.focus_request.take() } // Only Ctrl combinations are taken; a plain key that is answered here would never reach the field. fn on_key_with_modifiers( &mut self, keysym: Keysym, ctrl: bool, _shift: bool ) -> Option { if self.page == LOGIN && keysym == Keysym::Escape { std::process::exit( 0 ); } if self.page != SHELL || !ctrl { return None; } let name = keysym.name().unwrap_or( "?" ).trim_start_matches( "XK_" ).to_uppercase(); Some( Message::Interrupt( format!( "^{name}" ) ) ) } } // ── Entry point ─────────────────────────────────────────────────────────────── fn main() { ltk::run( InputsApp::new() ); }