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 paragraph.update(Text {
385 content: &label,
386 ..option_text
387 });
388 }
389
390 if let Some(placeholder) = &self.placeholder {
391 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 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(layout.position() + translation, bounds.height))
725 } else {
726 None
727 }
728 }
729}
730
731impl<'a, T, L, V, Message, Theme, Renderer>
732 From<PickList<'a, T, L, V, Message, Theme, Renderer>>
733 for Element<'a, Message, Theme, Renderer>
734where
735 T: Clone + ToString + PartialEq + 'a,
736 L: Borrow<[T]> + 'a,
737 V: Borrow<T> + 'a,
738 Message: Clone + 'a,
739 Theme: Catalog + 'a,
740 Renderer: text::Renderer + 'a,
741{
742 fn from(
743 pick_list: PickList<'a, T, L, V, Message, Theme, Renderer>,
744 ) -> Self {
745 Self::new(pick_list)
746 }
747}
748
749#[derive(Debug)]
750struct State<P: text::Paragraph> {
751 menu: menu::State,
752 keyboard_modifiers: keyboard::Modifiers,
753 is_open: bool,
754 hovered_option: Option<usize>,
755 options: Vec<paragraph::Plain<P>>,
756 placeholder: paragraph::Plain<P>,
757}
758
759impl<P: text::Paragraph> State<P> {
760 fn new() -> Self {
762 Self {
763 menu: menu::State::default(),
764 keyboard_modifiers: keyboard::Modifiers::default(),
765 is_open: bool::default(),
766 hovered_option: Option::default(),
767 options: Vec::new(),
768 placeholder: paragraph::Plain::default(),
769 }
770 }
771}
772
773impl<P: text::Paragraph> Default for State<P> {
774 fn default() -> Self {
775 Self::new()
776 }
777}
778
779#[derive(Debug, Clone, PartialEq)]
781pub enum Handle<Font> {
782 Arrow {
786 size: Option<Pixels>,
788 },
789 Static(Icon<Font>),
791 Dynamic {
793 closed: Icon<Font>,
795 open: Icon<Font>,
797 },
798 None,
800}
801
802impl<Font> Default for Handle<Font> {
803 fn default() -> Self {
804 Self::Arrow { size: None }
805 }
806}
807
808#[derive(Debug, Clone, PartialEq)]
810pub struct Icon<Font> {
811 pub font: Font,
813 pub code_point: char,
815 pub size: Option<Pixels>,
817 pub line_height: text::LineHeight,
819 pub shaping: text::Shaping,
821}
822
823#[derive(Debug, Clone, Copy, PartialEq, Eq)]
825pub enum Status {
826 Active,
828 Hovered,
830 Opened {
832 is_hovered: bool,
834 },
835}
836
837#[derive(Debug, Clone, Copy, PartialEq)]
839pub struct Style {
840 pub text_color: Color,
842 pub placeholder_color: Color,
844 pub handle_color: Color,
846 pub background: Background,
848 pub border: Border,
850}
851
852pub trait Catalog: menu::Catalog {
854 type Class<'a>;
856
857 fn default<'a>() -> <Self as Catalog>::Class<'a>;
859
860 fn default_menu<'a>() -> <Self as menu::Catalog>::Class<'a> {
862 <Self as menu::Catalog>::default()
863 }
864
865 fn style(
867 &self,
868 class: &<Self as Catalog>::Class<'_>,
869 status: Status,
870 ) -> Style;
871}
872
873pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Style + 'a>;
877
878impl Catalog for Theme {
879 type Class<'a> = StyleFn<'a, Self>;
880
881 fn default<'a>() -> StyleFn<'a, Self> {
882 Box::new(default)
883 }
884
885 fn style(&self, class: &StyleFn<'_, Self>, status: Status) -> Style {
886 class(self, status)
887 }
888}
889
890pub fn default(theme: &Theme, status: Status) -> Style {
892 let palette = theme.extended_palette();
893
894 let active = Style {
895 text_color: palette.background.weak.text,
896 background: palette.background.weak.color.into(),
897 placeholder_color: palette.background.strong.color,
898 handle_color: palette.background.weak.text,
899 border: Border {
900 radius: 2.0.into(),
901 width: 1.0,
902 color: palette.background.strong.color,
903 },
904 };
905
906 match status {
907 Status::Active => active,
908 Status::Hovered | Status::Opened { .. } => Style {
909 border: Border {
910 color: palette.primary.strong.color,
911 ..active.border
912 },
913 ..active
914 },
915 }
916}