widgetry/widgets/
autocomplete.rs

1use abstutil::MultiMap;
2
3use crate::{
4    Choice, EventCtx, GfxCtx, Menu, Outcome, ScreenDims, ScreenPt, TextBox, Widget, WidgetImpl,
5    WidgetOutput,
6};
7
8// TODO I don't even think we need to declare Clone...
9// If multiple names map to the same data, all of the possible values will be returned
10pub 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        // "anything matching" is silly if we've resolved to exactly one choice
78        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            // This will return Outcome::Changed to the caller with a dummy ID for the textbox
104            self.current_line = self.tb.get_line();
105            self.recalc_menu(ctx);
106            output.redo_layout = true;
107        } else {
108            // Don't let the menu fill out the real outcome. Should we use Outcome::Changed to
109            // indicate the autocomplete is finished, instead of the caller polling
110            // autocomplete_done?
111            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}