use crate::egui_ext::WidgetTextExt as _;
use crate::egui_ext::boxed_widget::{BoxedWidget, BoxedWidgetExt as _};
use crate::{DesignTokens, UiExt as _, icons};
use eframe::emath::Align;
use eframe::epaint::FontFamily;
use egui::{
Atom, AtomExt as _, AtomLayout, Atoms, Button, FontId, Frame, Id, Layout, Margin, Pos2, Rect,
Response, Sense, TextStyle, Ui, UiBuilder, Vec2, Widget, WidgetText,
};
pub struct ComboItem<'a> {
label: WidgetText,
selected: bool,
value: Option<BoxedWidget<'a>>,
error: Option<String>,
}
impl<'a> ComboItem<'a> {
pub fn new(label: impl Into<WidgetText>) -> Self {
Self {
label: label.into(),
selected: false,
value: None,
error: None,
}
}
pub fn error(mut self, error: Option<String>) -> Self {
self.error = error;
self
}
pub fn selected(mut self, selected: bool) -> Self {
self.selected = selected;
self
}
pub fn value(mut self, value: impl Into<WidgetText> + Send + Sync + 'a) -> Self {
let value = value
.into()
.force_size(DesignTokens::combo_item_small_font_size());
self.value = Some((|ui: &mut Ui| ui.label(value)).boxed());
self
}
pub fn value_widget(mut self, value: impl Widget + Send + Sync + 'a) -> Self {
self.value = Some(value.boxed());
self
}
}
impl Widget for ComboItem<'_> {
fn ui(self, ui: &mut Ui) -> Response {
let Self {
mut label,
selected,
value,
error,
} = self;
ui.spacing_mut().icon_spacing = 2.0;
ui.spacing_mut().button_padding.x = 0.0;
if error.is_some() {
label = label.color(ui.tokens().error_fg_color);
}
let check_icon_size = Vec2::splat(12.0);
let check_icon = if selected {
icons::CHECKED
.as_image()
.tint(ui.tokens().text_strong)
.atom_size(check_icon_size)
} else {
Atom::default().atom_size(check_icon_size)
};
let mut atoms = Atoms::new((check_icon, label));
let error_id = Id::new("error");
let value_scope_id = ui.next_auto_id().with("value_scope");
if error.is_some() {
atoms.push_right(Atom::grow().atom_size(Vec2::new(16.0, 0.0)));
atoms.push_right(Atom::custom(error_id, ui.tokens().small_icon_size));
} else if value.is_some() {
let value_scope_response = ui.read_response(value_scope_id);
let size = value_scope_response
.map(|r| r.rect.size())
.unwrap_or_default();
atoms.push_right(Atom::grow().atom_size(Vec2::new(16.0, 0.0)));
atoms.push_right(Atom::custom(value_scope_id, size));
}
atoms.push_right(Atom::default().atom_size(Vec2::new(2.0, 0.0)));
let response = Button::new(atoms).atom_ui(ui);
if let Some(rect) = response.rect(error_id) {
icons::ERROR
.as_image()
.tint(ui.tokens().alert_error.icon)
.paint_at(ui, rect);
if let Some(error) = error
&& !error.is_empty()
{
ui.interact(
rect,
response.response.id.with("error_hover"),
Sense::hover(),
)
.on_hover_text(error);
}
} else if let Some(rect) = response.rect(value_scope_id)
&& let Some(widget) = value
{
let rect = Rect::from_min_max(
Pos2::new(
rect.max.x - DesignTokens::combo_item_max_value_width(),
rect.min.y,
),
rect.max,
);
let mut child_ui = ui.new_child(
UiBuilder::new()
.id(value_scope_id)
.max_rect(rect)
.layout(Layout::right_to_left(Align::Center)),
);
child_ui.style_mut().interaction.selectable_labels = false;
for text_style in [TextStyle::Body, TextStyle::Monospace, TextStyle::Button] {
if let Some(font) = child_ui.style_mut().text_styles.get_mut(&text_style) {
font.size = DesignTokens::combo_item_small_font_size();
}
}
child_ui.add(widget);
}
response.response
}
}
pub struct ComboItemHeader {
label: WidgetText,
}
impl ComboItemHeader {
pub fn new(label: impl Into<WidgetText>) -> Self {
Self {
label: label.into(),
}
}
}
impl Widget for ComboItemHeader {
fn ui(self, ui: &mut Ui) -> Response {
ui.add(
AtomLayout::new(self.label)
.frame(Frame::new().inner_margin(Margin {
bottom: 0,
left: 14, right: 4,
top: 4,
}))
.min_size(Vec2::new(0.0, 22.0))
.fallback_font(FontId::new(10.0, FontFamily::Proportional)),
)
}
}
#[cfg(test)]
pub mod tests {
use crate::menu::menu_style;
use crate::syntax_highlighting::SyntaxHighlightedBuilder;
use crate::{ComboItem, ComboItemHeader};
use egui::ComboBox;
use egui_kittest::kittest::Queryable as _;
use egui_kittest::{Harness, OsThreshold, SnapshotOptions};
#[test]
pub fn test_combo_item() {
let mut harness = Harness::new_ui(|ui| {
crate::apply_style_and_install_loaders(ui.ctx());
ComboBox::new("combo_item_example", "")
.selected_text("ComboItem Example")
.popup_style(menu_style())
.height(300.0)
.show_ui(ui, |ui| {
ui.add(ComboItemHeader::new("Recommended:"));
ui.add(
ComboItem::new("vertex_normals")
.error(Some("Invalid selector".to_owned()))
.selected(true),
);
let mut code = SyntaxHighlightedBuilder::new();
code.append_syntax("[")
.append_primitive("0.000")
.append_syntax(",")
.append_primitive("0.000")
.append_syntax("]");
ui.add(ComboItemHeader::new("Other values:"));
ui.add(ComboItem::new("vertex_positions"));
ui.add(
ComboItem::new("Rerun default").value(code.into_widget_text(ui.style())),
);
});
});
harness.get_by_value("ComboItem Example").click();
harness.run();
harness.fit_contents();
let options = SnapshotOptions::new()
.threshold(OsThreshold::default().macos(2.5))
.failed_pixel_count_threshold(OsThreshold::default().macos(5));
harness.snapshot_options("combo_item", &options);
}
}