use egui::{Color32, FontId, Rect, Response, Sense, Stroke, StrokeKind, Ui, Vec2, Widget, vec2};
use super::{alpha, corner};
use crate::{Icon, Palette, RADIUS, SPACING, palette_of};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ButtonVariant {
Primary,
Secondary,
Ghost,
Danger,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ButtonSize {
Sm,
#[default]
Md,
Lg,
}
impl ButtonSize {
fn height(self) -> f32 {
match self {
Self::Sm => 26.0,
Self::Md => 32.0,
Self::Lg => 40.0,
}
}
fn font(self) -> f32 {
match self {
Self::Sm => 12.0,
Self::Md => 14.0,
Self::Lg => 15.0,
}
}
fn pad_x(self) -> f32 {
match self {
Self::Sm => SPACING.s2,
Self::Md => SPACING.s3,
Self::Lg => SPACING.s4,
}
}
fn icon(self) -> f32 {
match self {
Self::Sm => 14.0,
Self::Md => 16.0,
Self::Lg => 18.0,
}
}
}
pub struct Button<'a> {
label: &'a str,
variant: ButtonVariant,
size: ButtonSize,
leading: Option<Icon>,
trailing: Option<Icon>,
disabled: bool,
full_width: bool,
}
impl<'a> Button<'a> {
pub fn new(label: &'a str) -> Self {
Self {
label,
variant: ButtonVariant::Primary,
size: ButtonSize::Md,
leading: None,
trailing: None,
disabled: false,
full_width: false,
}
}
pub fn primary(label: &'a str) -> Self {
Self::new(label).variant(ButtonVariant::Primary)
}
pub fn secondary(label: &'a str) -> Self {
Self::new(label).variant(ButtonVariant::Secondary)
}
pub fn ghost(label: &'a str) -> Self {
Self::new(label).variant(ButtonVariant::Ghost)
}
pub fn danger(label: &'a str) -> Self {
Self::new(label).variant(ButtonVariant::Danger)
}
pub fn variant(mut self, v: ButtonVariant) -> Self {
self.variant = v;
self
}
pub fn size(mut self, s: ButtonSize) -> Self {
self.size = s;
self
}
pub fn leading(mut self, icon: Icon) -> Self {
self.leading = Some(icon);
self
}
pub fn trailing(mut self, icon: Icon) -> Self {
self.trailing = Some(icon);
self
}
pub fn disabled(mut self, disabled: bool) -> Self {
self.disabled = disabled;
self
}
pub fn full_width(mut self) -> Self {
self.full_width = true;
self
}
}
impl<'a> Widget for Button<'a> {
fn ui(self, ui: &mut Ui) -> Response {
let palette = palette_of(ui.ctx());
let font = FontId::new(self.size.font(), egui::FontFamily::Proportional);
let text_color = text_color(&palette, self.variant);
let galley = ui
.painter()
.layout_no_wrap(self.label.into(), font.clone(), text_color);
let icon_size = self.size.icon();
let gap = SPACING.s2;
let pad_x = self.size.pad_x();
let mut content_w = galley.size().x;
if self.leading.is_some() {
content_w += icon_size + gap;
}
if self.trailing.is_some() {
content_w += icon_size + gap;
}
let min_w = content_w + pad_x * 2.0;
let height = self.size.height();
let desired = if self.full_width {
vec2(ui.available_width(), height)
} else {
vec2(min_w, height)
};
let sense = if self.disabled {
Sense::hover()
} else {
Sense::click()
};
let (rect, response) = ui.allocate_exact_size(desired, sense);
let (fill, stroke_color) = colors(&palette, self.variant, &response, self.disabled);
let stroke = match self.variant {
ButtonVariant::Secondary => Stroke::new(1.0, stroke_color),
ButtonVariant::Ghost if response.hovered() => Stroke::new(1.0, stroke_color),
_ => Stroke::NONE,
};
ui.painter()
.rect(rect, corner(RADIUS.md), fill, stroke, StrokeKind::Inside);
if response.has_focus() {
let ring = rect.expand(2.0);
ui.painter().rect_stroke(
ring,
corner(RADIUS.md + 2.0),
Stroke::new(2.0, palette.focus_ring),
StrokeKind::Outside,
);
}
let text_w = galley.size().x;
let mut total_w = text_w;
if self.leading.is_some() {
total_w += icon_size + gap;
}
if self.trailing.is_some() {
total_w += icon_size + gap;
}
let mut cursor_x = rect.center().x - total_w / 2.0;
let cy = rect.center().y;
if let Some(icon) = self.leading {
let r = Rect::from_min_size(
egui::pos2(cursor_x, cy - icon_size / 2.0),
Vec2::splat(icon_size),
);
icon.paint(ui.painter(), r, text_color);
cursor_x += icon_size + gap;
}
let text_pos = egui::pos2(cursor_x, cy - galley.size().y / 2.0);
ui.painter().galley(text_pos, galley, text_color);
cursor_x += text_w;
if let Some(icon) = self.trailing {
cursor_x += gap;
let r = Rect::from_min_size(
egui::pos2(cursor_x, cy - icon_size / 2.0),
Vec2::splat(icon_size),
);
icon.paint(ui.painter(), r, text_color);
}
response
}
}
pub struct IconButton {
icon: Icon,
variant: ButtonVariant,
size: ButtonSize,
disabled: bool,
tooltip: Option<String>,
}
impl IconButton {
pub fn new(icon: Icon) -> Self {
Self {
icon,
variant: ButtonVariant::Ghost,
size: ButtonSize::Md,
disabled: false,
tooltip: None,
}
}
pub fn variant(mut self, v: ButtonVariant) -> Self {
self.variant = v;
self
}
pub fn size(mut self, s: ButtonSize) -> Self {
self.size = s;
self
}
pub fn disabled(mut self, disabled: bool) -> Self {
self.disabled = disabled;
self
}
pub fn tooltip(mut self, text: impl Into<String>) -> Self {
self.tooltip = Some(text.into());
self
}
}
impl Widget for IconButton {
fn ui(self, ui: &mut Ui) -> Response {
let palette = palette_of(ui.ctx());
let h = self.size.height();
let sense = if self.disabled {
Sense::hover()
} else {
Sense::click()
};
let (rect, response) = ui.allocate_exact_size(vec2(h, h), sense);
let (fill, stroke_color) = colors(&palette, self.variant, &response, self.disabled);
let stroke = match self.variant {
ButtonVariant::Secondary => Stroke::new(1.0, stroke_color),
ButtonVariant::Ghost if response.hovered() => Stroke::new(1.0, stroke_color),
_ => Stroke::NONE,
};
ui.painter()
.rect(rect, corner(RADIUS.md), fill, stroke, StrokeKind::Inside);
if response.has_focus() {
let ring = rect.expand(2.0);
ui.painter().rect_stroke(
ring,
corner(RADIUS.md + 2.0),
Stroke::new(2.0, palette.focus_ring),
StrokeKind::Outside,
);
}
let icon_size = self.size.icon();
let r = Rect::from_center_size(rect.center(), Vec2::splat(icon_size));
self.icon
.paint(ui.painter(), r, text_color(&palette, self.variant));
if let Some(tip) = &self.tooltip {
return response.on_hover_text(tip);
}
response
}
}
fn text_color(p: &Palette, v: ButtonVariant) -> Color32 {
match v {
ButtonVariant::Primary => p.text_on_brand,
ButtonVariant::Secondary => p.text_primary,
ButtonVariant::Ghost => p.text_primary,
ButtonVariant::Danger => p.text_on_brand,
}
}
fn colors(
p: &Palette,
v: ButtonVariant,
response: &Response,
disabled: bool,
) -> (Color32, Color32) {
let (mut fill, mut stroke) = match v {
ButtonVariant::Primary => (p.brand_default, p.brand_default),
ButtonVariant::Secondary => (p.bg_surface, p.border_default),
ButtonVariant::Ghost => (Color32::TRANSPARENT, p.border_default),
ButtonVariant::Danger => (p.error, p.error),
};
if !disabled {
if response.is_pointer_button_down_on() {
fill = match v {
ButtonVariant::Primary => p.brand_pressed,
ButtonVariant::Secondary => p.bg_pressed,
ButtonVariant::Ghost => p.bg_pressed,
ButtonVariant::Danger => alpha(p.error, 0.85),
};
stroke = fill;
} else if response.hovered() {
fill = match v {
ButtonVariant::Primary => p.brand_hover,
ButtonVariant::Secondary => p.bg_hover,
ButtonVariant::Ghost => p.bg_hover,
ButtonVariant::Danger => alpha(p.error, 0.92),
};
stroke = match v {
ButtonVariant::Secondary => p.brand_default,
_ => fill,
};
}
}
if disabled {
fill = alpha(fill, 0.45);
stroke = alpha(stroke, 0.45);
}
(fill, stroke)
}