1use std::borrow::Cow;
7use std::hash::Hash;
8
9use egui::{Color32, ComboBox, Response, Stroke, Ui, Widget, WidgetInfo, WidgetText, WidgetType};
10
11use crate::theme::Theme;
12
13#[must_use = "Add with `ui.add(...)`."]
46pub struct Select<'a, T: PartialEq + Clone> {
47 id_salt: egui::Id,
48 value: &'a mut T,
49 label: Option<WidgetText>,
50 options: Vec<(T, Cow<'a, str>)>,
51 width: Option<f32>,
52}
53
54impl<'a, T: PartialEq + Clone> std::fmt::Debug for Select<'a, T> {
55 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
56 let labels: Vec<&str> = self.options.iter().map(|(_, l)| l.as_ref()).collect();
57 f.debug_struct("Select")
58 .field("id_salt", &self.id_salt)
59 .field("option_labels", &labels)
60 .field("width", &self.width)
61 .finish()
62 }
63}
64
65impl<'a, T: PartialEq + Clone> Select<'a, T> {
66 pub fn new(id_salt: impl Hash, value: &'a mut T) -> Self {
69 Self {
70 id_salt: egui::Id::new(id_salt),
71 value,
72 label: None,
73 options: Vec::new(),
74 width: None,
75 }
76 }
77
78 pub fn label(mut self, label: impl Into<WidgetText>) -> Self {
80 self.label = Some(label.into());
81 self
82 }
83
84 pub fn options<I, S>(mut self, options: I) -> Self
87 where
88 I: IntoIterator<Item = (T, S)>,
89 S: Into<Cow<'a, str>>,
90 {
91 self.options = options.into_iter().map(|(v, l)| (v, l.into())).collect();
92 self
93 }
94
95 pub fn width(mut self, width: f32) -> Self {
98 self.width = Some(width);
99 self
100 }
101}
102
103impl<'a> Select<'a, String> {
104 pub fn strings<I, S>(id_salt: impl Hash, value: &'a mut String, options: I) -> Self
115 where
116 I: IntoIterator<Item = S>,
117 S: Into<Cow<'a, str>>,
118 {
119 let options: Vec<(String, Cow<'a, str>)> = options
120 .into_iter()
121 .map(|s| {
122 let label: Cow<'a, str> = s.into();
123 let value = label.as_ref().to_owned();
124 (value, label)
125 })
126 .collect();
127 Self {
128 id_salt: egui::Id::new(id_salt),
129 value,
130 label: None,
131 options,
132 width: None,
133 }
134 }
135}
136
137impl<'a, T: PartialEq + Clone> Widget for Select<'a, T> {
138 fn ui(self, ui: &mut Ui) -> Response {
139 let theme = Theme::current(ui.ctx());
140 let p = &theme.palette;
141 let t = &theme.typography;
142
143 ui.vertical(|ui| {
144 if let Some(label) = &self.label {
145 let rich = egui::RichText::new(label.text())
146 .color(p.text_muted)
147 .size(t.label);
148 ui.add(egui::Label::new(rich).wrap_mode(egui::TextWrapMode::Extend));
149 ui.add_space(2.0);
150 }
151
152 let width = self.width.unwrap_or(160.0);
153 let chevron_color = p.text_muted;
154
155 let selected_label: String = self
159 .options
160 .iter()
161 .find(|(v, _)| v == &*self.value)
162 .map(|(_, l)| l.as_ref().to_owned())
163 .unwrap_or_default();
164 let field_label = self.label.as_ref().map(|l| l.text().to_string());
165
166 let response = crate::theme::with_themed_visuals(ui, |ui| {
167 let v = ui.visuals_mut();
168 crate::theme::themed_input_visuals(v, &theme, p.input_bg);
169 for w in [
170 &mut v.widgets.inactive,
171 &mut v.widgets.hovered,
172 &mut v.widgets.active,
173 &mut v.widgets.open,
174 ] {
175 w.fg_stroke = Stroke::new(1.0, p.text);
176 }
177 v.override_text_color = Some(p.text);
178
179 ComboBox::from_id_salt(self.id_salt)
180 .width(width)
181 .selected_text(
182 egui::RichText::new(&selected_label)
183 .color(p.text)
184 .size(t.body),
185 )
186 .icon(move |ui, rect, _visuals, is_popup_open| {
187 paint_chevron(ui, rect, chevron_color, is_popup_open);
188 })
189 .show_ui(ui, |ui| {
190 ui.set_min_width(width);
191 for (opt_value, opt_label) in self.options.iter() {
192 let label = egui::RichText::new(opt_label.as_ref())
193 .color(p.text)
194 .size(t.body);
195 if ui
196 .selectable_label(opt_value == &*self.value, label)
197 .clicked()
198 {
199 *self.value = opt_value.clone();
200 }
201 }
202 })
203 .response
204 });
205
206 if let Some(field_label) = field_label {
207 let selected_label = selected_label.clone();
208 response.widget_info(|| {
209 let mut info = WidgetInfo::labeled(WidgetType::ComboBox, true, &field_label);
210 info.current_text_value = Some(selected_label.clone());
211 info
212 });
213 }
214
215 response
216 })
217 .inner
218 }
219}
220
221fn paint_chevron(ui: &egui::Ui, rect: egui::Rect, color: Color32, is_popup_open: bool) {
224 let painter = ui.painter();
225 let stroke = Stroke::new(1.4, color);
226
227 let half_w = (rect.width() * 0.35).min(5.0);
228 let half_h = (rect.height() * 0.18).min(3.0);
229 let c = rect.center();
230
231 let (left, right, tip) = if is_popup_open {
232 (
233 egui::pos2(c.x - half_w, c.y + half_h * 0.5),
234 egui::pos2(c.x + half_w, c.y + half_h * 0.5),
235 egui::pos2(c.x, c.y - half_h * 1.5),
236 )
237 } else {
238 (
239 egui::pos2(c.x - half_w, c.y - half_h * 0.5),
240 egui::pos2(c.x + half_w, c.y - half_h * 0.5),
241 egui::pos2(c.x, c.y + half_h * 1.5),
242 )
243 };
244
245 painter.line_segment([left, tip], stroke);
246 painter.line_segment([tip, right], stroke);
247}