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:
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 pub const DEFAULT_GAP: f32 = 4.0;
75
76 pub const DEFAULT_PADDING: Padding = Padding::new(8.0);
78
79 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 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 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 }
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 }
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#[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 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#[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#[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 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 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#[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#[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#[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#[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}