//! `cargo run --example combo` //! //! Showcases the `combo` widget — a select / dropdown with editable //! query, multi-select chips and a scrollable popup. //! //! The trigger lives in the main `view()` tree; the popup is returned //! from [`App::overlays`] as a real Wayland **xdg-popup** child of the //! main window, so it can extend outside the parent surface (the //! canonical select / dropdown behaviour). The compositor positions the //! popup adjacent to the trigger pill and dismisses it via //! `popup_done` when the user clicks outside, which fires the spec's //! `on_dismiss` message. //! //! Tap on an item to add it to the selection; tap on the `×` next to a //! chip to remove it; tap outside the popup to dismiss. Esc exits. use ltk:: { combo, column, row, separator, spacer, text, App, Color, ComboState, Element, Keysym, OverlaySpec, WidgetId, }; // Distinct anchor ids so each combo's popup can find its own trigger // pill in the previous-frame layout snapshot. const FRUIT_ANCHOR: WidgetId = WidgetId( "demo-fruit-trigger" ); const REGION_ANCHOR: WidgetId = WidgetId( "demo-region-trigger" ); #[ derive( Clone, Debug ) ] enum Msg { // Multi-select fruit picker. FruitToggle, FruitQuery( String ), FruitSelect( usize ), FruitUnselect( usize ), FruitDismiss, // Single-select region picker (non-searchable). RegionToggle, RegionSelect( usize ), RegionDismiss, } struct Demo { fruits: ComboState, fruit_items: Vec, region: ComboState, region_items: Vec, } impl Demo { fn new() -> Self { Self { fruits: ComboState::new(), fruit_items: [ "Apple", "Apricot", "Avocado", "Banana", "Blackberry", "Blueberry", "Cherry", "Date", "Fig", "Grape", "Kiwi", "Lemon", "Lime", "Mango", "Orange", "Papaya", "Peach", "Pear", "Pineapple", "Plum", "Raspberry", "Strawberry", ].iter().map( |s| s.to_string() ).collect(), region: ComboState::new(), region_items: [ "Galicia", "Asturias", "Cantabria", "País Vasco", "Navarra", "La Rioja", "Aragón", "Cataluña", ].iter().map( |s| s.to_string() ).collect(), } } fn fruit_combo( &self ) -> ltk::Combo { combo( self.fruits.clone(), self.fruit_items.clone() ) .label( "Fruits" ) .description( "Pick the ones you want in the smoothie." ) .placeholder( "Type to filter…" ) .helper( "Selected fruits show up as chips above the field." ) .multi_select( true ) .searchable( true ) .anchor_id( FRUIT_ANCHOR ) .popup_max_height( 280.0 ) .on_query_change( Msg::FruitQuery ) .on_toggle_open( Msg::FruitToggle ) .on_select_idx( Msg::FruitSelect ) .on_unselect_idx( Msg::FruitUnselect ) .on_dismiss( Msg::FruitDismiss ) } fn region_combo( &self ) -> ltk::Combo { combo( self.region.clone(), self.region_items.clone() ) .label( "Region" ) .description( "Single-select, non-searchable." ) .placeholder( "Choose a region…" ) .anchor_id( REGION_ANCHOR ) .popup_max_height( 240.0 ) .on_toggle_open( Msg::RegionToggle ) .on_select_idx( Msg::RegionSelect ) .on_dismiss( Msg::RegionDismiss ) } fn body( &self ) -> Element { let palette = ltk::theme_palette(); column::() .padding( 32.0 ) .spacing( 24.0 ) .center_y( false ) .push( text( "ltk — combo / select demo" ) .size( 24.0 ) .color( palette.text_primary ) .align_center(), ) .push( separator() ) .push( self.fruit_combo().trigger() ) .push( separator() ) .push( self.region_combo().trigger() ) .push( spacer() ) .push( row::() .padding( 0.0 ) .spacing( 8.0 ) .push( text( format!( "Fruits selected: {}", self.fruits.selected.len() ) ) .size( 14.0 ) .color( palette.text_secondary ), ) .push( spacer() ) .push( text( self.region.selected.first() .and_then( |&i| self.region_items.get( i ) ) .map( |s| format!( "Region: {s}" ) ) .unwrap_or_else( || "Region: —".to_string() ), ) .size( 14.0 ) .color( palette.text_secondary ), ), ) .push( text( "Esc = quit" ) .size( 12.0 ) .color( palette.text_secondary ) .align_center(), ) .into() } } impl App for Demo { type Message = Msg; fn view( &self ) -> Element { self.body() } fn overlays( &self ) -> Vec> { // Each open combo contributes one xdg-popup overlay anchored // to its trigger pill; closed combos contribute nothing. let mut out = Vec::new(); if let Some( o ) = self.fruit_combo().overlay() { out.push( o ); } if let Some( o ) = self.region_combo().overlay() { out.push( o ); } out } fn update( &mut self, msg: Msg ) { match msg { Msg::FruitToggle => self.fruits.toggle_open(), Msg::FruitQuery( q ) => self.fruits.query = q, Msg::FruitSelect( i ) => { if self.fruits.selected.contains( &i ) { self.fruits.unselect( i ); } else { self.fruits.select( i ); } } Msg::FruitUnselect( i ) => self.fruits.unselect( i ), Msg::FruitDismiss => { self.fruits.is_open = false; self.fruits.query.clear(); } Msg::RegionToggle => self.region.toggle_open(), Msg::RegionSelect( i ) => { // Single-select: replace the whole `selected` vector // with just this index, then close. self.region.selected = vec![ i ]; self.region.is_open = false; } Msg::RegionDismiss => self.region.is_open = false, } } fn on_key( &mut self, keysym: Keysym ) -> Option { if keysym == Keysym::Escape { std::process::exit( 0 ); } None } fn background_color( &self ) -> Color { ltk::theme_palette().bg } } fn main() { ltk::run( Demo::new() ); }