use std::time::Instant; use ltk::{ App, Element, Keysym, WidgetId, column, row, text, text_edit, spacer, separator, slider, toggle, checkbox, radio, progress_bar, scroll, spinner, tabs, }; #[derive( Clone )] enum Msg { ToggleWifi, ToggleBluetooth, ToggleDarkMode, CheckNotifications, CheckUpdates, SelectThemeLight, SelectThemeDark, SelectThemeAuto, SliderChanged( f32 ), SelectTab( usize ), NoteChanged( String ), Tick, } struct WidgetsApp { wifi: bool, bluetooth: bool, dark_mode: bool, notifications: bool, updates: bool, theme: usize, progress: f32, volume: f32, tab: usize, note: String, started_at: Instant, } impl WidgetsApp { fn new() -> Self { Self { wifi: true, bluetooth: false, dark_mode: true, notifications: true, updates: false, theme: 2, progress: 0.65, volume: 0.7, tab: 0, note: String::new(), started_at: Instant::now(), } } } impl App for WidgetsApp { type Message = Msg; fn view( &self ) -> Element { // Pull text colours from the active theme so the example // reads correctly under both light and dark mode without // touching the example code. let palette = ltk::theme_palette(); let primary = palette.text_primary; let secondary = palette.text_secondary; let title = text( "ltk widgets" ).size( 24.0 ).color( primary ).align_center(); // Segmented tab strip — selection just rendered as a banner below. let tabs_strip: Element = tabs( [ "General", "Audio", "Network" ] ) .selected( self.tab ) .on_select( Msg::SelectTab ) .into(); let tab_label = format!( "Active tab: {}", [ "General", "Audio", "Network" ][ self.tab.min( 2 ) ], ); let toggles = column::() .spacing( 0.0 ) .push( text( "Toggle" ).size( 13.0 ).color( secondary ) ) .push( toggle( self.wifi ).label( "Wi-Fi" ).on_toggle( Msg::ToggleWifi ) ) .push( toggle( self.bluetooth ).label( "Bluetooth" ).on_toggle( Msg::ToggleBluetooth ) ) .push( toggle( self.dark_mode ).label( "Dark mode" ).on_toggle( Msg::ToggleDarkMode ) ); let checkboxes = column::() .spacing( 0.0 ) .push( text( "Checkbox" ).size( 13.0 ).color( secondary ) ) .push( checkbox( self.notifications ).label( "Notifications" ).on_toggle( Msg::CheckNotifications ) ) .push( checkbox( self.updates ).label( "Auto updates" ).on_toggle( Msg::CheckUpdates ) ); let radios = column::() .spacing( 0.0 ) .push( text( "Radio" ).size( 13.0 ).color( secondary ) ) .push( radio( self.theme == 0 ).label( "Light" ).on_select( Msg::SelectThemeLight ) ) .push( radio( self.theme == 1 ).label( "Dark" ).on_select( Msg::SelectThemeDark ) ) .push( radio( self.theme == 2 ).label( "Auto" ).on_select( Msg::SelectThemeAuto ) ); let vol_label = format!( "Volume: {:.0}%", self.volume * 100.0 ); let prog_label = format!( "Progress: {:.0}%", self.progress * 100.0 ); // Spinner — animation phase derived from the wall clock so the // arc rotates at one revolution per second. let phase = self.started_at.elapsed().as_secs_f32(); let spin_row: Element = row::() .spacing( 12.0 ) .push( spinner().phase( phase ) ) .push( text( "Loading…" ).size( 14.0 ).color( primary ) ) .into(); // Multiline text area — Enter inserts a newline, Tab leaves it. let textarea = text_edit( "Type a note (multi-line)", &self.note ) .multiline( true ) .rows( 4 ) .id( WidgetId( "widgets/note" ) ) .on_change( Msg::NoteChanged ); let content = column::() .padding( 32.0 ) .spacing( 12.0 ) .push( title ) .push( tabs_strip ) .push( text( tab_label ).size( 13.0 ).color( secondary ) ) .push( separator() ) .push( toggles ) .push( separator() ) .push( checkboxes ) .push( separator() ) .push( radios ) .push( separator() ) .push( text( vol_label ).size( 13.0 ).color( secondary ) ) .push( slider( self.volume ).on_change( Msg::SliderChanged ) ) .push( text( prog_label ).size( 13.0 ).color( secondary ) ) .push( progress_bar( self.progress ) ) .push( separator() ) .push( text( "Spinner" ).size( 13.0 ).color( secondary ) ) .push( spin_row ) .push( separator() ) .push( text( "Multiline text edit" ).size( 13.0 ).color( secondary ) ) .push( textarea ) .push( spacer() ) .push( text( "Esc = quit" ) .size( 12.0 ) .color( secondary ) .align_center(), ); scroll( content ).into() } fn update( &mut self, msg: Msg ) { match msg { Msg::ToggleWifi => self.wifi = !self.wifi, Msg::ToggleBluetooth => self.bluetooth = !self.bluetooth, Msg::ToggleDarkMode => self.dark_mode = !self.dark_mode, Msg::CheckNotifications => self.notifications = !self.notifications, Msg::CheckUpdates => self.updates = !self.updates, Msg::SelectThemeLight => self.theme = 0, Msg::SelectThemeDark => self.theme = 1, Msg::SelectThemeAuto => self.theme = 2, Msg::SliderChanged( v ) => self.volume = v, Msg::SelectTab( i ) => self.tab = i, Msg::NoteChanged( s ) => self.note = s, Msg::Tick => {} } } fn on_key( &mut self, keysym: Keysym ) -> Option { if keysym == Keysym::Escape { std::process::exit( 0 ); } None } // Drive the spinner — the runtime keeps redrawing while this returns // `true`, and `view()` reads the wall clock to compute the phase, so // no explicit Tick message is even needed for the visual to advance. fn is_animating( &self ) -> bool { true } fn poll_interval( &self ) -> Option { // Suppress the dead-code warning on Msg::Tick; a real app would // also use the timer to drive non-visual periodic work here. Some( std::time::Duration::from_millis( 250 ) ) } fn poll_external( &mut self ) -> Vec { // Consumed: the timer above keeps the runtime warm, but the // spinner reads the wall clock directly so no Tick is required. // We still emit one occasionally so the message variant is // exercised — keeps the example honest about the pattern apps // usually use for clock work. if self.started_at.elapsed().as_millis() % 1_000 < 250 { vec![ Msg::Tick ] } else { vec![] } } } fn main() { ltk::run( WidgetsApp::new() ); }