widgetry/widgets/
dropdown.rs

1use geom::{CornerRadii, Distance, Polygon, Pt2D};
2
3use crate::{
4    Button, Choice, Color, ControlState, CornerRounding, EdgeInsets, EventCtx, GeomBatch, GfxCtx,
5    Menu, Outcome, ScreenDims, ScreenPt, ScreenRectangle, WidgetImpl, WidgetOutput,
6};
7
8pub struct Dropdown<T: Clone> {
9    current_idx: usize,
10    btn: Button,
11    // TODO Why not T?
12    menu: Option<Menu<usize>>,
13    label: String,
14    is_persisten_split: bool,
15
16    choices: Vec<Choice<T>>,
17}
18
19impl<T: 'static + PartialEq + Clone + std::fmt::Debug> Dropdown<T> {
20    pub fn new(
21        ctx: &EventCtx,
22        label: &str,
23        default_value: T,
24        choices: Vec<Choice<T>>,
25        // TODO Ideally builder style
26        is_persisten_split: bool,
27    ) -> Dropdown<T> {
28        let current_idx = if let Some(idx) = choices.iter().position(|c| c.data == default_value) {
29            idx
30        } else {
31            panic!(
32                "Dropdown {} has default_value {:?}, but none of the choices match that",
33                label, default_value
34            );
35        };
36
37        Dropdown {
38            current_idx,
39            btn: make_btn(ctx, &choices[current_idx].label, label, is_persisten_split),
40            menu: None,
41            label: label.to_string(),
42            is_persisten_split,
43            choices,
44        }
45    }
46}
47
48impl<T: 'static + PartialEq + Clone> Dropdown<T> {
49    pub fn current_value(&self) -> T {
50        self.choices[self.current_idx].data.clone()
51    }
52    pub(crate) fn current_value_label(&self) -> &str {
53        &self.choices[self.current_idx].label
54    }
55}
56
57impl<T: 'static + Clone> Dropdown<T> {
58    fn open_menu(&mut self, ctx: &mut EventCtx) {
59        let mut menu = Menu::new(
60            ctx,
61            self.choices
62                .iter()
63                .enumerate()
64                .map(|(idx, c)| c.with_value(idx))
65                .collect(),
66        );
67        menu.set_current(self.current_idx);
68        let y1_below = self.btn.top_left.y + self.btn.dims.height + 15.0;
69
70        menu.set_pos(ScreenPt::new(
71            self.btn.top_left.x,
72            // top_left_for_corner doesn't quite work
73            if y1_below + menu.get_dims().height < ctx.canvas.window_height {
74                y1_below
75            } else {
76                self.btn.top_left.y - 15.0 - menu.get_dims().height
77            },
78        ));
79        self.menu = Some(menu);
80    }
81}
82
83impl<T: 'static + Clone> WidgetImpl for Dropdown<T> {
84    fn get_dims(&self) -> ScreenDims {
85        self.btn.get_dims()
86    }
87
88    fn set_pos(&mut self, top_left: ScreenPt) {
89        self.btn.set_pos(top_left);
90    }
91
92    fn event(&mut self, ctx: &mut EventCtx, output: &mut WidgetOutput) {
93        if let Some(ref mut m) = self.menu {
94            let mut tmp_ouput = WidgetOutput::new();
95            m.event(ctx, &mut tmp_ouput);
96            if let Outcome::Clicked(_) = tmp_ouput.outcome {
97                self.current_idx = self.menu.take().unwrap().take_current_choice();
98                output.outcome = Outcome::Changed(self.label.clone());
99                let top_left = self.btn.top_left;
100                self.btn = make_btn(
101                    ctx,
102                    &self.choices[self.current_idx].label,
103                    &self.label,
104                    self.is_persisten_split,
105                );
106                self.btn.set_pos(top_left);
107                output.redo_layout = true;
108            } else if ctx.normal_left_click() {
109                if let Some(pt) = ctx.canvas.get_cursor_in_screen_space() {
110                    if !ScreenRectangle::top_left(m.top_left, m.get_dims()).contains(pt) {
111                        self.menu = None;
112                    }
113                } else {
114                    self.menu = None;
115                }
116                if self.menu.is_some() {
117                    ctx.input.unconsume_event();
118                }
119            }
120        } else {
121            self.btn.event(ctx, output);
122            if let Outcome::Clicked(_) = output.outcome {
123                output.outcome = Outcome::Nothing;
124                self.open_menu(ctx);
125            }
126        }
127
128        if self.menu.is_some() {
129            output.outcome = Outcome::Focused(self.label.clone());
130        }
131    }
132
133    fn draw(&self, g: &mut GfxCtx) {
134        self.btn.draw(g);
135        if let Some(ref m) = self.menu {
136            // We need a background too! Add some padding and an outline.
137            // TODO Little embedded Panel could make more sense?
138            let pad = 5.0;
139            let width = m.get_dims().width + 2.0 * pad;
140            let height = m.get_dims().height + 2.0 * pad;
141            let rect = Polygon::rounded_rectangle(width, height, 5.0);
142
143            let mut batch = GeomBatch::new();
144            batch.push(g.style().field_bg, rect.clone());
145            batch.push(
146                g.style().dropdown_border,
147                rect.to_outline(Distance::meters(1.0)),
148            );
149            let draw_bg = g.upload(batch);
150            g.fork(
151                Pt2D::new(0.0, 0.0),
152                ScreenPt::new(m.top_left.x - pad, m.top_left.y - pad),
153                1.0,
154                Some(crate::drawing::MENU_Z),
155            );
156            g.redraw(&draw_bg);
157            g.unfork();
158
159            m.draw(g);
160
161            // Dropdown menus often leak out of their Panel
162            g.canvas
163                .mark_covered_area(ScreenRectangle::top_left(m.top_left, m.get_dims()));
164        }
165    }
166
167    fn can_restore(&self) -> bool {
168        true
169    }
170    fn restore(&mut self, ctx: &mut EventCtx, prev: &dyn WidgetImpl) {
171        let prev = prev.downcast_ref::<Dropdown<T>>().unwrap();
172        if prev.menu.is_some() {
173            self.open_menu(ctx);
174            // TODO Preserve menu hovered item. Only matters if we've moved the cursor off the
175            // menu.
176        }
177    }
178}
179
180fn make_btn(ctx: &EventCtx, label: &str, tooltip: &str, is_persisten_split: bool) -> Button {
181    // If we want to make Dropdown configurable, pass in or expose its button builder?
182    let builder = if is_persisten_split {
183        // Quick hacks to make PersistentSplit's dropdown look a little better.
184        // It's not ideal, but we only use one persistent split in the whole app
185        // and it's front and center - we'll notice if something breaks.
186        ctx.style()
187            .btn_solid
188            .dropdown()
189            .padding(EdgeInsets {
190                top: 15.0,
191                bottom: 15.0,
192                left: 8.0,
193                right: 8.0,
194            })
195            .corner_rounding(CornerRounding::CornerRadii(CornerRadii {
196                top_left: 0.0,
197                bottom_left: 0.0,
198                bottom_right: 2.0,
199                top_right: 2.0,
200            }))
201            // override any outline element within persistent split
202            .outline((0.0, Color::CLEAR), ControlState::Default)
203    } else {
204        ctx.style().btn_outline.dropdown().label_text(label)
205    };
206
207    builder.build(ctx, tooltip)
208}