Files
ltk/examples/combo.rs

228 lines
5.6 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! `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<String>,
region: ComboState,
region_items: Vec<String>,
}
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<Msg>
{
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<Msg>
{
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<Msg>
{
let palette = ltk::theme_palette();
column::<Msg>()
.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::<Msg>()
.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<Msg>
{
self.body()
}
fn overlays( &self ) -> Vec<OverlaySpec<Msg>>
{
// 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<Msg>
{
if keysym == Keysym::Escape
{
std::process::exit( 0 );
}
None
}
fn background_color( &self ) -> Color
{
ltk::theme_palette().bg
}
}
fn main()
{
ltk::run( Demo::new() );
}