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 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 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 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 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 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 }
177 }
178}
179
180fn make_btn(ctx: &EventCtx, label: &str, tooltip: &str, is_persisten_split: bool) -> Button {
181 let builder = if is_persisten_split {
183 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 .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}