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