use std::rc::Rc;
use std::sync::Arc;
use crate::color::Color;
use crate::event::{Event, EventResult, MouseButton};
use crate::geometry::{Rect, Size};
use crate::draw_ctx::DrawCtx;
use crate::layout_props::{HAnchor, Insets, VAnchor, WidgetBase};
use crate::text::Font;
use crate::widget::Widget;
use crate::widgets::label::{Label, LabelAlign};
pub struct ButtonTheme {
pub background: Color,
pub background_hovered: Color,
pub background_pressed: Color,
pub label_color: Color,
pub border_radius: f64,
pub focus_ring_color: Color,
pub focus_ring_width: f64,
}
impl Default for ButtonTheme {
fn default() -> Self {
Self {
background: Color::rgb(0.22, 0.45, 0.88),
background_hovered: Color::rgb(0.30, 0.52, 0.92),
background_pressed: Color::rgb(0.16, 0.36, 0.72),
label_color: Color::white(),
border_radius: 6.0,
focus_ring_color: Color::rgba(0.22, 0.45, 0.88, 0.55),
focus_ring_width: 2.5,
}
}
}
pub struct Button {
bounds: Rect,
children: Vec<Box<dyn Widget>>,
base: WidgetBase,
label_text: String,
font: Arc<Font>,
font_size: f64,
pub theme: ButtonTheme,
on_click: Option<Box<dyn FnMut()>>,
enabled_fn: Option<Rc<dyn Fn() -> bool>>,
hovered: bool,
pressed: bool,
focused: bool,
}
impl Button {
pub fn new(label: impl Into<String>, font: Arc<Font>) -> Self {
let label_text: String = label.into();
let font_size = 14.0;
let theme = ButtonTheme::default();
let child = Self::build_label(&label_text, &font, font_size, &theme);
Self {
bounds: Rect::default(),
children: vec![child],
base: WidgetBase::new(),
label_text,
font,
font_size,
theme,
on_click: None,
enabled_fn: None,
hovered: false,
pressed: false,
focused: false,
}
}
pub fn with_font_size(mut self, size: f64) -> Self {
self.font_size = size;
self.children[0] = Self::build_label(&self.label_text, &self.font, size, &self.theme);
self
}
pub fn with_theme(mut self, theme: ButtonTheme) -> Self {
self.theme = theme;
self.children[0] = Self::build_label(&self.label_text, &self.font, self.font_size, &self.theme);
self
}
pub fn on_click(mut self, cb: impl FnMut() + 'static) -> Self {
self.on_click = Some(Box::new(cb));
self
}
pub fn with_enabled_fn(mut self, f: impl Fn() -> bool + 'static) -> Self {
self.enabled_fn = Some(Rc::new(f));
self
}
fn is_enabled(&self) -> bool {
self.enabled_fn.as_ref().map(|f| f()).unwrap_or(true)
}
pub fn with_margin(mut self, m: Insets) -> Self { self.base.margin = m; self }
pub fn with_h_anchor(mut self, h: HAnchor) -> Self { self.base.h_anchor = h; self }
pub fn with_v_anchor(mut self, v: VAnchor) -> Self { self.base.v_anchor = v; self }
pub fn with_min_size(mut self, s: Size) -> Self { self.base.min_size = s; self }
pub fn with_max_size(mut self, s: Size) -> Self { self.base.max_size = s; self }
fn fire_click(&mut self) {
if let Some(cb) = self.on_click.as_mut() {
cb();
}
}
fn build_label(
text: &str,
font: &Arc<Font>,
font_size: f64,
theme: &ButtonTheme,
) -> Box<dyn Widget> {
Box::new(
Label::new(text, Arc::clone(font))
.with_font_size(font_size)
.with_color(theme.label_color)
.with_align(LabelAlign::Center),
)
}
}
impl Widget for Button {
fn type_name(&self) -> &'static str { "Button" }
fn bounds(&self) -> Rect { self.bounds }
fn set_bounds(&mut self, bounds: Rect) { self.bounds = bounds; }
fn children(&self) -> &[Box<dyn Widget>] { &self.children }
fn children_mut(&mut self) -> &mut Vec<Box<dyn Widget>> { &mut self.children }
fn is_focusable(&self) -> bool { self.is_enabled() }
fn margin(&self) -> Insets { self.base.margin }
fn h_anchor(&self) -> HAnchor { self.base.h_anchor }
fn v_anchor(&self) -> VAnchor { self.base.v_anchor }
fn min_size(&self) -> Size { self.base.min_size }
fn max_size(&self) -> Size { self.base.max_size }
fn layout(&mut self, available: Size) -> Size {
let height = (self.font_size * 2.4).max(28.0);
let pad_h = self.font_size * 1.4;
let label_size = self.children[0].layout(Size::new(available.width, height));
let natural_w = (label_size.width + pad_h).max(48.0);
let width = natural_w.min(available.width);
let size = Size::new(width, height);
let label_x = ((size.width - label_size.width) * 0.5).max(0.0);
let label_y = ((size.height - label_size.height) * 0.5).max(0.0);
self.children[0].set_bounds(Rect::new(label_x, label_y, label_size.width, label_size.height));
size
}
fn paint(&mut self, ctx: &mut dyn DrawCtx) {
let w = self.bounds.width;
let h = self.bounds.height;
let r = self.theme.border_radius;
let enabled = self.is_enabled();
if enabled && self.focused {
let ring = self.theme.focus_ring_width;
ctx.set_stroke_color(self.theme.focus_ring_color);
ctx.set_line_width(ring);
ctx.begin_path();
ctx.rounded_rect(-ring * 0.5, -ring * 0.5, w + ring, h + ring, r + ring * 0.5);
ctx.stroke();
}
let base_bg = if self.pressed {
self.theme.background_pressed
} else if self.hovered {
self.theme.background_hovered
} else {
self.theme.background
};
let bg = if enabled {
base_bg
} else {
let k = 0.45;
Color::rgba(
base_bg.r * k + 0.5 * (1.0 - k),
base_bg.g * k + 0.5 * (1.0 - k),
base_bg.b * k + 0.5 * (1.0 - k),
base_bg.a,
)
};
ctx.set_fill_color(bg);
ctx.begin_path();
ctx.rounded_rect(0.0, 0.0, w, h, r);
ctx.fill();
}
fn on_event(&mut self, event: &Event) -> EventResult {
if !self.is_enabled() {
self.hovered = false;
self.pressed = false;
return EventResult::Ignored;
}
match event {
Event::MouseMove { pos } => {
let was_hovered = self.hovered;
let was_pressed = self.pressed;
self.hovered = self.hit_test(*pos);
if !self.hovered {
self.pressed = false;
}
if was_hovered != self.hovered || was_pressed != self.pressed {
crate::animation::request_tick();
}
EventResult::Ignored
}
Event::MouseDown { button: MouseButton::Left, .. } => {
if !self.pressed { crate::animation::request_tick(); }
self.pressed = true;
EventResult::Consumed
}
Event::MouseUp { button: MouseButton::Left, .. } => {
let was_pressed = self.pressed;
self.pressed = false;
if was_pressed { crate::animation::request_tick(); }
if was_pressed && self.hovered {
self.fire_click();
crate::animation::request_tick();
}
EventResult::Consumed
}
Event::KeyDown { key, .. } => {
use crate::event::Key;
match key {
Key::Enter | Key::Char(' ') => {
self.fire_click();
crate::animation::request_tick();
EventResult::Consumed
}
_ => EventResult::Ignored,
}
}
Event::FocusGained => {
self.focused = true;
crate::animation::request_tick();
EventResult::Ignored
}
Event::FocusLost => {
self.focused = false;
self.pressed = false;
crate::animation::request_tick();
EventResult::Ignored
}
_ => EventResult::Ignored,
}
}
fn properties(&self) -> Vec<(&'static str, String)> {
vec![
("label", self.label_text.clone()),
("font_size", format!("{:.1}", self.font_size)),
]
}
}