use egui::{Color32, CornerRadius, Rect, Response, Sense, Stroke, StrokeKind, Ui, Vec2, WidgetInfo, WidgetType};
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct ButtonVisuals {
pub fill: Color32,
pub stroke: Stroke,
pub icon: Color32,
}
#[derive(Clone, Copy, Debug)]
pub struct IconButtonStyle {
pub normal: ButtonVisuals,
pub hovered: ButtonVisuals,
pub pressed: Option<ButtonVisuals>,
pub selected: ButtonVisuals,
pub corner_radius: CornerRadius,
pub focus_stroke: Stroke,
}
const MAX_SIDE: f32 = 4096.0;
pub struct IconButton<'a> {
label: &'a str,
size: Vec2,
style: &'a IconButtonStyle,
selected: bool,
single_shape: bool,
}
impl<'a> IconButton<'a> {
pub fn new(label: &'a str, size: Vec2, style: &'a IconButtonStyle) -> Self {
Self { label, size, style, selected: false, single_shape: false }
}
pub fn selected(mut self, selected: bool) -> Self {
self.selected = selected;
self
}
pub fn single_shape_frame(mut self) -> Self {
self.single_shape = true;
self
}
pub fn show(self, ui: &mut Ui, paint_icon: impl FnOnce(&Ui, Rect, Color32)) -> Response {
let (label, selected) = (self.label, self.selected);
let size_ok = self.size.is_finite() && self.size.x > 0.0 && self.size.y > 0.0 && self.size.x <= MAX_SIDE && self.size.y <= MAX_SIDE;
if !size_ok {
let (_, response) = ui.allocate_exact_size(Vec2::ZERO, Sense::hover());
response.widget_info(|| WidgetInfo::selected(WidgetType::Button, false, selected, label));
return response;
}
let (rect, response) = ui.allocate_exact_size(self.size, Sense::click());
let enabled = ui.is_enabled();
response.widget_info(|| WidgetInfo::selected(WidgetType::Button, enabled, selected, label));
let style = self.style;
let visuals = if selected {
style.selected
} else if let Some(pressed) = style.pressed.filter(|_| response.is_pointer_button_down_on()) {
pressed
} else if response.hovered() {
style.hovered
} else {
style.normal
};
let painter = ui.painter();
let radius = style.corner_radius;
let stroke = bounded(visuals.stroke, rect);
if self.single_shape {
painter.rect(rect, radius, visuals.fill, stroke, StrokeKind::Inside);
} else {
if visuals.fill != Color32::TRANSPARENT {
painter.rect_filled(rect, radius, visuals.fill);
}
if !stroke.is_empty() {
painter.rect_stroke(rect, radius, stroke, StrokeKind::Inside);
}
}
paint_icon(ui, rect, visuals.icon);
if response.has_focus() && rect.width() > 4.0 && rect.height() > 4.0 {
let inner = rect.shrink(2.0);
let focus = bounded(style.focus_stroke, inner);
if !focus.is_empty() {
ui.painter().rect_stroke(inner, radius, focus, StrokeKind::Inside);
}
}
response
}
}
fn bounded(stroke: Stroke, rect: Rect) -> Stroke {
if !stroke.width.is_finite() || stroke.width <= 0.0 {
return Stroke::NONE;
}
Stroke::new(stroke.width.min(rect.width().min(rect.height()) / 2.0), stroke.color)
}
#[cfg(test)]
mod tests;