iced_widget/
pick_list.rs

1//! Pick lists display a dropdown list of selectable options.
2//!
3//! # Example
4//! ```no_run
5//! # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; }
6//! # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
7//! #
8//! use iced::widget::pick_list;
9//!
10//! struct State {
11//!    favorite: Option<Fruit>,
12//! }
13//!
14//! #[derive(Debug, Clone, Copy, PartialEq, Eq)]
15//! enum Fruit {
16//!     Apple,
17//!     Orange,
18//!     Strawberry,
19//!     Tomato,
20//! }
21//!
22//! #[derive(Debug, Clone)]
23//! enum Message {
24//!     FruitSelected(Fruit),
25//! }
26//!
27//! fn view(state: &State) -> Element<'_, Message> {
28//!     let fruits = [
29//!         Fruit::Apple,
30//!         Fruit::Orange,
31//!         Fruit::Strawberry,
32//!         Fruit::Tomato,
33//!     ];
34//!
35//!     pick_list(
36//!         fruits,
37//!         state.favorite,
38//!         Message::FruitSelected,
39//!     )
40//!     .placeholder("Select your favorite fruit...")
41//!     .into()
42//! }
43//!
44//! fn update(state: &mut State, message: Message) {
45//!     match message {
46//!         Message::FruitSelected(fruit) => {
47//!             state.favorite = Some(fruit);
48//!         }
49//!     }
50//! }
51//!
52//! impl std::fmt::Display for Fruit {
53//!     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
54//!         f.write_str(match self {
55//!             Self::Apple => "Apple",
56//!             Self::Orange => "Orange",
57//!             Self::Strawberry => "Strawberry",
58//!             Self::Tomato => "Tomato",
59//!         })
60//!     }
61//! }
62//! ```
63use crate::core::alignment;
64use crate::core::keyboard;
65use crate::core::layout;
66use crate::core::mouse;
67use crate::core::overlay;
68use crate::core::renderer;
69use crate::core::text::paragraph;
70use crate::core::text::{self, Text};
71use crate::core::touch;
72use crate::core::widget::tree::{self, Tree};
73use crate::core::window;
74use crate::core::{
75    Background, Border, Clipboard, Color, Element, Event, Layout, Length, Padding, Pixels, Point,
76    Rectangle, Shell, Size, Theme, Vector, Widget,
77};
78use crate::overlay::menu::{self, Menu};
79
80use std::borrow::Borrow;
81use std::f32;
82
83/// A widget for selecting a single value from a list of options.
84///
85/// # Example
86/// ```no_run
87/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; }
88/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
89/// #
90/// use iced::widget::pick_list;
91///
92/// struct State {
93///    favorite: Option<Fruit>,
94/// }
95///
96/// #[derive(Debug, Clone, Copy, PartialEq, Eq)]
97/// enum Fruit {
98///     Apple,
99///     Orange,
100///     Strawberry,
101///     Tomato,
102/// }
103///
104/// #[derive(Debug, Clone)]
105/// enum Message {
106///     FruitSelected(Fruit),
107/// }
108///
109/// fn view(state: &State) -> Element<'_, Message> {
110///     let fruits = [
111///         Fruit::Apple,
112///         Fruit::Orange,
113///         Fruit::Strawberry,
114///         Fruit::Tomato,
115///     ];
116///
117///     pick_list(
118///         fruits,
119///         state.favorite,
120///         Message::FruitSelected,
121///     )
122///     .placeholder("Select your favorite fruit...")
123///     .into()
124/// }
125///
126/// fn update(state: &mut State, message: Message) {
127///     match message {
128///         Message::FruitSelected(fruit) => {
129///             state.favorite = Some(fruit);
130///         }
131///     }
132/// }
133///
134/// impl std::fmt::Display for Fruit {
135///     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
136///         f.write_str(match self {
137///             Self::Apple => "Apple",
138///             Self::Orange => "Orange",
139///             Self::Strawberry => "Strawberry",
140///             Self::Tomato => "Tomato",
141///         })
142///     }
143/// }
144/// ```
145pub struct PickList<'a, T, L, V, Message, Theme = crate::Theme, Renderer = crate::Renderer>
146where
147    T: ToString + PartialEq + Clone,
148    L: Borrow<[T]> + 'a,
149    V: Borrow<T> + 'a,
150    Theme: Catalog,
151    Renderer: text::Renderer,
152{
153    on_select: Box<dyn Fn(T) -> Message + 'a>,
154    on_open: Option<Message>,
155    on_close: Option<Message>,
156    options: L,
157    placeholder: Option<String>,
158    selected: Option<V>,
159    width: Length,
160    padding: Padding,
161    text_size: Option<Pixels>,
162    text_line_height: text::LineHeight,
163    text_shaping: text::Shaping,
164    font: Option<Renderer::Font>,
165    handle: Handle<Renderer::Font>,
166    class: <Theme as Catalog>::Class<'a>,
167    menu_class: <Theme as menu::Catalog>::Class<'a>,
168    last_status: Option<Status>,
169    menu_height: Length,
170}
171
172impl<'a, T, L, V, Message, Theme, Renderer> PickList<'a, T, L, V, Message, Theme, Renderer>
173where
174    T: ToString + PartialEq + Clone,
175    L: Borrow<[T]> + 'a,
176    V: Borrow<T> + 'a,
177    Message: Clone,
178    Theme: Catalog,
179    Renderer: text::Renderer,
180{
181    /// Creates a new [`PickList`] with the given list of options, the current
182    /// selected value, and the message to produce when an option is selected.
183    pub fn new(options: L, selected: Option<V>, on_select: impl Fn(T) -> Message + 'a) -> Self {
184        Self {
185            on_select: Box::new(on_select),
186            on_open: None,
187            on_close: None,
188            options,
189            placeholder: None,
190            selected,
191            width: Length::Shrink,
192            padding: crate::button::DEFAULT_PADDING,
193            text_size: None,
194            text_line_height: text::LineHeight::default(),
195            text_shaping: text::Shaping::default(),
196            font: None,
197            handle: Handle::default(),
198            class: <Theme as Catalog>::default(),
199            menu_class: <Theme as Catalog>::default_menu(),
200            last_status: None,
201            menu_height: Length::Shrink,
202        }
203    }
204
205    /// Sets the placeholder of the [`PickList`].
206    pub fn placeholder(mut self, placeholder: impl Into<String>) -> Self {
207        self.placeholder = Some(placeholder.into());
208        self
209    }
210
211    /// Sets the width of the [`PickList`].
212    pub fn width(mut self, width: impl Into<Length>) -> Self {
213        self.width = width.into();
214        self
215    }
216
217    /// Sets the height of the [`Menu`].
218    pub fn menu_height(mut self, menu_height: impl Into<Length>) -> Self {
219        self.menu_height = menu_height.into();
220        self
221    }
222
223    /// Sets the [`Padding`] of the [`PickList`].
224    pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self {
225        self.padding = padding.into();
226        self
227    }
228
229    /// Sets the text size of the [`PickList`].
230    pub fn text_size(mut self, size: impl Into<Pixels>) -> Self {
231        self.text_size = Some(size.into());
232        self
233    }
234
235    /// Sets the text [`text::LineHeight`] of the [`PickList`].
236    pub fn text_line_height(mut self, line_height: impl Into<text::LineHeight>) -> Self {
237        self.text_line_height = line_height.into();
238        self
239    }
240
241    /// Sets the [`text::Shaping`] strategy of the [`PickList`].
242    pub fn text_shaping(mut self, shaping: text::Shaping) -> Self {
243        self.text_shaping = shaping;
244        self
245    }
246
247    /// Sets the font of the [`PickList`].
248    pub fn font(mut self, font: impl Into<Renderer::Font>) -> Self {
249        self.font = Some(font.into());
250        self
251    }
252
253    /// Sets the [`Handle`] of the [`PickList`].
254    pub fn handle(mut self, handle: Handle<Renderer::Font>) -> Self {
255        self.handle = handle;
256        self
257    }
258
259    /// Sets the message that will be produced when the [`PickList`] is opened.
260    pub fn on_open(mut self, on_open: Message) -> Self {
261        self.on_open = Some(on_open);
262        self
263    }
264
265    /// Sets the message that will be produced when the [`PickList`] is closed.
266    pub fn on_close(mut self, on_close: Message) -> Self {
267        self.on_close = Some(on_close);
268        self
269    }
270
271    /// Sets the style of the [`PickList`].
272    #[must_use]
273    pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self
274    where
275        <Theme as Catalog>::Class<'a>: From<StyleFn<'a, Theme>>,
276    {
277        self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
278        self
279    }
280
281    /// Sets the style of the [`Menu`].
282    #[must_use]
283    pub fn menu_style(mut self, style: impl Fn(&Theme) -> menu::Style + 'a) -> Self
284    where
285        <Theme as menu::Catalog>::Class<'a>: From<menu::StyleFn<'a, Theme>>,
286    {
287        self.menu_class = (Box::new(style) as menu::StyleFn<'a, Theme>).into();
288        self
289    }
290
291    /// Sets the style class of the [`PickList`].
292    #[cfg(feature = "advanced")]
293    #[must_use]
294    pub fn class(mut self, class: impl Into<<Theme as Catalog>::Class<'a>>) -> Self {
295        self.class = class.into();
296        self
297    }
298
299    /// Sets the style class of the [`Menu`].
300    #[cfg(feature = "advanced")]
301    #[must_use]
302    pub fn menu_class(mut self, class: impl Into<<Theme as menu::Catalog>::Class<'a>>) -> Self {
303        self.menu_class = class.into();
304        self
305    }
306}
307
308impl<'a, T, L, V, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
309    for PickList<'a, T, L, V, Message, Theme, Renderer>
310where
311    T: Clone + ToString + PartialEq + 'a,
312    L: Borrow<[T]>,
313    V: Borrow<T>,
314    Message: Clone + 'a,
315    Theme: Catalog + 'a,
316    Renderer: text::Renderer + 'a,
317{
318    fn tag(&self) -> tree::Tag {
319        tree::Tag::of::<State<Renderer::Paragraph>>()
320    }
321
322    fn state(&self) -> tree::State {
323        tree::State::new(State::<Renderer::Paragraph>::new())
324    }
325
326    fn size(&self) -> Size<Length> {
327        Size {
328            width: self.width,
329            height: Length::Shrink,
330        }
331    }
332
333    fn layout(
334        &mut self,
335        tree: &mut Tree,
336        renderer: &Renderer,
337        limits: &layout::Limits,
338    ) -> layout::Node {
339        let state = tree.state.downcast_mut::<State<Renderer::Paragraph>>();
340
341        let font = self.font.unwrap_or_else(|| renderer.default_font());
342        let text_size = self.text_size.unwrap_or_else(|| renderer.default_size());
343        let options = self.options.borrow();
344
345        state.options.resize_with(options.len(), Default::default);
346
347        let option_text = Text {
348            content: "",
349            bounds: Size::new(
350                f32::INFINITY,
351                self.text_line_height.to_absolute(text_size).into(),
352            ),
353            size: text_size,
354            line_height: self.text_line_height,
355            font,
356            align_x: text::Alignment::Default,
357            align_y: alignment::Vertical::Center,
358            shaping: self.text_shaping,
359            wrapping: text::Wrapping::default(),
360        };
361
362        for (option, paragraph) in options.iter().zip(state.options.iter_mut()) {
363            let label = option.to_string();
364
365            let _ = paragraph.update(Text {
366                content: &label,
367                ..option_text
368            });
369        }
370
371        if let Some(placeholder) = &self.placeholder {
372            let _ = state.placeholder.update(Text {
373                content: placeholder,
374                ..option_text
375            });
376        }
377
378        let max_width = match self.width {
379            Length::Shrink => {
380                let labels_width = state.options.iter().fold(0.0, |width, paragraph| {
381                    f32::max(width, paragraph.min_width())
382                });
383
384                labels_width.max(
385                    self.placeholder
386                        .as_ref()
387                        .map(|_| state.placeholder.min_width())
388                        .unwrap_or(0.0),
389                )
390            }
391            _ => 0.0,
392        };
393
394        let size = {
395            let intrinsic = Size::new(
396                max_width + text_size.0 + self.padding.left,
397                f32::from(self.text_line_height.to_absolute(text_size)),
398            );
399
400            limits
401                .width(self.width)
402                .shrink(self.padding)
403                .resolve(self.width, Length::Shrink, intrinsic)
404                .expand(self.padding)
405        };
406
407        layout::Node::new(size)
408    }
409
410    fn update(
411        &mut self,
412        tree: &mut Tree,
413        event: &Event,
414        layout: Layout<'_>,
415        cursor: mouse::Cursor,
416        _renderer: &Renderer,
417        _clipboard: &mut dyn Clipboard,
418        shell: &mut Shell<'_, Message>,
419        _viewport: &Rectangle,
420    ) {
421        let state = tree.state.downcast_mut::<State<Renderer::Paragraph>>();
422
423        match event {
424            Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
425            | Event::Touch(touch::Event::FingerPressed { .. }) => {
426                if state.is_open {
427                    // Event wasn't processed by overlay, so cursor was clicked either outside its
428                    // bounds or on the drop-down, either way we close the overlay.
429                    state.is_open = false;
430
431                    if let Some(on_close) = &self.on_close {
432                        shell.publish(on_close.clone());
433                    }
434
435                    shell.capture_event();
436                } else if cursor.is_over(layout.bounds()) {
437                    let selected = self.selected.as_ref().map(Borrow::borrow);
438
439                    state.is_open = true;
440                    state.hovered_option = self
441                        .options
442                        .borrow()
443                        .iter()
444                        .position(|option| Some(option) == selected);
445
446                    if let Some(on_open) = &self.on_open {
447                        shell.publish(on_open.clone());
448                    }
449
450                    shell.capture_event();
451                }
452            }
453            Event::Mouse(mouse::Event::WheelScrolled {
454                delta: mouse::ScrollDelta::Lines { y, .. },
455            }) => {
456                if state.keyboard_modifiers.command()
457                    && cursor.is_over(layout.bounds())
458                    && !state.is_open
459                {
460                    fn find_next<'a, T: PartialEq>(
461                        selected: &'a T,
462                        mut options: impl Iterator<Item = &'a T>,
463                    ) -> Option<&'a T> {
464                        let _ = options.find(|&option| option == selected);
465
466                        options.next()
467                    }
468
469                    let options = self.options.borrow();
470                    let selected = self.selected.as_ref().map(Borrow::borrow);
471
472                    let next_option = if *y < 0.0 {
473                        if let Some(selected) = selected {
474                            find_next(selected, options.iter())
475                        } else {
476                            options.first()
477                        }
478                    } else if *y > 0.0 {
479                        if let Some(selected) = selected {
480                            find_next(selected, options.iter().rev())
481                        } else {
482                            options.last()
483                        }
484                    } else {
485                        None
486                    };
487
488                    if let Some(next_option) = next_option {
489                        shell.publish((self.on_select)(next_option.clone()));
490                    }
491
492                    shell.capture_event();
493                }
494            }
495            Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => {
496                state.keyboard_modifiers = *modifiers;
497            }
498            _ => {}
499        };
500
501        let status = {
502            let is_hovered = cursor.is_over(layout.bounds());
503
504            if state.is_open {
505                Status::Opened { is_hovered }
506            } else if is_hovered {
507                Status::Hovered
508            } else {
509                Status::Active
510            }
511        };
512
513        if let Event::Window(window::Event::RedrawRequested(_now)) = event {
514            self.last_status = Some(status);
515        } else if self
516            .last_status
517            .is_some_and(|last_status| last_status != status)
518        {
519            shell.request_redraw();
520        }
521    }
522
523    fn mouse_interaction(
524        &self,
525        _tree: &Tree,
526        layout: Layout<'_>,
527        cursor: mouse::Cursor,
528        _viewport: &Rectangle,
529        _renderer: &Renderer,
530    ) -> mouse::Interaction {
531        let bounds = layout.bounds();
532        let is_mouse_over = cursor.is_over(bounds);
533
534        if is_mouse_over {
535            mouse::Interaction::Pointer
536        } else {
537            mouse::Interaction::default()
538        }
539    }
540
541    fn draw(
542        &self,
543        tree: &Tree,
544        renderer: &mut Renderer,
545        theme: &Theme,
546        _style: &renderer::Style,
547        layout: Layout<'_>,
548        _cursor: mouse::Cursor,
549        viewport: &Rectangle,
550    ) {
551        let font = self.font.unwrap_or_else(|| renderer.default_font());
552        let selected = self.selected.as_ref().map(Borrow::borrow);
553        let state = tree.state.downcast_ref::<State<Renderer::Paragraph>>();
554
555        let bounds = layout.bounds();
556
557        let style = Catalog::style(
558            theme,
559            &self.class,
560            self.last_status.unwrap_or(Status::Active),
561        );
562
563        renderer.fill_quad(
564            renderer::Quad {
565                bounds,
566                border: style.border,
567                ..renderer::Quad::default()
568            },
569            style.background,
570        );
571
572        let handle = match &self.handle {
573            Handle::Arrow { size } => Some((
574                Renderer::ICON_FONT,
575                Renderer::ARROW_DOWN_ICON,
576                *size,
577                text::LineHeight::default(),
578                text::Shaping::Basic,
579            )),
580            Handle::Static(Icon {
581                font,
582                code_point,
583                size,
584                line_height,
585                shaping,
586            }) => Some((*font, *code_point, *size, *line_height, *shaping)),
587            Handle::Dynamic { open, closed } => {
588                if state.is_open {
589                    Some((
590                        open.font,
591                        open.code_point,
592                        open.size,
593                        open.line_height,
594                        open.shaping,
595                    ))
596                } else {
597                    Some((
598                        closed.font,
599                        closed.code_point,
600                        closed.size,
601                        closed.line_height,
602                        closed.shaping,
603                    ))
604                }
605            }
606            Handle::None => None,
607        };
608
609        if let Some((font, code_point, size, line_height, shaping)) = handle {
610            let size = size.unwrap_or_else(|| renderer.default_size());
611
612            renderer.fill_text(
613                Text {
614                    content: code_point.to_string(),
615                    size,
616                    line_height,
617                    font,
618                    bounds: Size::new(bounds.width, f32::from(line_height.to_absolute(size))),
619                    align_x: text::Alignment::Right,
620                    align_y: alignment::Vertical::Center,
621                    shaping,
622                    wrapping: text::Wrapping::default(),
623                },
624                Point::new(
625                    bounds.x + bounds.width - self.padding.right,
626                    bounds.center_y(),
627                ),
628                style.handle_color,
629                *viewport,
630            );
631        }
632
633        let label = selected.map(ToString::to_string);
634
635        if let Some(label) = label.or_else(|| self.placeholder.clone()) {
636            let text_size = self.text_size.unwrap_or_else(|| renderer.default_size());
637
638            renderer.fill_text(
639                Text {
640                    content: label,
641                    size: text_size,
642                    line_height: self.text_line_height,
643                    font,
644                    bounds: Size::new(
645                        bounds.width - self.padding.x(),
646                        f32::from(self.text_line_height.to_absolute(text_size)),
647                    ),
648                    align_x: text::Alignment::Default,
649                    align_y: alignment::Vertical::Center,
650                    shaping: self.text_shaping,
651                    wrapping: text::Wrapping::default(),
652                },
653                Point::new(bounds.x + self.padding.left, bounds.center_y()),
654                if selected.is_some() {
655                    style.text_color
656                } else {
657                    style.placeholder_color
658                },
659                *viewport,
660            );
661        }
662    }
663
664    fn overlay<'b>(
665        &'b mut self,
666        tree: &'b mut Tree,
667        layout: Layout<'_>,
668        renderer: &Renderer,
669        viewport: &Rectangle,
670        translation: Vector,
671    ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
672        let state = tree.state.downcast_mut::<State<Renderer::Paragraph>>();
673        let font = self.font.unwrap_or_else(|| renderer.default_font());
674
675        if state.is_open {
676            let bounds = layout.bounds();
677
678            let on_select = &self.on_select;
679
680            let mut menu = Menu::new(
681                &mut state.menu,
682                self.options.borrow(),
683                &mut state.hovered_option,
684                |option| {
685                    state.is_open = false;
686
687                    (on_select)(option)
688                },
689                None,
690                &self.menu_class,
691            )
692            .width(bounds.width)
693            .padding(self.padding)
694            .font(font)
695            .text_shaping(self.text_shaping);
696
697            if let Some(text_size) = self.text_size {
698                menu = menu.text_size(text_size);
699            }
700
701            Some(menu.overlay(
702                layout.position() + translation,
703                *viewport,
704                bounds.height,
705                self.menu_height,
706            ))
707        } else {
708            None
709        }
710    }
711}
712
713impl<'a, T, L, V, Message, Theme, Renderer> From<PickList<'a, T, L, V, Message, Theme, Renderer>>
714    for Element<'a, Message, Theme, Renderer>
715where
716    T: Clone + ToString + PartialEq + 'a,
717    L: Borrow<[T]> + 'a,
718    V: Borrow<T> + 'a,
719    Message: Clone + 'a,
720    Theme: Catalog + 'a,
721    Renderer: text::Renderer + 'a,
722{
723    fn from(pick_list: PickList<'a, T, L, V, Message, Theme, Renderer>) -> Self {
724        Self::new(pick_list)
725    }
726}
727
728#[derive(Debug)]
729struct State<P: text::Paragraph> {
730    menu: menu::State,
731    keyboard_modifiers: keyboard::Modifiers,
732    is_open: bool,
733    hovered_option: Option<usize>,
734    options: Vec<paragraph::Plain<P>>,
735    placeholder: paragraph::Plain<P>,
736}
737
738impl<P: text::Paragraph> State<P> {
739    /// Creates a new [`State`] for a [`PickList`].
740    fn new() -> Self {
741        Self {
742            menu: menu::State::default(),
743            keyboard_modifiers: keyboard::Modifiers::default(),
744            is_open: bool::default(),
745            hovered_option: Option::default(),
746            options: Vec::new(),
747            placeholder: paragraph::Plain::default(),
748        }
749    }
750}
751
752impl<P: text::Paragraph> Default for State<P> {
753    fn default() -> Self {
754        Self::new()
755    }
756}
757
758/// The handle to the right side of the [`PickList`].
759#[derive(Debug, Clone, PartialEq)]
760pub enum Handle<Font> {
761    /// Displays an arrow icon (▼).
762    ///
763    /// This is the default.
764    Arrow {
765        /// Font size of the content.
766        size: Option<Pixels>,
767    },
768    /// A custom static handle.
769    Static(Icon<Font>),
770    /// A custom dynamic handle.
771    Dynamic {
772        /// The [`Icon`] used when [`PickList`] is closed.
773        closed: Icon<Font>,
774        /// The [`Icon`] used when [`PickList`] is open.
775        open: Icon<Font>,
776    },
777    /// No handle will be shown.
778    None,
779}
780
781impl<Font> Default for Handle<Font> {
782    fn default() -> Self {
783        Self::Arrow { size: None }
784    }
785}
786
787/// The icon of a [`Handle`].
788#[derive(Debug, Clone, PartialEq)]
789pub struct Icon<Font> {
790    /// Font that will be used to display the `code_point`,
791    pub font: Font,
792    /// The unicode code point that will be used as the icon.
793    pub code_point: char,
794    /// Font size of the content.
795    pub size: Option<Pixels>,
796    /// Line height of the content.
797    pub line_height: text::LineHeight,
798    /// The shaping strategy of the icon.
799    pub shaping: text::Shaping,
800}
801
802/// The possible status of a [`PickList`].
803#[derive(Debug, Clone, Copy, PartialEq, Eq)]
804pub enum Status {
805    /// The [`PickList`] can be interacted with.
806    Active,
807    /// The [`PickList`] is being hovered.
808    Hovered,
809    /// The [`PickList`] is open.
810    Opened {
811        /// Whether the [`PickList`] is hovered, while open.
812        is_hovered: bool,
813    },
814}
815
816/// The appearance of a pick list.
817#[derive(Debug, Clone, Copy, PartialEq)]
818pub struct Style {
819    /// The text [`Color`] of the pick list.
820    pub text_color: Color,
821    /// The placeholder [`Color`] of the pick list.
822    pub placeholder_color: Color,
823    /// The handle [`Color`] of the pick list.
824    pub handle_color: Color,
825    /// The [`Background`] of the pick list.
826    pub background: Background,
827    /// The [`Border`] of the pick list.
828    pub border: Border,
829}
830
831/// The theme catalog of a [`PickList`].
832pub trait Catalog: menu::Catalog {
833    /// The item class of the [`Catalog`].
834    type Class<'a>;
835
836    /// The default class produced by the [`Catalog`].
837    fn default<'a>() -> <Self as Catalog>::Class<'a>;
838
839    /// The default class for the menu of the [`PickList`].
840    fn default_menu<'a>() -> <Self as menu::Catalog>::Class<'a> {
841        <Self as menu::Catalog>::default()
842    }
843
844    /// The [`Style`] of a class with the given status.
845    fn style(&self, class: &<Self as Catalog>::Class<'_>, status: Status) -> Style;
846}
847
848/// A styling function for a [`PickList`].
849///
850/// This is just a boxed closure: `Fn(&Theme, Status) -> Style`.
851pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Style + 'a>;
852
853impl Catalog for Theme {
854    type Class<'a> = StyleFn<'a, Self>;
855
856    fn default<'a>() -> StyleFn<'a, Self> {
857        Box::new(default)
858    }
859
860    fn style(&self, class: &StyleFn<'_, Self>, status: Status) -> Style {
861        class(self, status)
862    }
863}
864
865/// The default style of the field of a [`PickList`].
866pub fn default(theme: &Theme, status: Status) -> Style {
867    let palette = theme.extended_palette();
868
869    let active = Style {
870        text_color: palette.background.weak.text,
871        background: palette.background.weak.color.into(),
872        placeholder_color: palette.secondary.base.color,
873        handle_color: palette.background.weak.text,
874        border: Border {
875            radius: 2.0.into(),
876            width: 1.0,
877            color: palette.background.strong.color,
878        },
879    };
880
881    match status {
882        Status::Active => active,
883        Status::Hovered | Status::Opened { .. } => Style {
884            border: Border {
885                color: palette.primary.strong.color,
886                ..active.border
887            },
888            ..active
889        },
890    }
891}