//! Combo boxes display a dropdown list of searchable and selectable options. //! //! # Example //! ```no_run //! # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; } //! # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; //! # //! use iced::widget::combo_box; //! //! struct State { //! fruits: combo_box::State, //! favorite: Option, //! } //! //! #[derive(Debug, Clone)] //! enum Fruit { //! Apple, //! Orange, //! Strawberry, //! Tomato, //! } //! //! #[derive(Debug, Clone)] //! enum Message { //! FruitSelected(Fruit), //! } //! //! fn view(state: &State) -> Element<'_, Message> { //! combo_box( //! &state.fruits, //! "Select your favorite fruit...", //! state.favorite.as_ref(), //! Message::FruitSelected //! ) //! .into() //! } //! //! fn update(state: &mut State, message: Message) { //! match message { //! Message::FruitSelected(fruit) => { //! state.favorite = Some(fruit); //! } //! } //! } //! //! impl std::fmt::Display for Fruit { //! fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { //! f.write_str(match self { //! Self::Apple => "Apple", //! Self::Orange => "Orange", //! Self::Strawberry => "Strawberry", //! Self::Tomato => "Tomato", //! }) //! } //! } //! ``` use crate::core::keyboard; use crate::core::keyboard::key; use crate::core::layout::{self, Layout}; use crate::core::mouse; use crate::core::overlay; use crate::core::renderer; use crate::core::text; use crate::core::time::Instant; use crate::core::widget::{self, Widget}; use crate::core::{ Clipboard, Element, Event, Length, Padding, Rectangle, Shell, Size, Theme, Vector, }; use crate::overlay::menu; use crate::text::LineHeight; use crate::text_input::{self, TextInput}; use std::cell::RefCell; use std::fmt::Display; /// A widget for searching and selecting a single value from a list of options. /// /// # Example /// ```no_run /// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; } /// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; /// # /// use iced::widget::combo_box; /// /// struct State { /// fruits: combo_box::State, /// favorite: Option, /// } /// /// #[derive(Debug, Clone)] /// enum Fruit { /// Apple, /// Orange, /// Strawberry, /// Tomato, /// } /// /// #[derive(Debug, Clone)] /// enum Message { /// FruitSelected(Fruit), /// } /// /// fn view(state: &State) -> Element<'_, Message> { /// combo_box( /// &state.fruits, /// "Select your favorite fruit...", /// state.favorite.as_ref(), /// Message::FruitSelected /// ) /// .into() /// } /// /// fn update(state: &mut State, message: Message) { /// match message { /// Message::FruitSelected(fruit) => { /// state.favorite = Some(fruit); /// } /// } /// } /// /// impl std::fmt::Display for Fruit { /// fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { /// f.write_str(match self { /// Self::Apple => "Apple", /// Self::Orange => "Orange", /// Self::Strawberry => "Strawberry", /// Self::Tomato => "Tomato", /// }) /// } /// } /// ``` #[allow(missing_debug_implementations)] pub struct ComboBox< 'a, T, Message, Theme = crate::Theme, Renderer = crate::Renderer, > where Theme: Catalog, Renderer: text::Renderer, { state: &'a State, text_input: TextInput<'a, TextInputEvent, Theme, Renderer>, font: Option, selection: text_input::Value, on_selected: Box Message>, on_option_hovered: Option Message>>, on_open: Option, on_close: Option, on_input: Option Message>>, menu_class: ::Class<'a>, padding: Padding, size: Option, } impl<'a, T, Message, Theme, Renderer> ComboBox<'a, T, Message, Theme, Renderer> where T: std::fmt::Display + Clone, Theme: Catalog, Renderer: text::Renderer, { /// Creates a new [`ComboBox`] with the given list of options, a placeholder, /// the current selected value, and the message to produce when an option is /// selected. pub fn new( state: &'a State, placeholder: &str, selection: Option<&T>, on_selected: impl Fn(T) -> Message + 'static, ) -> Self { let text_input = TextInput::new(placeholder, &state.value()) .on_input(TextInputEvent::TextChanged) .class(Theme::default_input()); let selection = selection.map(T::to_string).unwrap_or_default(); Self { state, text_input, font: None, selection: text_input::Value::new(&selection), on_selected: Box::new(on_selected), on_option_hovered: None, on_input: None, on_open: None, on_close: None, menu_class: ::default_menu(), padding: text_input::DEFAULT_PADDING, size: None, } } /// Sets the message that should be produced when some text is typed into /// the [`TextInput`] of the [`ComboBox`]. pub fn on_input( mut self, on_input: impl Fn(String) -> Message + 'static, ) -> Self { self.on_input = Some(Box::new(on_input)); self } /// Sets the message that will be produced when an option of the /// [`ComboBox`] is hovered using the arrow keys. pub fn on_option_hovered( mut self, on_option_hovered: impl Fn(T) -> Message + 'static, ) -> Self { self.on_option_hovered = Some(Box::new(on_option_hovered)); self } /// Sets the message that will be produced when the [`ComboBox`] is /// opened. pub fn on_open(mut self, message: Message) -> Self { self.on_open = Some(message); self } /// Sets the message that will be produced when the outside area /// of the [`ComboBox`] is pressed. pub fn on_close(mut self, message: Message) -> Self { self.on_close = Some(message); self } /// Sets the [`Padding`] of the [`ComboBox`]. pub fn padding(mut self, padding: impl Into) -> Self { self.padding = padding.into(); self.text_input = self.text_input.padding(self.padding); self } /// Sets the [`Renderer::Font`] of the [`ComboBox`]. /// /// [`Renderer::Font`]: text::Renderer pub fn font(mut self, font: Renderer::Font) -> Self { self.text_input = self.text_input.font(font); self.font = Some(font); self } /// Sets the [`text_input::Icon`] of the [`ComboBox`]. pub fn icon(mut self, icon: text_input::Icon) -> Self { self.text_input = self.text_input.icon(icon); self } /// Sets the text sixe of the [`ComboBox`]. pub fn size(mut self, size: f32) -> Self { self.text_input = self.text_input.size(size); self.size = Some(size); self } /// Sets the [`LineHeight`] of the [`ComboBox`]. pub fn line_height(self, line_height: impl Into) -> Self { Self { text_input: self.text_input.line_height(line_height), ..self } } /// Sets the width of the [`ComboBox`]. pub fn width(self, width: impl Into) -> Self { Self { text_input: self.text_input.width(width), ..self } } /// Sets the style of the input of the [`ComboBox`]. #[must_use] pub fn input_style( mut self, style: impl Fn(&Theme, text_input::Status) -> text_input::Style + 'a, ) -> Self where ::Class<'a>: From>, { self.text_input = self.text_input.style(style); self } /// Sets the style of the menu of the [`ComboBox`]. #[must_use] pub fn menu_style( mut self, style: impl Fn(&Theme) -> menu::Style + 'a, ) -> Self where ::Class<'a>: From>, { self.menu_class = (Box::new(style) as menu::StyleFn<'a, Theme>).into(); self } /// Sets the style class of the input of the [`ComboBox`]. #[cfg(feature = "advanced")] #[must_use] pub fn input_class( mut self, class: impl Into<::Class<'a>>, ) -> Self { self.text_input = self.text_input.class(class); self } /// Sets the style class of the menu of the [`ComboBox`]. #[cfg(feature = "advanced")] #[must_use] pub fn menu_class( mut self, class: impl Into<::Class<'a>>, ) -> Self { self.menu_class = class.into(); self } } /// The local state of a [`ComboBox`]. #[derive(Debug, Clone)] pub struct State { options: Vec, inner: RefCell>, } #[derive(Debug, Clone)] struct Inner { value: String, option_matchers: Vec, filtered_options: Filtered, } #[derive(Debug, Clone)] struct Filtered { options: Vec, updated: Instant, } impl State where T: Display + Clone, { /// Creates a new [`State`] for a [`ComboBox`] with the given list of options. pub fn new(options: Vec) -> Self { Self::with_selection(options, None) } /// Creates a new [`State`] for a [`ComboBox`] with the given list of options /// and selected value. pub fn with_selection(options: Vec, selection: Option<&T>) -> Self { let value = selection.map(T::to_string).unwrap_or_default(); // Pre-build "matcher" strings ahead of time so that search is fast let option_matchers = build_matchers(&options); let filtered_options = Filtered::new( search(&options, &option_matchers, &value) .cloned() .collect(), ); Self { options, inner: RefCell::new(Inner { value, option_matchers, filtered_options, }), } } /// Returns the options of the [`State`]. /// /// These are the options provided when the [`State`] /// was constructed with [`State::new`]. pub fn options(&self) -> &[T] { &self.options } fn value(&self) -> String { let inner = self.inner.borrow(); inner.value.clone() } fn with_inner(&self, f: impl FnOnce(&Inner) -> O) -> O { let inner = self.inner.borrow(); f(&inner) } fn with_inner_mut(&self, f: impl FnOnce(&mut Inner)) { let mut inner = self.inner.borrow_mut(); f(&mut inner); } fn sync_filtered_options(&self, options: &mut Filtered) { let inner = self.inner.borrow(); inner.filtered_options.sync(options); } } impl Default for State where T: Display + Clone, { fn default() -> Self { Self::new(Vec::new()) } } impl Filtered where T: Clone, { fn new(options: Vec) -> Self { Self { options, updated: Instant::now(), } } fn empty() -> Self { Self { options: vec![], updated: Instant::now(), } } fn update(&mut self, options: Vec) { self.options = options; self.updated = Instant::now(); } fn sync(&self, other: &mut Filtered) { if other.updated != self.updated { *other = self.clone(); } } } struct Menu { menu: menu::State, hovered_option: Option, new_selection: Option, filtered_options: Filtered, } #[derive(Debug, Clone)] enum TextInputEvent { TextChanged(String), } impl Widget for ComboBox<'_, T, Message, Theme, Renderer> where T: Display + Clone + 'static, Message: Clone, Theme: Catalog, Renderer: text::Renderer, { fn size(&self) -> Size { Widget::::size(&self.text_input) } fn layout( &self, tree: &mut widget::Tree, renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { let is_focused = { let text_input_state = tree.children[0] .state .downcast_ref::>(); text_input_state.is_focused() }; self.text_input.layout( &mut tree.children[0], renderer, limits, (!is_focused).then_some(&self.selection), ) } fn tag(&self) -> widget::tree::Tag { widget::tree::Tag::of::>() } fn state(&self) -> widget::tree::State { widget::tree::State::new(Menu:: { menu: menu::State::new(), filtered_options: Filtered::empty(), hovered_option: Some(0), new_selection: None, }) } fn children(&self) -> Vec { vec![widget::Tree::new(&self.text_input as &dyn Widget<_, _, _>)] } fn update( &mut self, tree: &mut widget::Tree, event: &Event, layout: Layout<'_>, cursor: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, viewport: &Rectangle, ) { let menu = tree.state.downcast_mut::>(); let started_focused = { let text_input_state = tree.children[0] .state .downcast_ref::>(); text_input_state.is_focused() }; // This is intended to check whether or not the message buffer was empty, // since `Shell` does not expose such functionality. let mut published_message_to_shell = false; // Create a new list of local messages let mut local_messages = Vec::new(); let mut local_shell = Shell::new(&mut local_messages); // Provide it to the widget self.text_input.update( &mut tree.children[0], event, layout, cursor, renderer, clipboard, &mut local_shell, viewport, ); if local_shell.is_event_captured() { shell.capture_event(); } shell.request_redraw_at(local_shell.redraw_request()); shell.request_input_method(local_shell.input_method()); // Then finally react to them here for message in local_messages { let TextInputEvent::TextChanged(new_value) = message; if let Some(on_input) = &self.on_input { shell.publish((on_input)(new_value.clone())); } // Couple the filtered options with the `ComboBox` // value and only recompute them when the value changes, // instead of doing it in every `view` call self.state.with_inner_mut(|state| { menu.hovered_option = Some(0); state.value = new_value; state.filtered_options.update( search( &self.state.options, &state.option_matchers, &state.value, ) .cloned() .collect(), ); }); shell.invalidate_layout(); shell.request_redraw(); } let is_focused = { let text_input_state = tree.children[0] .state .downcast_ref::>(); text_input_state.is_focused() }; if is_focused { self.state.with_inner(|state| { if !started_focused { if let Some(on_option_hovered) = &mut self.on_option_hovered { let hovered_option = menu.hovered_option.unwrap_or(0); if let Some(option) = state.filtered_options.options.get(hovered_option) { shell.publish(on_option_hovered(option.clone())); published_message_to_shell = true; } } } if let Event::Keyboard(keyboard::Event::KeyPressed { key: keyboard::Key::Named(named_key), modifiers, .. }) = event { let shift_modifier = modifiers.shift(); match (named_key, shift_modifier) { (key::Named::Enter, _) => { if let Some(index) = &menu.hovered_option { if let Some(option) = state.filtered_options.options.get(*index) { menu.new_selection = Some(option.clone()); } } shell.capture_event(); shell.request_redraw(); } (key::Named::ArrowUp, _) | (key::Named::Tab, true) => { if let Some(index) = &mut menu.hovered_option { if *index == 0 { *index = state .filtered_options .options .len() .saturating_sub(1); } else { *index = index.saturating_sub(1); } } else { menu.hovered_option = Some(0); } if let Some(on_option_hovered) = &mut self.on_option_hovered { if let Some(option) = menu.hovered_option.and_then(|index| { state .filtered_options .options .get(index) }) { // Notify the selection shell.publish((on_option_hovered)( option.clone(), )); published_message_to_shell = true; } } shell.capture_event(); shell.request_redraw(); } (key::Named::ArrowDown, _) | (key::Named::Tab, false) if !modifiers.shift() => { if let Some(index) = &mut menu.hovered_option { if *index >= state .filtered_options .options .len() .saturating_sub(1) { *index = 0; } else { *index = index.saturating_add(1).min( state .filtered_options .options .len() .saturating_sub(1), ); } } else { menu.hovered_option = Some(0); } if let Some(on_option_hovered) = &mut self.on_option_hovered { if let Some(option) = menu.hovered_option.and_then(|index| { state .filtered_options .options .get(index) }) { // Notify the selection shell.publish((on_option_hovered)( option.clone(), )); published_message_to_shell = true; } } shell.capture_event(); shell.request_redraw(); } _ => {} } } }); } // If the overlay menu has selected something self.state.with_inner_mut(|state| { if let Some(selection) = menu.new_selection.take() { // Clear the value and reset the options and menu state.value = String::new(); state.filtered_options.update(self.state.options.clone()); menu.menu = menu::State::default(); // Notify the selection shell.publish((self.on_selected)(selection)); published_message_to_shell = true; // Unfocus the input let mut local_messages = Vec::new(); let mut local_shell = Shell::new(&mut local_messages); self.text_input.update( &mut tree.children[0], &Event::Mouse(mouse::Event::ButtonPressed( mouse::Button::Left, )), layout, mouse::Cursor::Unavailable, renderer, clipboard, &mut local_shell, viewport, ); shell.request_input_method(local_shell.input_method()); } }); let is_focused = { let text_input_state = tree.children[0] .state .downcast_ref::>(); text_input_state.is_focused() }; if started_focused != is_focused { // Focus changed, invalidate widget tree to force a fresh `view` shell.invalidate_widgets(); if !published_message_to_shell { if is_focused { if let Some(on_open) = self.on_open.take() { shell.publish(on_open); } } else if let Some(on_close) = self.on_close.take() { shell.publish(on_close); } } } } fn mouse_interaction( &self, tree: &widget::Tree, layout: Layout<'_>, cursor: mouse::Cursor, viewport: &Rectangle, renderer: &Renderer, ) -> mouse::Interaction { self.text_input.mouse_interaction( &tree.children[0], layout, cursor, viewport, renderer, ) } fn draw( &self, tree: &widget::Tree, renderer: &mut Renderer, theme: &Theme, _style: &renderer::Style, layout: Layout<'_>, cursor: mouse::Cursor, viewport: &Rectangle, ) { let is_focused = { let text_input_state = tree.children[0] .state .downcast_ref::>(); text_input_state.is_focused() }; let selection = if is_focused || self.selection.is_empty() { None } else { Some(&self.selection) }; self.text_input.draw( &tree.children[0], renderer, theme, layout, cursor, selection, viewport, ); } fn overlay<'b>( &'b mut self, tree: &'b mut widget::Tree, layout: Layout<'_>, _renderer: &Renderer, translation: Vector, ) -> Option> { let is_focused = { let text_input_state = tree.children[0] .state .downcast_ref::>(); text_input_state.is_focused() }; if is_focused { let Menu { menu, filtered_options, hovered_option, .. } = tree.state.downcast_mut::>(); self.state.sync_filtered_options(filtered_options); if filtered_options.options.is_empty() { None } else { let bounds = layout.bounds(); let mut menu = menu::Menu::new( menu, &filtered_options.options, hovered_option, |x| { tree.children[0] .state .downcast_mut::>( ) .unfocus(); (self.on_selected)(x) }, self.on_option_hovered.as_deref(), &self.menu_class, ) .width(bounds.width) .padding(self.padding); if let Some(font) = self.font { menu = menu.font(font); } if let Some(size) = self.size { menu = menu.text_size(size); } Some( menu.overlay( layout.position() + translation, bounds.height, ), ) } } else { None } } } impl<'a, T, Message, Theme, Renderer> From> for Element<'a, Message, Theme, Renderer> where T: Display + Clone + 'static, Message: Clone + 'a, Theme: Catalog + 'a, Renderer: text::Renderer + 'a, { fn from(combo_box: ComboBox<'a, T, Message, Theme, Renderer>) -> Self { Self::new(combo_box) } } /// The theme catalog of a [`ComboBox`]. pub trait Catalog: text_input::Catalog + menu::Catalog { /// The default class for the text input of the [`ComboBox`]. fn default_input<'a>() -> ::Class<'a> { ::default() } /// The default class for the menu of the [`ComboBox`]. fn default_menu<'a>() -> ::Class<'a> { ::default() } } impl Catalog for Theme {} fn search<'a, T, A>( options: impl IntoIterator + 'a, option_matchers: impl IntoIterator + 'a, query: &'a str, ) -> impl Iterator + 'a where A: AsRef + 'a, { let query: Vec = query .to_lowercase() .split(|c: char| !c.is_ascii_alphanumeric()) .map(String::from) .collect(); options .into_iter() .zip(option_matchers) // Make sure each part of the query is found in the option .filter_map(move |(option, matcher)| { if query.iter().all(|part| matcher.as_ref().contains(part)) { Some(option) } else { None } }) } fn build_matchers<'a, T>( options: impl IntoIterator + 'a, ) -> Vec where T: Display + 'a, { options .into_iter() .map(|opt| { let mut matcher = opt.to_string(); matcher.retain(|c| c.is_ascii_alphanumeric()); matcher.to_lowercase() }) .collect() }