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;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum IconButtonSize {
Small,
#[default]
Medium,
Large,
}
impl IconButtonSize {
pub fn pixels(&self) -> f32 {
match self {
IconButtonSize::Small => 32.0,
IconButtonSize::Medium => 40.0,
IconButtonSize::Large => 48.0,
}
}
pub fn icon_size(&self) -> f32 {
match self {
IconButtonSize::Small => 16.0,
IconButtonSize::Medium => 20.0,
IconButtonSize::Large => 24.0,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum IconButtonVariant {
#[default]
Ghost,
Filled,
Outline,
Destructive,
}
pub struct IconButton {
base: WidgetBase,
icon: String,
tooltip: Option<String>,
size: IconButtonSize,
variant: IconButtonVariant,
disabled: bool,
on_click: Option<Box<dyn Fn() + Send + Sync>>,
}
impl IconButton {
pub fn new(icon: impl Into<String>) -> Self {
Self {
base: WidgetBase::new().with_class("icon-button"),
icon: icon.into(),
tooltip: None,
size: IconButtonSize::default(),
variant: IconButtonVariant::default(),
disabled: false,
on_click: None,
}
}
pub fn tooltip(mut self, text: impl Into<String>) -> Self {
self.tooltip = Some(text.into());
self
}
pub fn size(mut self, size: IconButtonSize) -> Self {
self.size = size;
self
}
pub fn variant(mut self, variant: IconButtonVariant) -> Self {
self.variant = variant;
self
}
pub fn disabled(mut self, disabled: bool) -> Self {
self.disabled = disabled;
self.base.state.disabled = disabled;
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 class(mut self, class: &str) -> Self {
self.base.classes.add(class);
self
}
fn background_color(&self, theme: &crate::theme::ThemeData) -> Color {
if self.disabled {
return Color::TRANSPARENT;
}
let base = match self.variant {
IconButtonVariant::Ghost => Color::TRANSPARENT,
IconButtonVariant::Filled => theme.colors.secondary,
IconButtonVariant::Outline => Color::TRANSPARENT,
IconButtonVariant::Destructive => {
if self.base.state.hovered || self.base.state.pressed {
theme.colors.destructive
} else {
Color::TRANSPARENT
}
}
};
if self.base.state.pressed {
match self.variant {
IconButtonVariant::Ghost => theme.colors.accent.with_alpha(0.3),
IconButtonVariant::Filled => base.darken(15.0),
IconButtonVariant::Outline => theme.colors.accent.with_alpha(0.2),
IconButtonVariant::Destructive => base.darken(15.0),
}
} else if self.base.state.hovered {
match self.variant {
IconButtonVariant::Ghost => theme.colors.accent.with_alpha(0.1),
IconButtonVariant::Filled => base.darken(10.0),
IconButtonVariant::Outline => theme.colors.accent.with_alpha(0.1),
IconButtonVariant::Destructive => base,
}
} else {
base
}
}
fn foreground_color(&self, theme: &crate::theme::ThemeData) -> Color {
if self.disabled {
return theme.colors.muted_foreground;
}
match self.variant {
IconButtonVariant::Ghost => theme.colors.foreground,
IconButtonVariant::Filled => theme.colors.secondary_foreground,
IconButtonVariant::Outline => theme.colors.foreground,
IconButtonVariant::Destructive => {
if self.base.state.hovered || self.base.state.pressed {
theme.colors.destructive_foreground
} else {
theme.colors.destructive
}
}
}
}
}
impl Widget for IconButton {
fn id(&self) -> WidgetId {
self.base.id
}
fn type_name(&self) -> &'static str {
"icon-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 size = self.size.pixels();
Size::new(size, size)
}
fn layout(&mut self, constraints: Constraints, ctx: &LayoutContext) -> LayoutResult {
let size = constraints.constrain(self.intrinsic_size(ctx));
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 radius = BorderRadius::all(theme.radii.md * theme.typography.base_size);
let bg_color = self.background_color(theme);
if bg_color != Color::TRANSPARENT {
painter.fill_rounded_rect(rect, bg_color, radius);
}
if self.variant == IconButtonVariant::Outline {
let border_color = if self.base.state.focused {
theme.colors.ring
} else {
theme.colors.border
};
painter.stroke_rect(rect, border_color, 1.0);
}
let icon_size = self.size.icon_size();
let icon_x = rect.x() + (rect.width() - icon_size * 0.6) / 2.0;
let icon_y = rect.y() + (rect.height() + icon_size * 0.8) / 2.0;
painter.draw_text(
&self.icon,
Point::new(icon_x, icon_y),
self.foreground_color(theme),
icon_size,
);
if self.base.state.focused && ctx.focus_visible {
let ring_rect = Rect::new(
rect.x() - 2.0,
rect.y() - 2.0,
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 self.disabled {
return EventResult::Ignored;
}
if let Event::Mouse(mouse) = event {
let in_bounds = self.bounds().contains(mouse.position);
match mouse.kind {
MouseEventKind::Move | MouseEventKind::Enter => {
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 => {
if 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;
}
}