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 menu_height: Length,
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 menu_height: Length::Shrink,
214 }
215 }
216
217 pub fn placeholder(mut self, placeholder: impl Into<String>) -> Self {
219 self.placeholder = Some(placeholder.into());
220 self
221 }
222
223 pub fn width(mut self, width: impl Into<Length>) -> Self {
225 self.width = width.into();
226 self
227 }
228
229 pub fn menu_height(mut self, menu_height: impl Into<Length>) -> Self {
231 self.menu_height = menu_height.into();
232 self
233 }
234
235 pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self {
237 self.padding = padding.into();
238 self
239 }
240
241 pub fn text_size(mut self, size: impl Into<Pixels>) -> Self {
243 self.text_size = Some(size.into());
244 self
245 }
246
247 pub fn text_line_height(
249 mut self,
250 line_height: impl Into<text::LineHeight>,
251 ) -> Self {
252 self.text_line_height = line_height.into();
253 self
254 }
255
256 pub fn text_shaping(mut self, shaping: text::Shaping) -> Self {
258 self.text_shaping = shaping;
259 self
260 }
261
262 pub fn font(mut self, font: impl Into<Renderer::Font>) -> Self {
264 self.font = Some(font.into());
265 self
266 }
267
268 pub fn handle(mut self, handle: Handle<Renderer::Font>) -> Self {
270 self.handle = handle;
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(
299 mut self,
300 style: impl Fn(&Theme) -> menu::Style + 'a,
301 ) -> 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(
313 mut self,
314 class: impl Into<<Theme as Catalog>::Class<'a>>,
315 ) -> Self {
316 self.class = class.into();
317 self
318 }
319
320 #[cfg(feature = "advanced")]
322 #[must_use]
323 pub fn menu_class(
324 mut self,
325 class: impl Into<<Theme as menu::Catalog>::Class<'a>>,
326 ) -> Self {
327 self.menu_class = class.into();
328 self
329 }
330}
331
332impl<'a, T, L, V, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
333 for PickList<'a, T, L, V, Message, Theme, Renderer>
334where
335 T: Clone + ToString + 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(
358 &mut self,
359 tree: &mut Tree,
360 renderer: &Renderer,
361 limits: &layout::Limits,
362 ) -> layout::Node {
363 let state = tree.state.downcast_mut::<State<Renderer::Paragraph>>();
364
365 let font = self.font.unwrap_or_else(|| renderer.default_font());
366 let text_size =
367 self.text_size.unwrap_or_else(|| renderer.default_size());
368 let options = self.options.borrow();
369
370 state.options.resize_with(options.len(), Default::default);
371
372 let option_text = Text {
373 content: "",
374 bounds: Size::new(
375 f32::INFINITY,
376 self.text_line_height.to_absolute(text_size).into(),
377 ),
378 size: text_size,
379 line_height: self.text_line_height,
380 font,
381 align_x: text::Alignment::Default,
382 align_y: alignment::Vertical::Center,
383 shaping: self.text_shaping,
384 wrapping: text::Wrapping::default(),
385 };
386
387 for (option, paragraph) in options.iter().zip(state.options.iter_mut())
388 {
389 let label = option.to_string();
390
391 let _ = paragraph.update(Text {
392 content: &label,
393 ..option_text
394 });
395 }
396
397 if let Some(placeholder) = &self.placeholder {
398 let _ = state.placeholder.update(Text {
399 content: placeholder,
400 ..option_text
401 });
402 }
403
404 let max_width = match self.width {
405 Length::Shrink => {
406 let labels_width =
407 state.options.iter().fold(0.0, |width, paragraph| {
408 f32::max(width, paragraph.min_width())
409 });
410
411 labels_width.max(
412 self.placeholder
413 .as_ref()
414 .map(|_| state.placeholder.min_width())
415 .unwrap_or(0.0),
416 )
417 }
418 _ => 0.0,
419 };
420
421 let size = {
422 let intrinsic = Size::new(
423 max_width + text_size.0 + self.padding.left,
424 f32::from(self.text_line_height.to_absolute(text_size)),
425 );
426
427 limits
428 .width(self.width)
429 .shrink(self.padding)
430 .resolve(self.width, Length::Shrink, intrinsic)
431 .expand(self.padding)
432 };
433
434 layout::Node::new(size)
435 }
436
437 fn update(
438 &mut self,
439 tree: &mut Tree,
440 event: &Event,
441 layout: Layout<'_>,
442 cursor: mouse::Cursor,
443 _renderer: &Renderer,
444 _clipboard: &mut dyn Clipboard,
445 shell: &mut Shell<'_, Message>,
446 _viewport: &Rectangle,
447 ) {
448 let state = tree.state.downcast_mut::<State<Renderer::Paragraph>>();
449
450 match event {
451 Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
452 | Event::Touch(touch::Event::FingerPressed { .. }) => {
453 if state.is_open {
454 state.is_open = false;
457
458 if let Some(on_close) = &self.on_close {
459 shell.publish(on_close.clone());
460 }
461
462 shell.capture_event();
463 } else if cursor.is_over(layout.bounds()) {
464 let selected = self.selected.as_ref().map(Borrow::borrow);
465
466 state.is_open = true;
467 state.hovered_option = self
468 .options
469 .borrow()
470 .iter()
471 .position(|option| Some(option) == selected);
472
473 if let Some(on_open) = &self.on_open {
474 shell.publish(on_open.clone());
475 }
476
477 shell.capture_event();
478 }
479 }
480 Event::Mouse(mouse::Event::WheelScrolled {
481 delta: mouse::ScrollDelta::Lines { y, .. },
482 }) => {
483 if state.keyboard_modifiers.command()
484 && cursor.is_over(layout.bounds())
485 && !state.is_open
486 {
487 fn find_next<'a, T: PartialEq>(
488 selected: &'a T,
489 mut options: impl Iterator<Item = &'a T>,
490 ) -> Option<&'a T> {
491 let _ = options.find(|&option| option == selected);
492
493 options.next()
494 }
495
496 let options = self.options.borrow();
497 let selected = self.selected.as_ref().map(Borrow::borrow);
498
499 let next_option = if *y < 0.0 {
500 if let Some(selected) = selected {
501 find_next(selected, options.iter())
502 } else {
503 options.first()
504 }
505 } else if *y > 0.0 {
506 if let Some(selected) = selected {
507 find_next(selected, options.iter().rev())
508 } else {
509 options.last()
510 }
511 } else {
512 None
513 };
514
515 if let Some(next_option) = next_option {
516 shell.publish((self.on_select)(next_option.clone()));
517 }
518
519 shell.capture_event();
520 }
521 }
522 Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => {
523 state.keyboard_modifiers = *modifiers;
524 }
525 _ => {}
526 };
527
528 let status = {
529 let is_hovered = cursor.is_over(layout.bounds());
530
531 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 mouse::Interaction::Pointer
563 } else {
564 mouse::Interaction::default()
565 }
566 }
567
568 fn draw(
569 &self,
570 tree: &Tree,
571 renderer: &mut Renderer,
572 theme: &Theme,
573 _style: &renderer::Style,
574 layout: Layout<'_>,
575 _cursor: mouse::Cursor,
576 viewport: &Rectangle,
577 ) {
578 let font = self.font.unwrap_or_else(|| renderer.default_font());
579 let selected = self.selected.as_ref().map(Borrow::borrow);
580 let state = tree.state.downcast_ref::<State<Renderer::Paragraph>>();
581
582 let bounds = layout.bounds();
583
584 let style = Catalog::style(
585 theme,
586 &self.class,
587 self.last_status.unwrap_or(Status::Active),
588 );
589
590 renderer.fill_quad(
591 renderer::Quad {
592 bounds,
593 border: style.border,
594 ..renderer::Quad::default()
595 },
596 style.background,
597 );
598
599 let handle = match &self.handle {
600 Handle::Arrow { size } => Some((
601 Renderer::ICON_FONT,
602 Renderer::ARROW_DOWN_ICON,
603 *size,
604 text::LineHeight::default(),
605 text::Shaping::Basic,
606 )),
607 Handle::Static(Icon {
608 font,
609 code_point,
610 size,
611 line_height,
612 shaping,
613 }) => Some((*font, *code_point, *size, *line_height, *shaping)),
614 Handle::Dynamic { open, closed } => {
615 if state.is_open {
616 Some((
617 open.font,
618 open.code_point,
619 open.size,
620 open.line_height,
621 open.shaping,
622 ))
623 } else {
624 Some((
625 closed.font,
626 closed.code_point,
627 closed.size,
628 closed.line_height,
629 closed.shaping,
630 ))
631 }
632 }
633 Handle::None => None,
634 };
635
636 if let Some((font, code_point, size, line_height, shaping)) = handle {
637 let size = size.unwrap_or_else(|| renderer.default_size());
638
639 renderer.fill_text(
640 Text {
641 content: code_point.to_string(),
642 size,
643 line_height,
644 font,
645 bounds: Size::new(
646 bounds.width,
647 f32::from(line_height.to_absolute(size)),
648 ),
649 align_x: text::Alignment::Right,
650 align_y: alignment::Vertical::Center,
651 shaping,
652 wrapping: text::Wrapping::default(),
653 },
654 Point::new(
655 bounds.x + bounds.width - self.padding.right,
656 bounds.center_y(),
657 ),
658 style.handle_color,
659 *viewport,
660 );
661 }
662
663 let label = selected.map(ToString::to_string);
664
665 if let Some(label) = label.or_else(|| self.placeholder.clone()) {
666 let text_size =
667 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.text_line_height,
674 font,
675 bounds: Size::new(
676 bounds.width - self.padding.x(),
677 f32::from(self.text_line_height.to_absolute(text_size)),
678 ),
679 align_x: text::Alignment::Default,
680 align_y: alignment::Vertical::Center,
681 shaping: self.text_shaping,
682 wrapping: text::Wrapping::default(),
683 },
684 Point::new(bounds.x + self.padding.left, bounds.center_y()),
685 if selected.is_some() {
686 style.text_color
687 } else {
688 style.placeholder_color
689 },
690 *viewport,
691 );
692 }
693 }
694
695 fn overlay<'b>(
696 &'b mut self,
697 tree: &'b mut Tree,
698 layout: Layout<'_>,
699 renderer: &Renderer,
700 viewport: &Rectangle,
701 translation: Vector,
702 ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
703 let state = tree.state.downcast_mut::<State<Renderer::Paragraph>>();
704 let font = self.font.unwrap_or_else(|| renderer.default_font());
705
706 if state.is_open {
707 let bounds = layout.bounds();
708
709 let on_select = &self.on_select;
710
711 let mut menu = Menu::new(
712 &mut state.menu,
713 self.options.borrow(),
714 &mut state.hovered_option,
715 |option| {
716 state.is_open = false;
717
718 (on_select)(option)
719 },
720 None,
721 &self.menu_class,
722 )
723 .width(bounds.width)
724 .padding(self.padding)
725 .font(font)
726 .text_shaping(self.text_shaping);
727
728 if let Some(text_size) = self.text_size {
729 menu = menu.text_size(text_size);
730 }
731
732 Some(menu.overlay(
733 layout.position() + translation,
734 *viewport,
735 bounds.height,
736 self.menu_height,
737 ))
738 } else {
739 None
740 }
741 }
742}
743
744impl<'a, T, L, V, Message, Theme, Renderer>
745 From<PickList<'a, T, L, V, Message, Theme, Renderer>>
746 for Element<'a, Message, Theme, Renderer>
747where
748 T: Clone + ToString + PartialEq + 'a,
749 L: Borrow<[T]> + 'a,
750 V: Borrow<T> + 'a,
751 Message: Clone + 'a,
752 Theme: Catalog + 'a,
753 Renderer: text::Renderer + 'a,
754{
755 fn from(
756 pick_list: PickList<'a, T, L, V, Message, Theme, Renderer>,
757 ) -> Self {
758 Self::new(pick_list)
759 }
760}
761
762#[derive(Debug)]
763struct State<P: text::Paragraph> {
764 menu: menu::State,
765 keyboard_modifiers: keyboard::Modifiers,
766 is_open: bool,
767 hovered_option: Option<usize>,
768 options: Vec<paragraph::Plain<P>>,
769 placeholder: paragraph::Plain<P>,
770}
771
772impl<P: text::Paragraph> State<P> {
773 fn new() -> Self {
775 Self {
776 menu: menu::State::default(),
777 keyboard_modifiers: keyboard::Modifiers::default(),
778 is_open: bool::default(),
779 hovered_option: Option::default(),
780 options: Vec::new(),
781 placeholder: paragraph::Plain::default(),
782 }
783 }
784}
785
786impl<P: text::Paragraph> Default for State<P> {
787 fn default() -> Self {
788 Self::new()
789 }
790}
791
792#[derive(Debug, Clone, PartialEq)]
794pub enum Handle<Font> {
795 Arrow {
799 size: Option<Pixels>,
801 },
802 Static(Icon<Font>),
804 Dynamic {
806 closed: Icon<Font>,
808 open: Icon<Font>,
810 },
811 None,
813}
814
815impl<Font> Default for Handle<Font> {
816 fn default() -> Self {
817 Self::Arrow { size: None }
818 }
819}
820
821#[derive(Debug, Clone, PartialEq)]
823pub struct Icon<Font> {
824 pub font: Font,
826 pub code_point: char,
828 pub size: Option<Pixels>,
830 pub line_height: text::LineHeight,
832 pub shaping: text::Shaping,
834}
835
836#[derive(Debug, Clone, Copy, PartialEq, Eq)]
838pub enum Status {
839 Active,
841 Hovered,
843 Opened {
845 is_hovered: bool,
847 },
848}
849
850#[derive(Debug, Clone, Copy, PartialEq)]
852pub struct Style {
853 pub text_color: Color,
855 pub placeholder_color: Color,
857 pub handle_color: Color,
859 pub background: Background,
861 pub border: Border,
863}
864
865pub trait Catalog: menu::Catalog {
867 type Class<'a>;
869
870 fn default<'a>() -> <Self as Catalog>::Class<'a>;
872
873 fn default_menu<'a>() -> <Self as menu::Catalog>::Class<'a> {
875 <Self as menu::Catalog>::default()
876 }
877
878 fn style(
880 &self,
881 class: &<Self as Catalog>::Class<'_>,
882 status: Status,
883 ) -> Style;
884}
885
886pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Style + 'a>;
890
891impl Catalog for Theme {
892 type Class<'a> = StyleFn<'a, Self>;
893
894 fn default<'a>() -> StyleFn<'a, Self> {
895 Box::new(default)
896 }
897
898 fn style(&self, class: &StyleFn<'_, Self>, status: Status) -> Style {
899 class(self, status)
900 }
901}
902
903pub fn default(theme: &Theme, status: Status) -> Style {
905 let palette = theme.extended_palette();
906
907 let active = Style {
908 text_color: palette.background.weak.text,
909 background: palette.background.weak.color.into(),
910 placeholder_color: palette.secondary.base.color,
911 handle_color: palette.background.weak.text,
912 border: Border {
913 radius: 2.0.into(),
914 width: 1.0,
915 color: palette.background.strong.color,
916 },
917 };
918
919 match status {
920 Status::Active => active,
921 Status::Hovered | Status::Opened { .. } => Style {
922 border: Border {
923 color: palette.primary.strong.color,
924 ..active.border
925 },
926 ..active
927 },
928 }
929}