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