use std::borrow::Cow;
use std::hash::Hash;
use egui::{
Color32, ComboBox, CornerRadius, Pos2, Response, Sense, Stroke, Ui, Vec2, Widget, WidgetInfo,
WidgetText, WidgetType,
};
use crate::theme::{with_alpha, Theme};
#[must_use = "Add with `ui.add(...)`."]
pub struct Select<'a, T: PartialEq + Clone> {
id_salt: egui::Id,
value: &'a mut T,
label: Option<WidgetText>,
options: Vec<(T, Cow<'a, str>)>,
width: Option<f32>,
}
impl<'a, T: PartialEq + Clone> std::fmt::Debug for Select<'a, T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let labels: Vec<&str> = self.options.iter().map(|(_, l)| l.as_ref()).collect();
f.debug_struct("Select")
.field("id_salt", &self.id_salt)
.field("option_labels", &labels)
.field("width", &self.width)
.finish()
}
}
impl<'a, T: PartialEq + Clone> Select<'a, T> {
pub fn new(id_salt: impl Hash, value: &'a mut T) -> Self {
Self {
id_salt: egui::Id::new(id_salt),
value,
label: None,
options: Vec::new(),
width: None,
}
}
pub fn label(mut self, label: impl Into<WidgetText>) -> Self {
self.label = Some(label.into());
self
}
pub fn options<I, S>(mut self, options: I) -> Self
where
I: IntoIterator<Item = (T, S)>,
S: Into<Cow<'a, str>>,
{
self.options = options.into_iter().map(|(v, l)| (v, l.into())).collect();
self
}
pub fn width(mut self, width: f32) -> Self {
self.width = Some(width);
self
}
}
impl<'a> Select<'a, String> {
pub fn strings<I, S>(id_salt: impl Hash, value: &'a mut String, options: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<Cow<'a, str>>,
{
let options: Vec<(String, Cow<'a, str>)> = options
.into_iter()
.map(|s| {
let label: Cow<'a, str> = s.into();
let value = label.as_ref().to_owned();
(value, label)
})
.collect();
Self {
id_salt: egui::Id::new(id_salt),
value,
label: None,
options,
width: None,
}
}
}
impl<'a, T: PartialEq + Clone> Widget for Select<'a, T> {
fn ui(self, ui: &mut Ui) -> Response {
let theme = Theme::current(ui.ctx());
let p = &theme.palette;
let t = &theme.typography;
ui.vertical(|ui| {
if let Some(label) = &self.label {
let rich = egui::RichText::new(label.text())
.color(p.text_muted)
.size(t.label);
ui.add(egui::Label::new(rich).wrap_mode(egui::TextWrapMode::Extend));
ui.add_space(2.0);
}
let width = self.width.unwrap_or(160.0);
let chevron_color = p.text_muted;
let selected_label: String = self
.options
.iter()
.find(|(v, _)| v == &*self.value)
.map(|(_, l)| l.as_ref().to_owned())
.unwrap_or_default();
let field_label = self.label.as_ref().map(|l| l.text().to_string());
let response = crate::theme::with_themed_visuals(ui, |ui| {
let v = ui.visuals_mut();
crate::theme::themed_input_visuals(v, &theme, p.input_bg);
for w in [
&mut v.widgets.inactive,
&mut v.widgets.hovered,
&mut v.widgets.active,
&mut v.widgets.open,
] {
w.fg_stroke = Stroke::new(1.0, p.text);
}
v.override_text_color = Some(p.text);
ComboBox::from_id_salt(self.id_salt)
.width(width)
.selected_text(
egui::RichText::new(&selected_label)
.color(p.text)
.size(t.body),
)
.icon(move |ui, rect, _visuals, is_popup_open| {
paint_chevron(ui, rect, chevron_color, is_popup_open);
})
.show_ui(ui, |ui| {
ui.set_min_width(width);
ui.spacing_mut().item_spacing.y = 2.0;
for (opt_value, opt_label) in self.options.iter() {
let selected = opt_value == &*self.value;
if select_option(ui, opt_label.as_ref(), selected, &theme).clicked() {
*self.value = opt_value.clone();
}
}
})
.response
});
if let Some(field_label) = field_label {
let selected_label = selected_label.clone();
response.widget_info(|| {
let mut info = WidgetInfo::labeled(WidgetType::ComboBox, true, &field_label);
info.current_text_value = Some(selected_label.clone());
info
});
}
response
})
.inner
}
}
fn paint_chevron(ui: &egui::Ui, rect: egui::Rect, color: Color32, is_popup_open: bool) {
let painter = ui.painter();
let stroke = Stroke::new(1.4, color);
let half_w = (rect.width() * 0.35).min(5.0);
let half_h = (rect.height() * 0.18).min(3.0);
let c = rect.center();
let (left, right, tip) = if is_popup_open {
(
egui::pos2(c.x - half_w, c.y + half_h * 0.5),
egui::pos2(c.x + half_w, c.y + half_h * 0.5),
egui::pos2(c.x, c.y - half_h * 1.5),
)
} else {
(
egui::pos2(c.x - half_w, c.y - half_h * 0.5),
egui::pos2(c.x + half_w, c.y - half_h * 0.5),
egui::pos2(c.x, c.y + half_h * 1.5),
)
};
painter.line_segment([left, tip], stroke);
painter.line_segment([tip, right], stroke);
}
fn select_option(ui: &mut Ui, label: &str, selected: bool, theme: &Theme) -> Response {
let p = &theme.palette;
let t = &theme.typography;
let pad_x = 10.0;
let pad_y = 6.0;
let galley = crate::theme::placeholder_galley(ui, label, t.body, false, f32::INFINITY);
let content_w = galley.size().x;
let desired = Vec2::new(
ui.available_width().max(content_w + pad_x * 2.0),
galley.size().y.max(t.body) + pad_y * 2.0,
);
let (rect, response) = ui.allocate_exact_size(desired, Sense::click());
if ui.is_rect_visible(rect) {
let bg = if response.hovered() {
with_alpha(p.sky, 60)
} else if selected {
with_alpha(p.sky, 40)
} else {
Color32::TRANSPARENT
};
if bg.a() > 0 {
let radius = CornerRadius::same((theme.control_radius as u8).saturating_sub(2));
ui.painter().rect_filled(rect, radius, bg);
}
let label_pos = Pos2::new(rect.min.x + pad_x, rect.center().y - galley.size().y * 0.5);
ui.painter().galley(label_pos, galley, p.text);
}
response
}