use epaint::Shape;
use crate::{
epaint, style::WidgetVisuals, vec2, Align2, Context, Id, InnerResponse, NumExt, Painter,
PopupCloseBehavior, Rect, Response, ScrollArea, Sense, Stroke, TextStyle, TextWrapMode, Ui,
UiBuilder, Vec2, WidgetInfo, WidgetText, WidgetType,
};
#[allow(unused_imports)] use crate::style::Spacing;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum AboveOrBelow {
Above,
Below,
}
pub type IconPainter = Box<dyn FnOnce(&Ui, Rect, &WidgetVisuals, bool, AboveOrBelow)>;
#[must_use = "You should call .show*"]
pub struct ComboBox {
id_salt: Id,
label: Option<WidgetText>,
selected_text: WidgetText,
width: Option<f32>,
height: Option<f32>,
icon: Option<IconPainter>,
wrap_mode: Option<TextWrapMode>,
close_behavior: Option<PopupCloseBehavior>,
}
impl ComboBox {
pub fn new(id_salt: impl std::hash::Hash, label: impl Into<WidgetText>) -> Self {
Self {
id_salt: Id::new(id_salt),
label: Some(label.into()),
selected_text: Default::default(),
width: None,
height: None,
icon: None,
wrap_mode: None,
close_behavior: None,
}
}
pub fn from_label(label: impl Into<WidgetText>) -> Self {
let label = label.into();
Self {
id_salt: Id::new(label.text()),
label: Some(label),
selected_text: Default::default(),
width: None,
height: None,
icon: None,
wrap_mode: None,
close_behavior: None,
}
}
pub fn from_id_salt(id_salt: impl std::hash::Hash) -> Self {
Self {
id_salt: Id::new(id_salt),
label: Default::default(),
selected_text: Default::default(),
width: None,
height: None,
icon: None,
wrap_mode: None,
close_behavior: None,
}
}
#[deprecated = "Renamed id_salt"]
pub fn from_id_source(id_salt: impl std::hash::Hash) -> Self {
Self::from_id_salt(id_salt)
}
#[inline]
pub fn width(mut self, width: f32) -> Self {
self.width = Some(width);
self
}
#[inline]
pub fn height(mut self, height: f32) -> Self {
self.height = Some(height);
self
}
#[inline]
pub fn selected_text(mut self, selected_text: impl Into<WidgetText>) -> Self {
self.selected_text = selected_text.into();
self
}
pub fn icon(
mut self,
icon_fn: impl FnOnce(&Ui, Rect, &WidgetVisuals, bool, AboveOrBelow) + 'static,
) -> Self {
self.icon = Some(Box::new(icon_fn));
self
}
#[inline]
pub fn wrap_mode(mut self, wrap_mode: TextWrapMode) -> Self {
self.wrap_mode = Some(wrap_mode);
self
}
#[inline]
pub fn wrap(mut self) -> Self {
self.wrap_mode = Some(TextWrapMode::Wrap);
self
}
#[inline]
pub fn truncate(mut self) -> Self {
self.wrap_mode = Some(TextWrapMode::Truncate);
self
}
#[inline]
pub fn close_behavior(mut self, close_behavior: PopupCloseBehavior) -> Self {
self.close_behavior = Some(close_behavior);
self
}
pub fn show_ui<R>(
self,
ui: &mut Ui,
menu_contents: impl FnOnce(&mut Ui) -> R,
) -> InnerResponse<Option<R>> {
self.show_ui_dyn(ui, Box::new(menu_contents))
}
fn show_ui_dyn<'c, R>(
self,
ui: &mut Ui,
menu_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
) -> InnerResponse<Option<R>> {
let Self {
id_salt,
label,
selected_text,
width,
height,
icon,
wrap_mode,
close_behavior,
} = self;
let button_id = ui.make_persistent_id(id_salt);
ui.horizontal(|ui| {
let mut ir = combo_box_dyn(
ui,
button_id,
selected_text,
menu_contents,
icon,
wrap_mode,
close_behavior,
(width, height),
);
if let Some(label) = label {
ir.response.widget_info(|| {
WidgetInfo::labeled(WidgetType::ComboBox, ui.is_enabled(), label.text())
});
ir.response |= ui.label(label);
} else {
ir.response
.widget_info(|| WidgetInfo::labeled(WidgetType::ComboBox, ui.is_enabled(), ""));
}
ir
})
.inner
}
pub fn show_index<Text: Into<WidgetText>>(
self,
ui: &mut Ui,
selected: &mut usize,
len: usize,
get: impl Fn(usize) -> Text,
) -> Response {
let slf = self.selected_text(get(*selected));
let mut changed = false;
let mut response = slf
.show_ui(ui, |ui| {
for i in 0..len {
if ui.selectable_label(i == *selected, get(i)).clicked() {
*selected = i;
changed = true;
}
}
})
.response;
if changed {
response.mark_changed();
}
response
}
pub fn is_open(ctx: &Context, id: Id) -> bool {
ctx.memory(|m| m.is_popup_open(Self::widget_to_popup_id(id)))
}
fn widget_to_popup_id(widget_id: Id) -> Id {
widget_id.with("popup")
}
}
#[allow(clippy::too_many_arguments)]
fn combo_box_dyn<'c, R>(
ui: &mut Ui,
button_id: Id,
selected_text: WidgetText,
menu_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
icon: Option<IconPainter>,
wrap_mode: Option<TextWrapMode>,
close_behavior: Option<PopupCloseBehavior>,
(width, height): (Option<f32>, Option<f32>),
) -> InnerResponse<Option<R>> {
let popup_id = ComboBox::widget_to_popup_id(button_id);
let is_popup_open = ui.memory(|m| m.is_popup_open(popup_id));
let popup_height = ui.memory(|m| {
m.areas()
.get(popup_id)
.and_then(|state| state.size)
.map_or(100.0, |size| size.y)
});
let above_or_below =
if ui.next_widget_position().y + ui.spacing().interact_size.y + popup_height
< ui.ctx().screen_rect().bottom()
{
AboveOrBelow::Below
} else {
AboveOrBelow::Above
};
let wrap_mode = wrap_mode.unwrap_or_else(|| ui.wrap_mode());
let close_behavior = close_behavior.unwrap_or(PopupCloseBehavior::CloseOnClick);
let margin = ui.spacing().button_padding;
let button_response = button_frame(ui, button_id, is_popup_open, Sense::click(), |ui| {
let icon_spacing = ui.spacing().icon_spacing;
let icon_size = Vec2::splat(ui.spacing().icon_width);
let minimum_width = width.unwrap_or_else(|| ui.spacing().combo_width) - 2.0 * margin.x;
let wrap_width = if wrap_mode == TextWrapMode::Extend {
f32::INFINITY
} else {
ui.available_width() - icon_spacing - icon_size.x
};
let galley = selected_text.into_galley(ui, Some(wrap_mode), wrap_width, TextStyle::Button);
let actual_width = (galley.size().x + icon_spacing + icon_size.x).at_least(minimum_width);
let actual_height = galley.size().y.max(icon_size.y);
let (_, rect) = ui.allocate_space(Vec2::new(actual_width, actual_height));
let button_rect = ui.min_rect().expand2(ui.spacing().button_padding);
let response = ui.interact(button_rect, button_id, Sense::click());
if ui.is_rect_visible(rect) {
let icon_rect = Align2::RIGHT_CENTER.align_size_within_rect(icon_size, rect);
let visuals = if is_popup_open {
&ui.visuals().widgets.open
} else {
ui.style().interact(&response)
};
if let Some(icon) = icon {
icon(
ui,
icon_rect.expand(visuals.expansion),
visuals,
is_popup_open,
above_or_below,
);
} else {
paint_default_icon(
ui.painter(),
icon_rect.expand(visuals.expansion),
visuals,
above_or_below,
);
}
let text_rect = Align2::LEFT_CENTER.align_size_within_rect(galley.size(), rect);
ui.painter()
.galley(text_rect.min, galley, visuals.text_color());
}
});
if button_response.clicked() {
ui.memory_mut(|mem| mem.toggle_popup(popup_id));
}
let height = height.unwrap_or_else(|| ui.spacing().combo_height);
let inner = crate::popup::popup_above_or_below_widget(
ui,
popup_id,
&button_response,
above_or_below,
close_behavior,
|ui| {
ScrollArea::vertical()
.max_height(height)
.show(ui, |ui| {
ui.style_mut().wrap_mode = Some(TextWrapMode::Extend);
menu_contents(ui)
})
.inner
},
);
InnerResponse {
inner,
response: button_response,
}
}
fn button_frame(
ui: &mut Ui,
id: Id,
is_popup_open: bool,
sense: Sense,
add_contents: impl FnOnce(&mut Ui),
) -> Response {
let where_to_put_background = ui.painter().add(Shape::Noop);
let margin = ui.spacing().button_padding;
let interact_size = ui.spacing().interact_size;
let mut outer_rect = ui.available_rect_before_wrap();
outer_rect.set_height(outer_rect.height().at_least(interact_size.y));
let inner_rect = outer_rect.shrink2(margin);
let mut content_ui = ui.new_child(UiBuilder::new().max_rect(inner_rect));
add_contents(&mut content_ui);
let mut outer_rect = content_ui.min_rect().expand2(margin);
outer_rect.set_height(outer_rect.height().at_least(interact_size.y));
let response = ui.interact(outer_rect, id, sense);
if ui.is_rect_visible(outer_rect) {
let visuals = if is_popup_open {
&ui.visuals().widgets.open
} else {
ui.style().interact(&response)
};
ui.painter().set(
where_to_put_background,
epaint::RectShape::new(
outer_rect.expand(visuals.expansion),
visuals.rounding,
visuals.weak_bg_fill,
visuals.bg_stroke,
),
);
}
ui.advance_cursor_after_rect(outer_rect);
response
}
fn paint_default_icon(
painter: &Painter,
rect: Rect,
visuals: &WidgetVisuals,
above_or_below: AboveOrBelow,
) {
let rect = Rect::from_center_size(
rect.center(),
vec2(rect.width() * 0.7, rect.height() * 0.45),
);
match above_or_below {
AboveOrBelow::Above => {
painter.add(Shape::convex_polygon(
vec![rect.left_bottom(), rect.right_bottom(), rect.center_top()],
visuals.fg_stroke.color,
Stroke::NONE,
));
}
AboveOrBelow::Below => {
painter.add(Shape::convex_polygon(
vec![rect.left_top(), rect.right_top(), rect.center_bottom()],
visuals.fg_stroke.color,
Stroke::NONE,
));
}
}
}