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