widgetry/widgets/
autocomplete.rs1use abstutil::MultiMap;
2
3use crate::{
4 Choice, EventCtx, GfxCtx, Menu, Outcome, ScreenDims, ScreenPt, TextBox, Widget, WidgetImpl,
5 WidgetOutput,
6};
7
8pub struct Autocomplete<T: Clone> {
11 choices: Vec<(String, Vec<T>)>,
12 num_search_results: usize,
13
14 tb: TextBox,
15 menu: Menu<()>,
16
17 current_line: String,
18 chosen_values: Option<Vec<T>>,
19}
20
21impl<T: 'static + Clone + Ord> Autocomplete<T> {
22 pub fn new_widget(
23 ctx: &mut EventCtx,
24 raw_choices: Vec<(String, T)>,
25 num_search_results: usize,
26 ) -> Widget {
27 let mut grouped: MultiMap<String, T> = MultiMap::new();
28 for (name, data) in raw_choices {
29 grouped.insert(name, data);
30 }
31 let choices: Vec<(String, Vec<T>)> = grouped
32 .consume()
33 .into_iter()
34 .map(|(k, v)| (k, v.into_iter().collect()))
35 .collect();
36
37 let mut a = Autocomplete {
38 choices,
39 num_search_results,
40
41 tb: TextBox::new(
42 ctx,
43 "autocomplete textbox".to_string(),
44 50,
45 String::new(),
46 true,
47 ),
48 menu: Menu::<()>::new(ctx, Vec::new()),
49
50 current_line: String::new(),
51 chosen_values: None,
52 };
53 a.recalc_menu(ctx);
54 Widget::new(Box::new(a))
55 }
56}
57
58impl<T: 'static + Clone> Autocomplete<T> {
59 pub fn take_final_value(&mut self) -> Option<Vec<T>> {
60 self.chosen_values.take()
61 }
62
63 fn recalc_menu(&mut self, ctx: &mut EventCtx) {
64 let mut choices = vec![Choice::new(
65 format!("anything matching \"{}\"", self.current_line),
66 (),
67 )];
68 let query = self.current_line.to_ascii_lowercase();
69 for (name, _) in &self.choices {
70 if name.to_ascii_lowercase().contains(&query) {
71 choices.push(Choice::new(name, ()));
72 }
73 if choices.len() == self.num_search_results {
74 break;
75 }
76 }
77 if choices.len() == 2 {
79 choices.remove(0);
80 }
81 self.menu = Menu::new(ctx, choices);
82 }
83}
84
85impl<T: 'static + Clone> WidgetImpl for Autocomplete<T> {
86 fn get_dims(&self) -> ScreenDims {
87 let d1 = self.tb.get_dims();
88 let d2 = self.menu.get_dims();
89 ScreenDims::new(d1.width.max(d2.width), d1.height + d2.height)
90 }
91
92 fn set_pos(&mut self, top_left: ScreenPt) {
93 self.tb.set_pos(top_left);
94 self.menu.set_pos(ScreenPt::new(
95 top_left.x,
96 top_left.y + self.tb.get_dims().height,
97 ));
98 }
99
100 fn event(&mut self, ctx: &mut EventCtx, output: &mut WidgetOutput) {
101 self.tb.event(ctx, output);
102 if self.tb.get_line() != self.current_line {
103 self.current_line = self.tb.get_line();
105 self.recalc_menu(ctx);
106 output.redo_layout = true;
107 } else {
108 let mut tmp_output = WidgetOutput::new();
112 self.menu.event(ctx, &mut tmp_output);
113 if let Outcome::Clicked(ref choice) = tmp_output.outcome {
114 if choice.starts_with("anything matching") {
115 let query = self.current_line.to_ascii_lowercase();
116 let mut matches = Vec::new();
117 for (name, choices) in &self.choices {
118 if name.to_ascii_lowercase().contains(&query) {
119 matches.extend(choices.clone());
120 }
121 }
122 self.chosen_values = Some(matches);
123 } else {
124 self.chosen_values = Some(
125 self.choices
126 .iter()
127 .find(|(name, _)| name == choice)
128 .unwrap()
129 .1
130 .clone(),
131 );
132 }
133 }
134 }
135 }
136
137 fn draw(&self, g: &mut GfxCtx) {
138 self.tb.draw(g);
139 self.menu.draw(g);
140 }
141}