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::tree::{self, Tree};
73use crate::core::window;
74use crate::core::{
75 Background, Border, Clipboard, Color, Element, Event, Layout, Length,
76 Padding, Pixels, Point, Rectangle, Shell, Size, Theme, Vector, Widget,
77};
78use crate::overlay::menu::{self, Menu};
79
80use std::borrow::Borrow;
81use std::f32;
82
83pub struct PickList<
146 'a,
147 T,
148 L,
149 V,
150 Message,
151 Theme = crate::Theme,
152 Renderer = crate::Renderer,
153> where
154 T: ToString + PartialEq + Clone,
155 L: Borrow<[T]> + 'a,
156 V: Borrow<T> + 'a,
157 Theme: Catalog,
158 Renderer: text::Renderer,
159{
160 on_select: Box<dyn Fn(T) -> Message + 'a>,
161 on_open: Option<Message>,
162 on_close: Option<Message>,
163 options: L,
164 placeholder: Option<String>,
165 selected: Option<V>,
166 width: Length,
167 padding: Padding,
168 text_size: Option<Pixels>,
169 text_line_height: text::LineHeight,
170 text_shaping: text::Shaping,
171 font: Option<Renderer::Font>,
172 handle: Handle<Renderer::Font>,
173 class: <Theme as Catalog>::Class<'a>,
174 menu_class: <Theme as menu::Catalog>::Class<'a>,
175 last_status: Option<Status>,
176}
177
178impl<'a, T, L, V, Message, Theme, Renderer>
179 PickList<'a, T, L, V, Message, Theme, Renderer>
180where
181 T: ToString + PartialEq + Clone,
182 L: Borrow<[T]> + 'a,
183 V: Borrow<T> + 'a,
184 Message: Clone,
185 Theme: Catalog,
186 Renderer: text::Renderer,
187{
188 pub fn new(
191 options: L,
192 selected: Option<V>,
193 on_select: impl Fn(T) -> Message + 'a,
194 ) -> Self {
195 Self {
196 on_select: Box::new(on_select),
197 on_open: None,
198 on_close: None,
199 options,
200 placeholder: None,
201 selected,
202 width: Length::Shrink,
203 padding: crate::button::DEFAULT_PADDING,
204 text_size: None,
205 text_line_height: text::LineHeight::default(),
206 text_shaping: text::Shaping::default(),
207 font: None,
208 handle: Handle::default(),
209 class: <Theme as Catalog>::default(),
210 menu_class: <Theme as Catalog>::default_menu(),
211 last_status: None,
212 }
213 }
214
215 pub fn placeholder(mut self, placeholder: impl Into<String>) -> Self {
217 self.placeholder = Some(placeholder.into());
218 self
219 }
220
221 pub fn width(mut self, width: impl Into<Length>) -> Self {
223 self.width = width.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 text_line_height(
241 mut self,
242 line_height: impl Into<text::LineHeight>,
243 ) -> Self {
244 self.text_line_height = line_height.into();
245 self
246 }
247
248 pub fn text_shaping(mut self, shaping: text::Shaping) -> Self {
250 self.text_shaping = shaping;
251 self
252 }
253
254 pub fn font(mut self, font: impl Into<Renderer::Font>) -> Self {
256 self.font = Some(font.into());
257 self
258 }
259
260 pub fn handle(mut self, handle: Handle<Renderer::Font>) -> Self {
262 self.handle = handle;
263 self
264 }
265
266 pub fn on_open(mut self, on_open: Message) -> Self {
268 self.on_open = Some(on_open);
269 self
270 }
271
272 pub fn on_close(mut self, on_close: Message) -> Self {
274 self.on_close = Some(on_close);
275 self
276 }
277
278 #[must_use]
280 pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self
281 where
282 <Theme as Catalog>::Class<'a>: From<StyleFn<'a, Theme>>,
283 {
284 self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
285 self
286 }
287
288 #[must_use]
290 pub fn menu_style(
291 mut self,
292 style: impl Fn(&Theme) -> menu::Style + 'a,
293 ) -> Self
294 where
295 <Theme as menu::Catalog>::Class<'a>: From<menu::StyleFn<'a, Theme>>,
296 {
297 self.menu_class = (Box::new(style) as menu::StyleFn<'a, Theme>).into();
298 self
299 }
300
301 #[cfg(feature = "advanced")]
303 #[must_use]
304 pub fn class(
305 mut self,
306 class: impl Into<<Theme as Catalog>::Class<'a>>,
307 ) -> Self {
308 self.class = class.into();
309 self
310 }
311
312 #[cfg(feature = "advanced")]
314 #[must_use]
315 pub fn menu_class(
316 mut self,
317 class: impl Into<<Theme as menu::Catalog>::Class<'a>>,
318 ) -> 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, Renderer>
326where
327 T: Clone + ToString + 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.default_font());
358 let text_size =
359 self.text_size.unwrap_or_else(|| renderer.default_size());
360 let options = self.options.borrow();
361
362 state.options.resize_with(options.len(), Default::default);
363
364 let option_text = Text {
365 content: "",
366 bounds: Size::new(
367 f32::INFINITY,
368 self.text_line_height.to_absolute(text_size).into(),
369 ),
370 size: text_size,
371 line_height: self.text_line_height,
372 font,
373 align_x: text::Alignment::Default,
374 align_y: alignment::Vertical::Center,
375 shaping: self.text_shaping,
376 wrapping: text::Wrapping::default(),
377 };
378
379 for (option, paragraph) in options.iter().zip(state.options.iter_mut())
380 {
381 let label = option.to_string();
382
383 let _ = paragraph.update(Text {
384 content: &label,
385 ..option_text
386 });
387 }
388
389 if let Some(placeholder) = &self.placeholder {
390 let _ = state.placeholder.update(Text {
391 content: placeholder,
392 ..option_text
393 });
394 }
395
396 let max_width = match self.width {
397 Length::Shrink => {
398 let labels_width =
399 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(self.text_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 _clipboard: &mut dyn Clipboard,
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 if state.keyboard_modifiers.command()
476 && cursor.is_over(layout.bounds())
477 && !state.is_open
478 {
479 fn find_next<'a, T: PartialEq>(
480 selected: &'a T,
481 mut options: impl Iterator<Item = &'a T>,
482 ) -> Option<&'a T> {
483 let _ = options.find(|&option| option == selected);
484
485 options.next()
486 }
487
488 let options = self.options.borrow();
489 let selected = self.selected.as_ref().map(Borrow::borrow);
490
491 let next_option = if *y < 0.0 {
492 if let Some(selected) = selected {
493 find_next(selected, options.iter())
494 } else {
495 options.first()
496 }
497 } else if *y > 0.0 {
498 if let Some(selected) = selected {
499 find_next(selected, options.iter().rev())
500 } else {
501 options.last()
502 }
503 } else {
504 None
505 };
506
507 if let Some(next_option) = next_option {
508 shell.publish((self.on_select)(next_option.clone()));
509 }
510
511 shell.capture_event();
512 }
513 }
514 Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => {
515 state.keyboard_modifiers = *modifiers;
516 }
517 _ => {}
518 };
519
520 let status = {
521 let is_hovered = cursor.is_over(layout.bounds());
522
523 if state.is_open {
524 Status::Opened { is_hovered }
525 } else if is_hovered {
526 Status::Hovered
527 } else {
528 Status::Active
529 }
530 };
531
532 if let Event::Window(window::Event::RedrawRequested(_now)) = event {
533 self.last_status = Some(status);
534 } else if self
535 .last_status
536 .is_some_and(|last_status| last_status != status)
537 {
538 shell.request_redraw();
539 }
540 }
541
542 fn mouse_interaction(
543 &self,
544 _tree: &Tree,
545 layout: Layout<'_>,
546 cursor: mouse::Cursor,
547 _viewport: &Rectangle,
548 _renderer: &Renderer,
549 ) -> mouse::Interaction {
550 let bounds = layout.bounds();
551 let is_mouse_over = cursor.is_over(bounds);
552
553 if is_mouse_over {
554 mouse::Interaction::Pointer
555 } else {
556 mouse::Interaction::default()
557 }
558 }
559
560 fn draw(
561 &self,
562 tree: &Tree,
563 renderer: &mut Renderer,
564 theme: &Theme,
565 _style: &renderer::Style,
566 layout: Layout<'_>,
567 _cursor: mouse::Cursor,
568 viewport: &Rectangle,
569 ) {
570 let font = self.font.unwrap_or_else(|| renderer.default_font());
571 let selected = self.selected.as_ref().map(Borrow::borrow);
572 let state = tree.state.downcast_ref::<State<Renderer::Paragraph>>();
573
574 let bounds = layout.bounds();
575
576 let style = Catalog::style(
577 theme,
578 &self.class,
579 self.last_status.unwrap_or(Status::Active),
580 );
581
582 renderer.fill_quad(
583 renderer::Quad {
584 bounds,
585 border: style.border,
586 ..renderer::Quad::default()
587 },
588 style.background,
589 );
590
591 let handle = match &self.handle {
592 Handle::Arrow { size } => Some((
593 Renderer::ICON_FONT,
594 Renderer::ARROW_DOWN_ICON,
595 *size,
596 text::LineHeight::default(),
597 text::Shaping::Basic,
598 )),
599 Handle::Static(Icon {
600 font,
601 code_point,
602 size,
603 line_height,
604 shaping,
605 }) => Some((*font, *code_point, *size, *line_height, *shaping)),
606 Handle::Dynamic { open, closed } => {
607 if state.is_open {
608 Some((
609 open.font,
610 open.code_point,
611 open.size,
612 open.line_height,
613 open.shaping,
614 ))
615 } else {
616 Some((
617 closed.font,
618 closed.code_point,
619 closed.size,
620 closed.line_height,
621 closed.shaping,
622 ))
623 }
624 }
625 Handle::None => None,
626 };
627
628 if let Some((font, code_point, size, line_height, shaping)) = handle {
629 let size = size.unwrap_or_else(|| renderer.default_size());
630
631 renderer.fill_text(
632 Text {
633 content: code_point.to_string(),
634 size,
635 line_height,
636 font,
637 bounds: Size::new(
638 bounds.width,
639 f32::from(line_height.to_absolute(size)),
640 ),
641 align_x: text::Alignment::Right,
642 align_y: alignment::Vertical::Center,
643 shaping,
644 wrapping: text::Wrapping::default(),
645 },
646 Point::new(
647 bounds.x + bounds.width - self.padding.right,
648 bounds.center_y(),
649 ),
650 style.handle_color,
651 *viewport,
652 );
653 }
654
655 let label = selected.map(ToString::to_string);
656
657 if let Some(label) = label.or_else(|| self.placeholder.clone()) {
658 let text_size =
659 self.text_size.unwrap_or_else(|| renderer.default_size());
660
661 renderer.fill_text(
662 Text {
663 content: label,
664 size: text_size,
665 line_height: self.text_line_height,
666 font,
667 bounds: Size::new(
668 bounds.width - self.padding.horizontal(),
669 f32::from(self.text_line_height.to_absolute(text_size)),
670 ),
671 align_x: text::Alignment::Default,
672 align_y: alignment::Vertical::Center,
673 shaping: self.text_shaping,
674 wrapping: text::Wrapping::default(),
675 },
676 Point::new(bounds.x + self.padding.left, bounds.center_y()),
677 if selected.is_some() {
678 style.text_color
679 } else {
680 style.placeholder_color
681 },
682 *viewport,
683 );
684 }
685 }
686
687 fn overlay<'b>(
688 &'b mut self,
689 tree: &'b mut Tree,
690 layout: Layout<'_>,
691 renderer: &Renderer,
692 viewport: &Rectangle,
693 translation: Vector,
694 ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
695 let state = tree.state.downcast_mut::<State<Renderer::Paragraph>>();
696 let font = self.font.unwrap_or_else(|| renderer.default_font());
697
698 if state.is_open {
699 let bounds = layout.bounds();
700
701 let on_select = &self.on_select;
702
703 let mut menu = Menu::new(
704 &mut state.menu,
705 self.options.borrow(),
706 &mut state.hovered_option,
707 |option| {
708 state.is_open = false;
709
710 (on_select)(option)
711 },
712 None,
713 &self.menu_class,
714 )
715 .width(bounds.width)
716 .padding(self.padding)
717 .font(font)
718 .text_shaping(self.text_shaping);
719
720 if let Some(text_size) = self.text_size {
721 menu = menu.text_size(text_size);
722 }
723
724 Some(menu.overlay(
725 layout.position() + translation,
726 *viewport,
727 bounds.height,
728 ))
729 } else {
730 None
731 }
732 }
733}
734
735impl<'a, T, L, V, Message, Theme, Renderer>
736 From<PickList<'a, T, L, V, Message, Theme, Renderer>>
737 for Element<'a, Message, Theme, Renderer>
738where
739 T: Clone + ToString + PartialEq + 'a,
740 L: Borrow<[T]> + 'a,
741 V: Borrow<T> + 'a,
742 Message: Clone + 'a,
743 Theme: Catalog + 'a,
744 Renderer: text::Renderer + 'a,
745{
746 fn from(
747 pick_list: PickList<'a, T, L, V, Message, Theme, Renderer>,
748 ) -> Self {
749 Self::new(pick_list)
750 }
751}
752
753#[derive(Debug)]
754struct State<P: text::Paragraph> {
755 menu: menu::State,
756 keyboard_modifiers: keyboard::Modifiers,
757 is_open: bool,
758 hovered_option: Option<usize>,
759 options: Vec<paragraph::Plain<P>>,
760 placeholder: paragraph::Plain<P>,
761}
762
763impl<P: text::Paragraph> State<P> {
764 fn new() -> Self {
766 Self {
767 menu: menu::State::default(),
768 keyboard_modifiers: keyboard::Modifiers::default(),
769 is_open: bool::default(),
770 hovered_option: Option::default(),
771 options: Vec::new(),
772 placeholder: paragraph::Plain::default(),
773 }
774 }
775}
776
777impl<P: text::Paragraph> Default for State<P> {
778 fn default() -> Self {
779 Self::new()
780 }
781}
782
783#[derive(Debug, Clone, PartialEq)]
785pub enum Handle<Font> {
786 Arrow {
790 size: Option<Pixels>,
792 },
793 Static(Icon<Font>),
795 Dynamic {
797 closed: Icon<Font>,
799 open: Icon<Font>,
801 },
802 None,
804}
805
806impl<Font> Default for Handle<Font> {
807 fn default() -> Self {
808 Self::Arrow { size: None }
809 }
810}
811
812#[derive(Debug, Clone, PartialEq)]
814pub struct Icon<Font> {
815 pub font: Font,
817 pub code_point: char,
819 pub size: Option<Pixels>,
821 pub line_height: text::LineHeight,
823 pub shaping: text::Shaping,
825}
826
827#[derive(Debug, Clone, Copy, PartialEq, Eq)]
829pub enum Status {
830 Active,
832 Hovered,
834 Opened {
836 is_hovered: bool,
838 },
839}
840
841#[derive(Debug, Clone, Copy, PartialEq)]
843pub struct Style {
844 pub text_color: Color,
846 pub placeholder_color: Color,
848 pub handle_color: Color,
850 pub background: Background,
852 pub border: Border,
854}
855
856pub trait Catalog: menu::Catalog {
858 type Class<'a>;
860
861 fn default<'a>() -> <Self as Catalog>::Class<'a>;
863
864 fn default_menu<'a>() -> <Self as menu::Catalog>::Class<'a> {
866 <Self as menu::Catalog>::default()
867 }
868
869 fn style(
871 &self,
872 class: &<Self as Catalog>::Class<'_>,
873 status: Status,
874 ) -> Style;
875}
876
877pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Style + 'a>;
881
882impl Catalog for Theme {
883 type Class<'a> = StyleFn<'a, Self>;
884
885 fn default<'a>() -> StyleFn<'a, Self> {
886 Box::new(default)
887 }
888
889 fn style(&self, class: &StyleFn<'_, Self>, status: Status) -> Style {
890 class(self, status)
891 }
892}
893
894pub fn default(theme: &Theme, status: Status) -> Style {
896 let palette = theme.extended_palette();
897
898 let active = Style {
899 text_color: palette.background.weak.text,
900 background: palette.background.weak.color.into(),
901 placeholder_color: palette.secondary.base.color,
902 handle_color: palette.background.weak.text,
903 border: Border {
904 radius: 2.0.into(),
905 width: 1.0,
906 color: palette.background.strong.color,
907 },
908 };
909
910 match status {
911 Status::Active => active,
912 Status::Hovered | Status::Opened { .. } => Style {
913 border: Border {
914 color: palette.primary.strong.color,
915 ..active.border
916 },
917 ..active
918 },
919 }
920}