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