use egui::{
Atom, AtomExt as _, AtomKind, Color32, AtomLayout, AtomLayoutResponse, Frame, Image, IntoAtoms,
NumExt as _, Response, Sense, Shadow, TextStyle, TextWrapMode, Ui, Vec2, Widget, WidgetInfo,
WidgetText, WidgetType,
};
use zeus_theme::visuals::ButtonVisuals;
#[must_use = "You should put this widget in a ui with `ui.add(widget);`"]
pub struct Button<'a> {
layout: AtomLayout<'a>,
visuals: Option<ButtonVisuals>,
bg_color: Option<Color32>,
small: bool,
frame_when_inactive: bool,
min_size: Vec2,
selected: bool,
image_tint_follows_text_color: bool,
limit_image_size: bool,
}
impl<'a> Button<'a> {
pub fn new(atoms: impl IntoAtoms<'a>) -> Self {
Self {
layout: AtomLayout::new(atoms.into_atoms())
.sense(Sense::click())
.fallback_font(TextStyle::Button),
visuals: None,
bg_color: None,
small: false,
frame_when_inactive: true,
min_size: Vec2::ZERO,
selected: false,
image_tint_follows_text_color: false,
limit_image_size: false,
}
}
pub fn selectable(selected: bool, atoms: impl IntoAtoms<'a>) -> Self {
Self::new(atoms).selected(selected).frame_when_inactive(selected)
}
pub fn image(image: impl Into<Image<'a>>) -> Self {
Self::opt_image_and_text(Some(image.into()), None)
}
pub fn image_and_text(image: impl Into<Image<'a>>, text: impl Into<WidgetText>) -> Self {
Self::opt_image_and_text(Some(image.into()), Some(text.into()))
}
pub fn opt_image_and_text(image: Option<Image<'a>>, text: Option<WidgetText>) -> Self {
let mut button = Self::new(());
if let Some(image) = image {
button.layout.push_right(image);
}
if let Some(text) = text {
button.layout.push_right(text);
}
button.limit_image_size = true;
button
}
#[inline]
pub fn wrap_mode(mut self, wrap_mode: TextWrapMode) -> Self {
self.layout = self.layout.wrap_mode(wrap_mode);
self
}
#[inline]
pub fn wrap(self) -> Self {
self.wrap_mode(TextWrapMode::Wrap)
}
#[inline]
pub fn truncate(self) -> Self {
self.wrap_mode(TextWrapMode::Truncate)
}
#[inline]
pub fn small(mut self) -> Self {
self.small = true;
self
}
#[inline]
pub fn frame_when_inactive(mut self, frame_when_inactive: bool) -> Self {
self.frame_when_inactive = frame_when_inactive;
self
}
#[inline]
pub fn sense(mut self, sense: Sense) -> Self {
self.layout = self.layout.sense(sense);
self
}
#[inline]
pub fn min_size(mut self, min_size: Vec2) -> Self {
self.min_size = min_size;
self
}
#[inline]
pub fn image_tint_follows_text_color(mut self, image_tint_follows_text_color: bool) -> Self {
self.image_tint_follows_text_color = image_tint_follows_text_color;
self
}
#[inline]
pub fn shortcut_text(mut self, shortcut_text: impl Into<Atom<'a>>) -> Self {
let mut atom = shortcut_text.into();
atom.kind = match atom.kind {
AtomKind::Text(text) => AtomKind::Text(text.weak()),
other => other,
};
self.layout.push_right(Atom::grow());
self.layout.push_right(atom);
self
}
#[inline]
pub fn right_text(mut self, right_text: impl Into<Atom<'a>>) -> Self {
self.layout.push_right(Atom::grow());
self.layout.push_right(right_text.into());
self
}
#[inline]
pub fn selected(mut self, selected: bool) -> Self {
self.selected = selected;
self
}
#[inline]
pub fn visuals(mut self, visuals: ButtonVisuals) -> Self {
self.visuals = Some(visuals);
self
}
#[inline]
pub fn bg_color(mut self, color: Color32) -> Self {
self.bg_color = Some(color);
self
}
pub fn atom_ui(self, ui: &mut Ui) -> AtomLayoutResponse {
let Button {
mut layout,
bg_color,
small,
visuals,
frame_when_inactive,
mut min_size,
selected,
image_tint_follows_text_color,
limit_image_size,
} = self;
if !small {
min_size.y = min_size.y.at_least(ui.spacing().interact_size.y);
}
if limit_image_size {
layout.map_atoms(|atom| {
if matches!(&atom.kind, AtomKind::Image(_)) {
atom.atom_max_height_font_size(ui)
} else {
atom
}
});
}
let text = layout.text().map(String::from);
let has_frame_margin = visuals.is_some() || ui.visuals().button_frame;
let mut button_padding = if has_frame_margin {
ui.spacing().button_padding
} else {
Vec2::ZERO
};
if small {
button_padding.y = 0.0;
}
let frame = Frame::new().inner_margin(button_padding);
let mut prepared = layout.frame(frame).min_size(min_size).allocate(ui);
let response = if ui.is_rect_visible(prepared.response.rect) {
let interact_visuals = ui.style().interact_selectable(&prepared.response, selected);
let is_active = prepared.response.is_pointer_button_down_on(); let is_hovered = prepared.response.hovered();
let (fill, stroke, corner_radius) = if let Some(v) = &visuals {
if is_active {
(v.bg_click, v.border_click, v.corner_radius)
} else if is_hovered {
(v.bg_hover, v.border_hover, v.corner_radius)
} else {
let bg = bg_color.unwrap_or(v.bg);
(bg, v.border, v.corner_radius)
}
} else {
(
interact_visuals.weak_bg_fill,
interact_visuals.bg_stroke,
interact_visuals.corner_radius,
)
};
let fill = match selected {
false => fill,
true => {
visuals.as_ref().map(|v| v.bg_selected).unwrap_or(interact_visuals.weak_bg_fill)
}
};
let text_color = visuals.as_ref().map(|v| v.text).unwrap_or(interact_visuals.text_color());
let visible_frame = if frame_when_inactive {
has_frame_margin
} else {
has_frame_margin && (is_hovered || is_active || prepared.response.has_focus())
};
if image_tint_follows_text_color {
prepared.map_images(|image| image.tint(text_color));
}
prepared.fallback_text_color = text_color;
if visible_frame {
let shadow = visuals.as_ref().map(|v| v.shadow).unwrap_or(Shadow::NONE);
prepared.frame = prepared
.frame
.inner_margin(
button_padding + Vec2::splat(interact_visuals.expansion)
- Vec2::splat(stroke.width),
)
.outer_margin(-Vec2::splat(interact_visuals.expansion))
.fill(fill)
.stroke(stroke)
.corner_radius(corner_radius)
.shadow(shadow);
}
prepared.paint(ui)
} else {
AtomLayoutResponse::empty(prepared.response)
};
response.response.widget_info(|| {
if let Some(text) = &text {
WidgetInfo::labeled(WidgetType::Button, ui.is_enabled(), text)
} else {
WidgetInfo::new(WidgetType::Button)
}
});
response
}
}
impl Widget for Button<'_> {
fn ui(self, ui: &mut Ui) -> Response {
self.atom_ui(ui).response
}
}