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: &'a [S],
38 #[setters]
39 icons: &'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: &'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: &[],
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::<
540 AppMessage,
541 Box<
542 dyn Fn() -> Element<'static, crate::Action<AppMessage>>
543 + Send
544 + Sync
545 + 'static,
546 >,
547 >(
548 move || {
549 SctkPopupSettings {
550 parent,
551 id,
552 input_zone: None,
553 positioner: SctkPositioner {
554 size: Some((selections_width as u32 + gap as u32 + pad_width as u32 + icon_width as u32, 10)),
555 anchor_rect,
556 anchor: cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Anchor::BottomLeft,
558 gravity: cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Gravity::BottomRight,
559 reactive: true,
560 offset: ((-padding.left - translation.x) as i32, -translation.y as i32),
561 constraint_adjustment: 9,
562 ..Default::default()
563 },
564 parent_size: None,
565 grab: true,
566 close_with_children: true,
567 }
568 },
569 Some(Box::new(move || {
570 let action_map = action_map.clone();
571 let on_selected = on_selected.clone();
572 let e: Element<'static, crate::Action<AppMessage>> =
573 Element::from(menu_widget(
574 bounds,
575 &state,
576 gap,
577 padding,
578 text_size.unwrap_or(14.0),
579 selections.clone(),
580 icons.clone(),
581 selected_option,
582 Arc::new(move |i| on_selected.clone()(i)),
583 Some(on_surface_action_clone(on_close.clone())),
584 ))
585 .map(move |m| crate::Action::App(action_map.clone()(m)));
586 e
587 })),
588 );
589 shell.publish(on_surface_action(get_popup_action));
590 }
591 event::Status::Captured
592 } else {
593 event::Status::Ignored
594 }
595 }
596 Event::Mouse(mouse::Event::WheelScrolled {
597 delta: mouse::ScrollDelta::Lines { .. },
598 }) => {
599 let state = state();
600 let is_open = state.is_open.load(Ordering::Relaxed);
601
602 if state.keyboard_modifiers.command() && cursor.is_over(layout.bounds()) && !is_open {
603 let next_index = selected.map(|index| index + 1).unwrap_or_default();
604
605 if selections.len() < next_index {
606 shell.publish((on_selected)(next_index));
607 }
608
609 event::Status::Captured
610 } else {
611 event::Status::Ignored
612 }
613 }
614 Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => {
615 let state = state();
616
617 state.keyboard_modifiers = *modifiers;
618
619 event::Status::Ignored
620 }
621 _ => event::Status::Ignored,
622 }
623}
624
625#[must_use]
627pub fn mouse_interaction(layout: Layout<'_>, cursor: mouse::Cursor) -> mouse::Interaction {
628 let bounds = layout.bounds();
629 let is_mouse_over = cursor.is_over(bounds);
630
631 if is_mouse_over {
632 mouse::Interaction::Pointer
633 } else {
634 mouse::Interaction::default()
635 }
636}
637
638#[cfg(all(feature = "winit", feature = "wayland"))]
639#[allow(clippy::too_many_arguments)]
641pub fn menu_widget<
642 S: AsRef<str> + Send + Sync + Clone + 'static,
643 Message: 'static + std::clone::Clone,
644>(
645 bounds: Rectangle,
646 state: &State,
647 gap: f32,
648 padding: Padding,
649 text_size: f32,
650 selections: Cow<'static, [S]>,
651 icons: Cow<'static, [icon::Handle]>,
652 selected_option: Option<usize>,
653 on_selected: Arc<dyn Fn(usize) -> Message + Send + Sync + 'static>,
654 close_on_selected: Option<Message>,
655) -> crate::Element<'static, Message>
656where
657 [S]: std::borrow::ToOwned,
658{
659 let icon_width = if icons.is_empty() { 0.0 } else { 24.0 };
660 let measure = |_label: &str, selection_paragraph: &crate::Paragraph| -> f32 {
661 selection_paragraph.min_width().round()
662 };
663 let selections_width = selections
664 .iter()
665 .zip(state.selections.iter())
666 .map(|(label, selection)| measure(label.as_ref(), selection.raw()))
667 .fold(0.0, |next, current| current.max(next));
668 let pad_width = padding.horizontal().mul_add(2.0, 16.0);
669
670 let width = selections_width + gap + pad_width + icon_width;
671 let is_open = state.is_open.clone();
672 let menu: Menu<'static, S, Message> = Menu::new(
673 state.menu.clone(),
674 selections,
675 icons,
676 state.hovered_option.clone(),
677 selected_option,
678 move |option| {
679 is_open.store(false, Ordering::Relaxed);
680
681 (on_selected)(option)
682 },
683 None,
684 close_on_selected,
685 )
686 .width(width)
687 .padding(padding)
688 .text_size(text_size);
689
690 crate::widget::autosize::autosize(
691 menu.popup(iced::Point::new(0., 0.), bounds.height),
692 AUTOSIZE_ID.clone(),
693 )
694 .auto_height(true)
695 .auto_width(true)
696 .min_height(1.)
697 .min_width(width)
698 .into()
699}
700
701#[allow(clippy::too_many_arguments)]
703pub fn overlay<'a, S: AsRef<str> + Send + Sync + Clone + 'static, Message: std::clone::Clone + 'a>(
704 layout: Layout<'_>,
705 _renderer: &crate::Renderer,
706 state: &'a mut State,
707 gap: f32,
708 padding: Padding,
709 text_size: f32,
710 _text_line_height: text::LineHeight,
711 _font: Option<crate::font::Font>,
712 selections: &'a [S],
713 icons: &'a [icon::Handle],
714 selected_option: Option<usize>,
715 on_selected: &'a dyn Fn(usize) -> Message,
716 translation: Vector,
717 close_on_selected: Option<Message>,
718) -> Option<overlay::Element<'a, Message, crate::Theme, crate::Renderer>>
719where
720 [S]: std::borrow::ToOwned,
721{
722 if state.is_open.load(Ordering::Relaxed) {
723 let bounds = layout.bounds();
724
725 let menu = Menu::new(
726 state.menu.clone(),
727 Cow::Borrowed(selections),
728 Cow::Borrowed(icons),
729 state.hovered_option.clone(),
730 selected_option,
731 |option| {
732 state.is_open.store(false, Ordering::Relaxed);
733
734 (on_selected)(option)
735 },
736 None,
737 close_on_selected,
738 )
739 .width({
740 let measure = |_label: &str, selection_paragraph: &crate::Paragraph| -> f32 {
741 selection_paragraph.min_width().round()
742 };
743
744 let pad_width = padding.horizontal().mul_add(2.0, 16.0);
745
746 let icon_width = if icons.is_empty() { 0.0 } else { 24.0 };
747
748 selections
749 .iter()
750 .zip(state.selections.iter_mut())
751 .map(|(label, selection)| measure(label.as_ref(), selection.raw()))
752 .fold(0.0, |next, current| current.max(next))
753 + gap
754 + pad_width
755 + icon_width
756 })
757 .padding(padding)
758 .text_size(text_size);
759
760 let mut position = layout.position();
761 position.x -= padding.left;
762 position.x += translation.x;
763 position.y += translation.y;
764 Some(menu.overlay(position, bounds.height))
765 } else {
766 None
767 }
768}
769
770#[allow(clippy::too_many_arguments)]
772pub fn draw<'a, S>(
773 renderer: &mut crate::Renderer,
774 theme: &crate::Theme,
775 layout: Layout<'_>,
776 cursor: mouse::Cursor,
777 gap: f32,
778 padding: Padding,
779 text_size: Option<f32>,
780 text_line_height: text::LineHeight,
781 font: crate::font::Font,
782 selected: Option<&'a S>,
783 icon: Option<&'a icon::Handle>,
784 state: &'a State,
785 viewport: &Rectangle,
786) where
787 S: AsRef<str> + 'a,
788{
789 let bounds = layout.bounds();
790 let is_mouse_over = cursor.is_over(bounds);
791
792 let style = if is_mouse_over {
793 theme.style(&(), pick_list::Status::Hovered)
794 } else {
795 theme.style(&(), pick_list::Status::Active)
796 };
797
798 iced_core::Renderer::fill_quad(
799 renderer,
800 renderer::Quad {
801 bounds,
802 border: style.border,
803 shadow: Shadow::default(),
804 },
805 style.background,
806 );
807
808 if let Some(handle) = state.icon.clone() {
809 let svg_handle = svg::Svg::new(handle).color(style.text_color);
810
811 svg::Renderer::draw_svg(
812 renderer,
813 svg_handle,
814 Rectangle {
815 x: bounds.x + bounds.width - gap - 16.0,
816 y: bounds.center_y() - 8.0,
817 width: 16.0,
818 height: 16.0,
819 },
820 );
821 }
822
823 if let Some(content) = selected.map(AsRef::as_ref) {
824 let text_size = text_size.unwrap_or_else(|| text::Renderer::default_size(renderer).0);
825
826 let mut bounds = Rectangle {
827 x: bounds.x + padding.left,
828 y: bounds.center_y(),
829 width: bounds.width - padding.horizontal(),
830 height: f32::from(text_line_height.to_absolute(Pixels(text_size))),
831 };
832
833 if let Some(handle) = icon {
834 let icon_bounds = Rectangle {
835 x: bounds.x,
836 y: bounds.y - (bounds.height / 2.0) - 2.0,
837 width: 20.0,
838 height: 20.0,
839 };
840
841 bounds.x += 24.0;
842 icon::draw(renderer, handle, icon_bounds);
843 }
844
845 text::Renderer::fill_text(
846 renderer,
847 Text {
848 content: content.to_string(),
849 size: iced::Pixels(text_size),
850 line_height: text_line_height,
851 font,
852 bounds: bounds.size(),
853 horizontal_alignment: alignment::Horizontal::Left,
854 vertical_alignment: alignment::Vertical::Center,
855 shaping: text::Shaping::Advanced,
856 wrapping: text::Wrapping::default(),
857 },
858 bounds.position(),
859 style.text_color,
860 *viewport,
861 );
862 }
863}