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