StratoSDK is a framework with a declarative approach similar to Flutter/React, written and designed entirely for Rust.
| 1 | //! Button widget implementation |
| 2 | //! |
| 3 | //! Provides interactive button components with various styles, states, and event handling. |
| 4 | |
| 5 | use crate::control::{ControlRole, ControlState}; |
| 6 | use crate::widget::{generate_id, Widget, WidgetContext, WidgetId, WidgetState}; |
| 7 | use std::{any::Any, sync::Arc}; |
| 8 | use strato_core::{ |
| 9 | event::{Event, EventResult}, |
| 10 | layout::{Constraints, Layout, Size}, |
| 11 | state::Signal, |
| 12 | taffy::{ |
| 13 | prelude::*, |
| 14 | style::{Dimension, LengthPercentage}, |
| 15 | }, |
| 16 | taffy_layout::{TaffyLayoutError, TaffyLayoutResult, TaffyWidget}, |
| 17 | theme::{Color, Theme}, |
| 18 | types::Rect, |
| 19 | types::{Point, Transform}, |
| 20 | }; |
| 21 | use strato_renderer::{batch::RenderBatch, vertex::VertexBuilder}; |
| 22 | |
| 23 | /// Button state is kept in sync with the shared widget state enum. |
| 24 | pub type ButtonState = WidgetState; |
| 25 | |
| 26 | /// Button style configuration |
| 27 | #[derive(Debug, Clone)] |
| 28 | pub struct ButtonStyle { |
| 29 | pub background_color: Color, |
| 30 | pub hover_color: Color, |
| 31 | pub pressed_color: Color, |
| 32 | pub disabled_color: Color, |
| 33 | pub text_color: Color, |
| 34 | pub border_radius: f32, |
| 35 | pub border_width: f32, |
| 36 | pub border_color: Color, |
| 37 | pub padding: f32, |
| 38 | pub font_size: f32, |
| 39 | pub min_width: f32, |
| 40 | pub min_height: f32, |
| 41 | } |
| 42 | |
| 43 | impl Default for ButtonStyle { |
| 44 | fn default() -> Self { |
| 45 | Self { |
| 46 | background_color: Color::rgba(0.2, 0.4, 0.8, 1.0), |
| 47 | hover_color: Color::rgba(0.3, 0.5, 0.9, 1.0), |
| 48 | pressed_color: Color::rgba(0.1, 0.3, 0.7, 1.0), |
| 49 | disabled_color: Color::rgba(0.5, 0.5, 0.5, 1.0), |
| 50 | text_color: Color::rgba(1.0, 1.0, 1.0, 1.0), |
| 51 | border_radius: 4.0, |
| 52 | border_width: 0.0, |
| 53 | border_color: Color::rgba(0.0, 0.0, 0.0, 0.0), |
| 54 | padding: 12.0, |
| 55 | font_size: 14.0, |
| 56 | min_width: 80.0, |
| 57 | min_height: 32.0, |
| 58 | } |
| 59 | } |
| 60 | } |
| 61 | |
| 62 | impl ButtonStyle { |
| 63 | /// Create a primary button style |
| 64 | pub fn primary() -> Self { |
| 65 | Self { |
| 66 | background_color: Color::rgba(0.0, 0.4, 0.8, 1.0), |
| 67 | hover_color: Color::rgba(0.1, 0.5, 0.9, 1.0), |
| 68 | pressed_color: Color::rgba(0.0, 0.3, 0.7, 1.0), |
| 69 | ..Default::default() |
| 70 | } |
| 71 | } |
| 72 | |
| 73 | /// Create a secondary button style |
| 74 | pub fn secondary() -> Self { |
| 75 | Self { |
| 76 | background_color: Color::rgba(0.6, 0.6, 0.6, 1.0), |
| 77 | hover_color: Color::rgba(0.7, 0.7, 0.7, 1.0), |
| 78 | pressed_color: Color::rgba(0.5, 0.5, 0.5, 1.0), |
| 79 | text_color: Color::rgba(0.0, 0.0, 0.0, 1.0), |
| 80 | ..Default::default() |
| 81 | } |
| 82 | } |
| 83 | |
| 84 | /// Create a danger button style |
| 85 | pub fn danger() -> Self { |
| 86 | Self { |
| 87 | background_color: Color::rgba(0.8, 0.2, 0.2, 1.0), |
| 88 | hover_color: Color::rgba(0.9, 0.3, 0.3, 1.0), |
| 89 | pressed_color: Color::rgba(0.7, 0.1, 0.1, 1.0), |
| 90 | ..Default::default() |
| 91 | } |
| 92 | } |
| 93 | |
| 94 | /// Create an outline button style |
| 95 | pub fn outline() -> Self { |
| 96 | Self { |
| 97 | background_color: Color::rgba(0.0, 0.0, 0.0, 0.0), |
| 98 | hover_color: Color::rgba(0.0, 0.4, 0.8, 0.1), |
| 99 | pressed_color: Color::rgba(0.0, 0.4, 0.8, 0.2), |
| 100 | text_color: Color::rgba(0.0, 0.4, 0.8, 1.0), |
| 101 | border_width: 1.0, |
| 102 | border_color: Color::rgba(0.0, 0.4, 0.8, 1.0), |
| 103 | ..Default::default() |
| 104 | } |
| 105 | } |
| 106 | |
| 107 | /// Create a ghost button style |
| 108 | pub fn ghost() -> Self { |
| 109 | Self { |
| 110 | background_color: Color::rgba(0.0, 0.0, 0.0, 0.0), |
| 111 | hover_color: Color::rgba(0.0, 0.0, 0.0, 0.05), |
| 112 | pressed_color: Color::rgba(0.0, 0.0, 0.0, 0.1), |
| 113 | text_color: Color::rgba(0.3, 0.3, 0.3, 1.0), |
| 114 | border_width: 0.0, |
| 115 | ..Default::default() |
| 116 | } |
| 117 | } |
| 118 | |
| 119 | /// Create a text button style (transparent background) |
| 120 | pub fn text() -> Self { |
| 121 | Self { |
| 122 | background_color: Color::rgba(0.0, 0.0, 0.0, 0.0), |
| 123 | hover_color: Color::rgba(0.0, 0.0, 0.0, 0.05), |
| 124 | pressed_color: Color::rgba(0.0, 0.0, 0.0, 0.1), |
| 125 | text_color: Color::rgba(0.0, 0.4, 0.8, 1.0), |
| 126 | border_width: 0.0, |
| 127 | ..Default::default() |
| 128 | } |
| 129 | } |
| 130 | } |
| 131 | |
| 132 | fn blend_colors(from: Color, to: Color, t: f32) -> Color { |
| 133 | let mix = |a: f32, b: f32| a + (b - a) * t; |
| 134 | Color::rgba( |
| 135 | mix(from.r, to.r), |
| 136 | mix(from.g, to.g), |
| 137 | mix(from.b, to.b), |
| 138 | mix(from.a, to.a), |
| 139 | ) |
| 140 | } |
| 141 | |
| 142 | /// Button widget |
| 143 | pub struct Button { |
| 144 | id: WidgetId, |
| 145 | text: String, |
| 146 | style: ButtonStyle, |
| 147 | control: ControlState, |
| 148 | bounds: Signal<Rect>, |
| 149 | enabled: Signal<bool>, |
| 150 | visible: Signal<bool>, |
| 151 | on_click: Option<Box<dyn Fn() + Send + Sync>>, |
| 152 | on_hover: Option<Box<dyn Fn(bool) + Send + Sync>>, |
| 153 | theme: Option<Arc<Theme>>, |
| 154 | } |
| 155 | |
| 156 | impl std::fmt::Debug for Button { |
| 157 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
| 158 | f.debug_struct("Button") |
| 159 | .field("id", &self.id) |
| 160 | .field("text", &self.text) |
| 161 | .field("style", &self.style) |
| 162 | .field("state", &self.control) |
| 163 | .field("bounds", &self.bounds) |
| 164 | .field("enabled", &self.enabled) |
| 165 | .field("visible", &self.visible) |
| 166 | .field( |
| 167 | "on_click", |
| 168 | &self.on_click.as_ref().map(|_| "Fn() + Send + Sync"), |
| 169 | ) |
| 170 | .field( |
| 171 | "on_hover", |
| 172 | &self.on_hover.as_ref().map(|_| "Fn(bool) + Send + Sync"), |
| 173 | ) |
| 174 | .field("theme", &self.theme) |
| 175 | .finish() |
| 176 | } |
| 177 | } |
| 178 | |
| 179 | impl Button { |
| 180 | /// Create a new button with text |
| 181 | pub fn new(text: impl Into<String>) -> Self { |
| 182 | let text_value = text.into(); |
| 183 | let mut control = ControlState::new(ControlRole::Button); |
| 184 | control.set_label(text_value.clone()); |
| 185 | Self { |
| 186 | id: generate_id(), |
| 187 | text: text_value, |
| 188 | style: ButtonStyle::default(), |
| 189 | control, |
| 190 | bounds: Signal::new(Rect::new(0.0, 0.0, 0.0, 0.0)), |
| 191 | enabled: Signal::new(true), |
| 192 | visible: Signal::new(true), |
| 193 | on_click: None, |
| 194 | on_hover: None, |
| 195 | theme: None, |
| 196 | } |
| 197 | } |
| 198 | |
| 199 | /// Set button style |
| 200 | pub fn style(mut self, style: ButtonStyle) -> Self { |
| 201 | self.style = style; |
| 202 | self |
| 203 | } |
| 204 | |
| 205 | /// Set primary style |
| 206 | pub fn primary(mut self) -> Self { |
| 207 | self.style = ButtonStyle::primary(); |
| 208 | self |
| 209 | } |
| 210 | |
| 211 | /// Set secondary style |
| 212 | pub fn secondary(mut self) -> Self { |
| 213 | self.style = ButtonStyle::secondary(); |
| 214 | self |
| 215 | } |
| 216 | |
| 217 | /// Set danger style |
| 218 | pub fn danger(mut self) -> Self { |
| 219 | self.style = ButtonStyle::danger(); |
| 220 | self |
| 221 | } |
| 222 | |
| 223 | /// Set outline style |
| 224 | pub fn outline(mut self) -> Self { |
| 225 | self.style = ButtonStyle::outline(); |
| 226 | self |
| 227 | } |
| 228 | |
| 229 | /// Set ghost style |
| 230 | pub fn ghost(mut self) -> Self { |
| 231 | self.style = ButtonStyle::ghost(); |
| 232 | self |
| 233 | } |
| 234 | |
| 235 | /// Set click handler |
| 236 | pub fn on_click<F>(mut self, handler: F) -> Self |
| 237 | where |
| 238 | F: Fn() + Send + Sync + 'static, |
| 239 | { |
| 240 | self.on_click = Some(Box::new(handler)); |
| 241 | self |
| 242 | } |
| 243 | |
| 244 | /// Set hover handler |
| 245 | pub fn on_hover<F>(mut self, handler: F) -> Self |
| 246 | where |
| 247 | F: Fn(bool) + Send + Sync + 'static, |
| 248 | { |
| 249 | self.on_hover = Some(Box::new(handler)); |
| 250 | self |
| 251 | } |
| 252 | |
| 253 | /// Set enabled state |
| 254 | pub fn enabled(self, enabled: bool) -> Self { |
| 255 | self.enabled.set(enabled); |
| 256 | self.control.set_disabled(!enabled); |
| 257 | self |
| 258 | } |
| 259 | |
| 260 | /// Set visible state |
| 261 | pub fn visible(self, visible: bool) -> Self { |
| 262 | self.visible.set(visible); |
| 263 | self |
| 264 | } |
| 265 | |
| 266 | /// Set theme |
| 267 | pub fn theme(mut self, theme: Arc<Theme>) -> Self { |
| 268 | self.theme = Some(theme); |
| 269 | self |
| 270 | } |
| 271 | |
| 272 | /// Set button size (width, height) |
| 273 | pub fn size(mut self, width: f32, height: f32) -> Self { |
| 274 | self.style.min_width = width; |
| 275 | self.style.min_height = height; |
| 276 | self |
| 277 | } |
| 278 | |
| 279 | /// Get button ID |
| 280 | pub fn id(&self) -> WidgetId { |
| 281 | self.id |
| 282 | } |
| 283 | |
| 284 | /// Get button text |
| 285 | pub fn text(&self) -> &str { |
| 286 | &self.text |
| 287 | } |
| 288 | |
| 289 | /// Set button text |
| 290 | pub fn set_text(&mut self, text: impl Into<String>) { |
| 291 | let text = text.into(); |
| 292 | self.text = text.clone(); |
| 293 | self.control.set_label(text); |
| 294 | } |
| 295 | |
| 296 | /// Override the accessibility label without changing the visible text. |
| 297 | pub fn accessibility_label(mut self, label: impl Into<String>) -> Self { |
| 298 | self.control.set_label(label); |
| 299 | self |
| 300 | } |
| 301 | |
| 302 | /// Provide an accessibility hint/description for assistive technologies. |
| 303 | pub fn accessibility_hint(mut self, hint: impl Into<String>) -> Self { |
| 304 | self.control.set_hint(hint); |
| 305 | self |
| 306 | } |
| 307 | |
| 308 | /// Get current state |
| 309 | pub fn get_state(&self) -> ButtonState { |
| 310 | self.control.state() |
| 311 | } |
| 312 | |
| 313 | /// Set button state |
| 314 | pub fn set_state(&self, state: ButtonState) { |
| 315 | self.control.set_state(state); |
| 316 | } |
| 317 | |
| 318 | /// Check if button is enabled |
| 319 | pub fn is_enabled(&self) -> bool { |
| 320 | self.enabled.get() && self.control.state() != ButtonState::Disabled |
| 321 | } |
| 322 | |
| 323 | /// Check if button is visible |
| 324 | pub fn is_visible(&self) -> bool { |
| 325 | self.visible.get() |
| 326 | } |
| 327 | |
| 328 | /// Handle mouse enter event |
| 329 | pub fn on_mouse_enter(&self) { |
| 330 | if self.is_enabled() && self.get_state() != ButtonState::Pressed { |
| 331 | self.control.hover(true); |
| 332 | if let Some(ref handler) = self.on_hover { |
| 333 | handler(true); |
| 334 | } |
| 335 | } |
| 336 | } |
| 337 | |
| 338 | /// Handle mouse leave event |
| 339 | pub fn on_mouse_leave(&self) { |
| 340 | if self.is_enabled() { |
| 341 | self.control.hover(false); |
| 342 | if let Some(ref handler) = self.on_hover { |
| 343 | handler(false); |
| 344 | } |
| 345 | } |
| 346 | } |
| 347 | |
| 348 | /// Handle mouse press event |
| 349 | pub fn on_mouse_press(&self, point: Point) -> bool { |
| 350 | if !self.is_enabled() || !self.is_visible() { |
| 351 | return false; |
| 352 | } |
| 353 | |
| 354 | let bounds = self.bounds.get(); |
| 355 | self.control.press(point, bounds) |
| 356 | } |
| 357 | |
| 358 | /// Handle mouse release event |
| 359 | pub fn on_mouse_release(&self, point: Point) -> bool { |
| 360 | if !self.is_enabled() || !self.is_visible() { |
| 361 | return false; |
| 362 | } |
| 363 | |
| 364 | let bounds = self.bounds.get(); |
| 365 | if self.control.release(point, bounds) { |
| 366 | if let Some(ref handler) = self.on_click { |
| 367 | handler(); |
| 368 | } |
| 369 | return true; |
| 370 | } |
| 371 | false |
| 372 | } |
| 373 | |
| 374 | /// Calculate button size |
| 375 | pub fn calculate_size(&self, available_size: Size) -> Size { |
| 376 | // Use accurate text measurement |
| 377 | let text_width = crate::text::measure_text_width(&self.text, self.style.font_size, 0.0); |
| 378 | let text_height = self.style.font_size; |
| 379 | |
| 380 | let width = (text_width + self.style.padding * 2.0).max(self.style.min_width); |
| 381 | let height = (text_height + self.style.padding * 2.0).max(self.style.min_height); |
| 382 | |
| 383 | Size::new( |
| 384 | width.min(available_size.width), |
| 385 | height.min(available_size.height), |
| 386 | ) |
| 387 | } |
| 388 | |
| 389 | /// Layout the button |
| 390 | pub fn layout(&self, bounds: Rect) { |
| 391 | self.bounds.set(bounds); |
| 392 | } |
| 393 | |
| 394 | /// Apply theme to button |
| 395 | pub fn apply_theme(&mut self, theme: &Theme) { |
| 396 | // Update style based on theme |
| 397 | self.style.background_color = theme.colors.primary; |
| 398 | self.style.text_color = theme.colors.on_primary; |
| 399 | self.style.border_radius = theme.spacing.md; |
| 400 | self.style.font_size = theme.typography.base_size; |
| 401 | } |
| 402 | } |
| 403 | |
| 404 | /// Button builder for fluent API |
| 405 | pub struct ButtonBuilder { |
| 406 | button: Button, |
| 407 | } |
| 408 | |
| 409 | impl ButtonBuilder { |
| 410 | /// Create a new button builder |
| 411 | pub fn new(text: impl Into<String>) -> Self { |
| 412 | Self { |
| 413 | button: Button::new(text), |
| 414 | } |
| 415 | } |
| 416 | |
| 417 | /// Set style |
| 418 | pub fn style(mut self, style: ButtonStyle) -> Self { |
| 419 | self.button = self.button.style(style); |
| 420 | self |
| 421 | } |
| 422 | |
| 423 | /// Set as primary button |
| 424 | pub fn primary(mut self) -> Self { |
| 425 | self.button = self.button.primary(); |
| 426 | self |
| 427 | } |
| 428 | |
| 429 | /// Set as secondary button |
| 430 | pub fn secondary(mut self) -> Self { |
| 431 | self.button = self.button.secondary(); |
| 432 | self |
| 433 | } |
| 434 | |
| 435 | /// Set as danger button |
| 436 | pub fn danger(mut self) -> Self { |
| 437 | self.button = self.button.danger(); |
| 438 | self |
| 439 | } |
| 440 | |
| 441 | /// Set as outline button |
| 442 | pub fn outline(mut self) -> Self { |
| 443 | self.button = self.button.outline(); |
| 444 | self |
| 445 | } |
| 446 | |
| 447 | /// Set as ghost button |
| 448 | pub fn ghost(mut self) -> Self { |
| 449 | self.button = self.button.ghost(); |
| 450 | self |
| 451 | } |
| 452 | |
| 453 | /// Set click handler |
| 454 | pub fn on_click<F>(mut self, handler: F) -> Self |
| 455 | where |
| 456 | F: Fn() + Send + Sync + 'static, |
| 457 | { |
| 458 | self.button = self.button.on_click(handler); |
| 459 | self |
| 460 | } |
| 461 | |
| 462 | /// Set hover handler |
| 463 | pub fn on_hover<F>(mut self, handler: F) -> Self |
| 464 | where |
| 465 | F: Fn(bool) + Send + Sync + 'static, |
| 466 | { |
| 467 | self.button = self.button.on_hover(handler); |
| 468 | self |
| 469 | } |
| 470 | |
| 471 | /// Set enabled state |
| 472 | pub fn enabled(mut self, enabled: bool) -> Self { |
| 473 | self.button = self.button.enabled(enabled); |
| 474 | self |
| 475 | } |
| 476 | |
| 477 | /// Set visible state |
| 478 | pub fn visible(mut self, visible: bool) -> Self { |
| 479 | self.button = self.button.visible(visible); |
| 480 | self |
| 481 | } |
| 482 | |
| 483 | /// Build the button |
| 484 | pub fn build(self) -> Button { |
| 485 | self.button |
| 486 | } |
| 487 | } |
| 488 | |
| 489 | #[cfg(test)] |
| 490 | mod tests { |
| 491 | use super::*; |
| 492 | |
| 493 | #[test] |
| 494 | fn test_button_creation() { |
| 495 | let button = Button::new("Test Button"); |
| 496 | assert_eq!(button.text(), "Test Button"); |
| 497 | assert_eq!(button.get_state(), ButtonState::Normal); |
| 498 | assert!(button.is_enabled()); |
| 499 | assert!(button.is_visible()); |
| 500 | } |
| 501 | |
| 502 | #[test] |
| 503 | fn test_button_styles() { |
| 504 | let primary = Button::new("Primary").primary(); |
| 505 | let secondary = Button::new("Secondary").secondary(); |
| 506 | let danger = Button::new("Danger").danger(); |
| 507 | |
| 508 | // Styles should be different |
| 509 | assert_ne!( |
| 510 | primary.style.background_color, |
| 511 | secondary.style.background_color |
| 512 | ); |
| 513 | assert_ne!( |
| 514 | secondary.style.background_color, |
| 515 | danger.style.background_color |
| 516 | ); |
| 517 | } |
| 518 | |
| 519 | #[test] |
| 520 | fn test_button_state_changes() { |
| 521 | let button = Button::new("Test"); |
| 522 | |
| 523 | assert_eq!(button.get_state(), ButtonState::Normal); |
| 524 | |
| 525 | button.on_mouse_enter(); |
| 526 | assert_eq!(button.get_state(), ButtonState::Hovered); |
| 527 | |
| 528 | button.on_mouse_leave(); |
| 529 | assert_eq!(button.get_state(), ButtonState::Normal); |
| 530 | } |
| 531 | |
| 532 | #[test] |
| 533 | fn test_button_builder() { |
| 534 | let button = ButtonBuilder::new("Builder Test") |
| 535 | .primary() |
| 536 | .enabled(true) |
| 537 | .build(); |
| 538 | |
| 539 | assert_eq!(button.text(), "Builder Test"); |
| 540 | assert!(button.is_enabled()); |
| 541 | } |
| 542 | |
| 543 | #[test] |
| 544 | fn test_button_size_calculation() { |
| 545 | let button = Button::new("Test"); |
| 546 | let available = Size::new(200.0, 100.0); |
| 547 | let size = button.calculate_size(available); |
| 548 | |
| 549 | assert!(size.width >= button.style.min_width); |
| 550 | assert!(size.height >= button.style.min_height); |
| 551 | assert!(size.width <= available.width); |
| 552 | assert!(size.height <= available.height); |
| 553 | } |
| 554 | } |
| 555 | |
| 556 | // Implement Widget trait for Button |
| 557 | impl Widget for Button { |
| 558 | fn id(&self) -> WidgetId { |
| 559 | self.id |
| 560 | } |
| 561 | |
| 562 | fn layout(&mut self, constraints: Constraints) -> Size { |
| 563 | let text_width = crate::text::measure_text_width(&self.text, self.style.font_size, 0.0); |
| 564 | let text_height = self.style.font_size; |
| 565 | |
| 566 | let content_width = text_width + self.style.padding * 2.0; |
| 567 | let content_height = text_height + self.style.padding * 2.0; |
| 568 | |
| 569 | let width = content_width.max(self.style.min_width); |
| 570 | let height = content_height.max(self.style.min_height); |
| 571 | |
| 572 | // Respect constraints |
| 573 | let width = width.min(constraints.max_width).max(constraints.min_width); |
| 574 | let height = height |
| 575 | .min(constraints.max_height) |
| 576 | .max(constraints.min_height); |
| 577 | |
| 578 | Size::new(width, height) |
| 579 | } |
| 580 | |
| 581 | fn render(&self, batch: &mut RenderBatch, layout: Layout) { |
| 582 | let bounds = Rect::new( |
| 583 | layout.position.x, |
| 584 | layout.position.y, |
| 585 | layout.size.width, |
| 586 | layout.size.height, |
| 587 | ); |
| 588 | self.bounds.set(bounds); |
| 589 | |
| 590 | if !self.is_visible() { |
| 591 | return; |
| 592 | } |
| 593 | |
| 594 | let state = self.get_state(); |
| 595 | let target_color = match state { |
| 596 | ButtonState::Normal => self.style.background_color, |
| 597 | ButtonState::Hovered => self.style.hover_color, |
| 598 | ButtonState::Pressed => self.style.pressed_color, |
| 599 | ButtonState::Disabled => self.style.disabled_color, |
| 600 | ButtonState::Focused => { |
| 601 | blend_colors(self.style.background_color, self.style.hover_color, 0.35) |
| 602 | } |
| 603 | }; |
| 604 | let background_color = if matches!(state, ButtonState::Disabled) { |
| 605 | self.style.disabled_color |
| 606 | } else { |
| 607 | blend_colors( |
| 608 | self.style.background_color, |
| 609 | target_color, |
| 610 | self.control.interaction_factor(), |
| 611 | ) |
| 612 | }; |
| 613 | |
| 614 | // Apply a subtle offset when pressed to give physical feedback |
| 615 | let mut draw_bounds = bounds; |
| 616 | if state == ButtonState::Pressed { |
| 617 | draw_bounds.x += 1.0; |
| 618 | draw_bounds.y += 1.0; |
| 619 | } |
| 620 | |
| 621 | // Draw background |
| 622 | batch.add_rect( |
| 623 | draw_bounds, |
| 624 | background_color.to_types_color(), |
| 625 | Transform::identity(), |
| 626 | ); |
| 627 | |
| 628 | // Render border if needed |
| 629 | if self.style.border_width > 0.0 { |
| 630 | let border_bounds = Rect::new( |
| 631 | draw_bounds.x - self.style.border_width / 2.0, |
| 632 | draw_bounds.y - self.style.border_width / 2.0, |
| 633 | draw_bounds.width + self.style.border_width, |
| 634 | draw_bounds.height + self.style.border_width, |
| 635 | ); |
| 636 | |
| 637 | if self.style.border_radius > 0.0 { |
| 638 | // Render rounded border (simplified - would need proper border rendering) |
| 639 | let (vertices, indices) = VertexBuilder::rounded_rectangle( |
| 640 | border_bounds.x, |
| 641 | border_bounds.y, |
| 642 | border_bounds.width, |
| 643 | border_bounds.height, |
| 644 | self.style.border_radius + self.style.border_width / 2.0, |
| 645 | self.style.border_color.to_array(), |
| 646 | 8, // corner segments |
| 647 | ); |
| 648 | batch.add_vertices(&vertices, &indices); |
| 649 | } |
| 650 | } |
| 651 | |
| 652 | // Render text |
| 653 | let text_x = draw_bounds.x + draw_bounds.width / 2.0; |
| 654 | let text_y = draw_bounds.y + draw_bounds.height / 2.0 - self.style.font_size / 2.0; |
| 655 | let mut text_color = self.style.text_color; |
| 656 | if matches!(state, ButtonState::Disabled) { |
| 657 | text_color.a *= 0.35; |
| 658 | } |
| 659 | |
| 660 | batch.add_text_aligned( |
| 661 | self.text.clone(), |
| 662 | (text_x, text_y), |
| 663 | text_color.to_types_color(), |
| 664 | self.style.font_size, |
| 665 | 0.0, // Default letter spacing |
| 666 | strato_core::text::TextAlign::Center, |
| 667 | ); |
| 668 | } |
| 669 | |
| 670 | fn update(&mut self, ctx: &WidgetContext) { |
| 671 | self.control.update(ctx.delta_time); |
| 672 | } |
| 673 | |
| 674 | fn handle_event(&mut self, event: &Event) -> EventResult { |
| 675 | let previous_state = self.get_state(); |
| 676 | let bounds = self.bounds.get(); |
| 677 | |
| 678 | // Pointer interactions and hover callbacks |
| 679 | if let EventResult::Handled = self.control.handle_pointer_event(event, bounds) { |
| 680 | if matches!(event, Event::MouseUp(_)) && matches!(previous_state, ButtonState::Pressed) |
| 681 | { |
| 682 | if let Some(handler) = &self.on_click { |
| 683 | handler(); |
| 684 | } |
| 685 | } |
| 686 | if let Event::MouseMove(mouse_event) = event { |
| 687 | let is_hovered = |
| 688 | bounds.contains(Point::new(mouse_event.position.x, mouse_event.position.y)); |
| 689 | if let Some(handler) = &self.on_hover { |
| 690 | handler(is_hovered); |
| 691 | } |
| 692 | } |
| 693 | return EventResult::Handled; |
| 694 | } |
| 695 | |
| 696 | // Keyboard accessibility |
| 697 | if let EventResult::Handled = self.control.handle_keyboard_activation(event) { |
| 698 | if matches!(event, Event::KeyUp(_)) { |
| 699 | if let Some(handler) = &self.on_click { |
| 700 | handler(); |
| 701 | } |
| 702 | } |
| 703 | return EventResult::Handled; |
| 704 | } |
| 705 | |
| 706 | EventResult::Ignored |
| 707 | } |
| 708 | |
| 709 | fn as_any(&self) -> &dyn Any { |
| 710 | self |
| 711 | } |
| 712 | |
| 713 | fn as_any_mut(&mut self) -> &mut dyn Any { |
| 714 | self |
| 715 | } |
| 716 | |
| 717 | fn clone_widget(&self) -> Box<dyn Widget> { |
| 718 | Box::new(Button { |
| 719 | id: generate_id(), |
| 720 | text: self.text.clone(), |
| 721 | style: self.style.clone(), |
| 722 | control: self.control.clone(), |
| 723 | bounds: Signal::new(self.bounds.get()), |
| 724 | enabled: Signal::new(self.enabled.get()), |
| 725 | visible: Signal::new(self.visible.get()), |
| 726 | on_click: None, |
| 727 | on_hover: None, |
| 728 | theme: self.theme.clone(), |
| 729 | }) |
| 730 | } |
| 731 | |
| 732 | fn as_taffy(&self) -> Option<&dyn TaffyWidget> { |
| 733 | Some(self) |
| 734 | } |
| 735 | } |
| 736 | |
| 737 | impl TaffyWidget for Button { |
| 738 | fn build_layout(&self, tree: &mut TaffyTree<()>) -> TaffyLayoutResult<NodeId> { |
| 739 | let text_width = crate::text::measure_text_width(&self.text, self.style.font_size, 0.0); |
| 740 | let text_height = self.style.font_size; |
| 741 | |
| 742 | let width = (text_width + self.style.padding * 2.0).max(self.style.min_width); |
| 743 | let height = (text_height + self.style.padding * 2.0).max(self.style.min_height); |
| 744 | |
| 745 | let style = Style { |
| 746 | size: strato_core::taffy::geometry::Size { |
| 747 | width: length(width), |
| 748 | height: length(height), |
| 749 | }, |
| 750 | min_size: strato_core::taffy::geometry::Size { |
| 751 | width: length(self.style.min_width), |
| 752 | height: length(self.style.min_height), |
| 753 | }, |
| 754 | padding: strato_core::taffy::geometry::Rect { |
| 755 | left: length(self.style.padding), |
| 756 | right: length(self.style.padding), |
| 757 | top: length(self.style.padding), |
| 758 | bottom: length(self.style.padding), |
| 759 | }, |
| 760 | ..Default::default() |
| 761 | }; |
| 762 | |
| 763 | tree.new_leaf(style).map_err(|e| TaffyLayoutError::from(e)) |
| 764 | } |
| 765 | } |
| 766 |