1#![allow(clippy::too_many_arguments)]
2
3use iced_native::alignment;
5use iced_native::event::{self, Event};
6use iced_native::keyboard;
7use iced_native::layout;
8use iced_native::mouse;
9use iced_native::overlay;
10use iced_native::overlay::menu::{self, Menu};
11use iced_native::renderer;
12use iced_native::text::{self, Text};
13use iced_native::touch;
14use iced_native::widget::text_input::{self, Id, Value};
15use iced_native::widget::{container, operation, scrollable, tree, Tree};
16use iced_native::{
17 Clipboard, Element, Layout, Length, Padding, Point, Rectangle, Shell, Size, Widget,
18};
19use std::borrow::Cow;
20
21pub use iced_style::pick_list::StyleSheet;
22
23#[allow(missing_debug_implementations)]
25pub struct PickList<'a, T: 'static, Message, Renderer: text::Renderer>
26where
27 [T]: ToOwned<Owned = Vec<T>>,
28 Message: Clone,
29 Renderer::Theme: StyleSheet
30 + scrollable::StyleSheet
31 + menu::StyleSheet
32 + container::StyleSheet
33 + text_input::StyleSheet,
34 <Renderer::Theme as menu::StyleSheet>::Style: From<<Renderer::Theme as StyleSheet>::Style>,
35{
36 id: Option<Id>,
37 on_selected: Box<dyn Fn(T) -> Message>,
38 options: Cow<'a, [T]>,
39 placeholder: Option<String>,
40 selected: Option<T>,
41 width: Length,
42 padding: Padding,
43 text_size: Option<u16>,
44 font: Renderer::Font,
45 style_sheet: <Renderer::Theme as StyleSheet>::Style,
46 text_style_sheet: <Renderer::Theme as text_input::StyleSheet>::Style,
47 value: text_input::Value,
48 on_change: Box<dyn Fn(String) -> Message>,
49 on_submit: Option<Message>,
50 on_paste: Option<Box<dyn Fn(String) -> Message>>,
51 on_focus: Option<Message>,
52}
53
54#[derive(Debug)]
56pub struct State<T> {
57 menu: menu::State,
58 keyboard_modifiers: keyboard::Modifiers,
59 is_open: bool,
60 hovered_option: Option<usize>,
61 last_selection: Option<T>,
62 text_input: text_input::State,
63}
64
65impl<T> State<T> {
66 pub fn new() -> Self {
68 Self {
69 menu: menu::State::default(),
70 keyboard_modifiers: keyboard::Modifiers::default(),
71 is_open: bool::default(),
72 hovered_option: Option::default(),
73 last_selection: Option::default(),
74 text_input: text_input::State::default(),
75 }
76 }
77
78 pub fn focus(&mut self) {
80 self.text_input.focus();
81 }
82
83 pub fn unfocus(&mut self) {
85 self.text_input.unfocus();
86 self.is_open = false;
87 }
88
89 pub fn pick(&mut self, element: T) {
91 self.is_open = false;
92 self.last_selection = Some(element);
93
94 self.unfocus();
95 }
96}
97
98impl<T> operation::Focusable for State<T> {
99 fn is_focused(&self) -> bool {
100 self.text_input.is_focused()
101 }
102
103 fn focus(&mut self) {
104 State::focus(self)
105 }
106
107 fn unfocus(&mut self) {
108 State::unfocus(self)
109 }
110}
111
112impl<T> Default for State<T> {
113 fn default() -> Self {
114 Self::new()
115 }
116}
117
118impl<'a, T: 'a, Message, Renderer: text::Renderer> PickList<'a, T, Message, Renderer>
119where
120 T: ToString + Eq,
121 [T]: ToOwned<Owned = Vec<T>>,
122 Message: Clone,
123 Renderer::Theme: StyleSheet
124 + scrollable::StyleSheet
125 + menu::StyleSheet
126 + container::StyleSheet
127 + text_input::StyleSheet,
128 <Renderer::Theme as menu::StyleSheet>::Style: From<<Renderer::Theme as StyleSheet>::Style>,
129{
130 pub const DEFAULT_PADDING: Padding = Padding::new(5);
132
133 pub fn new(
137 options: impl Into<Cow<'a, [T]>>,
138 selected: Option<T>,
139 on_selected: impl Fn(T) -> Message + 'static,
140 on_change: impl Fn(String) -> Message + 'static,
141 value: &str,
142 ) -> Self {
143 Self {
144 id: None,
145 on_selected: Box::new(on_selected),
146 options: options.into(),
147 placeholder: None,
148 selected,
149 width: Length::Shrink,
150 text_size: None,
151 padding: Self::DEFAULT_PADDING,
152 font: Default::default(),
153 style_sheet: Default::default(),
154 text_style_sheet: Default::default(),
155 value: Value::new(value),
156 on_change: Box::new(on_change),
157 on_submit: None,
158 on_paste: None,
159 on_focus: None,
160 }
161 }
162
163 pub fn id(mut self, id: Id) -> Self {
165 self.id = Some(id);
166 self
167 }
168
169 pub fn placeholder(mut self, placeholder: impl Into<String>) -> Self {
171 self.placeholder = Some(placeholder.into());
172 self
173 }
174
175 pub fn on_submit(mut self, on_submit: Message) -> Self {
177 self.on_submit = Some(on_submit);
178 self
179 }
180
181 pub fn on_focus(mut self, on_focus: Message) -> Self {
183 self.on_focus = Some(on_focus);
184 self
185 }
186
187 pub fn width(mut self, width: Length) -> Self {
189 self.width = width;
190 self
191 }
192
193 pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self {
195 self.padding = padding.into();
196 self
197 }
198
199 pub fn text_size(mut self, size: u16) -> Self {
201 self.text_size = Some(size);
202 self
203 }
204
205 pub fn font(mut self, font: Renderer::Font) -> Self {
207 self.font = font;
208 self
209 }
210
211 pub fn style(mut self, style: impl Into<<Renderer::Theme as StyleSheet>::Style>) -> Self {
213 self.style_sheet = style.into();
214 self
215 }
216
217 pub fn text_style(
219 mut self,
220 style: impl Into<<Renderer::Theme as text_input::StyleSheet>::Style>,
221 ) -> Self {
222 self.text_style_sheet = style.into();
223 self
224 }
225}
226
227pub fn layout<Renderer, T>(
229 renderer: &Renderer,
230 limits: &layout::Limits,
231 width: Length,
232 padding: Padding,
233 text_size: Option<u16>,
234 font: &Renderer::Font,
235 placeholder: Option<&str>,
236 options: &[T],
237) -> layout::Node
238where
239 Renderer: text::Renderer,
240 T: ToString,
241 Renderer::Theme: StyleSheet
242 + scrollable::StyleSheet
243 + menu::StyleSheet
244 + container::StyleSheet
245 + text_input::StyleSheet,
246 <Renderer::Theme as menu::StyleSheet>::Style: From<<Renderer::Theme as StyleSheet>::Style>,
247{
248 use std::f32;
249
250 let limits = limits.width(width).height(Length::Shrink).pad(padding);
251
252 let text_size = text_size.unwrap_or_else(|| renderer.default_size());
253
254 let max_width = match width {
255 Length::Shrink => {
256 let measure = |label: &str| -> u32 {
257 let (width, _) = renderer.measure(
258 label,
259 text_size,
260 font.clone(),
261 Size::new(f32::INFINITY, f32::INFINITY),
262 );
263
264 width.round() as u32
265 };
266
267 let labels = options.iter().map(ToString::to_string);
268
269 let labels_width = labels.map(|label| measure(&label)).max().unwrap_or(100);
270
271 let placeholder_width = placeholder.map(measure).unwrap_or(100);
272
273 labels_width.max(placeholder_width)
274 }
275 _ => 0,
276 };
277
278 let size = {
279 let intrinsic = Size::new(
280 max_width as f32 + f32::from(text_size) + f32::from(padding.left),
281 f32::from(text_size),
282 );
283
284 limits.resolve(intrinsic).pad(padding)
285 };
286
287 let mut text = layout::Node::new(limits.resolve(size));
288 text.move_to(Point::new(padding.left.into(), 0.));
289
290 layout::Node::with_children(size, vec![text])
291}
292
293pub fn update<'a, T, Message, Renderer>(
296 event: Event,
297 layout: Layout<'_>,
298 cursor_position: Point,
299 shell: &mut Shell<'_, Message>,
300 on_selected: &dyn Fn(T) -> Message,
301 selected: Option<&T>,
302 options: &[T],
303 state: impl FnOnce() -> &'a mut State<T>,
304 renderer: &Renderer,
305 clipboard: &mut dyn Clipboard,
306 value: &mut Value,
307 size: Option<u16>,
308 font: &Renderer::Font,
309 on_change: &dyn Fn(String) -> Message,
310 on_paste: Option<&dyn Fn(String) -> Message>,
311 on_submit: &Option<Message>,
312 on_focus: &Option<Message>,
313) -> event::Status
314where
315 T: PartialEq + Clone + 'a,
316 Message: Clone,
317 Renderer: text::Renderer,
318 Renderer::Theme: StyleSheet
319 + scrollable::StyleSheet
320 + menu::StyleSheet
321 + container::StyleSheet
322 + text_input::StyleSheet,
323 <Renderer::Theme as menu::StyleSheet>::Style: From<<Renderer::Theme as StyleSheet>::Style>,
324{
325 let state = state();
326 let mut propagate_event = |state: &mut text_input::State| {
327 text_input::update(
328 event.clone(),
329 layout,
330 cursor_position,
331 renderer,
332 clipboard,
333 shell,
334 value,
335 size,
336 font,
337 false,
338 on_change,
339 on_paste,
340 on_submit,
341 || state,
342 )
343 };
344
345 match event.clone() {
346 Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
347 | Event::Touch(touch::Event::FingerPressed { .. }) => {
348 let event_status = if state.is_open {
349 if layout.bounds().contains(cursor_position) {
351 if cursor_position.x >= 0.0 && cursor_position.y >= 0.0 {
352 state.unfocus();
353 } else {
354 propagate_event(&mut state.text_input);
355 }
356 }
357
358 event::Status::Captured
359 } else if layout.bounds().contains(cursor_position) {
360 state.is_open = true;
361 state.hovered_option = options.iter().position(|option| Some(option) == selected);
362 state.focus();
363 state.text_input.move_cursor_to_end();
364 propagate_event(&mut state.text_input);
365 if let Some(message) = on_focus.as_ref() {
366 shell.publish(message.clone())
367 }
368
369 event::Status::Captured
370 } else {
371 event::Status::Ignored
372 };
373
374 if let Some(last_selection) = state.last_selection.take() {
375 shell.publish((on_selected)(last_selection));
376
377 state.is_open = false;
378 state.unfocus();
379
380 event::Status::Captured
381 } else {
382 event_status
383 }
384 }
385 Event::Mouse(mouse::Event::WheelScrolled {
386 delta: mouse::ScrollDelta::Lines { y, .. },
387 }) => {
388 if state.keyboard_modifiers.command()
389 && layout.bounds().contains(cursor_position)
390 && !state.is_open
391 {
392 fn find_next<'a, T: PartialEq>(
393 selected: &'a T,
394 mut options: impl Iterator<Item = &'a T>,
395 ) -> Option<&'a T> {
396 let _ = options.find(|&option| option == selected);
397
398 options.next()
399 }
400
401 let next_option = if y < 0.0 {
402 if let Some(selected) = selected {
403 find_next(selected, options.iter())
404 } else {
405 options.first()
406 }
407 } else if y > 0.0 {
408 if let Some(selected) = selected {
409 find_next(selected, options.iter().rev())
410 } else {
411 options.last()
412 }
413 } else {
414 None
415 };
416
417 if let Some(next_option) = next_option {
418 shell.publish((on_selected)(next_option.clone()));
419 }
420
421 event::Status::Captured
422 } else {
423 event::Status::Ignored
424 }
425 }
426 Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => {
427 state.keyboard_modifiers = modifiers;
428 propagate_event(&mut state.text_input);
429
430 event::Status::Ignored
431 }
432 _ => propagate_event(&mut state.text_input),
433 }
434}
435
436pub fn mouse_interaction(layout: Layout<'_>, cursor_position: Point) -> mouse::Interaction {
438 let text_bounds = layout.children().next().unwrap().bounds();
439 let bounds = layout.bounds();
440 let is_mouse_over_text = text_bounds.contains(cursor_position);
441 let is_mouse_over = bounds.contains(cursor_position);
442
443 if is_mouse_over_text {
444 mouse::Interaction::Text
445 } else if is_mouse_over {
446 mouse::Interaction::Pointer
447 } else {
448 mouse::Interaction::default()
449 }
450}
451
452pub fn overlay<'a, T, Message, Renderer>(
454 layout: Layout<'_>,
455 state: &'a mut State<T>,
456 padding: Padding,
457 text_size: Option<u16>,
458 font: Renderer::Font,
459 options: &'a [T],
460 style_sheet: <Renderer::Theme as StyleSheet>::Style,
461) -> Option<overlay::Element<'a, Message, Renderer>>
462where
463 Message: 'a,
464 Renderer: text::Renderer + 'a,
465 T: Clone + ToString,
466 Renderer::Theme: StyleSheet
467 + scrollable::StyleSheet
468 + menu::StyleSheet
469 + container::StyleSheet
470 + text_input::StyleSheet,
471 <Renderer::Theme as menu::StyleSheet>::Style: From<<Renderer::Theme as StyleSheet>::Style>,
472{
473 if state.is_open {
474 let bounds = layout.bounds();
475
476 let mut menu = Menu::new(
477 &mut state.menu,
478 options,
479 &mut state.hovered_option,
480 &mut state.last_selection,
481 )
482 .width(bounds.width.round() as u16)
483 .padding(padding)
484 .font(font)
485 .style(style_sheet);
486
487 if let Some(text_size) = text_size {
488 menu = menu.text_size(text_size);
489 }
490
491 Some(menu.overlay(layout.position(), bounds.height))
492 } else {
493 None
494 }
495}
496
497pub fn draw<T, Renderer>(
499 renderer: &mut Renderer,
500 layout: Layout<'_>,
501 cursor_position: Point,
502 state: &State<T>,
503 value: &Value,
504 padding: Padding,
505 text_size: Option<u16>,
506 font: &Renderer::Font,
507 placeholder: Option<&str>,
508 selected: Option<&T>,
509 style_sheet: &<Renderer::Theme as StyleSheet>::Style,
510 text_style_sheet: &<Renderer::Theme as text_input::StyleSheet>::Style,
511 theme: &Renderer::Theme,
512) where
513 Renderer: text::Renderer,
514 T: ToString,
515 Renderer::Theme: StyleSheet
516 + scrollable::StyleSheet
517 + menu::StyleSheet
518 + container::StyleSheet
519 + text_input::StyleSheet,
520 <Renderer::Theme as menu::StyleSheet>::Style: From<<Renderer::Theme as StyleSheet>::Style>,
521{
522 let bounds = layout.bounds();
523 let is_mouse_over = bounds.contains(cursor_position);
524 let is_selected = selected.is_some();
525
526 let style = if is_mouse_over {
527 theme.hovered(style_sheet)
528 } else {
529 theme.active(style_sheet)
530 };
531
532 renderer.fill_quad(
533 renderer::Quad {
534 bounds,
535 border_color: style.border_color,
536 border_width: style.border_width,
537 border_radius: style.border_radius,
538 },
539 style.background,
540 );
541
542 renderer.fill_text(Text {
543 content: &Renderer::ARROW_DOWN_ICON.to_string(),
544 font: Renderer::ICON_FONT,
545 size: bounds.height * style.icon_size,
546 bounds: Rectangle {
547 x: bounds.x + bounds.width - f32::from(padding.horizontal()),
548 y: bounds.center_y(),
549 ..bounds
550 },
551 color: style.text_color,
552 horizontal_alignment: alignment::Horizontal::Right,
553 vertical_alignment: alignment::Vertical::Center,
554 });
555
556 let label = selected.map(ToString::to_string);
557
558 if state.text_input.is_focused() {
559 text_input::draw(
560 renderer,
561 theme,
562 layout,
563 cursor_position,
564 &state.text_input,
565 value,
566 placeholder.unwrap_or_default(),
567 text_size,
568 font,
569 false,
570 text_style_sheet,
571 );
572 } else if let Some(label) = label.as_deref().or(placeholder) {
573 let text_size = f32::from(text_size.unwrap_or_else(|| renderer.default_size()));
574
575 renderer.fill_text(Text {
576 content: label,
577 size: text_size,
578 font: font.clone(),
579 color: if is_selected {
580 style.text_color
581 } else {
582 style.placeholder_color
583 },
584 bounds: Rectangle {
585 x: bounds.x + f32::from(padding.left),
586 y: bounds.center_y() - text_size / 2.0,
587 width: bounds.width - f32::from(padding.horizontal()),
588 height: text_size,
589 },
590 horizontal_alignment: alignment::Horizontal::Left,
591 vertical_alignment: alignment::Vertical::Top,
592 });
593 }
594}
595
596impl<'a, T: 'static, Message, Renderer> Widget<Message, Renderer>
597 for PickList<'a, T, Message, Renderer>
598where
599 T: Clone + ToString + Eq,
600 [T]: ToOwned<Owned = Vec<T>>,
601 Message: 'static + Clone,
602 Renderer: text::Renderer + 'a,
603 Renderer::Theme: StyleSheet
604 + scrollable::StyleSheet
605 + menu::StyleSheet
606 + container::StyleSheet
607 + text_input::StyleSheet,
608 <Renderer::Theme as menu::StyleSheet>::Style: From<<Renderer::Theme as StyleSheet>::Style>,
609{
610 fn tag(&self) -> tree::Tag {
611 tree::Tag::of::<State<T>>()
612 }
613
614 fn state(&self) -> tree::State {
615 tree::State::new(State::<T>::new())
616 }
617
618 fn width(&self) -> Length {
619 self.width
620 }
621
622 fn height(&self) -> Length {
623 Length::Shrink
624 }
625
626 fn layout(&self, renderer: &Renderer, limits: &layout::Limits) -> layout::Node {
627 layout(
628 renderer,
629 limits,
630 self.width,
631 self.padding,
632 self.text_size,
633 &self.font,
634 self.placeholder.as_deref(),
635 &self.options,
636 )
637 }
638
639 fn on_event(
640 &mut self,
641 tree: &mut Tree,
642 event: Event,
643 layout: Layout<'_>,
644 cursor_position: Point,
645 renderer: &Renderer,
646 clipboard: &mut dyn Clipboard,
647 shell: &mut Shell<'_, Message>,
648 ) -> event::Status {
649 update(
650 event,
651 layout,
652 cursor_position,
653 shell,
654 self.on_selected.as_ref(),
655 self.selected.as_ref(),
656 &self.options,
657 || tree.state.downcast_mut::<State<T>>(),
658 renderer,
659 clipboard,
660 &mut self.value,
661 self.text_size,
662 &self.font,
663 &self.on_change,
664 self.on_paste.as_deref(),
665 &self.on_submit,
666 &self.on_focus,
667 )
668 }
669
670 fn mouse_interaction(
671 &self,
672 _state: &Tree,
673 layout: Layout<'_>,
674 cursor_position: Point,
675 _viewport: &Rectangle,
676 _renderer: &Renderer,
677 ) -> mouse::Interaction {
678 mouse_interaction(layout, cursor_position)
679 }
680
681 fn draw(
682 &self,
683 tree: &Tree,
684 renderer: &mut Renderer,
685 theme: &Renderer::Theme,
686 _style: &renderer::Style,
687 layout: Layout<'_>,
688 cursor_position: Point,
689 _viewport: &Rectangle,
690 ) {
691 draw(
692 renderer,
693 layout,
694 cursor_position,
695 tree.state.downcast_ref::<State<T>>(),
696 &self.value,
697 self.padding,
698 self.text_size,
699 &self.font,
700 self.placeholder.as_deref(),
701 self.selected.as_ref(),
702 &self.style_sheet,
703 &self.text_style_sheet,
704 theme,
705 )
706 }
707
708 fn overlay<'b>(
709 &'b self,
710 tree: &'b mut Tree,
711 layout: Layout<'_>,
712 _renderer: &Renderer,
713 ) -> Option<overlay::Element<'_, Message, Renderer>> {
714 let state = tree.state.downcast_mut::<State<T>>();
715 overlay(
716 layout,
717 state,
718 self.padding,
719 self.text_size,
720 self.font.clone(),
721 &self.options,
722 self.style_sheet.clone(),
723 )
724 }
725}
726
727impl<'a, T: 'static, Message, Renderer> From<PickList<'a, T, Message, Renderer>>
728 for Element<'a, Message, Renderer>
729where
730 T: Clone + ToString + Eq,
731 [T]: ToOwned<Owned = Vec<T>>,
732 Renderer: text::Renderer + 'a,
733 Message: 'static + Clone,
734 Renderer::Theme: StyleSheet
735 + scrollable::StyleSheet
736 + menu::StyleSheet
737 + container::StyleSheet
738 + text_input::StyleSheet,
739 <Renderer::Theme as menu::StyleSheet>::Style: From<<Renderer::Theme as StyleSheet>::Style>,
740{
741 fn from(val: PickList<'a, T, Message, Renderer>) -> Self {
742 Element::new(val)
743 }
744}