1use std::rc::Rc;
21use std::sync::Arc;
22
23use crate::color::Color;
24use crate::event::{Event, EventResult, MouseButton};
25use crate::geometry::{Rect, Size};
26use crate::draw_ctx::DrawCtx;
27use crate::layout_props::{HAnchor, Insets, VAnchor, WidgetBase};
28use crate::text::Font;
29use crate::widget::Widget;
30use crate::widgets::label::{Label, LabelAlign};
31
32pub struct ButtonTheme {
34 pub background: Color,
35 pub background_hovered: Color,
36 pub background_pressed: Color,
37 pub label_color: Color,
38 pub border_radius: f64,
39 pub focus_ring_color: Color,
40 pub focus_ring_width: f64,
41}
42
43impl Default for ButtonTheme {
44 fn default() -> Self {
45 Self {
46 background: Color::rgb(0.22, 0.45, 0.88),
47 background_hovered: Color::rgb(0.30, 0.52, 0.92),
48 background_pressed: Color::rgb(0.16, 0.36, 0.72),
49 label_color: Color::white(),
50 border_radius: 6.0,
51 focus_ring_color: Color::rgba(0.22, 0.45, 0.88, 0.55),
52 focus_ring_width: 2.5,
53 }
54 }
55}
56
57pub struct Button {
61 bounds: Rect,
62 children: Vec<Box<dyn Widget>>,
64 base: WidgetBase,
65 label_text: String,
67 font: Arc<Font>,
68 font_size: f64,
69 pub theme: ButtonTheme,
70 on_click: Option<Box<dyn FnMut()>>,
71 enabled_fn: Option<Rc<dyn Fn() -> bool>>,
77
78 hovered: bool,
79 pressed: bool,
80 focused: bool,
81}
82
83impl Button {
84 pub fn new(label: impl Into<String>, font: Arc<Font>) -> Self {
86 let label_text: String = label.into();
87 let font_size = 14.0;
88 let theme = ButtonTheme::default();
89 let child = Self::build_label(&label_text, &font, font_size, &theme);
90 Self {
91 bounds: Rect::default(),
92 children: vec![child],
93 base: WidgetBase::new(),
94 label_text,
95 font,
96 font_size,
97 theme,
98 on_click: None,
99 enabled_fn: None,
100 hovered: false,
101 pressed: false,
102 focused: false,
103 }
104 }
105
106 pub fn with_font_size(mut self, size: f64) -> Self {
107 self.font_size = size;
108 self.children[0] = Self::build_label(&self.label_text, &self.font, size, &self.theme);
109 self
110 }
111
112 pub fn with_theme(mut self, theme: ButtonTheme) -> Self {
113 self.theme = theme;
114 self.children[0] = Self::build_label(&self.label_text, &self.font, self.font_size, &self.theme);
115 self
116 }
117
118 pub fn on_click(mut self, cb: impl FnMut() + 'static) -> Self {
119 self.on_click = Some(Box::new(cb));
120 self
121 }
122
123 pub fn with_enabled_fn(mut self, f: impl Fn() -> bool + 'static) -> Self {
126 self.enabled_fn = Some(Rc::new(f));
127 self
128 }
129
130 fn is_enabled(&self) -> bool {
131 self.enabled_fn.as_ref().map(|f| f()).unwrap_or(true)
132 }
133
134 pub fn with_margin(mut self, m: Insets) -> Self { self.base.margin = m; self }
135 pub fn with_h_anchor(mut self, h: HAnchor) -> Self { self.base.h_anchor = h; self }
136 pub fn with_v_anchor(mut self, v: VAnchor) -> Self { self.base.v_anchor = v; self }
137 pub fn with_min_size(mut self, s: Size) -> Self { self.base.min_size = s; self }
138 pub fn with_max_size(mut self, s: Size) -> Self { self.base.max_size = s; self }
139
140 fn fire_click(&mut self) {
141 if let Some(cb) = self.on_click.as_mut() {
142 cb();
143 }
144 }
145
146 fn build_label(
151 text: &str,
152 font: &Arc<Font>,
153 font_size: f64,
154 theme: &ButtonTheme,
155 ) -> Box<dyn Widget> {
156 Box::new(
157 Label::new(text, Arc::clone(font))
158 .with_font_size(font_size)
159 .with_color(theme.label_color)
160 .with_align(LabelAlign::Center),
161 )
162 }
163}
164
165impl Widget for Button {
166 fn type_name(&self) -> &'static str { "Button" }
167 fn bounds(&self) -> Rect { self.bounds }
168 fn set_bounds(&mut self, bounds: Rect) { self.bounds = bounds; }
169
170 fn children(&self) -> &[Box<dyn Widget>] { &self.children }
171 fn children_mut(&mut self) -> &mut Vec<Box<dyn Widget>> { &mut self.children }
172
173 fn is_focusable(&self) -> bool { self.is_enabled() }
174
175 fn margin(&self) -> Insets { self.base.margin }
176 fn h_anchor(&self) -> HAnchor { self.base.h_anchor }
177 fn v_anchor(&self) -> VAnchor { self.base.v_anchor }
178 fn min_size(&self) -> Size { self.base.min_size }
179 fn max_size(&self) -> Size { self.base.max_size }
180
181 fn layout(&mut self, available: Size) -> Size {
182 let height = (self.font_size * 2.4).max(28.0);
183 let pad_h = self.font_size * 1.4;
191 let label_size = self.children[0].layout(Size::new(available.width, height));
192 let natural_w = (label_size.width + pad_h).max(48.0);
193 let width = natural_w.min(available.width);
194 let size = Size::new(width, height);
195 let label_x = ((size.width - label_size.width) * 0.5).max(0.0);
196 let label_y = ((size.height - label_size.height) * 0.5).max(0.0);
197 self.children[0].set_bounds(Rect::new(label_x, label_y, label_size.width, label_size.height));
198 size
199 }
200
201 fn paint(&mut self, ctx: &mut dyn DrawCtx) {
202 let w = self.bounds.width;
203 let h = self.bounds.height;
204 let r = self.theme.border_radius;
205 let enabled = self.is_enabled();
206
207 if enabled && self.focused {
210 let ring = self.theme.focus_ring_width;
211 ctx.set_stroke_color(self.theme.focus_ring_color);
212 ctx.set_line_width(ring);
213 ctx.begin_path();
214 ctx.rounded_rect(-ring * 0.5, -ring * 0.5, w + ring, h + ring, r + ring * 0.5);
215 ctx.stroke();
216 }
217
218 let base_bg = if self.pressed {
222 self.theme.background_pressed
223 } else if self.hovered {
224 self.theme.background_hovered
225 } else {
226 self.theme.background
227 };
228 let bg = if enabled {
229 base_bg
230 } else {
231 let k = 0.45;
232 Color::rgba(
233 base_bg.r * k + 0.5 * (1.0 - k),
234 base_bg.g * k + 0.5 * (1.0 - k),
235 base_bg.b * k + 0.5 * (1.0 - k),
236 base_bg.a,
237 )
238 };
239 ctx.set_fill_color(bg);
240 ctx.begin_path();
241 ctx.rounded_rect(0.0, 0.0, w, h, r);
242 ctx.fill();
243
244 }
247
248 fn on_event(&mut self, event: &Event) -> EventResult {
249 if !self.is_enabled() {
250 self.hovered = false;
253 self.pressed = false;
254 return EventResult::Ignored;
255 }
256 match event {
257 Event::MouseMove { pos } => {
258 let was_hovered = self.hovered;
259 let was_pressed = self.pressed;
260 self.hovered = self.hit_test(*pos);
261 if !self.hovered {
262 self.pressed = false;
263 }
264 if was_hovered != self.hovered || was_pressed != self.pressed {
265 crate::animation::request_tick();
266 }
267 EventResult::Ignored
268 }
269 Event::MouseDown { button: MouseButton::Left, .. } => {
270 if !self.pressed { crate::animation::request_tick(); }
271 self.pressed = true;
272 EventResult::Consumed
273 }
274 Event::MouseUp { button: MouseButton::Left, .. } => {
275 let was_pressed = self.pressed;
276 self.pressed = false;
277 if was_pressed { crate::animation::request_tick(); }
278 if was_pressed && self.hovered {
279 self.fire_click();
280 crate::animation::request_tick();
284 }
285 EventResult::Consumed
286 }
287 Event::KeyDown { key, .. } => {
288 use crate::event::Key;
289 match key {
290 Key::Enter | Key::Char(' ') => {
291 self.fire_click();
292 crate::animation::request_tick();
293 EventResult::Consumed
294 }
295 _ => EventResult::Ignored,
296 }
297 }
298 Event::FocusGained => {
299 self.focused = true;
300 crate::animation::request_tick();
301 EventResult::Ignored
302 }
303 Event::FocusLost => {
304 self.focused = false;
305 self.pressed = false;
306 crate::animation::request_tick();
307 EventResult::Ignored
308 }
309 _ => EventResult::Ignored,
310 }
311 }
312
313 fn properties(&self) -> Vec<(&'static str, String)> {
314 vec![
315 ("label", self.label_text.clone()),
316 ("font_size", format!("{:.1}", self.font_size)),
317 ]
318 }
319}