//! `cargo run --example showcase` //! //! Shows button variants, container with background, a slider, a //! viewport-relative image, plus the newer additions: a tab strip, a spinner //! driven by the wall clock, a multiline text edit, and on-demand toast / //! tooltip overlays. //! Tab / Shift+Tab cycles keyboard focus. Enter or Space activates the //! focused button. Esc exits. //! //! 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 std::sync::Arc; use std::time::{ Duration, Instant }; use ltk::{ App, Element, Keysym, ButtonVariant, Length, WidgetId, OverlaySpec, button, column, row, stack, text, text_edit, spacer, container, slider, scroll, spinner, tabs, toast, tooltip, img_widget, }; const IMG_W: u32 = 64; const IMG_H: u32 = 64; fn gradient_rgba() -> Arc> { let mut data = Vec::with_capacity( ( IMG_W * IMG_H * 4 ) as usize ); for y in 0..IMG_H { for x in 0..IMG_W { let r = ( 255 * x / IMG_W ) as u8; let g = ( 255 * y / IMG_H ) as u8; data.extend_from_slice( &[ r, g, 180, 255 ] ); } } Arc::new( data ) } // ── Messages ────────────────────────────────────────────────────────────────── #[ derive( Clone ) ] enum Message { Pressed( &'static str ), SliderChanged( f32 ), SelectTab( usize ), NoteChanged( String ), ShowToast, ToggleTooltip, HideToast, } // ── App state ───────────────────────────────────────────────────────────────── struct ShowcaseApp { last_pressed: String, slider_value: f32, tab: usize, note: String, started_at: Instant, toast_until: Option, tooltip_visible: bool, image: Arc>, } impl ShowcaseApp { fn new() -> Self { Self { last_pressed: String::from( "—" ), slider_value: 0.5, tab: 0, note: String::new(), started_at: Instant::now(), toast_until: None, tooltip_visible: false, image: gradient_rgba(), } } const SAVE_BUTTON_ID: WidgetId = WidgetId( "showcase/save" ); } // ── App trait ───────────────────────────────────────────────────────────────── impl App for ShowcaseApp { type Message = Message; fn view( &self ) -> Element { // Pull text colours from the active theme — the runtime // background defaults to `palette.bg`, so hard-coded white // text would be unreadable in light mode. let palette = ltk::theme_palette(); let primary = palette.text_primary; let secondary = palette.text_secondary; let status = format!( "Last pressed: {}", self.last_pressed ); // Tabs strip — the active tab just relabels a banner below. let tabs_strip: Element = tabs( [ "Buttons", "Inputs", "Hints" ] ) .selected( self.tab ) .on_select( Message::SelectTab ) .into(); let tab_banner = format!( "Active tab: {}", [ "Buttons", "Inputs", "Hints" ][ self.tab.min( 2 ) ], ); let buttons = row::() .spacing( 16.0 ) .push( button( "Primary" ) .variant( ButtonVariant::Primary ) .id( Self::SAVE_BUTTON_ID ) .tooltip( "Hover-dwell tooltip via .tooltip()" ) .on_press( Message::Pressed( "Primary" ) ), ) .push( button( "Secondary" ) .variant( ButtonVariant::Secondary ) .tooltip( "Secondary action" ) .on_press( Message::Pressed( "Secondary" ) ), ) .push( button( "Tertiary" ) .variant( ButtonVariant::Tertiary ) .tooltip( "Tertiary action" ) .on_press( Message::Pressed( "Tertiary" ) ), ); let card = container::( column::() .spacing( 8.0 ) .push( text( "container() demo" ).size( 14.0 ).color( primary ) ) .push( text( "Background color + padding + corner radius" ) .size( 12.0 ) .color( secondary ), ), ) .background( palette.surface_alt ) .radius( 12.0 ) .padding( 16.0 ); // Viewport-relative image: 30 % of the surface width by 10 % of its // height, so it rescales with the window instead of staying fixed. // // The second image uses the orientation-aware helpers: `short_side` // sizes it along the screen's short side (width in portrait, height // in landscape) while `orient` gives that side a different proportion // per orientation — 40 % of the width in portrait, 8 % of the height // in landscape — with the other axis following the source aspect. let banner = column::() .spacing( 4.0 ) .push( text( "img_widget().size( vw 30, vh 10 )" ).size( 12.0 ).color( secondary ) ) .push( img_widget( self.image.clone(), IMG_W, IMG_H ).size( Length::vw( 30.0 ), Length::vh( 10.0 ) ) ) .push( text( "short_side( orient( 40 %w, 8 %h ) ) — resize / rotate to compare" ) .size( Length::orient( 3.0, 2.2 ).clamp( 11.0, 16.0 ) ) .color( secondary ) ) .push( img_widget( self.image.clone(), IMG_W, IMG_H ).short_side( Length::orient( 40.0, 8.0 ) ) ); let vol_label = format!( "Volume: {:.0}%", self.slider_value * 100.0 ); // Spinner — phase comes straight from the wall clock so the arc // completes 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( "Working…" ).size( 14.0 ).color( primary ) ) .into(); // Multiline text area. let textarea = text_edit( "Type a note (multi-line)", &self.note ) .multiline( true ) .rows( 3 ) .on_change( Message::NoteChanged ); // Buttons that exercise the overlay-returning widgets. let overlay_row = row::() .spacing( 12.0 ) .push( button( "Show toast" ).on_press( Message::ShowToast ) ) .push( button( if self.tooltip_visible { "Hide tooltip" } else { "Show tooltip" } ) .on_press( Message::ToggleTooltip ) ); let body = column::() .padding( 32.0 ) .spacing( 16.0 ) .push( text( "ltk showcase" ).size( 24.0 ).color( primary ).align_center() ) .push( text( status ).size( 14.0 ).color( secondary ).align_center() ) .push( tabs_strip ) .push( text( tab_banner ).size( 12.0 ).color( secondary ).align_center() ) .push( spacer() ) .push( buttons ) .push( card ) .push( banner ) .push( text( vol_label ).size( 13.0 ).color( secondary ) ) .push( slider( self.slider_value ).on_change( Message::SliderChanged ) ) .push( spin_row ) .push( textarea ) .push( overlay_row ) .push( spacer() ) .push( text( "Tab = focus next · Enter / Space = press · Esc = quit" ) .size( 12.0 ) .color( secondary ) .align_center(), ); // Toast lives inside the main view (works on every // compositor — no `wlr-layer-shell` required). The Tooltip // stays in `overlays()` because xdg-popup is universal. let mut s = stack::().push( scroll( body ) ); if self.toast_until.is_some() { s = s.push( toast::( "Saved" ).view() ); } s.into() } fn update( &mut self, msg: Message ) { match msg { Message::Pressed( name ) => self.last_pressed = name.to_string(), Message::SliderChanged( val ) => self.slider_value = val, Message::SelectTab( i ) => self.tab = i, Message::NoteChanged( s ) => self.note = s, Message::ShowToast => { self.toast_until = Some( Instant::now() + Duration::from_secs( 2 ) ); } Message::HideToast => self.toast_until = None, Message::ToggleTooltip => self.tooltip_visible = !self.tooltip_visible, } } fn overlays( &self ) -> Vec> { // Tooltip is an xdg-popup → works on any xdg-shell compositor. // Toast is rendered inline in `view()` (see the Stack at the // bottom of the function) so it does not depend on // `wlr-layer-shell` either. if self.tooltip_visible { vec![ tooltip::( "Saves the document and closes the dialog", Self::SAVE_BUTTON_ID ).overlay() ] } else { vec![] } } fn on_key( &mut self, keysym: Keysym ) -> Option { if keysym == Keysym::Escape { std::process::exit( 0 ); } None } // Keep redrawing while the spinner is on screen and while a toast // is pending so its expiry timestamp is checked every frame. fn is_animating( &self ) -> bool { true } // Auto-clear an expired toast. fn poll_interval( &self ) -> Option { Some( Duration::from_millis( 250 ) ) } fn poll_external( &mut self ) -> Vec { match self.toast_until { Some( deadline ) if Instant::now() >= deadline => vec![ Message::HideToast ], _ => vec![], } } } // ── Entry point ─────────────────────────────────────────────────────────────── fn main() { ltk::run( ShowcaseApp::new() ); }