use super::{Widget, WidgetBase, WidgetId, LayoutContext, PaintContext, EventContext};
use crate::css::{ClassList, WidgetState};
use crate::event::{Event, EventResult, MouseEventKind, MouseButton};
use crate::geometry::{BorderRadius, Color, Point, Rect, Size};
use crate::layout::{Constraints, LayoutResult};
use crate::render::Painter;
pub struct Button {
base: WidgetBase,
label: String,
on_click: Option<Box<dyn Fn() + Send + Sync>>,
variant: ButtonVariant,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ButtonVariant {
#[default]
Primary,
Secondary,
Outline,
Ghost,
Destructive,
}
impl Button {
pub fn new(label: impl Into<String>) -> Self {
Self {
base: WidgetBase::new().with_class("button"),
label: label.into(),
on_click: None,
variant: ButtonVariant::Primary,
}
}
pub fn label(mut self, label: impl Into<String>) -> Self {
self.label = label.into();
self
}
pub fn on_click<F>(mut self, handler: F) -> Self
where
F: Fn() + Send + Sync + 'static,
{
self.on_click = Some(Box::new(handler));
self
}
pub fn variant(mut self, variant: ButtonVariant) -> Self {
self.variant = variant;
let class = match variant {
ButtonVariant::Primary => "btn-primary",
ButtonVariant::Secondary => "btn-secondary",
ButtonVariant::Outline => "btn-outline",
ButtonVariant::Ghost => "btn-ghost",
ButtonVariant::Destructive => "btn-destructive",
};
self.base.classes.add(class);
self
}
pub fn class(mut self, class: &str) -> Self {
self.base.classes.add(class);
self
}
pub fn id(mut self, id: &str) -> Self {
self.base.element_id = Some(id.to_string());
self
}
fn background_color(&self, theme: &crate::theme::ThemeData) -> Color {
let base = match self.variant {
ButtonVariant::Primary => theme.colors.primary,
ButtonVariant::Secondary => theme.colors.secondary,
ButtonVariant::Outline => Color::TRANSPARENT,
ButtonVariant::Ghost => Color::TRANSPARENT,
ButtonVariant::Destructive => theme.colors.destructive,
};
if self.base.state.disabled {
base.with_alpha(0.5)
} else if self.base.state.pressed {
base.darken(15.0)
} else if self.base.state.hovered {
base.darken(10.0)
} else {
base
}
}
fn text_color(&self, theme: &crate::theme::ThemeData) -> Color {
match self.variant {
ButtonVariant::Primary => theme.colors.primary_foreground,
ButtonVariant::Secondary => theme.colors.secondary_foreground,
ButtonVariant::Outline => theme.colors.foreground,
ButtonVariant::Ghost => theme.colors.foreground,
ButtonVariant::Destructive => theme.colors.destructive_foreground,
}
}
}
impl Widget for Button {
fn id(&self) -> WidgetId {
self.base.id
}
fn type_name(&self) -> &'static str {
"button"
}
fn element_id(&self) -> Option<&str> {
self.base.element_id.as_deref()
}
fn classes(&self) -> &ClassList {
&self.base.classes
}
fn state(&self) -> WidgetState {
self.base.state
}
fn intrinsic_size(&self, _ctx: &LayoutContext) -> Size {
let font_size = 14.0;
let char_width = font_size * 0.6;
let text_width = self.label.len() as f32 * char_width;
let padding_h = 16.0 * 2.0; let padding_v = 8.0 * 2.0; Size::new(text_width + padding_h, font_size * 1.5 + padding_v)
}
fn layout(&mut self, constraints: Constraints, ctx: &LayoutContext) -> LayoutResult {
let intrinsic = self.intrinsic_size(ctx);
let size = constraints.constrain(intrinsic);
self.base.bounds.size = size;
LayoutResult::new(size)
}
fn paint(&self, painter: &mut Painter, rect: Rect, ctx: &PaintContext) {
let theme = ctx.style_ctx.theme;
let bg_color = self.background_color(theme);
let radius = BorderRadius::all(theme.radii.md * theme.typography.base_size);
painter.fill_rounded_rect(rect, bg_color, radius);
if self.variant == ButtonVariant::Outline {
painter.stroke_rect(rect, theme.colors.border, 1.0);
}
let text_color = self.text_color(theme);
let font_size = 14.0;
let text_x = rect.x() + (rect.width() - self.label.len() as f32 * font_size * 0.6) / 2.0;
let text_y = rect.y() + (rect.height() + font_size * 0.8) / 2.0;
painter.draw_text(&self.label, Point::new(text_x, text_y), text_color, font_size);
if self.base.state.focused && ctx.focus_visible {
let ring_rect = rect.offset(-2.0, -2.0);
let ring_rect = Rect::new(
ring_rect.x(),
ring_rect.y(),
rect.width() + 4.0,
rect.height() + 4.0,
);
painter.stroke_rect(ring_rect, theme.colors.ring, 2.0);
}
}
fn handle_event(&mut self, event: &Event, ctx: &mut EventContext) -> EventResult {
if let Event::Mouse(mouse) = event {
let in_bounds = self.bounds().contains(mouse.position);
match mouse.kind {
MouseEventKind::Enter | MouseEventKind::Move => {
if in_bounds && !self.base.state.hovered {
self.base.state.hovered = true;
ctx.request_redraw();
} else if !in_bounds && self.base.state.hovered {
self.base.state.hovered = false;
ctx.request_redraw();
}
}
MouseEventKind::Leave => {
if self.base.state.hovered {
self.base.state.hovered = false;
ctx.request_redraw();
}
}
MouseEventKind::Down => {
if in_bounds && mouse.button == Some(MouseButton::Left) {
self.base.state.pressed = true;
ctx.request_focus(self.base.id);
ctx.request_redraw();
return EventResult::Handled;
}
}
MouseEventKind::Up => {
if self.base.state.pressed && mouse.button == Some(MouseButton::Left) {
self.base.state.pressed = false;
if in_bounds {
if let Some(handler) = &self.on_click {
handler();
}
}
ctx.request_redraw();
return EventResult::Handled;
}
}
_ => {}
}
}
EventResult::Ignored
}
fn bounds(&self) -> Rect {
self.base.bounds
}
fn set_bounds(&mut self, bounds: Rect) {
self.base.bounds = bounds;
}
}