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