1use crate::core::alignment;
65use crate::core::keyboard;
66use crate::core::keyboard::key;
67use crate::core::layout;
68use crate::core::mouse;
69use crate::core::overlay;
70use crate::core::renderer;
71use crate::core::text::paragraph;
72use crate::core::text::{self, Text};
73use crate::core::theme::palette;
74use crate::core::touch;
75use crate::core::widget::operation::Operation;
76use crate::core::widget::operation::accessible::{Accessible, HasPopup, Role, Value};
77use crate::core::widget::operation::focusable::{self, Focusable};
78use crate::core::widget::tree::{self, Tree};
79use crate::core::window;
80use crate::core::{
81 Background, Border, Color, Element, Event, Layout, Length, Padding, Pixels, Point, Rectangle,
82 Shadow, Shell, Size, Theme, Vector, Widget,
83};
84use crate::overlay::menu::{self, Menu};
85
86use std::borrow::Borrow;
87use std::f32;
88
89pub struct PickList<'a, T, L, V, Message, Theme = crate::Theme, Renderer = crate::Renderer>
153where
154 T: PartialEq + Clone,
155 L: Borrow<[T]> + 'a,
156 V: Borrow<T> + 'a,
157 Theme: Catalog,
158 Renderer: text::Renderer,
159{
160 options: L,
161 to_string: Box<dyn Fn(&T) -> String + 'a>,
162 on_select: Option<Box<dyn Fn(T) -> Message + 'a>>,
163 on_open: Option<Message>,
164 on_close: Option<Message>,
165 placeholder: Option<String>,
166 selected: Option<V>,
167 width: Length,
168 padding: Padding,
169 text_size: Option<Pixels>,
170 line_height: text::LineHeight,
171 shaping: text::Shaping,
172 ellipsis: text::Ellipsis,
173 font: Option<Renderer::Font>,
174 handle: Handle<Renderer::Font>,
175 class: <Theme as Catalog>::Class<'a>,
176 menu_class: <Theme as menu::Catalog>::Class<'a>,
177 last_status: Option<Status>,
178 menu_height: Length,
179}
180
181impl<'a, T, L, V, Message, Theme, Renderer> PickList<'a, T, L, V, Message, Theme, Renderer>
182where
183 T: PartialEq + Clone,
184 L: Borrow<[T]> + 'a,
185 V: Borrow<T> + 'a,
186 Message: Clone,
187 Theme: Catalog,
188 Renderer: text::Renderer,
189{
190 pub fn new(selected: Option<V>, options: L, to_string: impl Fn(&T) -> String + 'a) -> Self {
193 Self {
194 to_string: Box::new(to_string),
195 on_select: None,
196 on_open: None,
197 on_close: None,
198 options,
199 placeholder: None,
200 selected,
201 width: Length::Shrink,
202 padding: crate::button::DEFAULT_PADDING,
203 text_size: None,
204 line_height: text::LineHeight::default(),
205 shaping: text::Shaping::default(),
206 ellipsis: text::Ellipsis::End,
207 font: None,
208 handle: Handle::default(),
209 class: <Theme as Catalog>::default(),
210 menu_class: <Theme as Catalog>::default_menu(),
211 last_status: None,
212 menu_height: Length::Shrink,
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 menu_height(mut self, menu_height: impl Into<Length>) -> Self {
230 self.menu_height = menu_height.into();
231 self
232 }
233
234 pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self {
236 self.padding = padding.into();
237 self
238 }
239
240 pub fn text_size(mut self, size: impl Into<Pixels>) -> Self {
242 self.text_size = Some(size.into());
243 self
244 }
245
246 pub fn line_height(mut self, line_height: impl Into<text::LineHeight>) -> Self {
248 self.line_height = line_height.into();
249 self
250 }
251
252 pub fn shaping(mut self, shaping: text::Shaping) -> Self {
254 self.shaping = shaping;
255 self
256 }
257
258 pub fn ellipsis(mut self, ellipsis: text::Ellipsis) -> Self {
260 self.ellipsis = ellipsis;
261 self
262 }
263
264 pub fn font(mut self, font: impl Into<Renderer::Font>) -> Self {
266 self.font = Some(font.into());
267 self
268 }
269
270 pub fn handle(mut self, handle: Handle<Renderer::Font>) -> Self {
272 self.handle = handle;
273 self
274 }
275
276 pub fn on_select(mut self, on_select: impl Fn(T) -> Message + 'a) -> Self {
278 self.on_select = Some(Box::new(on_select));
279 self
280 }
281
282 pub fn on_open(mut self, on_open: Message) -> Self {
284 self.on_open = Some(on_open);
285 self
286 }
287
288 pub fn on_close(mut self, on_close: Message) -> Self {
290 self.on_close = Some(on_close);
291 self
292 }
293
294 #[must_use]
296 pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self
297 where
298 <Theme as Catalog>::Class<'a>: From<StyleFn<'a, Theme>>,
299 {
300 self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
301 self
302 }
303
304 #[must_use]
306 pub fn menu_style(mut self, style: impl Fn(&Theme) -> menu::Style + 'a) -> Self
307 where
308 <Theme as menu::Catalog>::Class<'a>: From<menu::StyleFn<'a, Theme>>,
309 {
310 self.menu_class = (Box::new(style) as menu::StyleFn<'a, Theme>).into();
311 self
312 }
313
314 #[cfg(feature = "advanced")]
316 #[must_use]
317 pub fn class(mut self, class: impl Into<<Theme as Catalog>::Class<'a>>) -> Self {
318 self.class = class.into();
319 self
320 }
321
322 #[cfg(feature = "advanced")]
324 #[must_use]
325 pub fn menu_class(mut self, class: impl Into<<Theme as menu::Catalog>::Class<'a>>) -> Self {
326 self.menu_class = class.into();
327 self
328 }
329}
330
331impl<'a, T, L, V, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
332 for PickList<'a, T, L, V, Message, Theme, Renderer>
333where
334 T: Clone + PartialEq + 'a,
335 L: Borrow<[T]>,
336 V: Borrow<T>,
337 Message: Clone + 'a,
338 Theme: Catalog + 'a,
339 Renderer: text::Renderer + 'a,
340{
341 fn tag(&self) -> tree::Tag {
342 tree::Tag::of::<State<Renderer::Paragraph>>()
343 }
344
345 fn state(&self) -> tree::State {
346 tree::State::new(State::<Renderer::Paragraph>::new())
347 }
348
349 fn size(&self) -> Size<Length> {
350 Size {
351 width: self.width,
352 height: Length::Shrink,
353 }
354 }
355
356 fn layout(
357 &mut self,
358 tree: &mut Tree,
359 renderer: &Renderer,
360 limits: &layout::Limits,
361 ) -> layout::Node {
362 let state = tree.state.downcast_mut::<State<Renderer::Paragraph>>();
363
364 let font = self.font.unwrap_or_else(|| renderer.default_font());
365 let text_size = self.text_size.unwrap_or_else(|| renderer.default_size());
366 let options = self.options.borrow();
367
368 let option_text = Text {
369 content: "",
370 bounds: Size::new(
371 limits.max().width,
372 self.line_height.to_absolute(text_size).into(),
373 ),
374 size: text_size,
375 line_height: self.line_height,
376 font,
377 align_x: text::Alignment::Default,
378 align_y: alignment::Vertical::Center,
379 shaping: self.shaping,
380 wrapping: text::Wrapping::None,
381 ellipsis: self.ellipsis,
382 hint_factor: renderer.scale_factor(),
383 };
384
385 if let Some(placeholder) = &self.placeholder {
386 let _ = state.placeholder.update(Text {
387 content: placeholder,
388 ..option_text
389 });
390 }
391
392 let max_width = match self.width {
393 Length::Shrink => {
394 state.options.resize_with(options.len(), Default::default);
395
396 for (option, paragraph) in options.iter().zip(state.options.iter_mut()) {
397 let label = (self.to_string)(option);
398
399 let _ = paragraph.update(Text {
400 content: &label,
401 ..option_text
402 });
403 }
404
405 let labels_width = state.options.iter().fold(0.0, |width, paragraph| {
406 f32::max(width, paragraph.min_width())
407 });
408
409 labels_width.max(
410 self.placeholder
411 .as_ref()
412 .map(|_| state.placeholder.min_width())
413 .unwrap_or(0.0),
414 )
415 }
416 _ => 0.0,
417 };
418
419 let size = {
420 let intrinsic = Size::new(
421 max_width + text_size.0 + self.padding.left,
422 f32::from(self.line_height.to_absolute(text_size)),
423 );
424
425 limits
426 .width(self.width)
427 .shrink(self.padding)
428 .resolve(self.width, Length::Shrink, intrinsic)
429 .expand(self.padding)
430 };
431
432 layout::Node::new(size)
433 }
434
435 fn operate(
436 &mut self,
437 tree: &mut Tree,
438 layout: Layout<'_>,
439 _renderer: &Renderer,
440 operation: &mut dyn Operation,
441 ) {
442 let state = tree.state.downcast_mut::<State<Renderer::Paragraph>>();
443 let selected_label = self.selected.as_ref().map(|v| (self.to_string)(v.borrow()));
444
445 operation.accessible(
446 None,
447 layout.bounds(),
448 &Accessible {
449 role: Role::ComboBox,
450 label: self.placeholder.as_deref(),
451 value: selected_label.as_deref().map(Value::Text),
452 expanded: Some(state.is_open),
453 disabled: self.on_select.is_none(),
454 has_popup: Some(HasPopup::Listbox),
455 ..Accessible::default()
456 },
457 );
458
459 if self.on_select.is_some() {
460 operation.focusable(None, layout.bounds(), state);
461 } else {
462 state.unfocus();
463 }
464 }
465
466 fn update(
467 &mut self,
468 tree: &mut Tree,
469 event: &Event,
470 layout: Layout<'_>,
471 cursor: mouse::Cursor,
472 _renderer: &Renderer,
473 shell: &mut Shell<'_, Message>,
474 _viewport: &Rectangle,
475 ) {
476 let state = tree.state.downcast_mut::<State<Renderer::Paragraph>>();
477
478 match event {
479 Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
480 | Event::Touch(touch::Event::FingerPressed { .. }) => {
481 if state.is_open {
482 state.is_open = false;
485
486 if let Some(on_close) = &self.on_close {
487 shell.publish(on_close.clone());
488 }
489
490 if !cursor.is_over(layout.bounds()) {
491 state.is_focused = false;
492 state.focus_visible = false;
493 }
494
495 shell.capture_event();
496 } else if cursor.is_over(layout.bounds()) {
497 if self.on_select.is_some() {
498 let selected = self.selected.as_ref().map(Borrow::borrow);
499
500 state.is_open = true;
501 state.is_focused = true;
502 state.focus_visible = false;
503 state.hovered_option = self
504 .options
505 .borrow()
506 .iter()
507 .position(|option| Some(option) == selected);
508
509 if let Some(on_open) = &self.on_open {
510 shell.publish(on_open.clone());
511 }
512
513 shell.capture_event();
514 }
515 } else {
516 state.is_focused = false;
517 state.focus_visible = false;
518 }
519 }
520 Event::Mouse(mouse::Event::WheelScrolled {
521 delta: mouse::ScrollDelta::Lines { y, .. },
522 }) => {
523 let Some(on_select) = &self.on_select else {
524 return;
525 };
526
527 if state.keyboard_modifiers.command()
528 && cursor.is_over(layout.bounds())
529 && !state.is_open
530 {
531 fn find_next<'a, T: PartialEq>(
532 selected: &'a T,
533 mut options: impl Iterator<Item = &'a T>,
534 ) -> Option<&'a T> {
535 let _ = options.find(|&option| option == selected);
536
537 options.next()
538 }
539
540 let options = self.options.borrow();
541 let selected = self.selected.as_ref().map(Borrow::borrow);
542
543 let next_option = if *y < 0.0 {
544 if let Some(selected) = selected {
545 find_next(selected, options.iter())
546 } else {
547 options.first()
548 }
549 } else if *y > 0.0 {
550 if let Some(selected) = selected {
551 find_next(selected, options.iter().rev())
552 } else {
553 options.last()
554 }
555 } else {
556 None
557 };
558
559 if let Some(next_option) = next_option {
560 shell.publish(on_select(next_option.clone()));
561 }
562
563 shell.capture_event();
564 }
565 }
566 Event::Keyboard(keyboard::Event::KeyPressed {
567 key: keyboard::Key::Named(named),
568 ..
569 }) => {
570 if self.on_select.is_some() && state.is_focused {
571 if state.is_open {
572 match named {
573 key::Named::ArrowDown => {
574 let options = self.options.borrow();
575 state.hovered_option = match state.hovered_option {
576 Some(i) if i + 1 < options.len() => Some(i + 1),
577 _ => Some(0),
578 };
579 shell.capture_event();
580 shell.request_redraw();
581 }
582 key::Named::ArrowUp => {
583 let options = self.options.borrow();
584 state.hovered_option = match state.hovered_option {
585 Some(0) | None => Some(options.len().saturating_sub(1)),
586 Some(i) => Some(i - 1),
587 };
588 shell.capture_event();
589 shell.request_redraw();
590 }
591 key::Named::Enter | key::Named::Space => {
592 if let Some(on_select) = &self.on_select
593 && let Some(index) = state.hovered_option
594 {
595 let options = self.options.borrow();
596 if let Some(option) = options.get(index) {
597 shell.publish(on_select(option.clone()));
598 }
599 }
600 state.is_open = false;
601 shell.capture_event();
602 shell.request_redraw();
603 }
604 key::Named::Escape => {
605 state.is_open = false;
606 shell.capture_event();
607 shell.request_redraw();
608 }
609 key::Named::Tab => {
610 state.is_open = false;
611 }
612 _ => {}
613 }
614 } else {
615 match named {
616 key::Named::Space
617 | key::Named::Enter
618 | key::Named::ArrowDown
619 | key::Named::ArrowUp => {
620 let selected = self.selected.as_ref().map(Borrow::borrow);
621
622 state.is_open = true;
623 state.hovered_option = self
624 .options
625 .borrow()
626 .iter()
627 .position(|option| Some(option) == selected);
628
629 if let Some(on_open) = &self.on_open {
630 shell.publish(on_open.clone());
631 }
632
633 shell.capture_event();
634 }
635 key::Named::Escape => {
636 state.is_focused = false;
637 state.focus_visible = false;
638 shell.capture_event();
639 }
640 _ => {}
641 }
642 }
643 }
644 }
645 Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => {
646 state.keyboard_modifiers = *modifiers;
647 }
648 _ => {}
649 };
650
651 let status = {
652 let is_hovered = cursor.is_over(layout.bounds());
653
654 if self.on_select.is_none() {
655 Status::Disabled
656 } else if state.is_open {
657 Status::Opened { is_hovered }
658 } else if state.focus_visible {
659 Status::Focused
660 } else if is_hovered {
661 Status::Hovered
662 } else {
663 Status::Active
664 }
665 };
666
667 if let Event::Window(window::Event::RedrawRequested(_now)) = event {
668 self.last_status = Some(status);
669 } else if self
670 .last_status
671 .is_some_and(|last_status| last_status != status)
672 {
673 shell.request_redraw();
674 }
675 }
676
677 fn mouse_interaction(
678 &self,
679 _tree: &Tree,
680 layout: Layout<'_>,
681 cursor: mouse::Cursor,
682 _viewport: &Rectangle,
683 _renderer: &Renderer,
684 ) -> mouse::Interaction {
685 let bounds = layout.bounds();
686 let is_mouse_over = cursor.is_over(bounds);
687
688 if is_mouse_over {
689 if self.on_select.is_some() {
690 mouse::Interaction::Pointer
691 } else {
692 mouse::Interaction::Idle
693 }
694 } else {
695 mouse::Interaction::default()
696 }
697 }
698
699 fn draw(
700 &self,
701 tree: &Tree,
702 renderer: &mut Renderer,
703 theme: &Theme,
704 _style: &renderer::Style,
705 layout: Layout<'_>,
706 _cursor: mouse::Cursor,
707 viewport: &Rectangle,
708 ) {
709 let font = self.font.unwrap_or_else(|| renderer.default_font());
710 let selected = self.selected.as_ref().map(Borrow::borrow);
711 let state = tree.state.downcast_ref::<State<Renderer::Paragraph>>();
712
713 let bounds = layout.bounds();
714
715 let style = Catalog::style(
716 theme,
717 &self.class,
718 self.last_status.unwrap_or(Status::Active),
719 );
720
721 renderer.fill_quad(
722 renderer::Quad {
723 bounds,
724 border: style.border,
725 shadow: style.shadow,
726 ..renderer::Quad::default()
727 },
728 style.background,
729 );
730
731 let handle = match &self.handle {
732 Handle::Arrow { size } => Some((
733 Renderer::ICON_FONT,
734 Renderer::ARROW_DOWN_ICON,
735 *size,
736 text::LineHeight::default(),
737 text::Shaping::Basic,
738 )),
739 Handle::Static(Icon {
740 font,
741 code_point,
742 size,
743 line_height,
744 shaping,
745 }) => Some((*font, *code_point, *size, *line_height, *shaping)),
746 Handle::Dynamic { open, closed } => {
747 if state.is_open {
748 Some((
749 open.font,
750 open.code_point,
751 open.size,
752 open.line_height,
753 open.shaping,
754 ))
755 } else {
756 Some((
757 closed.font,
758 closed.code_point,
759 closed.size,
760 closed.line_height,
761 closed.shaping,
762 ))
763 }
764 }
765 Handle::None => None,
766 };
767
768 if let Some((font, code_point, size, line_height, shaping)) = handle {
769 let size = size.unwrap_or_else(|| renderer.default_size());
770
771 renderer.fill_text(
772 Text {
773 content: code_point.to_string(),
774 size,
775 line_height,
776 font,
777 bounds: Size::new(bounds.width, f32::from(line_height.to_absolute(size))),
778 align_x: text::Alignment::Right,
779 align_y: alignment::Vertical::Center,
780 shaping,
781 wrapping: text::Wrapping::None,
782 ellipsis: text::Ellipsis::None,
783 hint_factor: None,
784 },
785 Point::new(
786 bounds.x + bounds.width - self.padding.right,
787 bounds.center_y(),
788 ),
789 style.handle_color,
790 *viewport,
791 );
792 }
793
794 let label = selected.map(&self.to_string);
795
796 if let Some(label) = label.or_else(|| self.placeholder.clone()) {
797 let text_size = self.text_size.unwrap_or_else(|| renderer.default_size());
798
799 renderer.fill_text(
800 Text {
801 content: label,
802 size: text_size,
803 line_height: self.line_height,
804 font,
805 bounds: Size::new(
806 bounds.width - self.padding.x(),
807 f32::from(self.line_height.to_absolute(text_size)),
808 ),
809 align_x: text::Alignment::Default,
810 align_y: alignment::Vertical::Center,
811 shaping: self.shaping,
812 wrapping: text::Wrapping::None,
813 ellipsis: self.ellipsis,
814 hint_factor: renderer.scale_factor(),
815 },
816 Point::new(bounds.x + self.padding.left, bounds.center_y()),
817 if selected.is_some() {
818 style.text_color
819 } else {
820 style.placeholder_color
821 },
822 *viewport,
823 );
824 }
825 }
826
827 fn overlay<'b>(
828 &'b mut self,
829 tree: &'b mut Tree,
830 layout: Layout<'_>,
831 renderer: &Renderer,
832 viewport: &Rectangle,
833 translation: Vector,
834 ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
835 let Some(on_select) = &self.on_select else {
836 return None;
837 };
838
839 let state = tree.state.downcast_mut::<State<Renderer::Paragraph>>();
840 let font = self.font.unwrap_or_else(|| renderer.default_font());
841
842 if state.is_open {
843 let bounds = layout.bounds();
844
845 let mut menu = Menu::new(
846 &mut state.menu,
847 self.options.borrow(),
848 &mut state.hovered_option,
849 &self.to_string,
850 |option| {
851 state.is_open = false;
852
853 (on_select)(option)
854 },
855 None,
856 &self.menu_class,
857 )
858 .width(bounds.width)
859 .padding(self.padding)
860 .font(font)
861 .ellipsis(self.ellipsis)
862 .shaping(self.shaping);
863
864 if let Some(text_size) = self.text_size {
865 menu = menu.text_size(text_size);
866 }
867
868 Some(menu.overlay(
869 layout.position() + translation,
870 *viewport,
871 bounds.height,
872 self.menu_height,
873 ))
874 } else {
875 None
876 }
877 }
878}
879
880impl<'a, T, L, V, Message, Theme, Renderer> From<PickList<'a, T, L, V, Message, Theme, Renderer>>
881 for Element<'a, Message, Theme, Renderer>
882where
883 T: Clone + PartialEq + 'a,
884 L: Borrow<[T]> + 'a,
885 V: Borrow<T> + 'a,
886 Message: Clone + 'a,
887 Theme: Catalog + 'a,
888 Renderer: text::Renderer + 'a,
889{
890 fn from(pick_list: PickList<'a, T, L, V, Message, Theme, Renderer>) -> Self {
891 Self::new(pick_list)
892 }
893}
894
895#[derive(Debug)]
896struct State<P: text::Paragraph> {
897 menu: menu::State,
898 keyboard_modifiers: keyboard::Modifiers,
899 is_open: bool,
900 is_focused: bool,
901 focus_visible: bool,
902 hovered_option: Option<usize>,
903 options: Vec<paragraph::Plain<P>>,
904 placeholder: paragraph::Plain<P>,
905}
906
907impl<P: text::Paragraph> State<P> {
908 fn new() -> Self {
910 Self {
911 menu: menu::State::default(),
912 keyboard_modifiers: keyboard::Modifiers::default(),
913 is_open: bool::default(),
914 is_focused: bool::default(),
915 focus_visible: bool::default(),
916 hovered_option: Option::default(),
917 options: Vec::new(),
918 placeholder: paragraph::Plain::default(),
919 }
920 }
921}
922
923impl<P: text::Paragraph> Default for State<P> {
924 fn default() -> Self {
925 Self::new()
926 }
927}
928
929impl<P: text::Paragraph> focusable::Focusable for State<P> {
930 fn is_focused(&self) -> bool {
931 self.is_focused
932 }
933
934 fn focus(&mut self) {
935 self.is_focused = true;
936 self.focus_visible = true;
937 }
938
939 fn unfocus(&mut self) {
940 self.is_focused = false;
941 self.focus_visible = false;
942 }
943}
944
945#[derive(Debug, Clone, PartialEq)]
947pub enum Handle<Font> {
948 Arrow {
952 size: Option<Pixels>,
954 },
955 Static(Icon<Font>),
957 Dynamic {
959 closed: Icon<Font>,
961 open: Icon<Font>,
963 },
964 None,
966}
967
968impl<Font> Default for Handle<Font> {
969 fn default() -> Self {
970 Self::Arrow { size: None }
971 }
972}
973
974#[derive(Debug, Clone, PartialEq)]
976pub struct Icon<Font> {
977 pub font: Font,
979 pub code_point: char,
981 pub size: Option<Pixels>,
983 pub line_height: text::LineHeight,
985 pub shaping: text::Shaping,
987}
988
989#[derive(Debug, Clone, Copy, PartialEq, Eq)]
991pub enum Status {
992 Active,
994 Hovered,
996 Opened {
998 is_hovered: bool,
1000 },
1001 Focused,
1003 Disabled,
1005}
1006
1007#[derive(Debug, Clone, Copy, PartialEq)]
1009pub struct Style {
1010 pub text_color: Color,
1012 pub placeholder_color: Color,
1014 pub handle_color: Color,
1016 pub background: Background,
1018 pub border: Border,
1020 pub shadow: Shadow,
1022}
1023
1024pub trait Catalog: menu::Catalog {
1026 type Class<'a>;
1028
1029 fn default<'a>() -> <Self as Catalog>::Class<'a>;
1031
1032 fn default_menu<'a>() -> <Self as menu::Catalog>::Class<'a> {
1034 <Self as menu::Catalog>::default()
1035 }
1036
1037 fn style(&self, class: &<Self as Catalog>::Class<'_>, status: Status) -> Style;
1039}
1040
1041pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Style + 'a>;
1045
1046impl Catalog for Theme {
1047 type Class<'a> = StyleFn<'a, Self>;
1048
1049 fn default<'a>() -> StyleFn<'a, Self> {
1050 Box::new(default)
1051 }
1052
1053 fn style(&self, class: &StyleFn<'_, Self>, status: Status) -> Style {
1054 class(self, status)
1055 }
1056}
1057
1058pub fn default(theme: &Theme, status: Status) -> Style {
1060 let palette = theme.palette();
1061
1062 let active = Style {
1063 text_color: palette.background.weak.text,
1064 background: palette.background.weak.color.into(),
1065 placeholder_color: palette.secondary.base.color,
1066 handle_color: palette.background.weak.text,
1067 border: Border {
1068 radius: 2.0.into(),
1069 width: 1.0,
1070 color: palette.background.strong.color,
1071 },
1072 shadow: Shadow::default(),
1073 };
1074
1075 match status {
1076 Status::Active => active,
1077 Status::Hovered | Status::Opened { .. } => Style {
1078 border: Border {
1079 color: palette.primary.strong.color,
1080 ..active.border
1081 },
1082 ..active
1083 },
1084 Status::Focused => {
1085 let page_bg = palette.background.base.color;
1086 Style {
1087 border: Border {
1088 color: palette::focus_border_color(
1089 palette.background.weak.color,
1090 palette.primary.strong.color,
1091 page_bg,
1092 ),
1093 width: 2.0,
1094 ..active.border
1095 },
1096 shadow: palette::focus_shadow_subtle(palette.primary.strong.color, page_bg),
1097 ..active
1098 }
1099 }
1100 Status::Disabled => Style {
1101 text_color: palette.background.strongest.color,
1102 background: palette.background.weaker.color.into(),
1103 placeholder_color: palette.background.strongest.color,
1104 handle_color: palette.background.strongest.color,
1105 border: Border {
1106 color: palette.background.weak.color,
1107 ..active.border
1108 },
1109 shadow: Shadow::default(),
1110 },
1111 }
1112}