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, Color, Element, Event, Layout, Length, Padding, Pixels, Point, Rectangle,
76 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 shell: &mut Shell<'_, Message>,
419 _viewport: &Rectangle,
420 ) {
421 let state = tree.state.downcast_mut::<State<Renderer::Paragraph>>();
422
423 match event {
424 Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
425 | Event::Touch(touch::Event::FingerPressed { .. }) => {
426 if state.is_open {
427 state.is_open = false;
430
431 if let Some(on_close) = &self.on_close {
432 shell.publish(on_close.clone());
433 }
434
435 shell.capture_event();
436 } else if cursor.is_over(layout.bounds()) {
437 let selected = self.selected.as_ref().map(Borrow::borrow);
438
439 state.is_open = true;
440 state.hovered_option = self
441 .options
442 .borrow()
443 .iter()
444 .position(|option| Some(option) == selected);
445
446 if let Some(on_open) = &self.on_open {
447 shell.publish(on_open.clone());
448 }
449
450 shell.capture_event();
451 }
452 }
453 Event::Mouse(mouse::Event::WheelScrolled {
454 delta: mouse::ScrollDelta::Lines { y, .. },
455 }) => {
456 if state.keyboard_modifiers.command()
457 && cursor.is_over(layout.bounds())
458 && !state.is_open
459 {
460 fn find_next<'a, T: PartialEq>(
461 selected: &'a T,
462 mut options: impl Iterator<Item = &'a T>,
463 ) -> Option<&'a T> {
464 let _ = options.find(|&option| option == selected);
465
466 options.next()
467 }
468
469 let options = self.options.borrow();
470 let selected = self.selected.as_ref().map(Borrow::borrow);
471
472 let next_option = if *y < 0.0 {
473 if let Some(selected) = selected {
474 find_next(selected, options.iter())
475 } else {
476 options.first()
477 }
478 } else if *y > 0.0 {
479 if let Some(selected) = selected {
480 find_next(selected, options.iter().rev())
481 } else {
482 options.last()
483 }
484 } else {
485 None
486 };
487
488 if let Some(next_option) = next_option {
489 shell.publish((self.on_select)(next_option.clone()));
490 }
491
492 shell.capture_event();
493 }
494 }
495 Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => {
496 state.keyboard_modifiers = *modifiers;
497 }
498 _ => {}
499 };
500
501 let status = {
502 let is_hovered = cursor.is_over(layout.bounds());
503
504 if state.is_open {
505 Status::Opened { is_hovered }
506 } else if is_hovered {
507 Status::Hovered
508 } else {
509 Status::Active
510 }
511 };
512
513 if let Event::Window(window::Event::RedrawRequested(_now)) = event {
514 self.last_status = Some(status);
515 } else if self
516 .last_status
517 .is_some_and(|last_status| last_status != status)
518 {
519 shell.request_redraw();
520 }
521 }
522
523 fn mouse_interaction(
524 &self,
525 _tree: &Tree,
526 layout: Layout<'_>,
527 cursor: mouse::Cursor,
528 _viewport: &Rectangle,
529 _renderer: &Renderer,
530 ) -> mouse::Interaction {
531 let bounds = layout.bounds();
532 let is_mouse_over = cursor.is_over(bounds);
533
534 if is_mouse_over {
535 mouse::Interaction::Pointer
536 } else {
537 mouse::Interaction::default()
538 }
539 }
540
541 fn draw(
542 &self,
543 tree: &Tree,
544 renderer: &mut Renderer,
545 theme: &Theme,
546 _style: &renderer::Style,
547 layout: Layout<'_>,
548 _cursor: mouse::Cursor,
549 viewport: &Rectangle,
550 ) {
551 let font = self.font.unwrap_or_else(|| renderer.default_font());
552 let selected = self.selected.as_ref().map(Borrow::borrow);
553 let state = tree.state.downcast_ref::<State<Renderer::Paragraph>>();
554
555 let bounds = layout.bounds();
556
557 let style = Catalog::style(
558 theme,
559 &self.class,
560 self.last_status.unwrap_or(Status::Active),
561 );
562
563 renderer.fill_quad(
564 renderer::Quad {
565 bounds,
566 border: style.border,
567 ..renderer::Quad::default()
568 },
569 style.background,
570 );
571
572 let handle = match &self.handle {
573 Handle::Arrow { size } => Some((
574 Renderer::ICON_FONT,
575 Renderer::ARROW_DOWN_ICON,
576 *size,
577 text::LineHeight::default(),
578 text::Shaping::Basic,
579 )),
580 Handle::Static(Icon {
581 font,
582 code_point,
583 size,
584 line_height,
585 shaping,
586 }) => Some((*font, *code_point, *size, *line_height, *shaping)),
587 Handle::Dynamic { open, closed } => {
588 if state.is_open {
589 Some((
590 open.font,
591 open.code_point,
592 open.size,
593 open.line_height,
594 open.shaping,
595 ))
596 } else {
597 Some((
598 closed.font,
599 closed.code_point,
600 closed.size,
601 closed.line_height,
602 closed.shaping,
603 ))
604 }
605 }
606 Handle::None => None,
607 };
608
609 if let Some((font, code_point, size, line_height, shaping)) = handle {
610 let size = size.unwrap_or_else(|| renderer.default_size());
611
612 renderer.fill_text(
613 Text {
614 content: code_point.to_string(),
615 size,
616 line_height,
617 font,
618 bounds: Size::new(bounds.width, f32::from(line_height.to_absolute(size))),
619 align_x: text::Alignment::Right,
620 align_y: alignment::Vertical::Center,
621 shaping,
622 wrapping: text::Wrapping::default(),
623 hint_factor: None,
624 },
625 Point::new(
626 bounds.x + bounds.width - self.padding.right,
627 bounds.center_y(),
628 ),
629 style.handle_color,
630 *viewport,
631 );
632 }
633
634 let label = selected.map(ToString::to_string);
635
636 if let Some(label) = label.or_else(|| self.placeholder.clone()) {
637 let text_size = self.text_size.unwrap_or_else(|| renderer.default_size());
638
639 renderer.fill_text(
640 Text {
641 content: label,
642 size: text_size,
643 line_height: self.text_line_height,
644 font,
645 bounds: Size::new(
646 bounds.width - self.padding.x(),
647 f32::from(self.text_line_height.to_absolute(text_size)),
648 ),
649 align_x: text::Alignment::Default,
650 align_y: alignment::Vertical::Center,
651 shaping: self.text_shaping,
652 wrapping: text::Wrapping::default(),
653 hint_factor: renderer.scale_factor(),
654 },
655 Point::new(bounds.x + self.padding.left, bounds.center_y()),
656 if selected.is_some() {
657 style.text_color
658 } else {
659 style.placeholder_color
660 },
661 *viewport,
662 );
663 }
664 }
665
666 fn overlay<'b>(
667 &'b mut self,
668 tree: &'b mut Tree,
669 layout: Layout<'_>,
670 renderer: &Renderer,
671 viewport: &Rectangle,
672 translation: Vector,
673 ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
674 let state = tree.state.downcast_mut::<State<Renderer::Paragraph>>();
675 let font = self.font.unwrap_or_else(|| renderer.default_font());
676
677 if state.is_open {
678 let bounds = layout.bounds();
679
680 let on_select = &self.on_select;
681
682 let mut menu = Menu::new(
683 &mut state.menu,
684 self.options.borrow(),
685 &mut state.hovered_option,
686 |option| {
687 state.is_open = false;
688
689 (on_select)(option)
690 },
691 None,
692 &self.menu_class,
693 )
694 .width(bounds.width)
695 .padding(self.padding)
696 .font(font)
697 .text_shaping(self.text_shaping);
698
699 if let Some(text_size) = self.text_size {
700 menu = menu.text_size(text_size);
701 }
702
703 Some(menu.overlay(
704 layout.position() + translation,
705 *viewport,
706 bounds.height,
707 self.menu_height,
708 ))
709 } else {
710 None
711 }
712 }
713}
714
715impl<'a, T, L, V, Message, Theme, Renderer> From<PickList<'a, T, L, V, Message, Theme, Renderer>>
716 for Element<'a, Message, Theme, Renderer>
717where
718 T: Clone + ToString + PartialEq + 'a,
719 L: Borrow<[T]> + 'a,
720 V: Borrow<T> + 'a,
721 Message: Clone + 'a,
722 Theme: Catalog + 'a,
723 Renderer: text::Renderer + 'a,
724{
725 fn from(pick_list: PickList<'a, T, L, V, Message, Theme, Renderer>) -> Self {
726 Self::new(pick_list)
727 }
728}
729
730#[derive(Debug)]
731struct State<P: text::Paragraph> {
732 menu: menu::State,
733 keyboard_modifiers: keyboard::Modifiers,
734 is_open: bool,
735 hovered_option: Option<usize>,
736 options: Vec<paragraph::Plain<P>>,
737 placeholder: paragraph::Plain<P>,
738}
739
740impl<P: text::Paragraph> State<P> {
741 fn new() -> Self {
743 Self {
744 menu: menu::State::default(),
745 keyboard_modifiers: keyboard::Modifiers::default(),
746 is_open: bool::default(),
747 hovered_option: Option::default(),
748 options: Vec::new(),
749 placeholder: paragraph::Plain::default(),
750 }
751 }
752}
753
754impl<P: text::Paragraph> Default for State<P> {
755 fn default() -> Self {
756 Self::new()
757 }
758}
759
760#[derive(Debug, Clone, PartialEq)]
762pub enum Handle<Font> {
763 Arrow {
767 size: Option<Pixels>,
769 },
770 Static(Icon<Font>),
772 Dynamic {
774 closed: Icon<Font>,
776 open: Icon<Font>,
778 },
779 None,
781}
782
783impl<Font> Default for Handle<Font> {
784 fn default() -> Self {
785 Self::Arrow { size: None }
786 }
787}
788
789#[derive(Debug, Clone, PartialEq)]
791pub struct Icon<Font> {
792 pub font: Font,
794 pub code_point: char,
796 pub size: Option<Pixels>,
798 pub line_height: text::LineHeight,
800 pub shaping: text::Shaping,
802}
803
804#[derive(Debug, Clone, Copy, PartialEq, Eq)]
806pub enum Status {
807 Active,
809 Hovered,
811 Opened {
813 is_hovered: bool,
815 },
816}
817
818#[derive(Debug, Clone, Copy, PartialEq)]
820pub struct Style {
821 pub text_color: Color,
823 pub placeholder_color: Color,
825 pub handle_color: Color,
827 pub background: Background,
829 pub border: Border,
831}
832
833pub trait Catalog: menu::Catalog {
835 type Class<'a>;
837
838 fn default<'a>() -> <Self as Catalog>::Class<'a>;
840
841 fn default_menu<'a>() -> <Self as menu::Catalog>::Class<'a> {
843 <Self as menu::Catalog>::default()
844 }
845
846 fn style(&self, class: &<Self as Catalog>::Class<'_>, status: Status) -> Style;
848}
849
850pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Style + 'a>;
854
855impl Catalog for Theme {
856 type Class<'a> = StyleFn<'a, Self>;
857
858 fn default<'a>() -> StyleFn<'a, Self> {
859 Box::new(default)
860 }
861
862 fn style(&self, class: &StyleFn<'_, Self>, status: Status) -> Style {
863 class(self, status)
864 }
865}
866
867pub fn default(theme: &Theme, status: Status) -> Style {
869 let palette = theme.extended_palette();
870
871 let active = Style {
872 text_color: palette.background.weak.text,
873 background: palette.background.weak.color.into(),
874 placeholder_color: palette.secondary.base.color,
875 handle_color: palette.background.weak.text,
876 border: Border {
877 radius: 2.0.into(),
878 width: 1.0,
879 color: palette.background.strong.color,
880 },
881 };
882
883 match status {
884 Status::Active => active,
885 Status::Hovered | Status::Opened { .. } => Style {
886 border: Border {
887 color: palette.primary.strong.color,
888 ..active.border
889 },
890 ..active
891 },
892 }
893}