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: false,
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        Event::Keyboard(keyboard::Event::KeyPressed {
722            key: keyboard::Key::Named(keyboard::key::Named::Escape),
723            ..
724        }) => {
725            let is_open = state.is_open.load(Ordering::Relaxed);
726            if is_open {
727                state.is_open.store(false, Ordering::Relaxed);
728                shell.request_redraw();
729                #[cfg(wayland_platform)]
730                if let Some(on_close) = on_surface_action {
731                    shell.publish(on_close(surface::action::destroy_popup(state.popup_id)));
732                }
733                shell.capture_event();
734            }
735        }
736
737        _ => {}
738    }
739}
740
741/// Returns the current [`mouse::Interaction`] of a [`Dropdown`].
742#[must_use]
743pub fn mouse_interaction(layout: Layout<'_>, cursor: mouse::Cursor) -> mouse::Interaction {
744    let bounds = layout.bounds();
745    let is_mouse_over = cursor.is_over(bounds);
746
747    if is_mouse_over {
748        mouse::Interaction::Pointer
749    } else {
750        mouse::Interaction::default()
751    }
752}
753
754#[cfg(wayland_platform)]
755/// Returns the current menu widget of a [`Dropdown`].
756#[allow(clippy::too_many_arguments)]
757pub fn menu_widget<
758    S: AsRef<str> + Send + Sync + Clone + 'static,
759    Message: 'static + std::clone::Clone,
760>(
761    bounds: Rectangle,
762    state: &State,
763    gap: f32,
764    padding: Padding,
765    text_size: f32,
766    selections: Cow<'static, [S]>,
767    icons: Cow<'static, [icon::Handle]>,
768    selected_option: Option<usize>,
769    on_selected: Arc<dyn Fn(usize) -> Message + Send + Sync + 'static>,
770    close_on_selected: Option<Message>,
771) -> crate::Element<'static, Message>
772where
773    [S]: std::borrow::ToOwned,
774{
775    let icon_width = if icons.is_empty() { 0.0 } else { 24.0 };
776    let measure = |_label: &str, selection_paragraph: &crate::Paragraph| -> f32 {
777        selection_paragraph.min_width().round()
778    };
779    let selections_width = selections
780        .iter()
781        .zip(state.selections.iter())
782        .map(|(label, selection)| measure(label.as_ref(), selection.raw()))
783        .fold(0.0, |next, current| current.max(next));
784    let pad_width = padding.x().mul_add(2.0, 16.0);
785
786    let width = selections_width + gap + pad_width + icon_width;
787    let is_open = state.is_open.clone();
788    let menu: Menu<'static, S, Message> = Menu::new(
789        state.menu.clone(),
790        selections,
791        icons,
792        state.hovered_option.clone(),
793        selected_option,
794        move |option| {
795            is_open.store(false, Ordering::Relaxed);
796
797            (on_selected)(option)
798        },
799        None,
800        close_on_selected,
801    )
802    .width(width)
803    .padding(padding)
804    .text_size(text_size);
805
806    crate::widget::autosize::autosize(
807        menu.popup(iced::Point::new(0., 0.), bounds.height),
808        AUTOSIZE_ID.clone(),
809    )
810    .auto_height(true)
811    .auto_width(true)
812    .min_height(1.)
813    .min_width(width)
814    .into()
815}
816
817/// Returns the current overlay of a [`Dropdown`].
818#[allow(clippy::too_many_arguments)]
819pub fn overlay<'a, S: AsRef<str> + Send + Sync + Clone + 'static, Message: std::clone::Clone + 'a>(
820    layout: Layout<'_>,
821    _renderer: &crate::Renderer,
822    state: &'a mut State,
823    gap: f32,
824    padding: Padding,
825    text_size: f32,
826    _text_line_height: text::LineHeight,
827    _font: Option<crate::font::Font>,
828    selections: &'a [S],
829    icons: &'a [icon::Handle],
830    selected_option: Option<usize>,
831    on_selected: &'a dyn Fn(usize) -> Message,
832    translation: Vector,
833    close_on_selected: Option<Message>,
834) -> Option<overlay::Element<'a, Message, crate::Theme, crate::Renderer>>
835where
836    [S]: std::borrow::ToOwned,
837{
838    if state.is_open.load(Ordering::Relaxed) {
839        let bounds = layout.bounds();
840
841        let menu = Menu::new(
842            state.menu.clone(),
843            Cow::Borrowed(selections),
844            Cow::Borrowed(icons),
845            state.hovered_option.clone(),
846            selected_option,
847            |option| {
848                state.is_open.store(false, Ordering::Relaxed);
849
850                (on_selected)(option)
851            },
852            None,
853            close_on_selected,
854        )
855        .width({
856            let measure = |_label: &str, selection_paragraph: &crate::Paragraph| -> f32 {
857                selection_paragraph.min_width().round()
858            };
859
860            let pad_width = padding.x().mul_add(2.0, 16.0);
861
862            let icon_width = if icons.is_empty() { 0.0 } else { 24.0 };
863
864            selections
865                .iter()
866                .zip(state.selections.iter_mut())
867                .map(|(label, selection)| measure(label.as_ref(), selection.raw()))
868                .fold(0.0, |next, current| current.max(next))
869                + gap
870                + pad_width
871                + icon_width
872        })
873        .padding(padding)
874        .text_size(text_size);
875
876        let mut position = layout.position();
877        position.x -= padding.left;
878        position.x += translation.x;
879        position.y += translation.y;
880        Some(menu.overlay(position, bounds.height))
881    } else {
882        None
883    }
884}
885
886/// Draws a [`Dropdown`].
887#[allow(clippy::too_many_arguments)]
888pub fn draw<'a, S>(
889    renderer: &mut crate::Renderer,
890    theme: &crate::Theme,
891    layout: Layout<'_>,
892    cursor: mouse::Cursor,
893    gap: f32,
894    padding: Padding,
895    text_size: Option<f32>,
896    text_line_height: text::LineHeight,
897    font: crate::font::Font,
898    selected: Option<&'a S>,
899    icon: Option<&'a icon::Handle>,
900    placeholder: Option<&'a str>,
901    state: &'a State,
902    viewport: &Rectangle,
903) where
904    S: AsRef<str> + 'a,
905{
906    let bounds = layout.bounds();
907    let is_mouse_over = cursor.is_over(bounds);
908
909    let style = if is_mouse_over {
910        theme.style(&(), pick_list::Status::Hovered)
911    } else {
912        theme.style(&(), pick_list::Status::Active)
913    };
914
915    iced_core::Renderer::fill_quad(
916        renderer,
917        renderer::Quad {
918            bounds,
919            border: style.border,
920            shadow: Shadow::default(),
921            snap: true,
922        },
923        style.background,
924    );
925
926    if let Some(handle) = state.icon.clone() {
927        let svg_handle = svg::Svg::new(handle).color(style.text_color);
928        let bounds = Rectangle {
929            x: bounds.x + bounds.width - gap - 16.0,
930            y: bounds.center_y() - 8.0,
931            width: 16.0,
932            height: 16.0,
933        };
934        svg::Renderer::draw_svg(renderer, svg_handle, bounds, bounds);
935    }
936
937    if let Some(content) = selected.map(AsRef::as_ref).or(placeholder) {
938        let text_size = text_size.unwrap_or_else(|| text::Renderer::default_size(renderer).0);
939
940        let mut bounds = Rectangle {
941            x: bounds.x + padding.left,
942            y: bounds.center_y(),
943            width: bounds.width - padding.x(),
944            height: f32::from(text_line_height.to_absolute(Pixels(text_size))),
945        };
946
947        if let Some(handle) = icon {
948            let icon_bounds = Rectangle {
949                x: bounds.x,
950                y: bounds.y - (bounds.height / 2.0) - 2.0,
951                width: 20.0,
952                height: 20.0,
953            };
954
955            bounds.x += 24.0;
956            icon::draw(renderer, handle, icon_bounds);
957        }
958
959        text::Renderer::fill_text(
960            renderer,
961            Text {
962                content: content.to_string(),
963                size: iced::Pixels(text_size),
964                line_height: text_line_height,
965                font,
966                bounds: bounds.size(),
967                align_x: text::Alignment::Left,
968                align_y: alignment::Vertical::Center,
969                shaping: text::Shaping::Advanced,
970                wrapping: text::Wrapping::default(),
971                ellipsize: text::Ellipsize::default(),
972            },
973            bounds.position(),
974            style.text_color,
975            *viewport,
976        );
977    }
978}