//! Display a dropdown list of selectable values. use crate::event::{self, Event}; use crate::layout; use crate::mouse; use crate::overlay; use crate::overlay::menu::{self, Menu}; use crate::scrollable; use crate::text; use crate::{ Clipboard, Element, Hasher, Layout, Length, Point, Rectangle, Size, Widget, }; use std::borrow::Cow; /// A widget for selecting a single value from a list of options. #[allow(missing_debug_implementations)] pub struct PickList<'a, T, Message, Renderer: self::Renderer> where [T]: ToOwned>, { menu: &'a mut menu::State, is_open: &'a mut bool, hovered_option: &'a mut Option, last_selection: &'a mut Option, on_selected: Box Message>, options: Cow<'a, [T]>, selected: Option, width: Length, padding: u16, text_size: Option, font: Renderer::Font, style: ::Style, } /// The local state of a [`PickList`]. #[derive(Debug, Clone)] pub struct State { menu: menu::State, is_open: bool, hovered_option: Option, last_selection: Option, } impl Default for State { fn default() -> Self { Self { menu: menu::State::default(), is_open: bool::default(), hovered_option: Option::default(), last_selection: Option::default(), } } } impl<'a, T: 'a, Message, Renderer: self::Renderer> PickList<'a, T, Message, Renderer> where T: ToString + Eq, [T]: ToOwned>, { /// Creates a new [`PickList`] with the given [`State`], a list of options, /// the current selected value, and the message to produce when an option is /// selected. pub fn new( state: &'a mut State, options: impl Into>, selected: Option, on_selected: impl Fn(T) -> Message + 'static, ) -> Self { let State { menu, is_open, hovered_option, last_selection, } = state; Self { menu, is_open, hovered_option, last_selection, on_selected: Box::new(on_selected), options: options.into(), selected, width: Length::Shrink, text_size: None, padding: Renderer::DEFAULT_PADDING, font: Default::default(), style: Default::default(), } } /// Sets the width of the [`PickList`]. pub fn width(mut self, width: Length) -> Self { self.width = width; self } /// Sets the padding of the [`PickList`]. pub fn padding(mut self, padding: u16) -> Self { self.padding = padding; self } /// Sets the text size of the [`PickList`]. pub fn text_size(mut self, size: u16) -> Self { self.text_size = Some(size); self } /// Sets the font of the [`PickList`]. pub fn font(mut self, font: Renderer::Font) -> Self { self.font = font; self } /// Sets the style of the [`PickList`]. pub fn style( mut self, style: impl Into<::Style>, ) -> Self { self.style = style.into(); self } } impl<'a, T: 'a, Message, Renderer> Widget for PickList<'a, T, Message, Renderer> where T: Clone + ToString + Eq, [T]: ToOwned>, Message: 'static, Renderer: self::Renderer + scrollable::Renderer + 'a, { fn width(&self) -> Length { self.width } fn height(&self) -> Length { Length::Shrink } fn layout( &self, renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { use std::f32; let limits = limits .width(self.width) .height(Length::Shrink) .pad(f32::from(self.padding)); let text_size = self.text_size.unwrap_or(renderer.default_size()); let max_width = match self.width { Length::Shrink => { let labels = self.options.iter().map(ToString::to_string); labels .map(|label| { let (width, _) = renderer.measure( &label, text_size, Renderer::Font::default(), Size::new(f32::INFINITY, f32::INFINITY), ); width.round() as u32 }) .max() .unwrap_or(100) } _ => 0, }; let size = { let intrinsic = Size::new( max_width as f32 + f32::from(text_size) + f32::from(self.padding), f32::from(text_size), ); limits.resolve(intrinsic).pad(f32::from(self.padding)) }; layout::Node::new(size) } fn hash_layout(&self, state: &mut Hasher) { use std::hash::Hash as _; match self.width { Length::Shrink => { self.options .iter() .map(ToString::to_string) .for_each(|label| label.hash(state)); } _ => { self.width.hash(state); } } } fn on_event( &mut self, event: Event, layout: Layout<'_>, cursor_position: Point, messages: &mut Vec, _renderer: &Renderer, _clipboard: Option<&dyn Clipboard>, ) -> event::Status { match event { Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => { let event_status = if *self.is_open { // TODO: Encode cursor availability in the type system *self.is_open = cursor_position.x < 0.0 || cursor_position.y < 0.0; event::Status::Captured } else if layout.bounds().contains(cursor_position) { let selected = self.selected.as_ref(); *self.is_open = true; *self.hovered_option = self .options .iter() .position(|option| Some(option) == selected); event::Status::Captured } else { event::Status::Ignored }; if let Some(last_selection) = self.last_selection.take() { messages.push((self.on_selected)(last_selection)); *self.is_open = false; event::Status::Captured } else { event_status } } _ => event::Status::Ignored, } } fn draw( &self, renderer: &mut Renderer, _defaults: &Renderer::Defaults, layout: Layout<'_>, cursor_position: Point, _viewport: &Rectangle, ) -> Renderer::Output { self::Renderer::draw( renderer, layout.bounds(), cursor_position, self.selected.as_ref().map(ToString::to_string), self.padding, self.text_size.unwrap_or(renderer.default_size()), self.font, &self.style, ) } fn overlay( &mut self, layout: Layout<'_>, ) -> Option> { if *self.is_open { let bounds = layout.bounds(); let mut menu = Menu::new( &mut self.menu, &self.options, &mut self.hovered_option, &mut self.last_selection, ) .width(bounds.width.round() as u16) .padding(self.padding) .font(self.font) .style(Renderer::menu_style(&self.style)); if let Some(text_size) = self.text_size { menu = menu.text_size(text_size); } Some(menu.overlay(layout.position(), bounds.height)) } else { None } } } /// The renderer of a [`PickList`]. /// /// Your [renderer] will need to implement this trait before being /// able to use a [`PickList`] in your user interface. /// /// [renderer]: crate::renderer pub trait Renderer: text::Renderer + menu::Renderer { /// The default padding of a [`PickList`]. const DEFAULT_PADDING: u16; /// The [`PickList`] style supported by this renderer. type Style: Default; /// Returns the style of the [`Menu`] of the [`PickList`]. fn menu_style( style: &::Style, ) -> ::Style; /// Draws a [`PickList`]. fn draw( &mut self, bounds: Rectangle, cursor_position: Point, selected: Option, padding: u16, text_size: u16, font: Self::Font, style: &::Style, ) -> Self::Output; } impl<'a, T: 'a, Message, Renderer> Into> for PickList<'a, T, Message, Renderer> where T: Clone + ToString + Eq, [T]: ToOwned>, Renderer: self::Renderer + 'a, Message: 'static, { fn into(self) -> Element<'a, Message, Renderer> { Element::new(self) } }