1use iced::advanced::{Clipboard, Layout, Shell, Widget, layout, renderer, text, widget};
58use iced::keyboard;
59use iced::keyboard::key;
60use iced::mouse;
61use iced::overlay;
62use iced::overlay::menu;
63use iced::time::Instant;
64use iced::{Element, Event, Length, Padding, Pixels, Rectangle, Size, Theme, Vector};
65
66use iced::advanced::text::LineHeight;
67use iced::widget::{TextInput, text_input};
68use std::cell::RefCell;
69use std::fmt::Display;
70
71pub struct SuggestionTextInput<'a, T, Message, Theme = iced::Theme, Renderer = iced::Renderer>
128where
129 Theme: Catalog,
130 Renderer: text::Renderer,
131{
132 state: &'a State<T>,
133 text_input: TextInput<'a, TextInputEvent, Theme, Renderer>,
134 font: Option<Renderer::Font>,
135 selection: text_input::Value,
136 on_selected: Box<dyn Fn(T) -> Message>,
137 on_option_hovered: Option<Box<dyn Fn(T) -> Message>>,
138 on_open: Option<Message>,
139 on_close: Option<Message>,
140 on_input: Option<Box<dyn Fn(String) -> Message>>,
141 padding: Padding,
142 size: Option<f32>,
143 text_shaping: text::Shaping,
144 menu_class: <Theme as menu::Catalog>::Class<'a>,
145 menu_height: Length,
146}
147
148impl<'a, T, Message, Theme, Renderer> SuggestionTextInput<'a, T, Message, Theme, Renderer>
149where
150 T: Display + Clone,
151 Theme: Catalog,
152 Renderer: text::Renderer,
153{
154 pub fn new(
158 state: &'a State<T>,
159 placeholder: &str,
160 selection: Option<&T>,
161 on_selected: impl Fn(T) -> Message + 'static,
162 ) -> Self {
163 let text_input = TextInput::new(placeholder, &state.value())
164 .on_input(TextInputEvent::TextChanged)
165 .class(Theme::default_input());
166
167 let selection = selection.map(T::to_string).unwrap_or_default();
168
169 Self {
170 state,
171 text_input,
172 font: None,
173 selection: text_input::Value::new(&selection),
174 on_selected: Box::new(on_selected),
175 on_option_hovered: None,
176 on_input: None,
177 on_open: None,
178 on_close: None,
179 padding: text_input::DEFAULT_PADDING,
180 size: None,
181 text_shaping: text::Shaping::default(),
182 menu_class: <Theme as Catalog>::default_menu(),
183 menu_height: Length::Shrink,
184 }
185 }
186
187 pub fn on_input(mut self, on_input: impl Fn(String) -> Message + 'static) -> Self {
190 self.on_input = Some(Box::new(on_input));
191 self
192 }
193
194 pub fn on_option_hovered(mut self, on_option_hovered: impl Fn(T) -> Message + 'static) -> Self {
197 self.on_option_hovered = Some(Box::new(on_option_hovered));
198 self
199 }
200
201 pub fn on_open(mut self, message: Message) -> Self {
204 self.on_open = Some(message);
205 self
206 }
207
208 pub fn on_close(mut self, message: Message) -> Self {
211 self.on_close = Some(message);
212 self
213 }
214
215 pub fn padding(mut self, padding: impl Into<Padding>) -> Self {
217 self.padding = padding.into();
218 self.text_input = self.text_input.padding(self.padding);
219 self
220 }
221
222 pub fn font(mut self, font: Renderer::Font) -> Self {
226 self.text_input = self.text_input.font(font);
227 self.font = Some(font);
228 self
229 }
230
231 pub fn icon(mut self, icon: text_input::Icon<Renderer::Font>) -> Self {
233 self.text_input = self.text_input.icon(icon);
234 self
235 }
236
237 pub fn size(mut self, size: impl Into<Pixels>) -> Self {
239 let size = size.into();
240
241 self.text_input = self.text_input.size(size);
242 self.size = Some(size.0);
243
244 self
245 }
246
247 pub fn line_height(self, line_height: impl Into<LineHeight>) -> Self {
249 Self {
250 text_input: self.text_input.line_height(line_height),
251 ..self
252 }
253 }
254
255 pub fn width(self, width: impl Into<Length>) -> Self {
257 Self {
258 text_input: self.text_input.width(width),
259 ..self
260 }
261 }
262
263 pub fn menu_height(mut self, menu_height: impl Into<Length>) -> Self {
265 self.menu_height = menu_height.into();
266 self
267 }
268
269 pub fn text_shaping(mut self, shaping: text::Shaping) -> Self {
271 self.text_shaping = shaping;
272 self
273 }
274
275 #[must_use]
277 pub fn input_style(
278 mut self,
279 style: impl Fn(&Theme, text_input::Status) -> text_input::Style + 'a,
280 ) -> Self
281 where
282 <Theme as text_input::Catalog>::Class<'a>: From<text_input::StyleFn<'a, Theme>>,
283 {
284 self.text_input = self.text_input.style(style);
285 self
286 }
287
288 #[must_use]
290 pub fn menu_style(mut self, style: impl Fn(&Theme) -> menu::Style + 'a) -> Self
291 where
292 <Theme as menu::Catalog>::Class<'a>: From<menu::StyleFn<'a, Theme>>,
293 {
294 self.menu_class = (Box::new(style) as menu::StyleFn<'a, Theme>).into();
295 self
296 }
297
298 #[must_use]
300 pub fn input_class(
301 mut self,
302 class: impl Into<<Theme as text_input::Catalog>::Class<'a>>,
303 ) -> Self {
304 self.text_input = self.text_input.class(class);
305 self
306 }
307
308 #[must_use]
310 pub fn menu_class(mut self, class: impl Into<<Theme as menu::Catalog>::Class<'a>>) -> Self {
311 self.menu_class = class.into();
312 self
313 }
314}
315
316#[derive(Debug, Clone)]
318pub struct State<T> {
319 options: Vec<T>,
320 inner: RefCell<Inner<T>>,
321}
322
323#[derive(Debug, Clone)]
324struct Inner<T> {
325 value: String,
326 option_matchers: Vec<String>,
327 filtered_options: Filtered<T>,
328}
329
330#[derive(Debug, Clone)]
331struct Filtered<T> {
332 options: Vec<T>,
333 updated: Instant,
334}
335
336impl<T> State<T>
337where
338 T: Display + Clone,
339{
340 pub fn new(options: Vec<T>) -> Self {
342 Self::with_selection(options, None)
343 }
344
345 pub fn with_selection(options: Vec<T>, selection: Option<&T>) -> Self {
348 let value = selection.map(T::to_string).unwrap_or_default();
349
350 let option_matchers = build_matchers(&options);
352
353 let filtered_options = Filtered::new(
354 search(&options, &option_matchers, &value)
355 .cloned()
356 .collect(),
357 );
358
359 Self {
360 options,
361 inner: RefCell::new(Inner {
362 value,
363 option_matchers,
364 filtered_options,
365 }),
366 }
367 }
368
369 pub fn options(&self) -> &[T] {
374 &self.options
375 }
376
377 pub fn push(&mut self, new_option: T) {
379 let mut inner = self.inner.borrow_mut();
380
381 inner.option_matchers.push(build_matcher(&new_option));
382 self.options.push(new_option);
383
384 inner.filtered_options = Filtered::new(
385 search(&self.options, &inner.option_matchers, &inner.value)
386 .cloned()
387 .collect(),
388 );
389 }
390
391 pub fn into_options(self) -> Vec<T> {
393 self.options
394 }
395
396 fn value(&self) -> String {
397 let inner = self.inner.borrow();
398
399 inner.value.clone()
400 }
401
402 fn with_inner<O>(&self, f: impl FnOnce(&Inner<T>) -> O) -> O {
403 let inner = self.inner.borrow();
404
405 f(&inner)
406 }
407
408 fn with_inner_mut(&self, f: impl FnOnce(&mut Inner<T>)) {
409 let mut inner = self.inner.borrow_mut();
410
411 f(&mut inner);
412 }
413
414 fn sync_filtered_options(&self, options: &mut Filtered<T>) {
415 let inner = self.inner.borrow();
416
417 inner.filtered_options.sync(options);
418 }
419}
420
421impl<T> Default for State<T>
422where
423 T: Display + Clone,
424{
425 fn default() -> Self {
426 Self::new(Vec::new())
427 }
428}
429
430impl<T> Filtered<T>
431where
432 T: Clone,
433{
434 fn new(options: Vec<T>) -> Self {
435 Self {
436 options,
437 updated: Instant::now(),
438 }
439 }
440
441 fn empty() -> Self {
442 Self {
443 options: vec![],
444 updated: Instant::now(),
445 }
446 }
447
448 fn update(&mut self, options: Vec<T>) {
449 self.options = options;
450 self.updated = Instant::now();
451 }
452
453 fn sync(&self, other: &mut Filtered<T>) {
454 if other.updated != self.updated {
455 *other = self.clone();
456 }
457 }
458}
459
460struct Menu<T> {
461 menu: menu::State,
462 hovered_option: Option<usize>,
463 new_selection: Option<T>,
464 filtered_options: Filtered<T>,
465}
466
467#[derive(Debug, Clone)]
468enum TextInputEvent {
469 TextChanged(String),
470}
471
472impl<T, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
473 for SuggestionTextInput<'_, T, Message, Theme, Renderer>
474where
475 T: Display + Clone + 'static,
476 Message: Clone,
477 Theme: Catalog,
478 Renderer: text::Renderer,
479{
480 fn size(&self) -> Size<Length> {
481 Widget::<TextInputEvent, Theme, Renderer>::size(&self.text_input)
482 }
483
484 fn layout(
485 &mut self,
486 tree: &mut widget::Tree,
487 renderer: &Renderer,
488 limits: &layout::Limits,
489 ) -> layout::Node {
490 let is_focused = {
491 let text_input_state = tree.children[0]
492 .state
493 .downcast_ref::<text_input::State<Renderer::Paragraph>>();
494
495 text_input_state.is_focused()
496 };
497
498 self.text_input.layout(
499 &mut tree.children[0],
500 renderer,
501 limits,
502 (!is_focused).then_some(&self.selection),
503 )
504 }
505
506 fn draw(
507 &self,
508 tree: &widget::Tree,
509 renderer: &mut Renderer,
510 theme: &Theme,
511 _style: &renderer::Style,
512 layout: Layout<'_>,
513 cursor: mouse::Cursor,
514 viewport: &Rectangle,
515 ) {
516 let is_focused = {
517 let text_input_state = tree.children[0]
518 .state
519 .downcast_ref::<text_input::State<Renderer::Paragraph>>();
520
521 text_input_state.is_focused()
522 };
523
524 let selection = if is_focused || self.selection.is_empty() {
525 None
526 } else {
527 Some(&self.selection)
528 };
529
530 self.text_input.draw(
531 &tree.children[0],
532 renderer,
533 theme,
534 layout,
535 cursor,
536 selection,
537 viewport,
538 );
539 }
540
541 fn tag(&self) -> widget::tree::Tag {
542 widget::tree::Tag::of::<Menu<T>>()
543 }
544
545 fn state(&self) -> widget::tree::State {
546 widget::tree::State::new(Menu::<T> {
547 menu: menu::State::new(),
548 filtered_options: Filtered::empty(),
549 hovered_option: Some(0),
550 new_selection: None,
551 })
552 }
553
554 fn children(&self) -> Vec<widget::Tree> {
555 vec![widget::Tree::new(&self.text_input as &dyn Widget<_, _, _>)]
556 }
557
558 fn diff(&self, _tree: &mut widget::Tree) {
559 }
561
562 fn update(
563 &mut self,
564 tree: &mut widget::Tree,
565 event: &Event,
566 layout: Layout<'_>,
567 cursor: mouse::Cursor,
568 renderer: &Renderer,
569 clipboard: &mut dyn Clipboard,
570 shell: &mut Shell<'_, Message>,
571 viewport: &Rectangle,
572 ) {
573 let menu = tree.state.downcast_mut::<Menu<T>>();
574
575 let started_focused = {
576 let text_input_state = tree.children[0]
577 .state
578 .downcast_ref::<text_input::State<Renderer::Paragraph>>();
579
580 text_input_state.is_focused()
581 };
582
583 let mut local_messages = Vec::new();
585 let mut local_shell = Shell::new(&mut local_messages);
586
587 self.text_input.update(
589 &mut tree.children[0],
590 event,
591 layout,
592 cursor,
593 renderer,
594 clipboard,
595 &mut local_shell,
596 viewport,
597 );
598
599 if local_shell.is_event_captured() {
600 shell.capture_event();
601 }
602
603 shell.request_redraw_at(local_shell.redraw_request());
604 shell.request_input_method(local_shell.input_method());
605
606 for message in local_messages {
608 let TextInputEvent::TextChanged(new_value) = message;
609
610 if let Some(on_input) = &self.on_input {
611 shell.publish(on_input(new_value.clone()));
612 }
613
614 self.state.with_inner_mut(|state| {
618 menu.hovered_option = Some(0);
619 state.value = new_value;
620
621 state.filtered_options.update(
622 search(&self.state.options, &state.option_matchers, &state.value)
623 .cloned()
624 .collect(),
625 );
626 });
627 shell.invalidate_layout();
628 shell.request_redraw();
629 }
630
631 let is_focused = {
632 let text_input_state = tree.children[0]
633 .state
634 .downcast_ref::<text_input::State<Renderer::Paragraph>>();
635
636 text_input_state.is_focused()
637 };
638
639 if is_focused {
640 self.state.with_inner(|state| {
641 if !started_focused && let Some(on_option_hovered) = &mut self.on_option_hovered {
642 let hovered_option = menu.hovered_option.unwrap_or(0);
643
644 if let Some(option) = state.filtered_options.options.get(hovered_option) {
645 shell.publish(on_option_hovered(option.clone()));
646 }
647 }
648
649 if let Event::Keyboard(keyboard::Event::KeyPressed {
650 key: keyboard::Key::Named(named_key),
651 modifiers,
652 ..
653 }) = event
654 {
655 let shift_modifier = modifiers.shift();
656 match (named_key, shift_modifier) {
657 (key::Named::Enter, _) => {
658 if let Some(index) = &menu.hovered_option
659 && let Some(option) = state.filtered_options.options.get(*index)
660 {
661 menu.new_selection = Some(option.clone());
662 }
663
664 shell.capture_event();
665 shell.request_redraw();
666 }
667 (key::Named::ArrowUp, _) | (key::Named::Tab, true) => {
668 if let Some(index) = &mut menu.hovered_option {
669 if *index == 0 {
670 *index = state.filtered_options.options.len().saturating_sub(1);
671 } else {
672 *index = index.saturating_sub(1);
673 }
674 } else {
675 menu.hovered_option = Some(0);
676 }
677
678 if let Some(on_option_hovered) = &mut self.on_option_hovered
679 && let Some(option) = menu
680 .hovered_option
681 .and_then(|index| state.filtered_options.options.get(index))
682 {
683 shell.publish(on_option_hovered(option.clone()));
685 }
686
687 shell.capture_event();
688 shell.request_redraw();
689 }
690 (key::Named::ArrowDown, _) | (key::Named::Tab, false)
691 if !modifiers.shift() =>
692 {
693 if let Some(index) = &mut menu.hovered_option {
694 if *index >= state.filtered_options.options.len().saturating_sub(1)
695 {
696 *index = 0;
697 } else {
698 *index = index.saturating_add(1).min(
699 state.filtered_options.options.len().saturating_sub(1),
700 );
701 }
702 } else {
703 menu.hovered_option = Some(0);
704 }
705
706 if let Some(on_option_hovered) = &mut self.on_option_hovered
707 && let Some(option) = menu
708 .hovered_option
709 .and_then(|index| state.filtered_options.options.get(index))
710 {
711 shell.publish(on_option_hovered(option.clone()));
713 }
714
715 shell.capture_event();
716 shell.request_redraw();
717 }
718 _ => {}
719 }
720 }
721 });
722 }
723
724 self.state.with_inner_mut(|state| {
726 if let Some(selection) = menu.new_selection.take() {
727 state.value = String::new();
729 state.filtered_options.update(self.state.options.clone());
730 menu.menu = menu::State::default();
731
732 shell.publish((self.on_selected)(selection));
734
735 let mut local_messages = Vec::new();
737 let mut local_shell = Shell::new(&mut local_messages);
738 self.text_input.update(
739 &mut tree.children[0],
740 &Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)),
741 layout,
742 mouse::Cursor::Unavailable,
743 renderer,
744 clipboard,
745 &mut local_shell,
746 viewport,
747 );
748 shell.request_input_method(local_shell.input_method());
749 }
750 });
751
752 let is_focused = {
753 let text_input_state = tree.children[0]
754 .state
755 .downcast_ref::<text_input::State<Renderer::Paragraph>>();
756
757 text_input_state.is_focused()
758 };
759
760 if started_focused != is_focused {
761 shell.invalidate_widgets();
763
764 if is_focused {
765 if let Some(on_open) = self.on_open.take() {
766 shell.publish(on_open);
767 }
768 } else if let Some(on_close) = self.on_close.take() {
769 shell.publish(on_close);
770 }
771 }
772 }
773
774 fn mouse_interaction(
775 &self,
776 tree: &widget::Tree,
777 layout: Layout<'_>,
778 cursor: mouse::Cursor,
779 viewport: &Rectangle,
780 renderer: &Renderer,
781 ) -> mouse::Interaction {
782 self.text_input
783 .mouse_interaction(&tree.children[0], layout, cursor, viewport, renderer)
784 }
785
786 fn overlay<'b>(
787 &'b mut self,
788 tree: &'b mut widget::Tree,
789 layout: Layout<'_>,
790 _renderer: &Renderer,
791 viewport: &Rectangle,
792 translation: Vector,
793 ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
794 let is_focused = {
795 let text_input_state = tree.children[0]
796 .state
797 .downcast_ref::<text_input::State<Renderer::Paragraph>>();
798
799 text_input_state.is_focused()
800 };
801
802 if is_focused {
803 let Menu {
804 menu,
805 filtered_options,
806 hovered_option,
807 ..
808 } = tree.state.downcast_mut::<Menu<T>>();
809
810 self.state.sync_filtered_options(filtered_options);
811
812 if filtered_options.options.is_empty() {
813 None
814 } else {
815 let bounds = layout.bounds();
816
817 let mut menu = menu::Menu::new(
818 menu,
819 &filtered_options.options,
820 hovered_option,
821 |selection| {
822 self.state.with_inner_mut(|state| {
823 state.value = String::new();
824 state.filtered_options.update(self.state.options.clone());
825 });
826
827 tree.children[0]
828 .state
829 .downcast_mut::<text_input::State<Renderer::Paragraph>>()
830 .unfocus();
831
832 (self.on_selected)(selection)
833 },
834 self.on_option_hovered.as_deref(),
835 &self.menu_class,
836 )
837 .width(bounds.width)
838 .padding(self.padding)
839 .text_shaping(self.text_shaping);
840
841 if let Some(font) = self.font {
842 menu = menu.font(font);
843 }
844
845 if let Some(size) = self.size {
846 menu = menu.text_size(size);
847 }
848
849 Some(menu.overlay(
850 layout.position() + translation,
851 *viewport,
852 bounds.height,
853 self.menu_height,
854 ))
855 }
856 } else {
857 None
858 }
859 }
860}
861
862impl<'a, T, Message, Theme, Renderer> From<SuggestionTextInput<'a, T, Message, Theme, Renderer>>
863 for Element<'a, Message, Theme, Renderer>
864where
865 T: Display + Clone + 'static,
866 Message: Clone + 'a,
867 Theme: Catalog + 'a,
868 Renderer: text::Renderer + 'a,
869{
870 fn from(suggestion_text_input: SuggestionTextInput<'a, T, Message, Theme, Renderer>) -> Self {
871 Self::new(suggestion_text_input)
872 }
873}
874
875pub trait Catalog: text_input::Catalog + menu::Catalog {
877 fn default_input<'a>() -> <Self as text_input::Catalog>::Class<'a> {
879 <Self as text_input::Catalog>::default()
880 }
881
882 fn default_menu<'a>() -> <Self as menu::Catalog>::Class<'a> {
884 <Self as menu::Catalog>::default()
885 }
886}
887
888impl Catalog for Theme {}
889
890fn search<'a, T, A>(
891 options: impl IntoIterator<Item = T> + 'a,
892 option_matchers: impl IntoIterator<Item = &'a A> + 'a,
893 query: &'a str,
894) -> impl Iterator<Item = T> + 'a
895where
896 A: AsRef<str> + 'a,
897{
898 let query: Vec<String> = query
899 .to_lowercase()
900 .split(|c: char| !c.is_ascii_alphanumeric())
901 .map(String::from)
902 .collect();
903
904 options
905 .into_iter()
906 .zip(option_matchers)
907 .filter_map(move |(option, matcher)| {
909 if query.iter().all(|part| matcher.as_ref().contains(part)) {
910 Some(option)
911 } else {
912 None
913 }
914 })
915}
916
917fn build_matchers<'a, T>(options: impl IntoIterator<Item = T> + 'a) -> Vec<String>
918where
919 T: Display + 'a,
920{
921 options.into_iter().map(build_matcher).collect()
922}
923
924fn build_matcher<T>(option: T) -> String
925where
926 T: Display,
927{
928 let mut matcher = option.to_string();
929 matcher.retain(|c| c.is_ascii_alphanumeric());
930 matcher.to_lowercase()
931}