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 };
361
362 for (option, paragraph) in options.iter().zip(state.options.iter_mut()) {
363 let label = option.to_string();
364
365 let _ = paragraph.update(Text {
366 content: &label,
367 ..option_text
368 });
369 }
370
371 if let Some(placeholder) = &self.placeholder {
372 let _ = state.placeholder.update(Text {
373 content: placeholder,
374 ..option_text
375 });
376 }
377
378 let max_width = match self.width {
379 Length::Shrink => {
380 let labels_width = state.options.iter().fold(0.0, |width, paragraph| {
381 f32::max(width, paragraph.min_width())
382 });
383
384 labels_width.max(
385 self.placeholder
386 .as_ref()
387 .map(|_| state.placeholder.min_width())
388 .unwrap_or(0.0),
389 )
390 }
391 _ => 0.0,
392 };
393
394 let size = {
395 let intrinsic = Size::new(
396 max_width + text_size.0 + self.padding.left,
397 f32::from(self.text_line_height.to_absolute(text_size)),
398 );
399
400 limits
401 .width(self.width)
402 .shrink(self.padding)
403 .resolve(self.width, Length::Shrink, intrinsic)
404 .expand(self.padding)
405 };
406
407 layout::Node::new(size)
408 }
409
410 fn update(
411 &mut self,
412 tree: &mut Tree,
413 event: &Event,
414 layout: Layout<'_>,
415 cursor: mouse::Cursor,
416 _renderer: &Renderer,
417 _clipboard: &mut dyn Clipboard,
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 },
624 Point::new(
625 bounds.x + bounds.width - self.padding.right,
626 bounds.center_y(),
627 ),
628 style.handle_color,
629 *viewport,
630 );
631 }
632
633 let label = selected.map(ToString::to_string);
634
635 if let Some(label) = label.or_else(|| self.placeholder.clone()) {
636 let text_size = self.text_size.unwrap_or_else(|| renderer.default_size());
637
638 renderer.fill_text(
639 Text {
640 content: label,
641 size: text_size,
642 line_height: self.text_line_height,
643 font,
644 bounds: Size::new(
645 bounds.width - self.padding.x(),
646 f32::from(self.text_line_height.to_absolute(text_size)),
647 ),
648 align_x: text::Alignment::Default,
649 align_y: alignment::Vertical::Center,
650 shaping: self.text_shaping,
651 wrapping: text::Wrapping::default(),
652 },
653 Point::new(bounds.x + self.padding.left, bounds.center_y()),
654 if selected.is_some() {
655 style.text_color
656 } else {
657 style.placeholder_color
658 },
659 *viewport,
660 );
661 }
662 }
663
664 fn overlay<'b>(
665 &'b mut self,
666 tree: &'b mut Tree,
667 layout: Layout<'_>,
668 renderer: &Renderer,
669 viewport: &Rectangle,
670 translation: Vector,
671 ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
672 let state = tree.state.downcast_mut::<State<Renderer::Paragraph>>();
673 let font = self.font.unwrap_or_else(|| renderer.default_font());
674
675 if state.is_open {
676 let bounds = layout.bounds();
677
678 let on_select = &self.on_select;
679
680 let mut menu = Menu::new(
681 &mut state.menu,
682 self.options.borrow(),
683 &mut state.hovered_option,
684 |option| {
685 state.is_open = false;
686
687 (on_select)(option)
688 },
689 None,
690 &self.menu_class,
691 )
692 .width(bounds.width)
693 .padding(self.padding)
694 .font(font)
695 .text_shaping(self.text_shaping);
696
697 if let Some(text_size) = self.text_size {
698 menu = menu.text_size(text_size);
699 }
700
701 Some(menu.overlay(
702 layout.position() + translation,
703 *viewport,
704 bounds.height,
705 self.menu_height,
706 ))
707 } else {
708 None
709 }
710 }
711}
712
713impl<'a, T, L, V, Message, Theme, Renderer> From<PickList<'a, T, L, V, Message, Theme, Renderer>>
714 for Element<'a, Message, Theme, Renderer>
715where
716 T: Clone + ToString + PartialEq + 'a,
717 L: Borrow<[T]> + 'a,
718 V: Borrow<T> + 'a,
719 Message: Clone + 'a,
720 Theme: Catalog + 'a,
721 Renderer: text::Renderer + 'a,
722{
723 fn from(pick_list: PickList<'a, T, L, V, Message, Theme, Renderer>) -> Self {
724 Self::new(pick_list)
725 }
726}
727
728#[derive(Debug)]
729struct State<P: text::Paragraph> {
730 menu: menu::State,
731 keyboard_modifiers: keyboard::Modifiers,
732 is_open: bool,
733 hovered_option: Option<usize>,
734 options: Vec<paragraph::Plain<P>>,
735 placeholder: paragraph::Plain<P>,
736}
737
738impl<P: text::Paragraph> State<P> {
739 fn new() -> Self {
741 Self {
742 menu: menu::State::default(),
743 keyboard_modifiers: keyboard::Modifiers::default(),
744 is_open: bool::default(),
745 hovered_option: Option::default(),
746 options: Vec::new(),
747 placeholder: paragraph::Plain::default(),
748 }
749 }
750}
751
752impl<P: text::Paragraph> Default for State<P> {
753 fn default() -> Self {
754 Self::new()
755 }
756}
757
758#[derive(Debug, Clone, PartialEq)]
760pub enum Handle<Font> {
761 Arrow {
765 size: Option<Pixels>,
767 },
768 Static(Icon<Font>),
770 Dynamic {
772 closed: Icon<Font>,
774 open: Icon<Font>,
776 },
777 None,
779}
780
781impl<Font> Default for Handle<Font> {
782 fn default() -> Self {
783 Self::Arrow { size: None }
784 }
785}
786
787#[derive(Debug, Clone, PartialEq)]
789pub struct Icon<Font> {
790 pub font: Font,
792 pub code_point: char,
794 pub size: Option<Pixels>,
796 pub line_height: text::LineHeight,
798 pub shaping: text::Shaping,
800}
801
802#[derive(Debug, Clone, Copy, PartialEq, Eq)]
804pub enum Status {
805 Active,
807 Hovered,
809 Opened {
811 is_hovered: bool,
813 },
814}
815
816#[derive(Debug, Clone, Copy, PartialEq)]
818pub struct Style {
819 pub text_color: Color,
821 pub placeholder_color: Color,
823 pub handle_color: Color,
825 pub background: Background,
827 pub border: Border,
829}
830
831pub trait Catalog: menu::Catalog {
833 type Class<'a>;
835
836 fn default<'a>() -> <Self as Catalog>::Class<'a>;
838
839 fn default_menu<'a>() -> <Self as menu::Catalog>::Class<'a> {
841 <Self as menu::Catalog>::default()
842 }
843
844 fn style(&self, class: &<Self as Catalog>::Class<'_>, status: Status) -> Style;
846}
847
848pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Style + 'a>;
852
853impl Catalog for Theme {
854 type Class<'a> = StyleFn<'a, Self>;
855
856 fn default<'a>() -> StyleFn<'a, Self> {
857 Box::new(default)
858 }
859
860 fn style(&self, class: &StyleFn<'_, Self>, status: Status) -> Style {
861 class(self, status)
862 }
863}
864
865pub fn default(theme: &Theme, status: Status) -> Style {
867 let palette = theme.extended_palette();
868
869 let active = Style {
870 text_color: palette.background.weak.text,
871 background: palette.background.weak.color.into(),
872 placeholder_color: palette.secondary.base.color,
873 handle_color: palette.background.weak.text,
874 border: Border {
875 radius: 2.0.into(),
876 width: 1.0,
877 color: palette.background.strong.color,
878 },
879 };
880
881 match status {
882 Status::Active => active,
883 Status::Hovered | Status::Opened { .. } => Style {
884 border: Border {
885 color: palette.primary.strong.color,
886 ..active.border
887 },
888 ..active
889 },
890 }
891}