use egui::{
vec2, Color32, CornerRadius, Response, Sense, Stroke, Ui, Vec2, Widget, WidgetInfo, WidgetText,
WidgetType,
};
use crate::theme::{mix, Accent, Theme};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ButtonSize {
Small,
Medium,
Large,
}
impl ButtonSize {
pub fn padding(self, theme: &Theme) -> Vec2 {
match self {
ButtonSize::Small => vec2(theme.control_padding_x * 0.6, theme.control_padding_y * 0.6),
ButtonSize::Medium => vec2(theme.control_padding_x, theme.control_padding_y),
ButtonSize::Large => vec2(
theme.control_padding_x * 1.25,
theme.control_padding_y * 1.2,
),
}
}
pub fn font_size(self, theme: &Theme) -> f32 {
match self {
ButtonSize::Small => theme.typography.small,
ButtonSize::Medium => theme.typography.button,
ButtonSize::Large => theme.typography.body + 1.0,
}
}
}
#[must_use = "Call `ui.add(...)` to render the button."]
pub struct Button {
text: WidgetText,
accent: Accent,
size: ButtonSize,
outline: bool,
min_width: Option<f32>,
full_width: bool,
enabled: bool,
}
impl std::fmt::Debug for Button {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Button")
.field("accent", &self.accent)
.field("size", &self.size)
.field("outline", &self.outline)
.field("min_width", &self.min_width)
.field("full_width", &self.full_width)
.field("enabled", &self.enabled)
.finish()
}
}
impl Button {
pub fn new(text: impl Into<WidgetText>) -> Self {
Self {
text: text.into(),
accent: Accent::Blue,
size: ButtonSize::Medium,
outline: false,
min_width: None,
full_width: false,
enabled: true,
}
}
#[inline]
pub fn accent(mut self, accent: Accent) -> Self {
self.accent = accent;
self
}
#[inline]
pub fn outline(mut self) -> Self {
self.outline = true;
self
}
#[inline]
pub fn size(mut self, size: ButtonSize) -> Self {
self.size = size;
self
}
#[inline]
pub fn min_width(mut self, w: f32) -> Self {
self.min_width = Some(w);
self
}
#[inline]
pub fn full_width(mut self) -> Self {
self.full_width = true;
self
}
#[inline]
pub fn enabled(mut self, enabled: bool) -> Self {
self.enabled = enabled;
self
}
fn padding(&self, theme: &Theme) -> Vec2 {
self.size.padding(theme)
}
fn font_size(&self, theme: &Theme) -> f32 {
self.size.font_size(theme)
}
}
impl Widget for Button {
fn ui(self, ui: &mut Ui) -> Response {
let theme = Theme::current(ui.ctx());
let padding = self.padding(&theme);
let font_size = self.font_size(&theme);
let wrap_width = (ui.available_width() - 2.0 * padding.x).max(0.0);
let galley =
crate::theme::placeholder_galley(ui, self.text.text(), font_size, false, wrap_width);
let mut desired = galley.size() + 2.0 * padding;
desired.y = desired.y.max(font_size + 2.0 * padding.y);
if let Some(min_w) = self.min_width {
desired.x = desired.x.max(min_w);
}
if self.full_width {
desired.x = ui.available_width().max(desired.x);
}
let sense = if self.enabled {
Sense::click()
} else {
Sense::hover()
};
let (rect, response) = ui.allocate_exact_size(desired, sense);
let visible = ui.is_rect_visible(rect);
if visible {
let (fill, stroke, text_color) =
resolve_colors(&theme, self.accent, self.outline, self.enabled, &response);
let radius = CornerRadius::same(theme.control_radius as u8);
ui.painter()
.rect(rect, radius, fill, stroke, egui::StrokeKind::Inside);
let text_pos = rect.center();
ui.painter()
.galley(galley_top_left(rect, galley.size()), galley, text_color);
let _ = text_pos;
}
response.widget_info(|| {
WidgetInfo::labeled(WidgetType::Button, self.enabled, self.text.text())
});
response
}
}
fn galley_top_left(rect: egui::Rect, galley_size: Vec2) -> egui::Pos2 {
let center = rect.center();
center - galley_size * 0.5
}
fn resolve_colors(
theme: &Theme,
accent: Accent,
outline: bool,
enabled: bool,
response: &Response,
) -> (Color32, Stroke, Color32) {
let p = &theme.palette;
if !enabled {
if outline {
return (
Color32::TRANSPARENT,
Stroke::new(1.0, p.border),
mix(p.text_muted, p.card, 0.4),
);
}
return (
mix(p.accent_fill(accent), p.card, 0.55),
Stroke::NONE,
mix(p.text, p.card, 0.4),
);
}
let is_down = response.is_pointer_button_down_on();
let is_hovered = response.hovered();
if outline {
let text = if is_hovered { p.text } else { p.text_muted };
let stroke_color = if is_hovered { p.text_muted } else { p.border };
let fill = if is_down {
with_alpha(p.text_muted, 30)
} else if is_hovered {
with_alpha(p.text_muted, 20)
} else {
Color32::TRANSPARENT
};
return (fill, Stroke::new(1.0, stroke_color), text);
}
let resting = p.accent_fill(accent);
let hover = p.accent_hover(accent);
let fill = if is_down {
mix(hover, Color32::BLACK, 0.08)
} else if is_hovered {
hover
} else {
resting
};
let stroke = if response.has_focus() {
Stroke::new(2.0, with_alpha(p.sky, 180))
} else {
Stroke::NONE
};
(fill, stroke, Color32::WHITE)
}
fn with_alpha(c: Color32, alpha: u8) -> Color32 {
crate::theme::with_alpha(c, alpha)
}