1use crate::core::alignment;
66use crate::core::event::{self, Event};
67use crate::core::keyboard;
68use crate::core::layout;
69use crate::core::mouse;
70use crate::core::overlay;
71use crate::core::renderer;
72use crate::core::text::paragraph;
73use crate::core::text::{self, Text};
74use crate::core::touch;
75use crate::core::widget::tree::{self, Tree};
76use crate::core::{
77 Background, Border, Clipboard, Color, Element, Layout, Length, Padding,
78 Pixels, Point, Rectangle, Shell, Size, Theme, Vector, Widget,
79};
80use crate::overlay::menu::{self, Menu};
81
82use std::borrow::Borrow;
83use std::f32;
84
85#[allow(missing_debug_implementations)]
148pub struct PickList<
149 'a,
150 T,
151 L,
152 V,
153 Message,
154 Theme = crate::Theme,
155 Renderer = crate::Renderer,
156> where
157 T: ToString + PartialEq + Clone,
158 L: Borrow<[T]> + 'a,
159 V: Borrow<T> + 'a,
160 Theme: Catalog,
161 Renderer: text::Renderer,
162{
163 on_select: Box<dyn Fn(T) -> Message + 'a>,
164 on_open: Option<Message>,
165 on_close: Option<Message>,
166 options: L,
167 placeholder: Option<String>,
168 selected: Option<V>,
169 width: Length,
170 padding: Padding,
171 text_size: Option<Pixels>,
172 text_line_height: text::LineHeight,
173 text_shaping: text::Shaping,
174 text_wrap: text::Wrapping,
175 font: Option<Renderer::Font>,
176 handle: Handle<Renderer::Font>,
177 class: <Theme as Catalog>::Class<'a>,
178 menu_class: <Theme as menu::Catalog>::Class<'a>,
179}
180
181impl<'a, T, L, V, Message, Theme, Renderer>
182 PickList<'a, T, L, V, Message, Theme, Renderer>
183where
184 T: ToString + PartialEq + Clone,
185 L: Borrow<[T]> + 'a,
186 V: Borrow<T> + 'a,
187 Message: Clone,
188 Theme: Catalog,
189 Renderer: text::Renderer,
190{
191 pub fn new(
194 options: L,
195 selected: Option<V>,
196 on_select: impl Fn(T) -> Message + 'a,
197 ) -> Self {
198 Self {
199 on_select: Box::new(on_select),
200 on_open: None,
201 on_close: None,
202 options,
203 placeholder: None,
204 selected,
205 width: Length::Shrink,
206 padding: crate::button::DEFAULT_PADDING,
207 text_size: None,
208 text_line_height: text::LineHeight::default(),
209 text_shaping: text::Shaping::Advanced,
210 text_wrap: text::Wrapping::default(),
211 font: None,
212 handle: Handle::default(),
213 class: <Theme as Catalog>::default(),
214 menu_class: <Theme as Catalog>::default_menu(),
215 }
216 }
217
218 pub fn placeholder(mut self, placeholder: impl Into<String>) -> Self {
220 self.placeholder = Some(placeholder.into());
221 self
222 }
223
224 pub fn width(mut self, width: impl Into<Length>) -> Self {
226 self.width = width.into();
227 self
228 }
229
230 pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self {
232 self.padding = padding.into();
233 self
234 }
235
236 pub fn text_size(mut self, size: impl Into<Pixels>) -> Self {
238 self.text_size = Some(size.into());
239 self
240 }
241
242 pub fn text_line_height(
244 mut self,
245 line_height: impl Into<text::LineHeight>,
246 ) -> Self {
247 self.text_line_height = line_height.into();
248 self
249 }
250
251 pub fn text_shaping(mut self, shaping: text::Shaping) -> Self {
253 self.text_shaping = shaping;
254 self
255 }
256
257 pub fn text_wrap(mut self, wrap: text::Wrapping) -> Self {
259 self.text_wrap = wrap;
260 self
261 }
262
263 pub fn font(mut self, font: impl Into<Renderer::Font>) -> Self {
265 self.font = Some(font.into());
266 self
267 }
268
269 pub fn handle(mut self, handle: Handle<Renderer::Font>) -> Self {
271 self.handle = handle;
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(
300 mut self,
301 style: impl Fn(&Theme) -> menu::Style + 'a,
302 ) -> Self
303 where
304 <Theme as menu::Catalog>::Class<'a>: From<menu::StyleFn<'a, Theme>>,
305 {
306 self.menu_class = (Box::new(style) as menu::StyleFn<'a, Theme>).into();
307 self
308 }
309
310 #[cfg(feature = "advanced")]
312 #[must_use]
313 pub fn class(
314 mut self,
315 class: impl Into<<Theme as Catalog>::Class<'a>>,
316 ) -> Self {
317 self.class = class.into();
318 self
319 }
320
321 #[cfg(feature = "advanced")]
323 #[must_use]
324 pub fn menu_class(
325 mut self,
326 class: impl Into<<Theme as menu::Catalog>::Class<'a>>,
327 ) -> Self {
328 self.menu_class = class.into();
329 self
330 }
331}
332
333impl<'a, T, L, V, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
334 for PickList<'a, T, L, V, Message, Theme, Renderer>
335where
336 T: Clone + ToString + PartialEq + 'a,
337 L: Borrow<[T]>,
338 V: Borrow<T>,
339 Message: Clone + 'a,
340 Theme: Catalog + 'a,
341 Renderer: text::Renderer + 'a,
342{
343 fn tag(&self) -> tree::Tag {
344 tree::Tag::of::<State<Renderer::Paragraph>>()
345 }
346
347 fn state(&self) -> tree::State {
348 tree::State::new(State::<Renderer::Paragraph>::new())
349 }
350
351 fn size(&self) -> Size<Length> {
352 Size {
353 width: self.width,
354 height: Length::Shrink,
355 }
356 }
357
358 fn layout(
359 &self,
360 tree: &mut Tree,
361 renderer: &Renderer,
362 limits: &layout::Limits,
363 ) -> layout::Node {
364 let state = tree.state.downcast_mut::<State<Renderer::Paragraph>>();
365
366 let font = self.font.unwrap_or_else(|| renderer.default_font());
367 let text_size =
368 self.text_size.unwrap_or_else(|| renderer.default_size());
369 let options = self.options.borrow();
370
371 state.options.resize_with(options.len(), Default::default);
372
373 let option_text = Text {
374 content: "",
375 bounds: Size::new(
376 f32::INFINITY,
377 self.text_line_height.to_absolute(text_size).into(),
378 ),
379 size: text_size,
380 line_height: self.text_line_height,
381 font,
382 horizontal_alignment: alignment::Horizontal::Left,
383 vertical_alignment: alignment::Vertical::Center,
384 shaping: self.text_shaping,
385 wrapping: self.text_wrap,
386 };
387
388 for (option, paragraph) in options.iter().zip(state.options.iter_mut())
389 {
390 let label = option.to_string();
391
392 paragraph.update(Text {
393 content: &label,
394 ..option_text
395 });
396 }
397
398 if let Some(placeholder) = &self.placeholder {
399 state.placeholder.update(Text {
400 content: placeholder,
401 ..option_text
402 });
403 }
404
405 let max_width = match self.width {
406 Length::Shrink => {
407 let labels_width =
408 state.options.iter().fold(0.0, |width, paragraph| {
409 f32::max(width, paragraph.min_width())
410 });
411
412 labels_width.max(
413 self.placeholder
414 .as_ref()
415 .map(|_| state.placeholder.min_width())
416 .unwrap_or(0.0),
417 )
418 }
419 _ => 0.0,
420 };
421
422 let size = {
423 let intrinsic = Size::new(
424 max_width + text_size.0 + self.padding.left,
425 f32::from(self.text_line_height.to_absolute(text_size)),
426 );
427
428 limits
429 .width(self.width)
430 .shrink(self.padding)
431 .resolve(self.width, Length::Shrink, intrinsic)
432 .expand(self.padding)
433 };
434
435 layout::Node::new(size)
436 }
437
438 fn on_event(
439 &mut self,
440 tree: &mut Tree,
441 event: Event,
442 layout: Layout<'_>,
443 cursor: mouse::Cursor,
444 _renderer: &Renderer,
445 _clipboard: &mut dyn Clipboard,
446 shell: &mut Shell<'_, Message>,
447 _viewport: &Rectangle,
448 ) -> event::Status {
449 match event {
450 Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
451 | Event::Touch(touch::Event::FingerPressed { .. }) => {
452 let state =
453 tree.state.downcast_mut::<State<Renderer::Paragraph>>();
454
455 if state.is_open {
456 state.is_open = false;
459
460 if let Some(on_close) = &self.on_close {
461 shell.publish(on_close.clone());
462 }
463
464 event::Status::Captured
465 } else if cursor.is_over(layout.bounds()) {
466 let selected = self.selected.as_ref().map(Borrow::borrow);
467
468 state.is_open = true;
469 state.hovered_option = self
470 .options
471 .borrow()
472 .iter()
473 .position(|option| Some(option) == selected);
474
475 if let Some(on_open) = &self.on_open {
476 shell.publish(on_open.clone());
477 }
478
479 event::Status::Captured
480 } else {
481 event::Status::Ignored
482 }
483 }
484 Event::Mouse(mouse::Event::WheelScrolled {
485 delta: mouse::ScrollDelta::Lines { y, .. },
486 }) => {
487 let state =
488 tree.state.downcast_mut::<State<Renderer::Paragraph>>();
489
490 if state.keyboard_modifiers.command()
491 && cursor.is_over(layout.bounds())
492 && !state.is_open
493 {
494 fn find_next<'a, T: PartialEq>(
495 selected: &'a T,
496 mut options: impl Iterator<Item = &'a T>,
497 ) -> Option<&'a T> {
498 let _ = options.find(|&option| option == selected);
499
500 options.next()
501 }
502
503 let options = self.options.borrow();
504 let selected = self.selected.as_ref().map(Borrow::borrow);
505
506 let next_option = if y < 0.0 {
507 if let Some(selected) = selected {
508 find_next(selected, options.iter())
509 } else {
510 options.first()
511 }
512 } else if y > 0.0 {
513 if let Some(selected) = selected {
514 find_next(selected, options.iter().rev())
515 } else {
516 options.last()
517 }
518 } else {
519 None
520 };
521
522 if let Some(next_option) = next_option {
523 shell.publish((self.on_select)(next_option.clone()));
524 }
525
526 event::Status::Captured
527 } else {
528 event::Status::Ignored
529 }
530 }
531 Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => {
532 let state =
533 tree.state.downcast_mut::<State<Renderer::Paragraph>>();
534
535 state.keyboard_modifiers = modifiers;
536
537 event::Status::Ignored
538 }
539 _ => event::Status::Ignored,
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 let is_mouse_over = cursor.is_over(bounds);
577 let is_selected = selected.is_some();
578
579 let status = if state.is_open {
580 Status::Opened
581 } else if is_mouse_over {
582 Status::Hovered
583 } else {
584 Status::Active
585 };
586
587 let style = Catalog::style(theme, &self.class, status);
588
589 renderer.fill_quad(
590 renderer::Quad {
591 bounds,
592 border: style.border,
593 ..renderer::Quad::default()
594 },
595 style.background,
596 );
597
598 let handle = match &self.handle {
599 Handle::Arrow { size } => Some((
600 Renderer::ICON_FONT,
601 Renderer::ARROW_DOWN_ICON,
602 *size,
603 text::LineHeight::default(),
604 text::Shaping::Basic,
605 text::Wrapping::default(),
606 )),
607 Handle::Static(Icon {
608 font,
609 code_point,
610 size,
611 line_height,
612 shaping,
613 wrap,
614 }) => {
615 Some((*font, *code_point, *size, *line_height, *shaping, *wrap))
616 }
617 Handle::Dynamic { open, closed } => {
618 if state.is_open {
619 Some((
620 open.font,
621 open.code_point,
622 open.size,
623 open.line_height,
624 open.shaping,
625 open.wrap,
626 ))
627 } else {
628 Some((
629 closed.font,
630 closed.code_point,
631 closed.size,
632 closed.line_height,
633 closed.shaping,
634 closed.wrap,
635 ))
636 }
637 }
638 Handle::None => None,
639 };
640
641 if let Some((font, code_point, size, line_height, shaping, wrap)) =
642 handle
643 {
644 let size = size.unwrap_or_else(|| renderer.default_size());
645
646 renderer.fill_text(
647 Text {
648 content: code_point.to_string(),
649 size,
650 line_height,
651 font,
652 bounds: Size::new(
653 bounds.width,
654 f32::from(line_height.to_absolute(size)),
655 ),
656 horizontal_alignment: alignment::Horizontal::Right,
657 vertical_alignment: alignment::Vertical::Center,
658 shaping,
659 wrapping: wrap,
660 },
661 Point::new(
662 bounds.x + bounds.width - self.padding.right,
663 bounds.center_y(),
664 ),
665 style.handle_color,
666 *viewport,
667 );
668 }
669
670 let label = selected.map(ToString::to_string);
671
672 if let Some(label) = label.or_else(|| self.placeholder.clone()) {
673 let text_size =
674 self.text_size.unwrap_or_else(|| renderer.default_size());
675
676 renderer.fill_text(
677 Text {
678 content: label,
679 size: text_size,
680 line_height: self.text_line_height,
681 font,
682 bounds: Size::new(
683 bounds.width - self.padding.horizontal(),
684 f32::from(self.text_line_height.to_absolute(text_size)),
685 ),
686 horizontal_alignment: alignment::Horizontal::Left,
687 vertical_alignment: alignment::Vertical::Center,
688 shaping: self.text_shaping,
689 wrapping: self.text_wrap,
690 },
691 Point::new(bounds.x + self.padding.left, bounds.center_y()),
692 if is_selected {
693 style.text_color
694 } else {
695 style.placeholder_color
696 },
697 *viewport,
698 );
699 }
700 }
701
702 fn overlay<'b>(
703 &'b mut self,
704 tree: &'b mut Tree,
705 layout: Layout<'_>,
706 renderer: &Renderer,
707 translation: Vector,
708 ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
709 let state = tree.state.downcast_mut::<State<Renderer::Paragraph>>();
710 let font = self.font.unwrap_or_else(|| renderer.default_font());
711
712 if state.is_open {
713 let bounds = layout.bounds();
714
715 let on_select = &self.on_select;
716
717 let mut menu = Menu::new(
718 &mut state.menu,
719 self.options.borrow(),
720 &mut state.hovered_option,
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 .text_shaping(self.text_shaping);
733
734 if let Some(text_size) = self.text_size {
735 menu = menu.text_size(text_size);
736 }
737
738 Some(menu.overlay(layout.position() + translation, bounds.height))
739 } else {
740 None
741 }
742 }
743}
744
745impl<'a, T, L, V, Message, Theme, Renderer>
746 From<PickList<'a, T, L, V, Message, Theme, Renderer>>
747 for Element<'a, Message, Theme, Renderer>
748where
749 T: Clone + ToString + PartialEq + 'a,
750 L: Borrow<[T]> + 'a,
751 V: Borrow<T> + 'a,
752 Message: Clone + 'a,
753 Theme: Catalog + 'a,
754 Renderer: text::Renderer + 'a,
755{
756 fn from(
757 pick_list: PickList<'a, T, L, V, Message, Theme, Renderer>,
758 ) -> Self {
759 Self::new(pick_list)
760 }
761}
762
763#[derive(Debug)]
764struct State<P: text::Paragraph> {
765 menu: menu::State,
766 keyboard_modifiers: keyboard::Modifiers,
767 is_open: bool,
768 hovered_option: Option<usize>,
769 options: Vec<paragraph::Plain<P>>,
770 placeholder: paragraph::Plain<P>,
771}
772
773impl<P: text::Paragraph> State<P> {
774 fn new() -> Self {
776 Self {
777 menu: menu::State::default(),
778 keyboard_modifiers: keyboard::Modifiers::default(),
779 is_open: bool::default(),
780 hovered_option: Option::default(),
781 options: Vec::new(),
782 placeholder: paragraph::Plain::default(),
783 }
784 }
785}
786
787impl<P: text::Paragraph> Default for State<P> {
788 fn default() -> Self {
789 Self::new()
790 }
791}
792
793#[derive(Debug, Clone, PartialEq)]
795pub enum Handle<Font> {
796 Arrow {
800 size: Option<Pixels>,
802 },
803 Static(Icon<Font>),
805 Dynamic {
807 closed: Icon<Font>,
809 open: Icon<Font>,
811 },
812 None,
814}
815
816impl<Font> Default for Handle<Font> {
817 fn default() -> Self {
818 Self::Arrow { size: None }
819 }
820}
821
822#[derive(Debug, Clone, PartialEq)]
824pub struct Icon<Font> {
825 pub font: Font,
827 pub code_point: char,
829 pub size: Option<Pixels>,
831 pub line_height: text::LineHeight,
833 pub shaping: text::Shaping,
835 pub wrap: text::Wrapping,
837}
838
839#[derive(Debug, Clone, Copy, PartialEq, Eq)]
841pub enum Status {
842 Active,
844 Hovered,
846 Opened,
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.background.strong.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}