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