//! Pick lists display a dropdown list of 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::pick_list; //! //! struct State { //! favorite: Option, //! } //! //! #[derive(Debug, Clone, Copy, PartialEq, Eq)] //! enum Fruit { //! Apple, //! Orange, //! Strawberry, //! Tomato, //! } //! //! #[derive(Debug, Clone)] //! enum Message { //! FruitSelected(Fruit), //! } //! //! fn view(state: &State) -> Element<'_, Message> { //! let fruits = [ //! Fruit::Apple, //! Fruit::Orange, //! Fruit::Strawberry, //! Fruit::Tomato, //! ]; //! //! pick_list( //! fruits, //! state.favorite, //! Message::FruitSelected, //! ) //! .placeholder("Select your favorite fruit...") //! .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::alignment; use crate::core::keyboard; use crate::core::layout; use crate::core::mouse; use crate::core::overlay; use crate::core::renderer; use crate::core::text::paragraph; use crate::core::text::{self, Text}; use crate::core::touch; use crate::core::widget::tree::{self, Tree}; use crate::core::window; use crate::core::{ Background, Border, Clipboard, Color, Element, Event, Layout, Length, Padding, Pixels, Point, Rectangle, Shell, Size, Theme, Vector, Widget, }; use crate::overlay::menu::{self, Menu}; use std::borrow::Borrow; use std::f32; /// A widget for 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::pick_list; /// /// struct State { /// favorite: Option, /// } /// /// #[derive(Debug, Clone, Copy, PartialEq, Eq)] /// enum Fruit { /// Apple, /// Orange, /// Strawberry, /// Tomato, /// } /// /// #[derive(Debug, Clone)] /// enum Message { /// FruitSelected(Fruit), /// } /// /// fn view(state: &State) -> Element<'_, Message> { /// let fruits = [ /// Fruit::Apple, /// Fruit::Orange, /// Fruit::Strawberry, /// Fruit::Tomato, /// ]; /// /// pick_list( /// fruits, /// state.favorite, /// Message::FruitSelected, /// ) /// .placeholder("Select your favorite fruit...") /// .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 PickList< 'a, T, L, V, Message, Theme = crate::Theme, Renderer = crate::Renderer, > where T: ToString + PartialEq + Clone, L: Borrow<[T]> + 'a, V: Borrow + 'a, Theme: Catalog, Renderer: text::Renderer, { on_select: Box Message + 'a>, on_open: Option, on_close: Option, options: L, placeholder: Option, selected: Option, width: Length, padding: Padding, text_size: Option, text_line_height: text::LineHeight, text_shaping: text::Shaping, font: Option, handle: Handle, class: ::Class<'a>, menu_class: ::Class<'a>, last_status: Option, } impl<'a, T, L, V, Message, Theme, Renderer> PickList<'a, T, L, V, Message, Theme, Renderer> where T: ToString + PartialEq + Clone, L: Borrow<[T]> + 'a, V: Borrow + 'a, Message: Clone, Theme: Catalog, Renderer: text::Renderer, { /// Creates a new [`PickList`] with the given list of options, the current /// selected value, and the message to produce when an option is selected. pub fn new( options: L, selected: Option, on_select: impl Fn(T) -> Message + 'a, ) -> Self { Self { on_select: Box::new(on_select), on_open: None, on_close: None, options, placeholder: None, selected, width: Length::Shrink, padding: crate::button::DEFAULT_PADDING, text_size: None, text_line_height: text::LineHeight::default(), text_shaping: text::Shaping::default(), font: None, handle: Handle::default(), class: ::default(), menu_class: ::default_menu(), last_status: None, } } /// Sets the placeholder of the [`PickList`]. pub fn placeholder(mut self, placeholder: impl Into) -> Self { self.placeholder = Some(placeholder.into()); self } /// Sets the width of the [`PickList`]. pub fn width(mut self, width: impl Into) -> Self { self.width = width.into(); self } /// Sets the [`Padding`] of the [`PickList`]. pub fn padding>(mut self, padding: P) -> Self { self.padding = padding.into(); self } /// Sets the text size of the [`PickList`]. pub fn text_size(mut self, size: impl Into) -> Self { self.text_size = Some(size.into()); self } /// Sets the text [`text::LineHeight`] of the [`PickList`]. pub fn text_line_height( mut self, line_height: impl Into, ) -> Self { self.text_line_height = line_height.into(); self } /// Sets the [`text::Shaping`] strategy of the [`PickList`]. pub fn text_shaping(mut self, shaping: text::Shaping) -> Self { self.text_shaping = shaping; self } /// Sets the font of the [`PickList`]. pub fn font(mut self, font: impl Into) -> Self { self.font = Some(font.into()); self } /// Sets the [`Handle`] of the [`PickList`]. pub fn handle(mut self, handle: Handle) -> Self { self.handle = handle; self } /// Sets the message that will be produced when the [`PickList`] is opened. pub fn on_open(mut self, on_open: Message) -> Self { self.on_open = Some(on_open); self } /// Sets the message that will be produced when the [`PickList`] is closed. pub fn on_close(mut self, on_close: Message) -> Self { self.on_close = Some(on_close); self } /// Sets the style of the [`PickList`]. #[must_use] pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self where ::Class<'a>: From>, { self.class = (Box::new(style) as StyleFn<'a, Theme>).into(); self } /// Sets the style of the [`Menu`]. #[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 [`PickList`]. #[cfg(feature = "advanced")] #[must_use] pub fn class( mut self, class: impl Into<::Class<'a>>, ) -> Self { self.class = class.into(); self } /// Sets the style class of the [`Menu`]. #[cfg(feature = "advanced")] #[must_use] pub fn menu_class( mut self, class: impl Into<::Class<'a>>, ) -> Self { self.menu_class = class.into(); self } } impl<'a, T, L, V, Message, Theme, Renderer> Widget for PickList<'a, T, L, V, Message, Theme, Renderer> where T: Clone + ToString + PartialEq + 'a, L: Borrow<[T]>, V: Borrow, Message: Clone + 'a, Theme: Catalog + 'a, Renderer: text::Renderer + 'a, { fn tag(&self) -> tree::Tag { tree::Tag::of::>() } fn state(&self) -> tree::State { tree::State::new(State::::new()) } fn size(&self) -> Size { Size { width: self.width, height: Length::Shrink, } } fn layout( &self, tree: &mut Tree, renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { let state = tree.state.downcast_mut::>(); let font = self.font.unwrap_or_else(|| renderer.default_font()); let text_size = self.text_size.unwrap_or_else(|| renderer.default_size()); let options = self.options.borrow(); state.options.resize_with(options.len(), Default::default); let option_text = Text { content: "", bounds: Size::new( f32::INFINITY, self.text_line_height.to_absolute(text_size).into(), ), size: text_size, line_height: self.text_line_height, font, horizontal_alignment: alignment::Horizontal::Left, vertical_alignment: alignment::Vertical::Center, shaping: self.text_shaping, wrapping: text::Wrapping::default(), }; for (option, paragraph) in options.iter().zip(state.options.iter_mut()) { let label = option.to_string(); paragraph.update(Text { content: &label, ..option_text }); } if let Some(placeholder) = &self.placeholder { state.placeholder.update(Text { content: placeholder, ..option_text }); } let max_width = match self.width { Length::Shrink => { let labels_width = state.options.iter().fold(0.0, |width, paragraph| { f32::max(width, paragraph.min_width()) }); labels_width.max( self.placeholder .as_ref() .map(|_| state.placeholder.min_width()) .unwrap_or(0.0), ) } _ => 0.0, }; let size = { let intrinsic = Size::new( max_width + text_size.0 + self.padding.left, f32::from(self.text_line_height.to_absolute(text_size)), ); limits .width(self.width) .shrink(self.padding) .resolve(self.width, Length::Shrink, intrinsic) .expand(self.padding) }; layout::Node::new(size) } fn update( &mut self, tree: &mut Tree, event: &Event, layout: Layout<'_>, cursor: mouse::Cursor, _renderer: &Renderer, _clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, _viewport: &Rectangle, ) { let state = tree.state.downcast_mut::>(); match event { Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) | Event::Touch(touch::Event::FingerPressed { .. }) => { if state.is_open { // Event wasn't processed by overlay, so cursor was clicked either outside its // bounds or on the drop-down, either way we close the overlay. state.is_open = false; if let Some(on_close) = &self.on_close { shell.publish(on_close.clone()); } shell.capture_event(); } else if cursor.is_over(layout.bounds()) { let selected = self.selected.as_ref().map(Borrow::borrow); state.is_open = true; state.hovered_option = self .options .borrow() .iter() .position(|option| Some(option) == selected); if let Some(on_open) = &self.on_open { shell.publish(on_open.clone()); } shell.capture_event(); } } Event::Mouse(mouse::Event::WheelScrolled { delta: mouse::ScrollDelta::Lines { y, .. }, }) => { if state.keyboard_modifiers.command() && cursor.is_over(layout.bounds()) && !state.is_open { fn find_next<'a, T: PartialEq>( selected: &'a T, mut options: impl Iterator, ) -> Option<&'a T> { let _ = options.find(|&option| option == selected); options.next() } let options = self.options.borrow(); let selected = self.selected.as_ref().map(Borrow::borrow); let next_option = if *y < 0.0 { if let Some(selected) = selected { find_next(selected, options.iter()) } else { options.first() } } else if *y > 0.0 { if let Some(selected) = selected { find_next(selected, options.iter().rev()) } else { options.last() } } else { None }; if let Some(next_option) = next_option { shell.publish((self.on_select)(next_option.clone())); } shell.capture_event(); } } Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => { state.keyboard_modifiers = *modifiers; } _ => {} }; let status = { let is_hovered = cursor.is_over(layout.bounds()); if state.is_open { Status::Opened { is_hovered } } else if is_hovered { Status::Hovered } else { Status::Active } }; if let Event::Window(window::Event::RedrawRequested(_now)) = event { self.last_status = Some(status); } else if self .last_status .is_some_and(|last_status| last_status != status) { shell.request_redraw(); } } fn mouse_interaction( &self, _tree: &Tree, layout: Layout<'_>, cursor: mouse::Cursor, _viewport: &Rectangle, _renderer: &Renderer, ) -> mouse::Interaction { let bounds = layout.bounds(); let is_mouse_over = cursor.is_over(bounds); if is_mouse_over { mouse::Interaction::Pointer } else { mouse::Interaction::default() } } fn draw( &self, tree: &Tree, renderer: &mut Renderer, theme: &Theme, _style: &renderer::Style, layout: Layout<'_>, _cursor: mouse::Cursor, viewport: &Rectangle, ) { let font = self.font.unwrap_or_else(|| renderer.default_font()); let selected = self.selected.as_ref().map(Borrow::borrow); let state = tree.state.downcast_ref::>(); let bounds = layout.bounds(); let style = Catalog::style( theme, &self.class, self.last_status.unwrap_or(Status::Active), ); renderer.fill_quad( renderer::Quad { bounds, border: style.border, ..renderer::Quad::default() }, style.background, ); let handle = match &self.handle { Handle::Arrow { size } => Some(( Renderer::ICON_FONT, Renderer::ARROW_DOWN_ICON, *size, text::LineHeight::default(), text::Shaping::Basic, )), Handle::Static(Icon { font, code_point, size, line_height, shaping, }) => Some((*font, *code_point, *size, *line_height, *shaping)), Handle::Dynamic { open, closed } => { if state.is_open { Some(( open.font, open.code_point, open.size, open.line_height, open.shaping, )) } else { Some(( closed.font, closed.code_point, closed.size, closed.line_height, closed.shaping, )) } } Handle::None => None, }; if let Some((font, code_point, size, line_height, shaping)) = handle { let size = size.unwrap_or_else(|| renderer.default_size()); renderer.fill_text( Text { content: code_point.to_string(), size, line_height, font, bounds: Size::new( bounds.width, f32::from(line_height.to_absolute(size)), ), horizontal_alignment: alignment::Horizontal::Right, vertical_alignment: alignment::Vertical::Center, shaping, wrapping: text::Wrapping::default(), }, Point::new( bounds.x + bounds.width - self.padding.right, bounds.center_y(), ), style.handle_color, *viewport, ); } let label = selected.map(ToString::to_string); if let Some(label) = label.or_else(|| self.placeholder.clone()) { let text_size = self.text_size.unwrap_or_else(|| renderer.default_size()); renderer.fill_text( Text { content: label, size: text_size, line_height: self.text_line_height, font, bounds: Size::new( bounds.width - self.padding.horizontal(), f32::from(self.text_line_height.to_absolute(text_size)), ), horizontal_alignment: alignment::Horizontal::Left, vertical_alignment: alignment::Vertical::Center, shaping: self.text_shaping, wrapping: text::Wrapping::default(), }, Point::new(bounds.x + self.padding.left, bounds.center_y()), if selected.is_some() { style.text_color } else { style.placeholder_color }, *viewport, ); } } fn overlay<'b>( &'b mut self, tree: &'b mut Tree, layout: Layout<'_>, renderer: &Renderer, translation: Vector, ) -> Option> { let state = tree.state.downcast_mut::>(); let font = self.font.unwrap_or_else(|| renderer.default_font()); if state.is_open { let bounds = layout.bounds(); let on_select = &self.on_select; let mut menu = Menu::new( &mut state.menu, self.options.borrow(), &mut state.hovered_option, |option| { state.is_open = false; (on_select)(option) }, None, &self.menu_class, ) .width(bounds.width) .padding(self.padding) .font(font) .text_shaping(self.text_shaping); if let Some(text_size) = self.text_size { menu = menu.text_size(text_size); } Some(menu.overlay(layout.position() + translation, bounds.height)) } else { None } } } impl<'a, T, L, V, Message, Theme, Renderer> From> for Element<'a, Message, Theme, Renderer> where T: Clone + ToString + PartialEq + 'a, L: Borrow<[T]> + 'a, V: Borrow + 'a, Message: Clone + 'a, Theme: Catalog + 'a, Renderer: text::Renderer + 'a, { fn from( pick_list: PickList<'a, T, L, V, Message, Theme, Renderer>, ) -> Self { Self::new(pick_list) } } #[derive(Debug)] struct State { menu: menu::State, keyboard_modifiers: keyboard::Modifiers, is_open: bool, hovered_option: Option, options: Vec>, placeholder: paragraph::Plain

, } impl State

{ /// Creates a new [`State`] for a [`PickList`]. fn new() -> Self { Self { menu: menu::State::default(), keyboard_modifiers: keyboard::Modifiers::default(), is_open: bool::default(), hovered_option: Option::default(), options: Vec::new(), placeholder: paragraph::Plain::default(), } } } impl Default for State

{ fn default() -> Self { Self::new() } } /// The handle to the right side of the [`PickList`]. #[derive(Debug, Clone, PartialEq)] pub enum Handle { /// Displays an arrow icon (▼). /// /// This is the default. Arrow { /// Font size of the content. size: Option, }, /// A custom static handle. Static(Icon), /// A custom dynamic handle. Dynamic { /// The [`Icon`] used when [`PickList`] is closed. closed: Icon, /// The [`Icon`] used when [`PickList`] is open. open: Icon, }, /// No handle will be shown. None, } impl Default for Handle { fn default() -> Self { Self::Arrow { size: None } } } /// The icon of a [`Handle`]. #[derive(Debug, Clone, PartialEq)] pub struct Icon { /// Font that will be used to display the `code_point`, pub font: Font, /// The unicode code point that will be used as the icon. pub code_point: char, /// Font size of the content. pub size: Option, /// Line height of the content. pub line_height: text::LineHeight, /// The shaping strategy of the icon. pub shaping: text::Shaping, } /// The possible status of a [`PickList`]. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Status { /// The [`PickList`] can be interacted with. Active, /// The [`PickList`] is being hovered. Hovered, /// The [`PickList`] is open. Opened { /// Whether the [`PickList`] is hovered, while open. is_hovered: bool, }, } /// The appearance of a pick list. #[derive(Debug, Clone, Copy, PartialEq)] pub struct Style { /// The text [`Color`] of the pick list. pub text_color: Color, /// The placeholder [`Color`] of the pick list. pub placeholder_color: Color, /// The handle [`Color`] of the pick list. pub handle_color: Color, /// The [`Background`] of the pick list. pub background: Background, /// The [`Border`] of the pick list. pub border: Border, } /// The theme catalog of a [`PickList`]. pub trait Catalog: menu::Catalog { /// The item class of the [`Catalog`]. type Class<'a>; /// The default class produced by the [`Catalog`]. fn default<'a>() -> ::Class<'a>; /// The default class for the menu of the [`PickList`]. fn default_menu<'a>() -> ::Class<'a> { ::default() } /// The [`Style`] of a class with the given status. fn style( &self, class: &::Class<'_>, status: Status, ) -> Style; } /// A styling function for a [`PickList`]. /// /// This is just a boxed closure: `Fn(&Theme, Status) -> Style`. pub type StyleFn<'a, Theme> = Box Style + 'a>; impl Catalog for Theme { type Class<'a> = StyleFn<'a, Self>; fn default<'a>() -> StyleFn<'a, Self> { Box::new(default) } fn style(&self, class: &StyleFn<'_, Self>, status: Status) -> Style { class(self, status) } } /// The default style of the field of a [`PickList`]. pub fn default(theme: &Theme, status: Status) -> Style { let palette = theme.extended_palette(); let active = Style { text_color: palette.background.weak.text, background: palette.background.weak.color.into(), placeholder_color: palette.background.strong.color, handle_color: palette.background.weak.text, border: Border { radius: 2.0.into(), width: 1.0, color: palette.background.strong.color, }, }; match status { Status::Active => active, Status::Hovered | Status::Opened { .. } => Style { border: Border { color: palette.primary.strong.color, ..active.border }, ..active }, } }