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