Skip to main content

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, Color, Element, Event, Layout, Length, Padding, Pixels, Point, Rectangle,
76    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            hint_factor: renderer.scale_factor(),
361        };
362
363        for (option, paragraph) in options.iter().zip(state.options.iter_mut()) {
364            let label = option.to_string();
365
366            let _ = paragraph.update(Text {
367                content: &label,
368                ..option_text
369            });
370        }
371
372        if let Some(placeholder) = &self.placeholder {
373            let _ = state.placeholder.update(Text {
374                content: placeholder,
375                ..option_text
376            });
377        }
378
379        let max_width = match self.width {
380            Length::Shrink => {
381                let labels_width = state.options.iter().fold(0.0, |width, paragraph| {
382                    f32::max(width, paragraph.min_width())
383                });
384
385                labels_width.max(
386                    self.placeholder
387                        .as_ref()
388                        .map(|_| state.placeholder.min_width())
389                        .unwrap_or(0.0),
390                )
391            }
392            _ => 0.0,
393        };
394
395        let size = {
396            let intrinsic = Size::new(
397                max_width + text_size.0 + self.padding.left,
398                f32::from(self.text_line_height.to_absolute(text_size)),
399            );
400
401            limits
402                .width(self.width)
403                .shrink(self.padding)
404                .resolve(self.width, Length::Shrink, intrinsic)
405                .expand(self.padding)
406        };
407
408        layout::Node::new(size)
409    }
410
411    fn update(
412        &mut self,
413        tree: &mut Tree,
414        event: &Event,
415        layout: Layout<'_>,
416        cursor: mouse::Cursor,
417        _renderer: &Renderer,
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                    hint_factor: None,
624                },
625                Point::new(
626                    bounds.x + bounds.width - self.padding.right,
627                    bounds.center_y(),
628                ),
629                style.handle_color,
630                *viewport,
631            );
632        }
633
634        let label = selected.map(ToString::to_string);
635
636        if let Some(label) = label.or_else(|| self.placeholder.clone()) {
637            let text_size = self.text_size.unwrap_or_else(|| renderer.default_size());
638
639            renderer.fill_text(
640                Text {
641                    content: label,
642                    size: text_size,
643                    line_height: self.text_line_height,
644                    font,
645                    bounds: Size::new(
646                        bounds.width - self.padding.x(),
647                        f32::from(self.text_line_height.to_absolute(text_size)),
648                    ),
649                    align_x: text::Alignment::Default,
650                    align_y: alignment::Vertical::Center,
651                    shaping: self.text_shaping,
652                    wrapping: text::Wrapping::default(),
653                    hint_factor: renderer.scale_factor(),
654                },
655                Point::new(bounds.x + self.padding.left, bounds.center_y()),
656                if selected.is_some() {
657                    style.text_color
658                } else {
659                    style.placeholder_color
660                },
661                *viewport,
662            );
663        }
664    }
665
666    fn overlay<'b>(
667        &'b mut self,
668        tree: &'b mut Tree,
669        layout: Layout<'_>,
670        renderer: &Renderer,
671        viewport: &Rectangle,
672        translation: Vector,
673    ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
674        let state = tree.state.downcast_mut::<State<Renderer::Paragraph>>();
675        let font = self.font.unwrap_or_else(|| renderer.default_font());
676
677        if state.is_open {
678            let bounds = layout.bounds();
679
680            let on_select = &self.on_select;
681
682            let mut menu = Menu::new(
683                &mut state.menu,
684                self.options.borrow(),
685                &mut state.hovered_option,
686                |option| {
687                    state.is_open = false;
688
689                    (on_select)(option)
690                },
691                None,
692                &self.menu_class,
693            )
694            .width(bounds.width)
695            .padding(self.padding)
696            .font(font)
697            .text_shaping(self.text_shaping);
698
699            if let Some(text_size) = self.text_size {
700                menu = menu.text_size(text_size);
701            }
702
703            Some(menu.overlay(
704                layout.position() + translation,
705                *viewport,
706                bounds.height,
707                self.menu_height,
708            ))
709        } else {
710            None
711        }
712    }
713}
714
715impl<'a, T, L, V, Message, Theme, Renderer> From<PickList<'a, T, L, V, Message, Theme, Renderer>>
716    for Element<'a, Message, Theme, Renderer>
717where
718    T: Clone + ToString + PartialEq + 'a,
719    L: Borrow<[T]> + 'a,
720    V: Borrow<T> + 'a,
721    Message: Clone + 'a,
722    Theme: Catalog + 'a,
723    Renderer: text::Renderer + 'a,
724{
725    fn from(pick_list: PickList<'a, T, L, V, Message, Theme, Renderer>) -> Self {
726        Self::new(pick_list)
727    }
728}
729
730#[derive(Debug)]
731struct State<P: text::Paragraph> {
732    menu: menu::State,
733    keyboard_modifiers: keyboard::Modifiers,
734    is_open: bool,
735    hovered_option: Option<usize>,
736    options: Vec<paragraph::Plain<P>>,
737    placeholder: paragraph::Plain<P>,
738}
739
740impl<P: text::Paragraph> State<P> {
741    /// Creates a new [`State`] for a [`PickList`].
742    fn new() -> Self {
743        Self {
744            menu: menu::State::default(),
745            keyboard_modifiers: keyboard::Modifiers::default(),
746            is_open: bool::default(),
747            hovered_option: Option::default(),
748            options: Vec::new(),
749            placeholder: paragraph::Plain::default(),
750        }
751    }
752}
753
754impl<P: text::Paragraph> Default for State<P> {
755    fn default() -> Self {
756        Self::new()
757    }
758}
759
760/// The handle to the right side of the [`PickList`].
761#[derive(Debug, Clone, PartialEq)]
762pub enum Handle<Font> {
763    /// Displays an arrow icon (▼).
764    ///
765    /// This is the default.
766    Arrow {
767        /// Font size of the content.
768        size: Option<Pixels>,
769    },
770    /// A custom static handle.
771    Static(Icon<Font>),
772    /// A custom dynamic handle.
773    Dynamic {
774        /// The [`Icon`] used when [`PickList`] is closed.
775        closed: Icon<Font>,
776        /// The [`Icon`] used when [`PickList`] is open.
777        open: Icon<Font>,
778    },
779    /// No handle will be shown.
780    None,
781}
782
783impl<Font> Default for Handle<Font> {
784    fn default() -> Self {
785        Self::Arrow { size: None }
786    }
787}
788
789/// The icon of a [`Handle`].
790#[derive(Debug, Clone, PartialEq)]
791pub struct Icon<Font> {
792    /// Font that will be used to display the `code_point`,
793    pub font: Font,
794    /// The unicode code point that will be used as the icon.
795    pub code_point: char,
796    /// Font size of the content.
797    pub size: Option<Pixels>,
798    /// Line height of the content.
799    pub line_height: text::LineHeight,
800    /// The shaping strategy of the icon.
801    pub shaping: text::Shaping,
802}
803
804/// The possible status of a [`PickList`].
805#[derive(Debug, Clone, Copy, PartialEq, Eq)]
806pub enum Status {
807    /// The [`PickList`] can be interacted with.
808    Active,
809    /// The [`PickList`] is being hovered.
810    Hovered,
811    /// The [`PickList`] is open.
812    Opened {
813        /// Whether the [`PickList`] is hovered, while open.
814        is_hovered: bool,
815    },
816}
817
818/// The appearance of a pick list.
819#[derive(Debug, Clone, Copy, PartialEq)]
820pub struct Style {
821    /// The text [`Color`] of the pick list.
822    pub text_color: Color,
823    /// The placeholder [`Color`] of the pick list.
824    pub placeholder_color: Color,
825    /// The handle [`Color`] of the pick list.
826    pub handle_color: Color,
827    /// The [`Background`] of the pick list.
828    pub background: Background,
829    /// The [`Border`] of the pick list.
830    pub border: Border,
831}
832
833/// The theme catalog of a [`PickList`].
834pub trait Catalog: menu::Catalog {
835    /// The item class of the [`Catalog`].
836    type Class<'a>;
837
838    /// The default class produced by the [`Catalog`].
839    fn default<'a>() -> <Self as Catalog>::Class<'a>;
840
841    /// The default class for the menu of the [`PickList`].
842    fn default_menu<'a>() -> <Self as menu::Catalog>::Class<'a> {
843        <Self as menu::Catalog>::default()
844    }
845
846    /// The [`Style`] of a class with the given status.
847    fn style(&self, class: &<Self as Catalog>::Class<'_>, status: Status) -> Style;
848}
849
850/// A styling function for a [`PickList`].
851///
852/// This is just a boxed closure: `Fn(&Theme, Status) -> Style`.
853pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Style + 'a>;
854
855impl Catalog for Theme {
856    type Class<'a> = StyleFn<'a, Self>;
857
858    fn default<'a>() -> StyleFn<'a, Self> {
859        Box::new(default)
860    }
861
862    fn style(&self, class: &StyleFn<'_, Self>, status: Status) -> Style {
863        class(self, status)
864    }
865}
866
867/// The default style of the field of a [`PickList`].
868pub fn default(theme: &Theme, status: Status) -> Style {
869    let palette = theme.extended_palette();
870
871    let active = Style {
872        text_color: palette.background.weak.text,
873        background: palette.background.weak.color.into(),
874        placeholder_color: palette.secondary.base.color,
875        handle_color: palette.background.weak.text,
876        border: Border {
877            radius: 2.0.into(),
878            width: 1.0,
879            color: palette.background.strong.color,
880        },
881    };
882
883    match status {
884        Status::Active => active,
885        Status::Hovered | Status::Opened { .. } => Style {
886            border: Border {
887                color: palette.primary.strong.color,
888                ..active.border
889            },
890            ..active
891        },
892    }
893}