iced_native/widget/
pick_list.rs

1//! Display a dropdown list of selectable values.
2use crate::alignment;
3use crate::event::{self, Event};
4use crate::keyboard;
5use crate::layout;
6use crate::mouse;
7use crate::overlay;
8use crate::overlay::menu::{self, Menu};
9use crate::renderer;
10use crate::text::{self, Text};
11use crate::touch;
12use crate::widget::container;
13use crate::widget::scrollable;
14use crate::widget::tree::{self, Tree};
15use crate::{
16    Clipboard, Element, Layout, Length, Padding, Pixels, Point, Rectangle,
17    Shell, Size, Widget,
18};
19use std::borrow::Cow;
20
21pub use iced_style::pick_list::{Appearance, StyleSheet};
22
23/// A widget for selecting a single value from a list of options.
24#[allow(missing_debug_implementations)]
25pub struct PickList<'a, T, Message, Renderer>
26where
27    [T]: ToOwned<Owned = Vec<T>>,
28    Renderer: text::Renderer,
29    Renderer::Theme: StyleSheet,
30{
31    on_selected: Box<dyn Fn(T) -> Message + 'a>,
32    options: Cow<'a, [T]>,
33    placeholder: Option<String>,
34    selected: Option<T>,
35    width: Length,
36    padding: Padding,
37    text_size: Option<f32>,
38    font: Renderer::Font,
39    handle: Handle<Renderer::Font>,
40    style: <Renderer::Theme as StyleSheet>::Style,
41}
42
43impl<'a, T: 'a, Message, Renderer> PickList<'a, T, Message, Renderer>
44where
45    T: ToString + Eq,
46    [T]: ToOwned<Owned = Vec<T>>,
47    Renderer: text::Renderer,
48    Renderer::Theme: StyleSheet
49        + scrollable::StyleSheet
50        + menu::StyleSheet
51        + container::StyleSheet,
52    <Renderer::Theme as menu::StyleSheet>::Style:
53        From<<Renderer::Theme as StyleSheet>::Style>,
54{
55    /// The default padding of a [`PickList`].
56    pub const DEFAULT_PADDING: Padding = Padding::new(5.0);
57
58    /// Creates a new [`PickList`] with the given list of options, the current
59    /// selected value, and the message to produce when an option is selected.
60    pub fn new(
61        options: impl Into<Cow<'a, [T]>>,
62        selected: Option<T>,
63        on_selected: impl Fn(T) -> Message + 'a,
64    ) -> Self {
65        Self {
66            on_selected: Box::new(on_selected),
67            options: options.into(),
68            placeholder: None,
69            selected,
70            width: Length::Shrink,
71            padding: Self::DEFAULT_PADDING,
72            text_size: None,
73            font: Default::default(),
74            handle: Default::default(),
75            style: Default::default(),
76        }
77    }
78
79    /// Sets the placeholder of the [`PickList`].
80    pub fn placeholder(mut self, placeholder: impl Into<String>) -> Self {
81        self.placeholder = Some(placeholder.into());
82        self
83    }
84
85    /// Sets the width of the [`PickList`].
86    pub fn width(mut self, width: impl Into<Length>) -> Self {
87        self.width = width.into();
88        self
89    }
90
91    /// Sets the [`Padding`] of the [`PickList`].
92    pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self {
93        self.padding = padding.into();
94        self
95    }
96
97    /// Sets the text size of the [`PickList`].
98    pub fn text_size(mut self, size: impl Into<Pixels>) -> Self {
99        self.text_size = Some(size.into().0);
100        self
101    }
102
103    /// Sets the font of the [`PickList`].
104    pub fn font(mut self, font: Renderer::Font) -> Self {
105        self.font = font;
106        self
107    }
108
109    /// Sets the [`Handle`] of the [`PickList`].
110    pub fn handle(mut self, handle: Handle<Renderer::Font>) -> Self {
111        self.handle = handle;
112        self
113    }
114
115    /// Sets the style of the [`PickList`].
116    pub fn style(
117        mut self,
118        style: impl Into<<Renderer::Theme as StyleSheet>::Style>,
119    ) -> Self {
120        self.style = style.into();
121        self
122    }
123}
124
125impl<'a, T: 'a, Message, Renderer> Widget<Message, Renderer>
126    for PickList<'a, T, Message, Renderer>
127where
128    T: Clone + ToString + Eq + 'static,
129    [T]: ToOwned<Owned = Vec<T>>,
130    Message: 'a,
131    Renderer: text::Renderer + 'a,
132    Renderer::Theme: StyleSheet
133        + scrollable::StyleSheet
134        + menu::StyleSheet
135        + container::StyleSheet,
136    <Renderer::Theme as menu::StyleSheet>::Style:
137        From<<Renderer::Theme as StyleSheet>::Style>,
138{
139    fn tag(&self) -> tree::Tag {
140        tree::Tag::of::<State<T>>()
141    }
142
143    fn state(&self) -> tree::State {
144        tree::State::new(State::<T>::new())
145    }
146
147    fn width(&self) -> Length {
148        self.width
149    }
150
151    fn height(&self) -> Length {
152        Length::Shrink
153    }
154
155    fn layout(
156        &self,
157        renderer: &Renderer,
158        limits: &layout::Limits,
159    ) -> layout::Node {
160        layout(
161            renderer,
162            limits,
163            self.width,
164            self.padding,
165            self.text_size,
166            &self.font,
167            self.placeholder.as_deref(),
168            &self.options,
169        )
170    }
171
172    fn on_event(
173        &mut self,
174        tree: &mut Tree,
175        event: Event,
176        layout: Layout<'_>,
177        cursor_position: Point,
178        _renderer: &Renderer,
179        _clipboard: &mut dyn Clipboard,
180        shell: &mut Shell<'_, Message>,
181    ) -> event::Status {
182        update(
183            event,
184            layout,
185            cursor_position,
186            shell,
187            self.on_selected.as_ref(),
188            self.selected.as_ref(),
189            &self.options,
190            || tree.state.downcast_mut::<State<T>>(),
191        )
192    }
193
194    fn mouse_interaction(
195        &self,
196        _tree: &Tree,
197        layout: Layout<'_>,
198        cursor_position: Point,
199        _viewport: &Rectangle,
200        _renderer: &Renderer,
201    ) -> mouse::Interaction {
202        mouse_interaction(layout, cursor_position)
203    }
204
205    fn draw(
206        &self,
207        tree: &Tree,
208        renderer: &mut Renderer,
209        theme: &Renderer::Theme,
210        _style: &renderer::Style,
211        layout: Layout<'_>,
212        cursor_position: Point,
213        _viewport: &Rectangle,
214    ) {
215        draw(
216            renderer,
217            theme,
218            layout,
219            cursor_position,
220            self.padding,
221            self.text_size,
222            &self.font,
223            self.placeholder.as_deref(),
224            self.selected.as_ref(),
225            &self.handle,
226            &self.style,
227            || tree.state.downcast_ref::<State<T>>(),
228        )
229    }
230
231    fn overlay<'b>(
232        &'b mut self,
233        tree: &'b mut Tree,
234        layout: Layout<'_>,
235        _renderer: &Renderer,
236    ) -> Option<overlay::Element<'b, Message, Renderer>> {
237        let state = tree.state.downcast_mut::<State<T>>();
238
239        overlay(
240            layout,
241            state,
242            self.padding,
243            self.text_size,
244            self.font.clone(),
245            &self.options,
246            self.style.clone(),
247        )
248    }
249}
250
251impl<'a, T: 'a, Message, Renderer> From<PickList<'a, T, Message, Renderer>>
252    for Element<'a, Message, Renderer>
253where
254    T: Clone + ToString + Eq + 'static,
255    [T]: ToOwned<Owned = Vec<T>>,
256    Message: 'a,
257    Renderer: text::Renderer + 'a,
258    Renderer::Theme: StyleSheet
259        + scrollable::StyleSheet
260        + menu::StyleSheet
261        + container::StyleSheet,
262    <Renderer::Theme as menu::StyleSheet>::Style:
263        From<<Renderer::Theme as StyleSheet>::Style>,
264{
265    fn from(pick_list: PickList<'a, T, Message, Renderer>) -> Self {
266        Self::new(pick_list)
267    }
268}
269
270/// The local state of a [`PickList`].
271#[derive(Debug)]
272pub struct State<T> {
273    menu: menu::State,
274    keyboard_modifiers: keyboard::Modifiers,
275    is_open: bool,
276    hovered_option: Option<usize>,
277    last_selection: Option<T>,
278}
279
280impl<T> State<T> {
281    /// Creates a new [`State`] for a [`PickList`].
282    pub fn new() -> Self {
283        Self {
284            menu: menu::State::default(),
285            keyboard_modifiers: keyboard::Modifiers::default(),
286            is_open: bool::default(),
287            hovered_option: Option::default(),
288            last_selection: Option::default(),
289        }
290    }
291}
292
293impl<T> Default for State<T> {
294    fn default() -> Self {
295        Self::new()
296    }
297}
298
299/// The handle to the right side of the [`PickList`].
300#[derive(Debug, Clone, PartialEq)]
301pub enum Handle<Font> {
302    /// Displays an arrow icon (▼).
303    ///
304    /// This is the default.
305    Arrow {
306        /// Font size of the content.
307        size: Option<f32>,
308    },
309    /// A custom static handle.
310    Static(Icon<Font>),
311    /// A custom dynamic handle.
312    Dynamic {
313        /// The [`Icon`] used when [`PickList`] is closed.
314        closed: Icon<Font>,
315        /// The [`Icon`] used when [`PickList`] is open.
316        open: Icon<Font>,
317    },
318    /// No handle will be shown.
319    None,
320}
321
322impl<Font> Default for Handle<Font> {
323    fn default() -> Self {
324        Self::Arrow { size: None }
325    }
326}
327
328/// The icon of a [`Handle`].
329#[derive(Debug, Clone, PartialEq)]
330pub struct Icon<Font> {
331    /// Font that will be used to display the `code_point`,
332    pub font: Font,
333    /// The unicode code point that will be used as the icon.
334    pub code_point: char,
335    /// Font size of the content.
336    pub size: Option<f32>,
337}
338
339/// Computes the layout of a [`PickList`].
340pub fn layout<Renderer, T>(
341    renderer: &Renderer,
342    limits: &layout::Limits,
343    width: Length,
344    padding: Padding,
345    text_size: Option<f32>,
346    font: &Renderer::Font,
347    placeholder: Option<&str>,
348    options: &[T],
349) -> layout::Node
350where
351    Renderer: text::Renderer,
352    T: ToString,
353{
354    use std::f32;
355
356    let limits = limits.width(width).height(Length::Shrink).pad(padding);
357    let text_size = text_size.unwrap_or_else(|| renderer.default_size());
358
359    let max_width = match width {
360        Length::Shrink => {
361            let measure = |label: &str| -> f32 {
362                let (width, _) = renderer.measure(
363                    label,
364                    text_size,
365                    font.clone(),
366                    Size::new(f32::INFINITY, f32::INFINITY),
367                );
368
369                width.round()
370            };
371
372            let labels = options.iter().map(ToString::to_string);
373
374            let labels_width = labels
375                .map(|label| measure(&label))
376                .fold(100.0, |candidate, current| current.max(candidate));
377
378            let placeholder_width = placeholder.map(measure).unwrap_or(100.0);
379
380            labels_width.max(placeholder_width)
381        }
382        _ => 0.0,
383    };
384
385    let size = {
386        let intrinsic =
387            Size::new(max_width + text_size + padding.left, text_size);
388
389        limits.resolve(intrinsic).pad(padding)
390    };
391
392    layout::Node::new(size)
393}
394
395/// Processes an [`Event`] and updates the [`State`] of a [`PickList`]
396/// accordingly.
397pub fn update<'a, T, Message>(
398    event: Event,
399    layout: Layout<'_>,
400    cursor_position: Point,
401    shell: &mut Shell<'_, Message>,
402    on_selected: &dyn Fn(T) -> Message,
403    selected: Option<&T>,
404    options: &[T],
405    state: impl FnOnce() -> &'a mut State<T>,
406) -> event::Status
407where
408    T: PartialEq + Clone + 'a,
409{
410    match event {
411        Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
412        | Event::Touch(touch::Event::FingerPressed { .. }) => {
413            let state = state();
414
415            let event_status = if state.is_open {
416                // Event wasn't processed by overlay, so cursor was clicked either outside it's
417                // bounds or on the drop-down, either way we close the overlay.
418                state.is_open = false;
419
420                event::Status::Captured
421            } else if layout.bounds().contains(cursor_position) {
422                state.is_open = true;
423                state.hovered_option =
424                    options.iter().position(|option| Some(option) == selected);
425
426                event::Status::Captured
427            } else {
428                event::Status::Ignored
429            };
430
431            if let Some(last_selection) = state.last_selection.take() {
432                shell.publish((on_selected)(last_selection));
433
434                state.is_open = false;
435
436                event::Status::Captured
437            } else {
438                event_status
439            }
440        }
441        Event::Mouse(mouse::Event::WheelScrolled {
442            delta: mouse::ScrollDelta::Lines { y, .. },
443        }) => {
444            let state = state();
445
446            if state.keyboard_modifiers.command()
447                && layout.bounds().contains(cursor_position)
448                && !state.is_open
449            {
450                fn find_next<'a, T: PartialEq>(
451                    selected: &'a T,
452                    mut options: impl Iterator<Item = &'a T>,
453                ) -> Option<&'a T> {
454                    let _ = options.find(|&option| option == selected);
455
456                    options.next()
457                }
458
459                let next_option = if y < 0.0 {
460                    if let Some(selected) = selected {
461                        find_next(selected, options.iter())
462                    } else {
463                        options.first()
464                    }
465                } else if y > 0.0 {
466                    if let Some(selected) = selected {
467                        find_next(selected, options.iter().rev())
468                    } else {
469                        options.last()
470                    }
471                } else {
472                    None
473                };
474
475                if let Some(next_option) = next_option {
476                    shell.publish((on_selected)(next_option.clone()));
477                }
478
479                event::Status::Captured
480            } else {
481                event::Status::Ignored
482            }
483        }
484        Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => {
485            let state = state();
486
487            state.keyboard_modifiers = modifiers;
488
489            event::Status::Ignored
490        }
491        _ => event::Status::Ignored,
492    }
493}
494
495/// Returns the current [`mouse::Interaction`] of a [`PickList`].
496pub fn mouse_interaction(
497    layout: Layout<'_>,
498    cursor_position: Point,
499) -> mouse::Interaction {
500    let bounds = layout.bounds();
501    let is_mouse_over = bounds.contains(cursor_position);
502
503    if is_mouse_over {
504        mouse::Interaction::Pointer
505    } else {
506        mouse::Interaction::default()
507    }
508}
509
510/// Returns the current overlay of a [`PickList`].
511pub fn overlay<'a, T, Message, Renderer>(
512    layout: Layout<'_>,
513    state: &'a mut State<T>,
514    padding: Padding,
515    text_size: Option<f32>,
516    font: Renderer::Font,
517    options: &'a [T],
518    style: <Renderer::Theme as StyleSheet>::Style,
519) -> Option<overlay::Element<'a, Message, Renderer>>
520where
521    T: Clone + ToString,
522    Message: 'a,
523    Renderer: text::Renderer + 'a,
524    Renderer::Theme: StyleSheet
525        + scrollable::StyleSheet
526        + menu::StyleSheet
527        + container::StyleSheet,
528    <Renderer::Theme as menu::StyleSheet>::Style:
529        From<<Renderer::Theme as StyleSheet>::Style>,
530{
531    if state.is_open {
532        let bounds = layout.bounds();
533
534        let mut menu = Menu::new(
535            &mut state.menu,
536            options,
537            &mut state.hovered_option,
538            &mut state.last_selection,
539        )
540        .width(bounds.width)
541        .padding(padding)
542        .font(font)
543        .style(style);
544
545        if let Some(text_size) = text_size {
546            menu = menu.text_size(text_size);
547        }
548
549        Some(menu.overlay(layout.position(), bounds.height))
550    } else {
551        None
552    }
553}
554
555/// Draws a [`PickList`].
556pub fn draw<'a, T, Renderer>(
557    renderer: &mut Renderer,
558    theme: &Renderer::Theme,
559    layout: Layout<'_>,
560    cursor_position: Point,
561    padding: Padding,
562    text_size: Option<f32>,
563    font: &Renderer::Font,
564    placeholder: Option<&str>,
565    selected: Option<&T>,
566    handle: &Handle<Renderer::Font>,
567    style: &<Renderer::Theme as StyleSheet>::Style,
568    state: impl FnOnce() -> &'a State<T>,
569) where
570    Renderer: text::Renderer,
571    Renderer::Theme: StyleSheet,
572    T: ToString + 'a,
573{
574    let bounds = layout.bounds();
575    let is_mouse_over = bounds.contains(cursor_position);
576    let is_selected = selected.is_some();
577
578    let style = if is_mouse_over {
579        theme.hovered(style)
580    } else {
581        theme.active(style)
582    };
583
584    renderer.fill_quad(
585        renderer::Quad {
586            bounds,
587            border_color: style.border_color,
588            border_width: style.border_width,
589            border_radius: style.border_radius.into(),
590        },
591        style.background,
592    );
593
594    let handle = match handle {
595        Handle::Arrow { size } => {
596            Some((Renderer::ICON_FONT, Renderer::ARROW_DOWN_ICON, *size))
597        }
598        Handle::Static(Icon {
599            font,
600            code_point,
601            size,
602        }) => Some((font.clone(), *code_point, *size)),
603        Handle::Dynamic { open, closed } => {
604            if state().is_open {
605                Some((open.font.clone(), open.code_point, open.size))
606            } else {
607                Some((closed.font.clone(), closed.code_point, closed.size))
608            }
609        }
610        Handle::None => None,
611    };
612
613    if let Some((font, code_point, size)) = handle {
614        let size = size.unwrap_or_else(|| renderer.default_size());
615
616        renderer.fill_text(Text {
617            content: &code_point.to_string(),
618            size,
619            font,
620            color: style.handle_color,
621            bounds: Rectangle {
622                x: bounds.x + bounds.width - padding.horizontal(),
623                y: bounds.center_y() - size / 2.0,
624                height: size,
625                ..bounds
626            },
627            horizontal_alignment: alignment::Horizontal::Right,
628            vertical_alignment: alignment::Vertical::Top,
629        });
630    }
631
632    let label = selected.map(ToString::to_string);
633
634    if let Some(label) = label.as_deref().or(placeholder) {
635        let text_size = text_size.unwrap_or_else(|| renderer.default_size());
636
637        renderer.fill_text(Text {
638            content: label,
639            size: text_size,
640            font: font.clone(),
641            color: if is_selected {
642                style.text_color
643            } else {
644                style.placeholder_color
645            },
646            bounds: Rectangle {
647                x: bounds.x + padding.left,
648                y: bounds.center_y() - text_size / 2.0,
649                width: bounds.width - padding.horizontal(),
650                height: text_size,
651            },
652            horizontal_alignment: alignment::Horizontal::Left,
653            vertical_alignment: alignment::Vertical::Top,
654        });
655    }
656}