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, Shell, Size, Vector, Widget,
16};
17use iced_core::{Shadow, alignment, keyboard, layout, mouse, overlay, renderer, svg, touch};
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(all(feature = "winit", feature = "wayland", target_os = "linux"))]
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(all(feature = "winit", feature = "wayland", target_os = "linux"))]
100 positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner::default(),
101 on_surface_action: None,
102 action_map: None,
103 }
104 }
105
106 #[cfg(all(feature = "winit", feature = "wayland", target_os = "linux"))]
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(all(feature = "winit", feature = "wayland", target_os = "linux"))]
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(all(feature = "winit", feature = "wayland", target_os = "linux"))]
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(all(feature = "winit", feature = "wayland", target_os = "linux"))]
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(all(feature = "winit", feature = "wayland", target_os = "linux"))]
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 let mut hovered_guard = state.hovered_option.lock().unwrap();
571 *hovered_guard = selected;
572 let id = window::Id::unique();
573 state.popup_id = id;
574 #[cfg(all(feature = "winit", feature = "wayland", target_os = "linux"))]
575 if let Some(((on_surface_action, parent), action_map)) = on_surface_action
576 .as_ref()
577 .zip(_window_id)
578 .zip(action_map.clone())
579 {
580 use iced_runtime::platform_specific::wayland::popup::{
581 SctkPopupSettings, SctkPositioner,
582 };
583 let bounds = layout.bounds();
584 let anchor_rect = Rectangle {
585 x: bounds.x as i32,
586 y: bounds.y as i32,
587 width: bounds.width as i32,
588 height: bounds.height as i32,
589 };
590 let icon_width = if icons.is_empty() { 0.0 } else { 24.0 };
591 let measure = |_label: &str, selection_paragraph: &crate::Paragraph| -> f32 {
592 selection_paragraph.min_width().round()
593 };
594 let pad_width = padding.x().mul_add(2.0, 16.0);
595
596 let selections_width = selections
597 .iter()
598 .zip(state.selections.iter_mut())
599 .map(|(label, selection)| measure(label.as_ref(), selection.raw()))
600 .fold(0.0, |next, current| current.max(next));
601
602 let icons: Cow<'static, [Handle]> = Cow::Owned(icons.to_vec());
603 let selections: Cow<'static, [S]> = Cow::Owned(selections.to_vec());
604 let state = state.clone();
605 let on_close = surface::action::destroy_popup(id);
606 let on_surface_action_clone = on_surface_action.clone();
607 let translation = layout.virtual_offset();
608 let get_popup_action = surface::action::simple_popup::<AppMessage>(
609 move || {
610 SctkPopupSettings {
611 parent,
612 id,
613 input_zone: None,
614 positioner: SctkPositioner {
615 size: Some((selections_width as u32 + gap as u32 + pad_width as u32 + icon_width as u32, 10)),
616 anchor_rect,
617 anchor: cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Anchor::BottomLeft,
619 gravity: cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Gravity::BottomRight,
620 reactive: true,
621 offset: ((-padding.left - translation.x) as i32, -translation.y as i32),
622 constraint_adjustment: 9,
623 ..Default::default()
624 },
625 parent_size: None,
626 grab: true,
627 close_with_children: true,
628 }
629 },
630 Some(Box::new(move || {
631 let action_map = action_map.clone();
632 let on_selected = on_selected.clone();
633 let e: Element<'static, crate::Action<AppMessage>> =
634 Element::from(menu_widget(
635 bounds,
636 &state,
637 gap,
638 padding,
639 text_size.unwrap_or(14.0),
640 selections.clone(),
641 icons.clone(),
642 selected_option,
643 Arc::new(move |i| on_selected.clone()(i)),
644 Some(on_surface_action_clone(on_close.clone())),
645 ))
646 .map(move |m| crate::Action::App(action_map.clone()(m)));
647 e
648 })),
649 );
650 shell.publish(on_surface_action(get_popup_action));
651 }
652 };
653
654 let is_open = state.is_open.load(Ordering::Relaxed);
655 let refresh = state.close_operation && state.open_operation;
656
657 if state.close_operation {
658 state.close_operation = false;
659 state.is_open.store(false, Ordering::SeqCst);
660 if is_open {
661 #[cfg(all(feature = "winit", feature = "wayland", target_os = "linux"))]
662 if let Some(ref on_close) = on_surface_action {
663 shell.publish(on_close(surface::action::destroy_popup(state.popup_id)));
664 }
665 }
666 }
667
668 if state.open_operation {
669 state.open_operation = false;
670 state.is_open.store(true, Ordering::SeqCst);
671 if (refresh && is_open) || (!refresh && !is_open) {
672 open(shell, state, on_selected.clone());
673 }
674 }
675
676 match event {
677 Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
678 | Event::Touch(touch::Event::FingerPressed { .. }) => {
679 let is_open = state.is_open.load(Ordering::Relaxed);
680 if is_open {
681 state.is_open.store(false, Ordering::Relaxed);
684 #[cfg(all(feature = "winit", feature = "wayland", target_os = "linux"))]
685 if let Some(on_close) = on_surface_action {
686 shell.publish(on_close(surface::action::destroy_popup(state.popup_id)));
687 }
688 shell.capture_event();
689 } else if cursor.is_over(layout.bounds()) {
690 open(shell, state, on_selected);
691 shell.capture_event();
692 }
693 }
694 Event::Mouse(mouse::Event::WheelScrolled {
695 delta: mouse::ScrollDelta::Lines { .. },
696 }) => {
697 let is_open = state.is_open.load(Ordering::Relaxed);
698
699 if state.keyboard_modifiers.command() && cursor.is_over(layout.bounds()) && !is_open {
700 let next_index = selected.map(|index| index + 1).unwrap_or_default();
701
702 if selections.len() < next_index {
703 shell.publish((on_selected)(next_index));
704 }
705
706 shell.capture_event();
707 }
708 }
709 Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => {
710 state.keyboard_modifiers = *modifiers;
711 }
712 _ => {}
713 }
714}
715
716#[must_use]
718pub fn mouse_interaction(layout: Layout<'_>, cursor: mouse::Cursor) -> mouse::Interaction {
719 let bounds = layout.bounds();
720 let is_mouse_over = cursor.is_over(bounds);
721
722 if is_mouse_over {
723 mouse::Interaction::Pointer
724 } else {
725 mouse::Interaction::default()
726 }
727}
728
729#[cfg(all(feature = "winit", feature = "wayland", target_os = "linux"))]
730#[allow(clippy::too_many_arguments)]
732pub fn menu_widget<
733 S: AsRef<str> + Send + Sync + Clone + 'static,
734 Message: 'static + std::clone::Clone,
735>(
736 bounds: Rectangle,
737 state: &State,
738 gap: f32,
739 padding: Padding,
740 text_size: f32,
741 selections: Cow<'static, [S]>,
742 icons: Cow<'static, [icon::Handle]>,
743 selected_option: Option<usize>,
744 on_selected: Arc<dyn Fn(usize) -> Message + Send + Sync + 'static>,
745 close_on_selected: Option<Message>,
746) -> crate::Element<'static, Message>
747where
748 [S]: std::borrow::ToOwned,
749{
750 let icon_width = if icons.is_empty() { 0.0 } else { 24.0 };
751 let measure = |_label: &str, selection_paragraph: &crate::Paragraph| -> f32 {
752 selection_paragraph.min_width().round()
753 };
754 let selections_width = selections
755 .iter()
756 .zip(state.selections.iter())
757 .map(|(label, selection)| measure(label.as_ref(), selection.raw()))
758 .fold(0.0, |next, current| current.max(next));
759 let pad_width = padding.x().mul_add(2.0, 16.0);
760
761 let width = selections_width + gap + pad_width + icon_width;
762 let is_open = state.is_open.clone();
763 let menu: Menu<'static, S, Message> = Menu::new(
764 state.menu.clone(),
765 selections,
766 icons,
767 state.hovered_option.clone(),
768 selected_option,
769 move |option| {
770 is_open.store(false, Ordering::Relaxed);
771
772 (on_selected)(option)
773 },
774 None,
775 close_on_selected,
776 )
777 .width(width)
778 .padding(padding)
779 .text_size(text_size);
780
781 crate::widget::autosize::autosize(
782 menu.popup(iced::Point::new(0., 0.), bounds.height),
783 AUTOSIZE_ID.clone(),
784 )
785 .auto_height(true)
786 .auto_width(true)
787 .min_height(1.)
788 .min_width(width)
789 .into()
790}
791
792#[allow(clippy::too_many_arguments)]
794pub fn overlay<'a, S: AsRef<str> + Send + Sync + Clone + 'static, Message: std::clone::Clone + 'a>(
795 layout: Layout<'_>,
796 _renderer: &crate::Renderer,
797 state: &'a mut State,
798 gap: f32,
799 padding: Padding,
800 text_size: f32,
801 _text_line_height: text::LineHeight,
802 _font: Option<crate::font::Font>,
803 selections: &'a [S],
804 icons: &'a [icon::Handle],
805 selected_option: Option<usize>,
806 on_selected: &'a dyn Fn(usize) -> Message,
807 translation: Vector,
808 close_on_selected: Option<Message>,
809) -> Option<overlay::Element<'a, Message, crate::Theme, crate::Renderer>>
810where
811 [S]: std::borrow::ToOwned,
812{
813 if state.is_open.load(Ordering::Relaxed) {
814 let bounds = layout.bounds();
815
816 let menu = Menu::new(
817 state.menu.clone(),
818 Cow::Borrowed(selections),
819 Cow::Borrowed(icons),
820 state.hovered_option.clone(),
821 selected_option,
822 |option| {
823 state.is_open.store(false, Ordering::Relaxed);
824
825 (on_selected)(option)
826 },
827 None,
828 close_on_selected,
829 )
830 .width({
831 let measure = |_label: &str, selection_paragraph: &crate::Paragraph| -> f32 {
832 selection_paragraph.min_width().round()
833 };
834
835 let pad_width = padding.x().mul_add(2.0, 16.0);
836
837 let icon_width = if icons.is_empty() { 0.0 } else { 24.0 };
838
839 selections
840 .iter()
841 .zip(state.selections.iter_mut())
842 .map(|(label, selection)| measure(label.as_ref(), selection.raw()))
843 .fold(0.0, |next, current| current.max(next))
844 + gap
845 + pad_width
846 + icon_width
847 })
848 .padding(padding)
849 .text_size(text_size);
850
851 let mut position = layout.position();
852 position.x -= padding.left;
853 position.x += translation.x;
854 position.y += translation.y;
855 Some(menu.overlay(position, bounds.height))
856 } else {
857 None
858 }
859}
860
861#[allow(clippy::too_many_arguments)]
863pub fn draw<'a, S>(
864 renderer: &mut crate::Renderer,
865 theme: &crate::Theme,
866 layout: Layout<'_>,
867 cursor: mouse::Cursor,
868 gap: f32,
869 padding: Padding,
870 text_size: Option<f32>,
871 text_line_height: text::LineHeight,
872 font: crate::font::Font,
873 selected: Option<&'a S>,
874 icon: Option<&'a icon::Handle>,
875 placeholder: Option<&'a str>,
876 state: &'a State,
877 viewport: &Rectangle,
878) where
879 S: AsRef<str> + 'a,
880{
881 let bounds = layout.bounds();
882 let is_mouse_over = cursor.is_over(bounds);
883
884 let style = if is_mouse_over {
885 theme.style(&(), pick_list::Status::Hovered)
886 } else {
887 theme.style(&(), pick_list::Status::Active)
888 };
889
890 iced_core::Renderer::fill_quad(
891 renderer,
892 renderer::Quad {
893 bounds,
894 border: style.border,
895 shadow: Shadow::default(),
896 snap: true,
897 },
898 style.background,
899 );
900
901 if let Some(handle) = state.icon.clone() {
902 let svg_handle = svg::Svg::new(handle).color(style.text_color);
903 let bounds = Rectangle {
904 x: bounds.x + bounds.width - gap - 16.0,
905 y: bounds.center_y() - 8.0,
906 width: 16.0,
907 height: 16.0,
908 };
909 svg::Renderer::draw_svg(renderer, svg_handle, bounds, bounds);
910 }
911
912 if let Some(content) = selected.map(AsRef::as_ref).or(placeholder) {
913 let text_size = text_size.unwrap_or_else(|| text::Renderer::default_size(renderer).0);
914
915 let mut bounds = Rectangle {
916 x: bounds.x + padding.left,
917 y: bounds.center_y(),
918 width: bounds.width - padding.x(),
919 height: f32::from(text_line_height.to_absolute(Pixels(text_size))),
920 };
921
922 if let Some(handle) = icon {
923 let icon_bounds = Rectangle {
924 x: bounds.x,
925 y: bounds.y - (bounds.height / 2.0) - 2.0,
926 width: 20.0,
927 height: 20.0,
928 };
929
930 bounds.x += 24.0;
931 icon::draw(renderer, handle, icon_bounds);
932 }
933
934 text::Renderer::fill_text(
935 renderer,
936 Text {
937 content: content.to_string(),
938 size: iced::Pixels(text_size),
939 line_height: text_line_height,
940 font,
941 bounds: bounds.size(),
942 align_x: text::Alignment::Left,
943 align_y: alignment::Vertical::Center,
944 shaping: text::Shaping::Advanced,
945 wrapping: text::Wrapping::default(),
946 ellipsize: text::Ellipsize::default(),
947 },
948 bounds.position(),
949 style.text_color,
950 *viewport,
951 );
952 }
953}