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