1use std::rc::Rc;
21use std::sync::Arc;
22
23use crate::color::Color;
24use crate::draw_ctx::DrawCtx;
25use crate::event::{Event, EventResult, MouseButton};
26use crate::geometry::{Rect, Size};
27use crate::layout_props::{HAnchor, Insets, VAnchor, WidgetBase};
28use crate::text::Font;
29use crate::widget::Widget;
30use crate::widgets::label::{Label, LabelAlign};
31
32#[derive(Clone, Copy, Debug, PartialEq)]
34pub struct ButtonTheme {
35 pub background: Color,
36 pub background_hovered: Color,
37 pub background_pressed: Color,
38 pub label_color: Color,
39 pub border_radius: f64,
40 pub focus_ring_color: Color,
41 pub focus_ring_width: f64,
42}
43
44impl Default for ButtonTheme {
45 fn default() -> Self {
46 Self {
47 background: Color::rgb(0.22, 0.45, 0.88),
48 background_hovered: Color::rgb(0.30, 0.52, 0.92),
49 background_pressed: Color::rgb(0.16, 0.36, 0.72),
50 label_color: Color::white(),
51 border_radius: 6.0,
52 focus_ring_color: Color::rgba(0.22, 0.45, 0.88, 0.55),
53 focus_ring_width: 2.5,
54 }
55 }
56}
57
58pub struct Button {
62 bounds: Rect,
63 children: Vec<Box<dyn Widget>>,
65 base: WidgetBase,
66 label_text: String,
68 font: Arc<Font>,
69 font_size: f64,
70 pub theme: ButtonTheme,
71 on_click: Option<Box<dyn FnMut()>>,
72 enabled_fn: Option<Rc<dyn Fn() -> bool>>,
78
79 hovered: bool,
80 pressed: bool,
81 focused: bool,
82}
83
84impl Button {
85 pub fn new(label: impl Into<String>, font: Arc<Font>) -> Self {
87 let label_text: String = label.into();
88 let font_size = 14.0;
89 let theme = ButtonTheme::default();
90 let child = Self::build_label(&label_text, &font, font_size, &theme);
91 Self {
92 bounds: Rect::default(),
93 children: vec![child],
94 base: WidgetBase::new(),
95 label_text,
96 font,
97 font_size,
98 theme,
99 on_click: None,
100 enabled_fn: None,
101 hovered: false,
102 pressed: false,
103 focused: false,
104 }
105 }
106
107 pub fn with_font_size(mut self, size: f64) -> Self {
108 self.font_size = size;
109 self.children[0] = Self::build_label(&self.label_text, &self.font, size, &self.theme);
110 self
111 }
112
113 pub fn with_theme(mut self, theme: ButtonTheme) -> Self {
114 self.theme = theme;
115 self.children[0] =
116 Self::build_label(&self.label_text, &self.font, self.font_size, &self.theme);
117 self
118 }
119
120 pub fn on_click(mut self, cb: impl FnMut() + 'static) -> Self {
121 self.on_click = Some(Box::new(cb));
122 self
123 }
124
125 pub fn with_enabled_fn(mut self, f: impl Fn() -> bool + 'static) -> Self {
128 self.enabled_fn = Some(Rc::new(f));
129 self
130 }
131
132 fn is_enabled(&self) -> bool {
133 self.enabled_fn.as_ref().map(|f| f()).unwrap_or(true)
134 }
135
136 pub fn with_margin(mut self, m: Insets) -> Self {
137 self.base.margin = m;
138 self
139 }
140 pub fn with_h_anchor(mut self, h: HAnchor) -> Self {
141 self.base.h_anchor = h;
142 self
143 }
144 pub fn with_v_anchor(mut self, v: VAnchor) -> Self {
145 self.base.v_anchor = v;
146 self
147 }
148 pub fn with_min_size(mut self, s: Size) -> Self {
149 self.base.min_size = s;
150 self
151 }
152 pub fn with_max_size(mut self, s: Size) -> Self {
153 self.base.max_size = s;
154 self
155 }
156
157 fn fire_click(&mut self) {
158 if let Some(cb) = self.on_click.as_mut() {
159 cb();
160 }
161 }
162
163 fn disabled_colors(v: &crate::theme::Visuals) -> (Color, Color, Color) {
164 let luma = v.bg_color.r * 0.299 + v.bg_color.g * 0.587 + v.bg_color.b * 0.114;
165 if luma < 0.5 {
166 (
167 v.window_fill,
168 Color::rgba(1.0, 1.0, 1.0, 0.22),
169 v.text_dim.with_alpha(0.42),
170 )
171 } else {
172 (v.track_bg, v.widget_stroke.with_alpha(0.45), v.text_dim)
173 }
174 }
175
176 fn build_label(
181 text: &str,
182 font: &Arc<Font>,
183 font_size: f64,
184 theme: &ButtonTheme,
185 ) -> Box<dyn Widget> {
186 Box::new(
187 Label::new(text, Arc::clone(font))
188 .with_font_size(font_size)
189 .with_color(theme.label_color)
190 .with_align(LabelAlign::Center),
191 )
192 }
193}
194
195impl Widget for Button {
196 fn type_name(&self) -> &'static str {
197 "Button"
198 }
199 fn bounds(&self) -> Rect {
200 self.bounds
201 }
202 fn set_bounds(&mut self, bounds: Rect) {
203 self.bounds = bounds;
204 }
205
206 fn children(&self) -> &[Box<dyn Widget>] {
207 &self.children
208 }
209 fn children_mut(&mut self) -> &mut Vec<Box<dyn Widget>> {
210 &mut self.children
211 }
212
213 fn is_focusable(&self) -> bool {
214 self.is_enabled()
215 }
216
217 fn margin(&self) -> Insets {
218 self.base.margin
219 }
220 fn h_anchor(&self) -> HAnchor {
221 self.base.h_anchor
222 }
223 fn v_anchor(&self) -> VAnchor {
224 self.base.v_anchor
225 }
226 fn min_size(&self) -> Size {
227 self.base.min_size
228 }
229 fn max_size(&self) -> Size {
230 self.base.max_size
231 }
232
233 fn layout(&mut self, available: Size) -> Size {
234 let height = (self.font_size * 1.7).max(24.0);
235 let pad_h = self.font_size * 1.2;
243 let label_size = self.children[0].layout(Size::new(available.width, height));
244 let natural_w = (label_size.width + pad_h).max(48.0);
245 let width = natural_w.min(available.width);
246 let size = Size::new(width, height);
247 let label_x = ((size.width - label_size.width) * 0.5).max(0.0);
248 let label_y = ((size.height - label_size.height) * 0.5).max(0.0);
249 self.children[0].set_bounds(Rect::new(
250 label_x,
251 label_y,
252 label_size.width,
253 label_size.height,
254 ));
255 size
256 }
257
258 fn paint(&mut self, ctx: &mut dyn DrawCtx) {
259 let w = self.bounds.width;
260 let h = self.bounds.height;
261 let r = self.theme.border_radius;
262 let enabled = self.is_enabled();
263 let v = ctx.visuals();
264 let use_visuals = self.theme == ButtonTheme::default();
265
266 if enabled && self.focused {
269 let ring = self.theme.focus_ring_width;
270 let focus_ring = if use_visuals {
271 v.accent_focus
272 } else {
273 self.theme.focus_ring_color
274 };
275 ctx.set_stroke_color(focus_ring);
276 ctx.set_line_width(ring);
277 ctx.begin_path();
278 ctx.rounded_rect(-ring * 0.5, -ring * 0.5, w + ring, h + ring, r + ring * 0.5);
279 ctx.stroke();
280 }
281
282 let base_bg = if use_visuals && self.pressed {
286 v.accent_pressed
287 } else if use_visuals && self.hovered {
288 v.accent_hovered
289 } else if use_visuals {
290 v.accent
291 } else if self.pressed {
292 self.theme.background_pressed
293 } else if self.hovered {
294 self.theme.background_hovered
295 } else {
296 self.theme.background
297 };
298 let (disabled_bg, disabled_stroke, _) = Self::disabled_colors(&v);
299 let bg = if enabled { base_bg } else { disabled_bg };
300 ctx.set_fill_color(bg);
301 ctx.begin_path();
302 ctx.rounded_rect(0.0, 0.0, w, h, r);
303 ctx.fill();
304
305 if !enabled {
306 ctx.set_stroke_color(disabled_stroke);
307 ctx.set_line_width(1.0);
308 ctx.begin_path();
309 ctx.rounded_rect(0.5, 0.5, (w - 1.0).max(0.0), (h - 1.0).max(0.0), r);
310 ctx.stroke();
311 }
312
313 }
316
317 fn paint_overlay(&mut self, ctx: &mut dyn DrawCtx) {
318 if self.is_enabled() {
319 return;
320 }
321
322 let w = self.bounds.width;
325 let h = self.bounds.height;
326 let r = self.theme.border_radius;
327 let v = ctx.visuals();
328 let (disabled_bg, disabled_stroke, disabled_text) = Self::disabled_colors(&v);
329
330 ctx.set_fill_color(disabled_bg);
331 ctx.begin_path();
332 ctx.rounded_rect(0.0, 0.0, w, h, r);
333 ctx.fill();
334
335 ctx.set_stroke_color(disabled_stroke);
336 ctx.set_line_width(1.0);
337 ctx.begin_path();
338 ctx.rounded_rect(0.5, 0.5, (w - 1.0).max(0.0), (h - 1.0).max(0.0), r);
339 ctx.stroke();
340
341 let font =
342 crate::font_settings::current_system_font().unwrap_or_else(|| Arc::clone(&self.font));
343 ctx.set_font(font);
344 ctx.set_font_size(self.font_size * crate::font_settings::current_font_size_scale());
345 ctx.set_fill_color(disabled_text);
346 if let Some(m) = ctx.measure_text(&self.label_text) {
347 let tx = ((w - m.width) * 0.5).max(0.0);
348 let ty = m.centered_baseline_y(h).max(0.0);
349 ctx.fill_text(&self.label_text, tx, ty);
350 }
351 }
352
353 fn on_event(&mut self, event: &Event) -> EventResult {
354 if !self.is_enabled() {
355 self.hovered = false;
358 self.pressed = false;
359 return EventResult::Ignored;
360 }
361 match event {
362 Event::MouseMove { pos } => {
363 let was_hovered = self.hovered;
364 let was_pressed = self.pressed;
365 self.hovered = self.hit_test(*pos);
366 if !self.hovered {
367 self.pressed = false;
368 }
369 if was_hovered != self.hovered || was_pressed != self.pressed {
370 crate::animation::request_draw();
371 return EventResult::Consumed;
372 }
373 EventResult::Ignored
374 }
375 Event::MouseDown {
376 button: MouseButton::Left,
377 ..
378 } => {
379 if !self.pressed {
380 crate::animation::request_draw();
381 }
382 self.pressed = true;
383 EventResult::Consumed
384 }
385 Event::MouseUp {
386 button: MouseButton::Left,
387 ..
388 } => {
389 let was_pressed = self.pressed;
390 self.pressed = false;
391 if was_pressed {
392 crate::animation::request_draw();
393 }
394 if was_pressed && self.hovered {
395 self.fire_click();
396 crate::animation::request_draw();
400 }
401 EventResult::Consumed
402 }
403 Event::KeyDown { key, .. } => {
404 use crate::event::Key;
405 match key {
406 Key::Enter | Key::Char(' ') => {
407 self.fire_click();
408 crate::animation::request_draw();
409 EventResult::Consumed
410 }
411 _ => EventResult::Ignored,
412 }
413 }
414 Event::FocusGained => {
415 let was = self.focused;
416 self.focused = true;
417 if !was {
418 crate::animation::request_draw();
419 EventResult::Consumed
420 } else {
421 EventResult::Ignored
422 }
423 }
424 Event::FocusLost => {
425 let was_focused = self.focused;
426 let was_pressed = self.pressed;
427 self.focused = false;
428 self.pressed = false;
429 if was_focused || was_pressed {
430 crate::animation::request_draw();
431 EventResult::Consumed
432 } else {
433 EventResult::Ignored
434 }
435 }
436 _ => EventResult::Ignored,
437 }
438 }
439
440 fn properties(&self) -> Vec<(&'static str, String)> {
441 vec![
442 ("label", self.label_text.clone()),
443 ("font_size", format!("{:.1}", self.font_size)),
444 ]
445 }
446}