Skip to main content

cosmic/widget/dropdown/
widget.rs

1// Copyright 2023 System76 <info@system76.com>
2// Copyright 2019 Héctor Ramón, Iced contributors
3// SPDX-License-Identifier: MPL-2.0 AND MIT
4
5use super::Id;
6use super::menu::{self, Menu};
7use crate::widget::icon::{self, Handle};
8use crate::{Element, surface};
9use derive_setters::Setters;
10use iced::window;
11use iced_core::event::{self, Event};
12use iced_core::text::{self, Paragraph, Text};
13use iced_core::widget::tree::{self, Tree};
14use iced_core::{
15    Clipboard, Layout, Length, Padding, Pixels, Rectangle, Shadow, Shell, Size, Vector, Widget,
16    alignment, keyboard, layout, mouse, overlay, renderer, svg, touch,
17};
18use iced_widget::pick_list::{self, Catalog};
19use std::borrow::Cow;
20use std::ffi::OsStr;
21use std::hash::{DefaultHasher, Hash, Hasher};
22use std::sync::atomic::{AtomicBool, Ordering};
23use std::sync::{Arc, LazyLock, Mutex};
24
25pub type DropdownView<Message> = Arc<dyn Fn() -> Element<'static, Message> + Send + Sync>;
26static AUTOSIZE_ID: LazyLock<crate::widget::Id> =
27    LazyLock::new(|| crate::widget::Id::new("cosmic-applet-autosize"));
28
29/// A widget for selecting a single value from a list of selections.
30#[derive(Setters)]
31pub struct Dropdown<'a, S: AsRef<str> + Send + Sync + Clone + 'static, Message, AppMessage>
32where
33    [S]: std::borrow::ToOwned,
34{
35    #[setters(skip)]
36    id: Option<Id>,
37    #[setters(skip)]
38    on_selected: Arc<dyn Fn(usize) -> Message + Send + Sync>,
39    #[setters(skip)]
40    selections: Cow<'a, [S]>,
41    #[setters]
42    icons: Cow<'a, [icon::Handle]>,
43    #[setters(skip)]
44    selected: Option<usize>,
45    #[setters(into)]
46    width: Length,
47    gap: f32,
48    #[setters(into)]
49    padding: Padding,
50    #[setters(strip_option, into)]
51    placeholder: Option<Cow<'a, str>>,
52    #[setters(strip_option)]
53    text_size: Option<f32>,
54    text_line_height: text::LineHeight,
55    #[setters(strip_option)]
56    font: Option<crate::font::Font>,
57    #[setters(skip)]
58    on_surface_action:
59        Option<Arc<dyn Fn(surface::Action<AppMessage>) -> Message + Send + Sync + 'static>>,
60    #[setters(skip)]
61    action_map: Option<Arc<dyn Fn(Message) -> AppMessage + 'static + Send + Sync>>,
62    #[setters(strip_option)]
63    window_id: Option<window::Id>,
64    #[cfg(wayland_platform)]
65    positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner,
66}
67
68impl<'a, S: AsRef<str> + Send + Sync + Clone + 'static, Message: 'static, AppMessage: 'static>
69    Dropdown<'a, S, Message, AppMessage>
70where
71    [S]: std::borrow::ToOwned,
72{
73    /// The default gap.
74    pub const DEFAULT_GAP: f32 = 4.0;
75
76    /// The default padding.
77    pub const DEFAULT_PADDING: Padding = Padding::new(8.0);
78
79    /// Creates a new [`Dropdown`] with the given list of selections, the current
80    /// selected value, and the message to produce when an option is selected.
81    pub fn new(
82        selections: Cow<'a, [S]>,
83        selected: Option<usize>,
84        on_selected: impl Fn(usize) -> Message + 'static + Send + Sync,
85    ) -> Self {
86        Self {
87            id: None,
88            on_selected: Arc::new(on_selected),
89            selections,
90            icons: Cow::Borrowed(&[]),
91            selected,
92            placeholder: None,
93            width: Length::Shrink,
94            gap: Self::DEFAULT_GAP,
95            padding: Self::DEFAULT_PADDING,
96            text_size: None,
97            text_line_height: text::LineHeight::Relative(1.2),
98            font: None,
99            window_id: None,
100            #[cfg(wayland_platform)]
101            positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner::default(),
102            on_surface_action: None,
103            action_map: None,
104        }
105    }
106
107    #[cfg(wayland_platform)]
108    /// Handle dropdown requests for popup creation.
109    /// Intended to be used with [`crate::app::message::get_popup`]
110    pub fn with_popup<NewAppMessage>(
111        self,
112        parent_id: window::Id,
113        on_surface_action: impl Fn(surface::Action<NewAppMessage>) -> Message + Send + Sync + 'static,
114        action_map: impl Fn(Message) -> NewAppMessage + Send + Sync + 'static,
115    ) -> Dropdown<'a, S, Message, NewAppMessage> {
116        let Self {
117            id,
118            on_selected,
119            selections,
120            icons,
121            selected,
122            placeholder,
123            width,
124            gap,
125            padding,
126            text_size,
127            text_line_height,
128            font,
129            positioner,
130            ..
131        } = self;
132
133        Dropdown::<'a, S, Message, NewAppMessage> {
134            id,
135            on_selected,
136            selections,
137            icons,
138            selected,
139            placeholder,
140            width,
141            gap,
142            padding,
143            text_size,
144            text_line_height,
145            font,
146            on_surface_action: Some(Arc::new(on_surface_action)),
147            action_map: Some(Arc::new(action_map)),
148            window_id: Some(parent_id),
149            positioner,
150        }
151    }
152
153    pub fn id(mut self, id: Id) -> Self {
154        self.id = Some(id);
155        self
156    }
157
158    #[cfg(wayland_platform)]
159    pub fn with_positioner(
160        mut self,
161        positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner,
162    ) -> Self {
163        self.positioner = positioner;
164        self
165    }
166}
167
168impl<
169    S: AsRef<str> + Send + Sync + Clone + 'static,
170    Message: 'static + Clone,
171    AppMessage: 'static + Clone,
172> Widget<Message, crate::Theme, crate::Renderer> for Dropdown<'_, S, Message, AppMessage>
173where
174    [S]: std::borrow::ToOwned,
175{
176    fn tag(&self) -> tree::Tag {
177        tree::Tag::of::<State>()
178    }
179
180    fn state(&self) -> tree::State {
181        tree::State::new(State::new())
182    }
183
184    fn diff(&mut self, tree: &mut Tree) {
185        let state = tree.state.downcast_mut::<State>();
186
187        let mut selections_changed = state.selections.len() != self.selections.len();
188
189        state
190            .selections
191            .resize_with(self.selections.len(), crate::Plain::default);
192        state.hashes.resize(self.selections.len(), 0);
193
194        for (i, selection) in self.selections.iter().enumerate() {
195            let mut hasher = DefaultHasher::new();
196            selection.as_ref().hash(&mut hasher);
197            let text_hash = hasher.finish();
198
199            if state.hashes[i] == text_hash {
200                continue;
201            }
202
203            selections_changed = true;
204            state.hashes[i] = text_hash;
205            state.selections[i].update(Text {
206                content: selection.as_ref(),
207                bounds: Size::INFINITE,
208                // TODO use the renderer default size
209                size: iced::Pixels(self.text_size.unwrap_or(14.0)),
210                line_height: self.text_line_height,
211                font: self.font.unwrap_or_else(crate::font::default),
212                align_x: text::Alignment::Left,
213                align_y: alignment::Vertical::Top,
214                shaping: text::Shaping::Advanced,
215                wrapping: text::Wrapping::default(),
216                ellipsize: text::Ellipsize::default(),
217            });
218        }
219
220        if state.is_open.load(Ordering::SeqCst) && selections_changed {
221            state.close_operation = true;
222            state.open_operation = true;
223        }
224    }
225
226    fn size(&self) -> Size<Length> {
227        Size::new(self.width, Length::Shrink)
228    }
229
230    fn layout(
231        &mut self,
232        tree: &mut Tree,
233        renderer: &crate::Renderer,
234        limits: &layout::Limits,
235    ) -> layout::Node {
236        layout(
237            renderer,
238            limits,
239            self.width,
240            self.gap,
241            self.padding,
242            self.text_size.unwrap_or(14.0),
243            self.text_line_height,
244            self.font,
245            self.selected.and_then(|id| {
246                self.selections
247                    .get(id)
248                    .map(AsRef::as_ref)
249                    .zip(tree.state.downcast_mut::<State>().selections.get_mut(id))
250            }),
251            self.placeholder.as_deref(),
252            !self.icons.is_empty(),
253        )
254    }
255
256    fn update(
257        &mut self,
258        tree: &mut Tree,
259        event: &Event,
260        layout: Layout<'_>,
261        cursor: mouse::Cursor,
262        _renderer: &crate::Renderer,
263        _clipboard: &mut dyn Clipboard,
264        shell: &mut Shell<'_, Message>,
265        _viewport: &Rectangle,
266    ) {
267        update::<S, Message, AppMessage>(
268            &event,
269            layout,
270            cursor,
271            shell,
272            #[cfg(wayland_platform)]
273            self.positioner.clone(),
274            self.on_selected.clone(),
275            self.selected,
276            &self.selections,
277            || tree.state.downcast_mut::<State>(),
278            self.window_id,
279            self.on_surface_action.clone(),
280            self.action_map.clone(),
281            &self.icons,
282            self.gap,
283            self.padding,
284            self.text_size,
285            self.font,
286            self.selected,
287        )
288    }
289
290    fn mouse_interaction(
291        &self,
292        _tree: &Tree,
293        layout: Layout<'_>,
294        cursor: mouse::Cursor,
295        _viewport: &Rectangle,
296        _renderer: &crate::Renderer,
297    ) -> mouse::Interaction {
298        mouse_interaction(layout, cursor)
299    }
300
301    fn draw(
302        &self,
303        tree: &Tree,
304        renderer: &mut crate::Renderer,
305        theme: &crate::Theme,
306        _style: &iced_core::renderer::Style,
307        layout: Layout<'_>,
308        cursor: mouse::Cursor,
309        viewport: &Rectangle,
310    ) {
311        let font = self.font.unwrap_or_else(crate::font::default);
312        draw(
313            renderer,
314            theme,
315            layout,
316            cursor,
317            self.gap,
318            self.padding,
319            self.text_size,
320            self.text_line_height,
321            font,
322            self.selected.and_then(|id| self.selections.get(id)),
323            self.selected.and_then(|id| self.icons.get(id)),
324            self.placeholder.as_deref(),
325            tree.state.downcast_ref::<State>(),
326            viewport,
327        );
328    }
329
330    fn operate(
331        &mut self,
332        tree: &mut Tree,
333        _layout: Layout<'_>,
334        _renderer: &crate::Renderer,
335        operation: &mut dyn iced_core::widget::Operation,
336    ) {
337        // TODO: double check operation handling
338        // let state = tree.state.downcast_mut::<State>();
339        // operation.custom(state, self.id.as_ref());
340    }
341
342    fn overlay<'b>(
343        &'b mut self,
344        tree: &'b mut Tree,
345        layout: Layout<'b>,
346        renderer: &crate::Renderer,
347        viewport: &Rectangle,
348        translation: Vector,
349    ) -> Option<overlay::Element<'b, Message, crate::Theme, crate::Renderer>> {
350        #[cfg(wayland_platform)]
351        if self.window_id.is_some() || self.on_surface_action.is_some() {
352            return None;
353        }
354
355        let state = tree.state.downcast_mut::<State>();
356
357        overlay(
358            layout,
359            renderer,
360            state,
361            self.gap,
362            self.padding,
363            self.text_size.unwrap_or(14.0),
364            self.text_line_height,
365            self.font,
366            &self.selections,
367            &self.icons,
368            self.selected,
369            self.on_selected.as_ref(),
370            translation,
371            None,
372        )
373    }
374
375    // #[cfg(feature = "a11y")]
376    // /// get the a11y nodes for the widget
377    // fn a11y_nodes(
378    //     &self,
379    //     layout: Layout<'_>,
380    //     state: &Tree,
381    //     p: mouse::Cursor,
382    // ) -> iced_accessibility::A11yTree {
383    //     // TODO
384    // }
385}
386
387impl<
388    'a,
389    S: AsRef<str> + Send + Sync + Clone + 'static,
390    Message: 'static + std::clone::Clone,
391    AppMessage: 'static + std::clone::Clone,
392> From<Dropdown<'a, S, Message, AppMessage>> for crate::Element<'a, Message>
393where
394    [S]: std::borrow::ToOwned,
395{
396    fn from(pick_list: Dropdown<'a, S, Message, AppMessage>) -> Self {
397        Self::new(pick_list)
398    }
399}
400
401/// The local state of a [`Dropdown`].
402#[derive(Debug, Clone)]
403pub struct State {
404    icon: Option<svg::Handle>,
405    menu: menu::State,
406    keyboard_modifiers: keyboard::Modifiers,
407    is_open: Arc<AtomicBool>,
408    close_operation: bool,
409    open_operation: bool,
410    hovered_option: Arc<Mutex<Option<usize>>>,
411    hashes: Vec<u64>,
412    selections: Vec<crate::Plain>,
413    popup_id: window::Id,
414}
415
416impl State {
417    /// Creates a new [`State`] for a [`Dropdown`].
418    pub fn new() -> Self {
419        Self {
420            icon: match icon::from_name("pan-down-symbolic").size(16).handle().data {
421                icon::Data::Svg(handle) => Some(handle),
422                icon::Data::Image(_) => None,
423            },
424            menu: menu::State::default(),
425            keyboard_modifiers: keyboard::Modifiers::default(),
426            is_open: Arc::new(AtomicBool::new(false)),
427            hovered_option: Arc::new(Mutex::new(None)),
428            selections: Vec::new(),
429            hashes: Vec::new(),
430            popup_id: window::Id::unique(),
431            close_operation: false,
432            open_operation: false,
433        }
434    }
435}
436
437impl Default for State {
438    fn default() -> Self {
439        Self::new()
440    }
441}
442
443impl super::operation::Dropdown for State {
444    fn close(&mut self) {
445        self.close_operation = true;
446    }
447
448    fn open(&mut self) {
449        self.open_operation = true;
450    }
451}
452
453/// Computes the layout of a [`Dropdown`].
454#[allow(clippy::too_many_arguments)]
455pub fn layout(
456    renderer: &crate::Renderer,
457    limits: &layout::Limits,
458    width: Length,
459    gap: f32,
460    padding: Padding,
461    text_size: f32,
462    text_line_height: text::LineHeight,
463    font: Option<crate::font::Font>,
464    selection: Option<(&str, &mut crate::Plain)>,
465    placeholder: Option<&str>,
466    has_icons: bool,
467) -> layout::Node {
468    use std::f32;
469
470    let limits = limits.width(width).height(Length::Shrink).shrink(padding);
471
472    let max_width = match width {
473        Length::Shrink => {
474            let measure = move |(label, paragraph): (_, Option<&mut crate::Plain>)| -> f32 {
475                let paragraph = match paragraph {
476                    Some(p) => {
477                        let text = Text {
478                            content: label,
479                            bounds: Size::new(f32::MAX, f32::MAX),
480                            size: iced::Pixels(text_size),
481                            line_height: text_line_height,
482                            font: font.unwrap_or_else(crate::font::default),
483                            align_x: text::Alignment::Left,
484                            align_y: alignment::Vertical::Top,
485                            shaping: text::Shaping::Advanced,
486                            wrapping: text::Wrapping::default(),
487                            ellipsize: text::Ellipsize::default(),
488                        };
489                        p.update(text);
490                        p
491                    }
492                    None => {
493                        let text = Text {
494                            content: label.to_string(),
495                            bounds: Size::new(f32::MAX, f32::MAX),
496                            size: iced::Pixels(text_size),
497                            line_height: text_line_height,
498                            font: font.unwrap_or_else(crate::font::default),
499                            align_x: text::Alignment::Left,
500                            align_y: alignment::Vertical::Top,
501                            shaping: text::Shaping::Advanced,
502                            wrapping: text::Wrapping::default(),
503                            ellipsize: text::Ellipsize::default(),
504                        };
505                        &mut crate::Plain::new(text)
506                    }
507                };
508                paragraph.min_width().round()
509            };
510
511            selection
512                .map(|(l, p)| (l, Some(p)))
513                .or_else(|| placeholder.map(|l| (l, None)))
514                .map(measure)
515                .unwrap_or_default()
516        }
517        _ => 0.0,
518    };
519
520    let icon_size = if has_icons { 24.0 } else { 0.0 };
521
522    let size = {
523        let intrinsic = Size::new(
524            max_width + icon_size + gap + 16.0,
525            f32::from(text_line_height.to_absolute(Pixels(text_size))),
526        );
527
528        limits
529            .resolve(width, Length::Shrink, intrinsic)
530            .expand(padding)
531    };
532
533    layout::Node::new(size)
534}
535
536/// Processes an [`Event`] and updates the [`State`] of a [`Dropdown`]
537/// accordingly.
538#[allow(clippy::too_many_arguments, clippy::too_many_lines)]
539pub fn update<
540    'a,
541    S: AsRef<str> + Send + Sync + Clone + 'static,
542    Message: Clone + 'static,
543    AppMessage: Clone + 'static,
544>(
545    event: &Event,
546    layout: Layout<'_>,
547    cursor: mouse::Cursor,
548    shell: &mut Shell<'_, Message>,
549    #[cfg(wayland_platform)]
550    positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner,
551    on_selected: Arc<dyn Fn(usize) -> Message + Send + Sync + 'static>,
552    selected: Option<usize>,
553    selections: &[S],
554    state: impl FnOnce() -> &'a mut State,
555    _window_id: Option<window::Id>,
556    on_surface_action: Option<
557        Arc<dyn Fn(surface::Action<AppMessage>) -> Message + Send + Sync + 'static>,
558    >,
559    action_map: Option<Arc<dyn Fn(Message) -> AppMessage + Send + Sync + 'static>>,
560    icons: &[icon::Handle],
561    gap: f32,
562    padding: Padding,
563    text_size: Option<f32>,
564    font: Option<crate::font::Font>,
565    selected_option: Option<usize>,
566) {
567    let state = state();
568
569    let open = |shell: &mut Shell<'_, Message>,
570                state: &mut State,
571                on_selected: Arc<dyn Fn(usize) -> Message + Send + Sync + 'static>| {
572        state.is_open.store(true, Ordering::Relaxed);
573        shell.request_redraw();
574        let mut hovered_guard = state.hovered_option.lock().unwrap();
575        *hovered_guard = selected;
576        let id = window::Id::unique();
577        state.popup_id = id;
578        #[cfg(wayland_platform)]
579        if let Some(((on_surface_action, parent), action_map)) = on_surface_action
580            .as_ref()
581            .zip(_window_id)
582            .zip(action_map.clone())
583        {
584            use iced_runtime::platform_specific::wayland::popup::{
585                SctkPopupSettings, SctkPositioner,
586            };
587
588            use crate::surface::action::LiveSettings;
589            let bounds = layout.bounds();
590            let anchor_rect = Rectangle {
591                x: bounds.x as i32,
592                y: bounds.y as i32,
593                width: bounds.width as i32,
594                height: bounds.height as i32,
595            };
596            let icon_width = if icons.is_empty() { 0.0 } else { 24.0 };
597            let measure = |_label: &str, selection_paragraph: &crate::Paragraph| -> f32 {
598                selection_paragraph.min_width().round()
599            };
600            let pad_width = padding.x().mul_add(2.0, 16.0);
601
602            let selections_width = selections
603                .iter()
604                .zip(state.selections.iter_mut())
605                .map(|(label, selection)| measure(label.as_ref(), selection.raw()))
606                .fold(0.0, |next, current| current.max(next));
607
608            let icons: Cow<'static, [Handle]> = Cow::Owned(icons.to_vec());
609            let selections: Cow<'static, [S]> = Cow::Owned(selections.to_vec());
610            let state = state.clone();
611            let on_close = surface::action::destroy_popup(id);
612            let on_surface_action_clone = on_surface_action.clone();
613            let translation = layout.virtual_offset();
614            let get_popup_action = surface::action::simple_popup::<AppMessage>(
615                || LiveSettings::default(),
616                move || {
617                    SctkPopupSettings {
618                parent,
619                id,
620                input_zone: None,
621                positioner: SctkPositioner {
622                    size: Some((selections_width as u32 + gap as u32 + pad_width as u32 + icon_width as u32, 10)),
623                    anchor_rect,
624                    // TODO: left or right alignment based on direction?
625                    anchor: cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Anchor::BottomLeft,
626                    gravity: cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Gravity::BottomRight,
627                    reactive: true,
628                    offset: ((-padding.left - translation.x) as i32, -translation.y as i32),
629                    constraint_adjustment: 9,
630                    ..Default::default()
631                },
632                parent_size: None,
633                grab: true,
634                close_with_children: true,
635            }
636                },
637                Some(Box::new(move || {
638                    let action_map = action_map.clone();
639                    let on_selected = on_selected.clone();
640                    let e: Element<'static, crate::Action<AppMessage>> =
641                        Element::from(menu_widget(
642                            bounds,
643                            &state,
644                            gap,
645                            padding,
646                            text_size.unwrap_or(14.0),
647                            selections.clone(),
648                            icons.clone(),
649                            selected_option,
650                            Arc::new(move |i| on_selected.clone()(i)),
651                            Some(on_surface_action_clone(on_close.clone())),
652                        ))
653                        .map(move |m| crate::Action::App(action_map.clone()(m)));
654                    e
655                })),
656            );
657            shell.publish(on_surface_action(get_popup_action));
658        }
659    };
660
661    let is_open = state.is_open.load(Ordering::Relaxed);
662    let refresh = state.close_operation && state.open_operation;
663
664    if state.close_operation {
665        state.close_operation = false;
666        state.is_open.store(false, Ordering::SeqCst);
667        if is_open {
668            shell.request_redraw();
669            #[cfg(wayland_platform)]
670            if let Some(ref on_close) = on_surface_action {
671                shell.publish(on_close(surface::action::destroy_popup(state.popup_id)));
672            }
673        }
674    }
675
676    if state.open_operation {
677        state.open_operation = false;
678        state.is_open.store(true, Ordering::SeqCst);
679        if (refresh && is_open) || (!refresh && !is_open) {
680            open(shell, state, on_selected.clone());
681        }
682    }
683
684    match event {
685        Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
686        | Event::Touch(touch::Event::FingerPressed { .. }) => {
687            let is_open = state.is_open.load(Ordering::Relaxed);
688            if is_open {
689                // Event wasn't processed by overlay, so cursor was clicked either outside it's
690                // bounds or on the drop-down, either way we close the overlay.
691                state.is_open.store(false, Ordering::Relaxed);
692                shell.request_redraw();
693                #[cfg(wayland_platform)]
694                if let Some(on_close) = on_surface_action {
695                    shell.publish(on_close(surface::action::destroy_popup(state.popup_id)));
696                }
697                shell.capture_event();
698            } else if cursor.is_over(layout.bounds()) {
699                open(shell, state, on_selected);
700                shell.capture_event();
701            }
702        }
703        Event::Mouse(mouse::Event::WheelScrolled {
704            delta: mouse::ScrollDelta::Lines { .. },
705        }) => {
706            let is_open = state.is_open.load(Ordering::Relaxed);
707
708            if state.keyboard_modifiers.command() && cursor.is_over(layout.bounds()) && !is_open {
709                let next_index = selected.map(|index| index + 1).unwrap_or_default();
710
711                if selections.len() < next_index {
712                    shell.publish((on_selected)(next_index));
713                }
714
715                shell.capture_event();
716            }
717        }
718        Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => {
719            state.keyboard_modifiers = *modifiers;
720        }
721        _ => {}
722    }
723}
724
725/// Returns the current [`mouse::Interaction`] of a [`Dropdown`].
726#[must_use]
727pub fn mouse_interaction(layout: Layout<'_>, cursor: mouse::Cursor) -> mouse::Interaction {
728    let bounds = layout.bounds();
729    let is_mouse_over = cursor.is_over(bounds);
730
731    if is_mouse_over {
732        mouse::Interaction::Pointer
733    } else {
734        mouse::Interaction::default()
735    }
736}
737
738#[cfg(wayland_platform)]
739/// Returns the current menu widget of a [`Dropdown`].
740#[allow(clippy::too_many_arguments)]
741pub fn menu_widget<
742    S: AsRef<str> + Send + Sync + Clone + 'static,
743    Message: 'static + std::clone::Clone,
744>(
745    bounds: Rectangle,
746    state: &State,
747    gap: f32,
748    padding: Padding,
749    text_size: f32,
750    selections: Cow<'static, [S]>,
751    icons: Cow<'static, [icon::Handle]>,
752    selected_option: Option<usize>,
753    on_selected: Arc<dyn Fn(usize) -> Message + Send + Sync + 'static>,
754    close_on_selected: Option<Message>,
755) -> crate::Element<'static, Message>
756where
757    [S]: std::borrow::ToOwned,
758{
759    let icon_width = if icons.is_empty() { 0.0 } else { 24.0 };
760    let measure = |_label: &str, selection_paragraph: &crate::Paragraph| -> f32 {
761        selection_paragraph.min_width().round()
762    };
763    let selections_width = selections
764        .iter()
765        .zip(state.selections.iter())
766        .map(|(label, selection)| measure(label.as_ref(), selection.raw()))
767        .fold(0.0, |next, current| current.max(next));
768    let pad_width = padding.x().mul_add(2.0, 16.0);
769
770    let width = selections_width + gap + pad_width + icon_width;
771    let is_open = state.is_open.clone();
772    let menu: Menu<'static, S, Message> = Menu::new(
773        state.menu.clone(),
774        selections,
775        icons,
776        state.hovered_option.clone(),
777        selected_option,
778        move |option| {
779            is_open.store(false, Ordering::Relaxed);
780
781            (on_selected)(option)
782        },
783        None,
784        close_on_selected,
785    )
786    .width(width)
787    .padding(padding)
788    .text_size(text_size);
789
790    crate::widget::autosize::autosize(
791        menu.popup(iced::Point::new(0., 0.), bounds.height),
792        AUTOSIZE_ID.clone(),
793    )
794    .auto_height(true)
795    .auto_width(true)
796    .min_height(1.)
797    .min_width(width)
798    .into()
799}
800
801/// Returns the current overlay of a [`Dropdown`].
802#[allow(clippy::too_many_arguments)]
803pub fn overlay<'a, S: AsRef<str> + Send + Sync + Clone + 'static, Message: std::clone::Clone + 'a>(
804    layout: Layout<'_>,
805    _renderer: &crate::Renderer,
806    state: &'a mut State,
807    gap: f32,
808    padding: Padding,
809    text_size: f32,
810    _text_line_height: text::LineHeight,
811    _font: Option<crate::font::Font>,
812    selections: &'a [S],
813    icons: &'a [icon::Handle],
814    selected_option: Option<usize>,
815    on_selected: &'a dyn Fn(usize) -> Message,
816    translation: Vector,
817    close_on_selected: Option<Message>,
818) -> Option<overlay::Element<'a, Message, crate::Theme, crate::Renderer>>
819where
820    [S]: std::borrow::ToOwned,
821{
822    if state.is_open.load(Ordering::Relaxed) {
823        let bounds = layout.bounds();
824
825        let menu = Menu::new(
826            state.menu.clone(),
827            Cow::Borrowed(selections),
828            Cow::Borrowed(icons),
829            state.hovered_option.clone(),
830            selected_option,
831            |option| {
832                state.is_open.store(false, Ordering::Relaxed);
833
834                (on_selected)(option)
835            },
836            None,
837            close_on_selected,
838        )
839        .width({
840            let measure = |_label: &str, selection_paragraph: &crate::Paragraph| -> f32 {
841                selection_paragraph.min_width().round()
842            };
843
844            let pad_width = padding.x().mul_add(2.0, 16.0);
845
846            let icon_width = if icons.is_empty() { 0.0 } else { 24.0 };
847
848            selections
849                .iter()
850                .zip(state.selections.iter_mut())
851                .map(|(label, selection)| measure(label.as_ref(), selection.raw()))
852                .fold(0.0, |next, current| current.max(next))
853                + gap
854                + pad_width
855                + icon_width
856        })
857        .padding(padding)
858        .text_size(text_size);
859
860        let mut position = layout.position();
861        position.x -= padding.left;
862        position.x += translation.x;
863        position.y += translation.y;
864        Some(menu.overlay(position, bounds.height))
865    } else {
866        None
867    }
868}
869
870/// Draws a [`Dropdown`].
871#[allow(clippy::too_many_arguments)]
872pub fn draw<'a, S>(
873    renderer: &mut crate::Renderer,
874    theme: &crate::Theme,
875    layout: Layout<'_>,
876    cursor: mouse::Cursor,
877    gap: f32,
878    padding: Padding,
879    text_size: Option<f32>,
880    text_line_height: text::LineHeight,
881    font: crate::font::Font,
882    selected: Option<&'a S>,
883    icon: Option<&'a icon::Handle>,
884    placeholder: Option<&'a str>,
885    state: &'a State,
886    viewport: &Rectangle,
887) where
888    S: AsRef<str> + 'a,
889{
890    let bounds = layout.bounds();
891    let is_mouse_over = cursor.is_over(bounds);
892
893    let style = if is_mouse_over {
894        theme.style(&(), pick_list::Status::Hovered)
895    } else {
896        theme.style(&(), pick_list::Status::Active)
897    };
898
899    iced_core::Renderer::fill_quad(
900        renderer,
901        renderer::Quad {
902            bounds,
903            border: style.border,
904            shadow: Shadow::default(),
905            snap: true,
906        },
907        style.background,
908    );
909
910    if let Some(handle) = state.icon.clone() {
911        let svg_handle = svg::Svg::new(handle).color(style.text_color);
912        let bounds = Rectangle {
913            x: bounds.x + bounds.width - gap - 16.0,
914            y: bounds.center_y() - 8.0,
915            width: 16.0,
916            height: 16.0,
917        };
918        svg::Renderer::draw_svg(renderer, svg_handle, bounds, bounds);
919    }
920
921    if let Some(content) = selected.map(AsRef::as_ref).or(placeholder) {
922        let text_size = text_size.unwrap_or_else(|| text::Renderer::default_size(renderer).0);
923
924        let mut bounds = Rectangle {
925            x: bounds.x + padding.left,
926            y: bounds.center_y(),
927            width: bounds.width - padding.x(),
928            height: f32::from(text_line_height.to_absolute(Pixels(text_size))),
929        };
930
931        if let Some(handle) = icon {
932            let icon_bounds = Rectangle {
933                x: bounds.x,
934                y: bounds.y - (bounds.height / 2.0) - 2.0,
935                width: 20.0,
936                height: 20.0,
937            };
938
939            bounds.x += 24.0;
940            icon::draw(renderer, handle, icon_bounds);
941        }
942
943        text::Renderer::fill_text(
944            renderer,
945            Text {
946                content: content.to_string(),
947                size: iced::Pixels(text_size),
948                line_height: text_line_height,
949                font,
950                bounds: bounds.size(),
951                align_x: text::Alignment::Left,
952                align_y: alignment::Vertical::Center,
953                shaping: text::Shaping::Advanced,
954                wrapping: text::Wrapping::default(),
955                ellipsize: text::Ellipsize::default(),
956            },
957            bounds.position(),
958            style.text_color,
959            *viewport,
960        );
961    }
962}