1use crate::core::alignment;
65use crate::core::keyboard;
66use crate::core::layout;
67use crate::core::mouse;
68use crate::core::overlay;
69use crate::core::renderer;
70use crate::core::text::paragraph;
71use crate::core::text::{self, Text};
72use crate::core::touch;
73use crate::core::widget::tree::{self, Tree};
74use crate::core::window;
75use crate::core::{
76 Background, Border, Color, Element, Event, Layout, Length, Padding, Pixels, Point, Rectangle,
77 Shell, Size, Theme, Vector, Widget,
78};
79use crate::overlay::menu::{self, Menu};
80
81use std::borrow::Borrow;
82use std::f32;
83
84pub struct PickList<'a, T, L, V, Message, Theme = crate::Theme, Renderer = crate::Renderer>
148where
149 T: PartialEq + Clone,
150 L: Borrow<[T]> + 'a,
151 V: Borrow<T> + 'a,
152 Theme: Catalog,
153 Renderer: text::Renderer,
154{
155 options: L,
156 to_string: Box<dyn Fn(&T) -> String + 'a>,
157 on_select: Option<Box<dyn Fn(T) -> Message + 'a>>,
158 on_open: Option<Message>,
159 on_close: Option<Message>,
160 placeholder: Option<String>,
161 selected: Option<V>,
162 width: Length,
163 padding: Padding,
164 text_size: Option<Pixels>,
165 line_height: text::LineHeight,
166 shaping: text::Shaping,
167 ellipsis: text::Ellipsis,
168 font: Option<Renderer::Font>,
169 handle: Handle<Renderer::Font>,
170 class: <Theme as Catalog>::Class<'a>,
171 menu_class: <Theme as menu::Catalog>::Class<'a>,
172 last_status: Option<Status>,
173 menu_height: Length,
174}
175
176impl<'a, T, L, V, Message, Theme, Renderer> PickList<'a, T, L, V, Message, Theme, Renderer>
177where
178 T: PartialEq + Clone,
179 L: Borrow<[T]> + 'a,
180 V: Borrow<T> + 'a,
181 Message: Clone,
182 Theme: Catalog,
183 Renderer: text::Renderer,
184{
185 pub fn new(selected: Option<V>, options: L, to_string: impl Fn(&T) -> String + 'a) -> Self {
188 Self {
189 to_string: Box::new(to_string),
190 on_select: None,
191 on_open: None,
192 on_close: None,
193 options,
194 placeholder: None,
195 selected,
196 width: Length::Shrink,
197 padding: crate::button::DEFAULT_PADDING,
198 text_size: None,
199 line_height: text::LineHeight::default(),
200 shaping: text::Shaping::default(),
201 ellipsis: text::Ellipsis::End,
202 font: None,
203 handle: Handle::default(),
204 class: <Theme as Catalog>::default(),
205 menu_class: <Theme as Catalog>::default_menu(),
206 last_status: None,
207 menu_height: Length::Shrink,
208 }
209 }
210
211 pub fn placeholder(mut self, placeholder: impl Into<String>) -> Self {
213 self.placeholder = Some(placeholder.into());
214 self
215 }
216
217 pub fn width(mut self, width: impl Into<Length>) -> Self {
219 self.width = width.into();
220 self
221 }
222
223 pub fn menu_height(mut self, menu_height: impl Into<Length>) -> Self {
225 self.menu_height = menu_height.into();
226 self
227 }
228
229 pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self {
231 self.padding = padding.into();
232 self
233 }
234
235 pub fn text_size(mut self, size: impl Into<Pixels>) -> Self {
237 self.text_size = Some(size.into());
238 self
239 }
240
241 pub fn line_height(mut self, line_height: impl Into<text::LineHeight>) -> Self {
243 self.line_height = line_height.into();
244 self
245 }
246
247 pub fn shaping(mut self, shaping: text::Shaping) -> Self {
249 self.shaping = shaping;
250 self
251 }
252
253 pub fn ellipsis(mut self, ellipsis: text::Ellipsis) -> Self {
255 self.ellipsis = ellipsis;
256 self
257 }
258
259 pub fn font(mut self, font: impl Into<Renderer::Font>) -> Self {
261 self.font = Some(font.into());
262 self
263 }
264
265 pub fn handle(mut self, handle: Handle<Renderer::Font>) -> Self {
267 self.handle = handle;
268 self
269 }
270
271 pub fn on_select(mut self, on_select: impl Fn(T) -> Message + 'a) -> Self {
273 self.on_select = Some(Box::new(on_select));
274 self
275 }
276
277 pub fn on_open(mut self, on_open: Message) -> Self {
279 self.on_open = Some(on_open);
280 self
281 }
282
283 pub fn on_close(mut self, on_close: Message) -> Self {
285 self.on_close = Some(on_close);
286 self
287 }
288
289 #[must_use]
291 pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self
292 where
293 <Theme as Catalog>::Class<'a>: From<StyleFn<'a, Theme>>,
294 {
295 self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
296 self
297 }
298
299 #[must_use]
301 pub fn menu_style(mut self, style: impl Fn(&Theme) -> menu::Style + 'a) -> Self
302 where
303 <Theme as menu::Catalog>::Class<'a>: From<menu::StyleFn<'a, Theme>>,
304 {
305 self.menu_class = (Box::new(style) as menu::StyleFn<'a, Theme>).into();
306 self
307 }
308
309 #[cfg(feature = "advanced")]
311 #[must_use]
312 pub fn class(mut self, class: impl Into<<Theme as Catalog>::Class<'a>>) -> Self {
313 self.class = class.into();
314 self
315 }
316
317 #[cfg(feature = "advanced")]
319 #[must_use]
320 pub fn menu_class(mut self, class: impl Into<<Theme as menu::Catalog>::Class<'a>>) -> Self {
321 self.menu_class = class.into();
322 self
323 }
324}
325
326impl<'a, T, L, V, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
327 for PickList<'a, T, L, V, Message, Theme, Renderer>
328where
329 T: Clone + PartialEq + 'a,
330 L: Borrow<[T]>,
331 V: Borrow<T>,
332 Message: Clone + 'a,
333 Theme: Catalog + 'a,
334 Renderer: text::Renderer + 'a,
335{
336 fn tag(&self) -> tree::Tag {
337 tree::Tag::of::<State<Renderer::Paragraph>>()
338 }
339
340 fn state(&self) -> tree::State {
341 tree::State::new(State::<Renderer::Paragraph>::new())
342 }
343
344 fn size(&self) -> Size<Length> {
345 Size {
346 width: self.width,
347 height: Length::Shrink,
348 }
349 }
350
351 fn layout(
352 &mut self,
353 tree: &mut Tree,
354 renderer: &Renderer,
355 limits: &layout::Limits,
356 ) -> layout::Node {
357 let state = tree.state.downcast_mut::<State<Renderer::Paragraph>>();
358
359 let font = self.font.unwrap_or_else(|| renderer.default_font());
360 let text_size = self.text_size.unwrap_or_else(|| renderer.default_size());
361 let options = self.options.borrow();
362
363 let option_text = Text {
364 content: "",
365 bounds: Size::new(
366 limits.max().width,
367 self.line_height.to_absolute(text_size).into(),
368 ),
369 size: text_size,
370 line_height: self.line_height,
371 font,
372 align_x: text::Alignment::Default,
373 align_y: alignment::Vertical::Center,
374 shaping: self.shaping,
375 wrapping: text::Wrapping::None,
376 ellipsis: self.ellipsis,
377 hint_factor: renderer.scale_factor(),
378 };
379
380 if let Some(placeholder) = &self.placeholder {
381 let _ = state.placeholder.update(Text {
382 content: placeholder,
383 ..option_text
384 });
385 }
386
387 let max_width = match self.width {
388 Length::Shrink => {
389 state.options.resize_with(options.len(), Default::default);
390
391 for (option, paragraph) in options.iter().zip(state.options.iter_mut()) {
392 let label = (self.to_string)(option);
393
394 let _ = paragraph.update(Text {
395 content: &label,
396 ..option_text
397 });
398 }
399
400 let labels_width = state.options.iter().fold(0.0, |width, paragraph| {
401 f32::max(width, paragraph.min_width())
402 });
403
404 labels_width.max(
405 self.placeholder
406 .as_ref()
407 .map(|_| state.placeholder.min_width())
408 .unwrap_or(0.0),
409 )
410 }
411 _ => 0.0,
412 };
413
414 let size = {
415 let intrinsic = Size::new(
416 max_width + text_size.0 + self.padding.left,
417 f32::from(self.line_height.to_absolute(text_size)),
418 );
419
420 limits
421 .width(self.width)
422 .shrink(self.padding)
423 .resolve(self.width, Length::Shrink, intrinsic)
424 .expand(self.padding)
425 };
426
427 layout::Node::new(size)
428 }
429
430 fn update(
431 &mut self,
432 tree: &mut Tree,
433 event: &Event,
434 layout: Layout<'_>,
435 cursor: mouse::Cursor,
436 _renderer: &Renderer,
437 shell: &mut Shell<'_, Message>,
438 _viewport: &Rectangle,
439 ) {
440 let state = tree.state.downcast_mut::<State<Renderer::Paragraph>>();
441
442 match event {
443 Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
444 | Event::Touch(touch::Event::FingerPressed { .. }) => {
445 if state.is_open {
446 state.is_open = false;
449
450 if let Some(on_close) = &self.on_close {
451 shell.publish(on_close.clone());
452 }
453
454 shell.capture_event();
455 } else if cursor.is_over(layout.bounds()) {
456 let selected = self.selected.as_ref().map(Borrow::borrow);
457
458 state.is_open = true;
459 state.hovered_option = self
460 .options
461 .borrow()
462 .iter()
463 .position(|option| Some(option) == selected);
464
465 if let Some(on_open) = &self.on_open {
466 shell.publish(on_open.clone());
467 }
468
469 shell.capture_event();
470 }
471 }
472 Event::Mouse(mouse::Event::WheelScrolled {
473 delta: mouse::ScrollDelta::Lines { y, .. },
474 }) => {
475 let Some(on_select) = &self.on_select else {
476 return;
477 };
478
479 if state.keyboard_modifiers.command()
480 && cursor.is_over(layout.bounds())
481 && !state.is_open
482 {
483 fn find_next<'a, T: PartialEq>(
484 selected: &'a T,
485 mut options: impl Iterator<Item = &'a T>,
486 ) -> Option<&'a T> {
487 let _ = options.find(|&option| option == selected);
488
489 options.next()
490 }
491
492 let options = self.options.borrow();
493 let selected = self.selected.as_ref().map(Borrow::borrow);
494
495 let next_option = if *y < 0.0 {
496 if let Some(selected) = selected {
497 find_next(selected, options.iter())
498 } else {
499 options.first()
500 }
501 } else if *y > 0.0 {
502 if let Some(selected) = selected {
503 find_next(selected, options.iter().rev())
504 } else {
505 options.last()
506 }
507 } else {
508 None
509 };
510
511 if let Some(next_option) = next_option {
512 shell.publish(on_select(next_option.clone()));
513 }
514
515 shell.capture_event();
516 }
517 }
518 Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => {
519 state.keyboard_modifiers = *modifiers;
520 }
521 _ => {}
522 };
523
524 let status = {
525 let is_hovered = cursor.is_over(layout.bounds());
526
527 if self.on_select.is_none() {
528 Status::Disabled
529 } else if state.is_open {
530 Status::Opened { is_hovered }
531 } else if is_hovered {
532 Status::Hovered
533 } else {
534 Status::Active
535 }
536 };
537
538 if let Event::Window(window::Event::RedrawRequested(_now)) = event {
539 self.last_status = Some(status);
540 } else if self
541 .last_status
542 .is_some_and(|last_status| last_status != status)
543 {
544 shell.request_redraw();
545 }
546 }
547
548 fn mouse_interaction(
549 &self,
550 _tree: &Tree,
551 layout: Layout<'_>,
552 cursor: mouse::Cursor,
553 _viewport: &Rectangle,
554 _renderer: &Renderer,
555 ) -> mouse::Interaction {
556 let bounds = layout.bounds();
557 let is_mouse_over = cursor.is_over(bounds);
558
559 if is_mouse_over {
560 if self.on_select.is_some() {
561 mouse::Interaction::Pointer
562 } else {
563 mouse::Interaction::Idle
564 }
565 } else {
566 mouse::Interaction::default()
567 }
568 }
569
570 fn draw(
571 &self,
572 tree: &Tree,
573 renderer: &mut Renderer,
574 theme: &Theme,
575 _style: &renderer::Style,
576 layout: Layout<'_>,
577 _cursor: mouse::Cursor,
578 viewport: &Rectangle,
579 ) {
580 let font = self.font.unwrap_or_else(|| renderer.default_font());
581 let selected = self.selected.as_ref().map(Borrow::borrow);
582 let state = tree.state.downcast_ref::<State<Renderer::Paragraph>>();
583
584 let bounds = layout.bounds();
585
586 let style = Catalog::style(
587 theme,
588 &self.class,
589 self.last_status.unwrap_or(Status::Active),
590 );
591
592 renderer.fill_quad(
593 renderer::Quad {
594 bounds,
595 border: style.border,
596 ..renderer::Quad::default()
597 },
598 style.background,
599 );
600
601 let handle = match &self.handle {
602 Handle::Arrow { size } => Some((
603 Renderer::ICON_FONT,
604 Renderer::ARROW_DOWN_ICON,
605 *size,
606 text::LineHeight::default(),
607 text::Shaping::Basic,
608 )),
609 Handle::Static(Icon {
610 font,
611 code_point,
612 size,
613 line_height,
614 shaping,
615 }) => Some((*font, *code_point, *size, *line_height, *shaping)),
616 Handle::Dynamic { open, closed } => {
617 if state.is_open {
618 Some((
619 open.font,
620 open.code_point,
621 open.size,
622 open.line_height,
623 open.shaping,
624 ))
625 } else {
626 Some((
627 closed.font,
628 closed.code_point,
629 closed.size,
630 closed.line_height,
631 closed.shaping,
632 ))
633 }
634 }
635 Handle::None => None,
636 };
637
638 if let Some((font, code_point, size, line_height, shaping)) = handle {
639 let size = size.unwrap_or_else(|| renderer.default_size());
640
641 renderer.fill_text(
642 Text {
643 content: code_point.to_string(),
644 size,
645 line_height,
646 font,
647 bounds: Size::new(bounds.width, f32::from(line_height.to_absolute(size))),
648 align_x: text::Alignment::Right,
649 align_y: alignment::Vertical::Center,
650 shaping,
651 wrapping: text::Wrapping::None,
652 ellipsis: text::Ellipsis::None,
653 hint_factor: None,
654 },
655 Point::new(
656 bounds.x + bounds.width - self.padding.right,
657 bounds.center_y(),
658 ),
659 style.handle_color,
660 *viewport,
661 );
662 }
663
664 let label = selected.map(&self.to_string);
665
666 if let Some(label) = label.or_else(|| self.placeholder.clone()) {
667 let text_size = self.text_size.unwrap_or_else(|| renderer.default_size());
668
669 renderer.fill_text(
670 Text {
671 content: label,
672 size: text_size,
673 line_height: self.line_height,
674 font,
675 bounds: Size::new(
676 bounds.width - self.padding.x(),
677 f32::from(self.line_height.to_absolute(text_size)),
678 ),
679 align_x: text::Alignment::Default,
680 align_y: alignment::Vertical::Center,
681 shaping: self.shaping,
682 wrapping: text::Wrapping::None,
683 ellipsis: self.ellipsis,
684 hint_factor: renderer.scale_factor(),
685 },
686 Point::new(bounds.x + self.padding.left, bounds.center_y()),
687 if selected.is_some() {
688 style.text_color
689 } else {
690 style.placeholder_color
691 },
692 *viewport,
693 );
694 }
695 }
696
697 fn overlay<'b>(
698 &'b mut self,
699 tree: &'b mut Tree,
700 layout: Layout<'_>,
701 renderer: &Renderer,
702 viewport: &Rectangle,
703 translation: Vector,
704 ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
705 let Some(on_select) = &self.on_select else {
706 return None;
707 };
708
709 let state = tree.state.downcast_mut::<State<Renderer::Paragraph>>();
710 let font = self.font.unwrap_or_else(|| renderer.default_font());
711
712 if state.is_open {
713 let bounds = layout.bounds();
714
715 let mut menu = Menu::new(
716 &mut state.menu,
717 self.options.borrow(),
718 &mut state.hovered_option,
719 &self.to_string,
720 |option| {
721 state.is_open = false;
722
723 (on_select)(option)
724 },
725 None,
726 &self.menu_class,
727 )
728 .width(bounds.width)
729 .padding(self.padding)
730 .font(font)
731 .ellipsis(self.ellipsis)
732 .shaping(self.shaping);
733
734 if let Some(text_size) = self.text_size {
735 menu = menu.text_size(text_size);
736 }
737
738 Some(menu.overlay(
739 layout.position() + translation,
740 *viewport,
741 bounds.height,
742 self.menu_height,
743 ))
744 } else {
745 None
746 }
747 }
748}
749
750impl<'a, T, L, V, Message, Theme, Renderer> From<PickList<'a, T, L, V, Message, Theme, Renderer>>
751 for Element<'a, Message, Theme, Renderer>
752where
753 T: Clone + PartialEq + 'a,
754 L: Borrow<[T]> + 'a,
755 V: Borrow<T> + 'a,
756 Message: Clone + 'a,
757 Theme: Catalog + 'a,
758 Renderer: text::Renderer + 'a,
759{
760 fn from(pick_list: PickList<'a, T, L, V, Message, Theme, Renderer>) -> Self {
761 Self::new(pick_list)
762 }
763}
764
765#[derive(Debug)]
766struct State<P: text::Paragraph> {
767 menu: menu::State,
768 keyboard_modifiers: keyboard::Modifiers,
769 is_open: bool,
770 hovered_option: Option<usize>,
771 options: Vec<paragraph::Plain<P>>,
772 placeholder: paragraph::Plain<P>,
773}
774
775impl<P: text::Paragraph> State<P> {
776 fn new() -> Self {
778 Self {
779 menu: menu::State::default(),
780 keyboard_modifiers: keyboard::Modifiers::default(),
781 is_open: bool::default(),
782 hovered_option: Option::default(),
783 options: Vec::new(),
784 placeholder: paragraph::Plain::default(),
785 }
786 }
787}
788
789impl<P: text::Paragraph> Default for State<P> {
790 fn default() -> Self {
791 Self::new()
792 }
793}
794
795#[derive(Debug, Clone, PartialEq)]
797pub enum Handle<Font> {
798 Arrow {
802 size: Option<Pixels>,
804 },
805 Static(Icon<Font>),
807 Dynamic {
809 closed: Icon<Font>,
811 open: Icon<Font>,
813 },
814 None,
816}
817
818impl<Font> Default for Handle<Font> {
819 fn default() -> Self {
820 Self::Arrow { size: None }
821 }
822}
823
824#[derive(Debug, Clone, PartialEq)]
826pub struct Icon<Font> {
827 pub font: Font,
829 pub code_point: char,
831 pub size: Option<Pixels>,
833 pub line_height: text::LineHeight,
835 pub shaping: text::Shaping,
837}
838
839#[derive(Debug, Clone, Copy, PartialEq, Eq)]
841pub enum Status {
842 Active,
844 Hovered,
846 Opened {
848 is_hovered: bool,
850 },
851 Disabled,
853}
854
855#[derive(Debug, Clone, Copy, PartialEq)]
857pub struct Style {
858 pub text_color: Color,
860 pub placeholder_color: Color,
862 pub handle_color: Color,
864 pub background: Background,
866 pub border: Border,
868}
869
870pub trait Catalog: menu::Catalog {
872 type Class<'a>;
874
875 fn default<'a>() -> <Self as Catalog>::Class<'a>;
877
878 fn default_menu<'a>() -> <Self as menu::Catalog>::Class<'a> {
880 <Self as menu::Catalog>::default()
881 }
882
883 fn style(&self, class: &<Self as Catalog>::Class<'_>, status: Status) -> Style;
885}
886
887pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Style + 'a>;
891
892impl Catalog for Theme {
893 type Class<'a> = StyleFn<'a, Self>;
894
895 fn default<'a>() -> StyleFn<'a, Self> {
896 Box::new(default)
897 }
898
899 fn style(&self, class: &StyleFn<'_, Self>, status: Status) -> Style {
900 class(self, status)
901 }
902}
903
904pub fn default(theme: &Theme, status: Status) -> Style {
906 let palette = theme.extended_palette();
907
908 let active = Style {
909 text_color: palette.background.weak.text,
910 background: palette.background.weak.color.into(),
911 placeholder_color: palette.secondary.base.color,
912 handle_color: palette.background.weak.text,
913 border: Border {
914 radius: 2.0.into(),
915 width: 1.0,
916 color: palette.background.strong.color,
917 },
918 };
919
920 match status {
921 Status::Active => active,
922 Status::Hovered | Status::Opened { .. } => Style {
923 border: Border {
924 color: palette.primary.strong.color,
925 ..active.border
926 },
927 ..active
928 },
929 Status::Disabled => Style {
930 text_color: palette.background.strongest.color,
931 background: palette.background.weaker.color.into(),
932 placeholder_color: palette.background.strongest.color,
933 handle_color: palette.background.strongest.color,
934 border: Border {
935 color: palette.background.weak.color,
936 ..active.border
937 },
938 },
939 }
940}