//! Button Widget - Clickable button //! //! An immediate mode button that returns true when clicked. //! Supports hover/active states and keyboard activation. //! //! Two modes: //! - Stateless: `button()`, `buttonEx()` - instant color changes //! - Stateful: `buttonStateful()` - smooth hover/press transitions const std = @import("std"); const Context = @import("../core/context.zig").Context; const Command = @import("../core/command.zig"); const Layout = @import("../core/layout.zig"); const Style = @import("../core/style.zig"); const Input = @import("../core/input.zig"); const animation = @import("../render/animation.zig"); /// Button importance level pub const Importance = enum { normal, primary, danger, }; /// Button state for smooth transitions (optional) /// Pass to buttonStateful() for animated hover/press effects. pub const ButtonState = struct { /// Hover/press transition (0=normal, 0.5=hover, 1.0=pressed) transition: animation.HoverTransition = .{}, /// Last frame time for delta calculation last_time_ms: u64 = 0, }; /// Button configuration pub const ButtonConfig = struct { /// Background color (overrides theme) bg: ?Style.Color = null, /// Foreground/text color (overrides theme) fg: ?Style.Color = null, /// Importance level importance: Importance = .normal, /// Disabled state disabled: bool = false, /// Padding around text padding: u32 = 10, /// Corner radius (0 = square, default 4 for fancy mode) corner_radius: u8 = 4, }; /// Draw a button and return true if clicked pub fn button(ctx: *Context, text: []const u8) bool { return buttonEx(ctx, text, .{}); } /// Draw a button with custom configuration pub fn buttonEx(ctx: *Context, text: []const u8, config: ButtonConfig) bool { const bounds = ctx.layout.nextRect(); return buttonRect(ctx, bounds, text, config); } /// Draw a button in a specific rectangle pub fn buttonRect(ctx: *Context, bounds: Layout.Rect, text: []const u8, config: ButtonConfig) bool { if (bounds.isEmpty()) return false; const id = ctx.getId(text); _ = id; // For future focus management // Check mouse interaction const mouse = ctx.input.mousePos(); const hovered = bounds.contains(mouse.x, mouse.y) and !config.disabled; const pressed = hovered and ctx.input.mouseDown(.left); const clicked = hovered and ctx.input.mouseReleased(.left); // Determine colors based on state (Z-Design: usar theme dinámico) const theme = Style.currentTheme().*; const base_bg = config.bg orelse switch (config.importance) { .normal => theme.button_bg, .primary => theme.primary, .danger => theme.danger, }; const bg_color = if (config.disabled) base_bg.darken(30) else if (pressed) base_bg.darken(20) else if (hovered) base_bg.lighten(10) else base_bg; const fg_color = config.fg orelse if (config.disabled) theme.button_fg.darken(40) else theme.button_fg; // Draw background and border based on render mode if (Style.isFancy() and config.corner_radius > 0) { // Fancy mode: rounded corners with subtle gradient for 3D effect ctx.pushCommand(Command.gradientButton(bounds.x, bounds.y, bounds.w, bounds.h, bg_color, config.corner_radius)); ctx.pushCommand(Command.roundedRectOutline(bounds.x, bounds.y, bounds.w, bounds.h, theme.border, config.corner_radius)); } else { // Simple mode: square corners ctx.pushCommand(Command.rect(bounds.x, bounds.y, bounds.w, bounds.h, bg_color)); ctx.pushCommand(Command.rectOutline(bounds.x, bounds.y, bounds.w, bounds.h, theme.border)); } // Bisel 3D completo: 4 lados (invierte cuando está pulsado) // Z-Design V2: Efecto 3D con las 4 esquinas completas if (bounds.h >= 4 and bounds.w >= 4 and !config.disabled) { const bevel_light = bg_color.lighten(15); const bevel_dark = bg_color.darken(15); const inner_h = bounds.h -| 2; // Altura interior para líneas verticales if (pressed) { // Pressed: bisel invertido = "hundido" // Arriba + izquierda: oscuro (sombra interior) ctx.pushCommand(Command.rect(bounds.x + 1, bounds.y + 1, bounds.w - 2, 1, bevel_dark)); // Top ctx.pushCommand(Command.rect(bounds.x + 1, bounds.y + 1, 1, inner_h, bevel_dark)); // Left // Abajo + derecha: claro (luz interior) ctx.pushCommand(Command.rect(bounds.x + 1, bounds.y + @as(i32, @intCast(bounds.h)) - 2, bounds.w - 2, 1, bevel_light)); // Bottom ctx.pushCommand(Command.rect(bounds.x + @as(i32, @intCast(bounds.w)) - 2, bounds.y + 1, 1, inner_h, bevel_light)); // Right } else { // Normal: bisel = "elevado" // Arriba + izquierda: claro (iluminado) ctx.pushCommand(Command.rect(bounds.x + 1, bounds.y + 1, bounds.w - 2, 1, bevel_light)); // Top ctx.pushCommand(Command.rect(bounds.x + 1, bounds.y + 1, 1, inner_h, bevel_light)); // Left // Abajo + derecha: oscuro (sombra) ctx.pushCommand(Command.rect(bounds.x + 1, bounds.y + @as(i32, @intCast(bounds.h)) - 2, bounds.w - 2, 1, bevel_dark)); // Bottom ctx.pushCommand(Command.rect(bounds.x + @as(i32, @intCast(bounds.w)) - 2, bounds.y + 1, 1, inner_h, bevel_dark)); // Right } } // Draw text centered (con offset +1px cuando está pulsado = "se hunde") // Z-Design V2: usar métricas del contexto para centrado correcto con TTF const text_width = ctx.measureText(text); const char_height = ctx.char_width; // Para fuentes cuadradas, height ≈ width const press_offset: i32 = if (pressed) 1 else 0; const text_x = bounds.x + @as(i32, @intCast((bounds.w -| text_width) / 2)) + press_offset; const text_y = bounds.y + @as(i32, @intCast((bounds.h -| char_height) / 2)) + press_offset; ctx.pushCommand(Command.text(text_x, text_y, text, fg_color)); return clicked and !config.disabled; } // ============================================================================= // Stateful Button (with smooth transitions) // ============================================================================= /// Draw a button with smooth hover/press transitions pub fn buttonStateful(ctx: *Context, state: *ButtonState, text: []const u8) bool { return buttonStatefulEx(ctx, state, text, .{}); } /// Draw a stateful button with custom configuration pub fn buttonStatefulEx(ctx: *Context, state: *ButtonState, text: []const u8, config: ButtonConfig) bool { const bounds = ctx.layout.nextRect(); return buttonStatefulRect(ctx, bounds, state, text, config); } /// Draw a stateful button in a specific rectangle pub fn buttonStatefulRect( ctx: *Context, bounds: Layout.Rect, state: *ButtonState, text: []const u8, config: ButtonConfig, ) bool { if (bounds.isEmpty()) return false; // Calculate delta time const current_time = ctx.current_time_ms; const dt_ms: u64 = if (state.last_time_ms > 0 and current_time > state.last_time_ms) current_time - state.last_time_ms else 16; // Default ~60fps state.last_time_ms = current_time; // Check mouse interaction const mouse = ctx.input.mousePos(); const hovered = bounds.contains(mouse.x, mouse.y) and !config.disabled; const pressed = hovered and ctx.input.mouseDown(.left); const clicked = hovered and ctx.input.mouseReleased(.left); // Update transition animation state.transition.updateWithPress(hovered, pressed, dt_ms); // Determine colors based on animated state (Z-Design: usar theme dinámico) const theme = Style.currentTheme().*; const base_bg = config.bg orelse switch (config.importance) { .normal => theme.button_bg, .primary => theme.primary, .danger => theme.danger, }; // Calculate animated background color const bg_color = if (config.disabled) base_bg.darken(30) else state.transition.blendThree( base_bg, // normal base_bg.lighten(10), // hover base_bg.darken(20), // pressed ); const fg_color = config.fg orelse if (config.disabled) theme.button_fg.darken(40) else theme.button_fg; // Draw background and border based on render mode if (Style.isFancy() and config.corner_radius > 0) { // Fancy mode: rounded corners with subtle gradient for 3D effect ctx.pushCommand(Command.gradientButton(bounds.x, bounds.y, bounds.w, bounds.h, bg_color, config.corner_radius)); ctx.pushCommand(Command.roundedRectOutline(bounds.x, bounds.y, bounds.w, bounds.h, theme.border, config.corner_radius)); } else { ctx.pushCommand(Command.rect(bounds.x, bounds.y, bounds.w, bounds.h, bg_color)); ctx.pushCommand(Command.rectOutline(bounds.x, bounds.y, bounds.w, bounds.h, theme.border)); } // Bisel 3D completo: 4 lados (invierte cuando está pulsado) // Z-Design V2: Efecto 3D con las 4 esquinas completas if (bounds.h >= 4 and bounds.w >= 4 and !config.disabled) { const bevel_light = bg_color.lighten(15); const bevel_dark = bg_color.darken(15); const inner_h = bounds.h -| 2; // Altura interior para líneas verticales if (pressed) { // Pressed: bisel invertido = "hundido" // Arriba + izquierda: oscuro (sombra interior) ctx.pushCommand(Command.rect(bounds.x + 1, bounds.y + 1, bounds.w - 2, 1, bevel_dark)); // Top ctx.pushCommand(Command.rect(bounds.x + 1, bounds.y + 1, 1, inner_h, bevel_dark)); // Left // Abajo + derecha: claro (luz interior) ctx.pushCommand(Command.rect(bounds.x + 1, bounds.y + @as(i32, @intCast(bounds.h)) - 2, bounds.w - 2, 1, bevel_light)); // Bottom ctx.pushCommand(Command.rect(bounds.x + @as(i32, @intCast(bounds.w)) - 2, bounds.y + 1, 1, inner_h, bevel_light)); // Right } else { // Normal: bisel = "elevado" // Arriba + izquierda: claro (iluminado) ctx.pushCommand(Command.rect(bounds.x + 1, bounds.y + 1, bounds.w - 2, 1, bevel_light)); // Top ctx.pushCommand(Command.rect(bounds.x + 1, bounds.y + 1, 1, inner_h, bevel_light)); // Left // Abajo + derecha: oscuro (sombra) ctx.pushCommand(Command.rect(bounds.x + 1, bounds.y + @as(i32, @intCast(bounds.h)) - 2, bounds.w - 2, 1, bevel_dark)); // Bottom ctx.pushCommand(Command.rect(bounds.x + @as(i32, @intCast(bounds.w)) - 2, bounds.y + 1, 1, inner_h, bevel_dark)); // Right } } // Draw text centered (con offset +1px cuando está pulsado = "se hunde") // Z-Design V2: usar métricas del contexto para centrado correcto con TTF const text_width = ctx.measureText(text); const char_height = ctx.char_width; // Para fuentes cuadradas, height ≈ width const press_offset: i32 = if (pressed) 1 else 0; const text_x = bounds.x + @as(i32, @intCast((bounds.w -| text_width) / 2)) + press_offset; const text_y = bounds.y + @as(i32, @intCast((bounds.h -| char_height) / 2)) + press_offset; ctx.pushCommand(Command.text(text_x, text_y, text, fg_color)); return clicked and !config.disabled; } // ============================================================================= // Convenience Functions // ============================================================================= /// Draw a primary button (convenience function) pub fn buttonPrimary(ctx: *Context, text: []const u8) bool { return buttonEx(ctx, text, .{ .importance = .primary }); } /// Draw a danger button (convenience function) pub fn buttonDanger(ctx: *Context, text: []const u8) bool { return buttonEx(ctx, text, .{ .importance = .danger }); } /// Draw a disabled button (convenience function) pub fn buttonDisabled(ctx: *Context, text: []const u8) bool { return buttonEx(ctx, text, .{ .disabled = true }); } // ============================================================================= // Tests // ============================================================================= test "button generates commands" { var ctx = try Context.init(std.testing.allocator, 800, 600); defer ctx.deinit(); ctx.beginFrame(); ctx.layout.row_height = 30; _ = button(&ctx, "Click me"); // Should generate: rect (background) + rect_outline (border) + 4 bevel lines + text = 7 try std.testing.expectEqual(@as(usize, 7), ctx.commands.items.len); ctx.endFrame(); } test "button click detection" { var ctx = try Context.init(std.testing.allocator, 800, 600); defer ctx.deinit(); // Frame 1: Mouse pressed inside button ctx.beginFrame(); ctx.layout.row_height = 30; ctx.input.setMousePos(50, 15); ctx.input.setMouseButton(.left, true); _ = button(&ctx, "Test"); ctx.endFrame(); // Frame 2: Mouse released inside button ctx.beginFrame(); ctx.layout.row_height = 30; ctx.input.setMousePos(50, 15); ctx.input.setMouseButton(.left, false); const clicked = button(&ctx, "Test"); try std.testing.expect(clicked); ctx.endFrame(); } test "button disabled no click" { var ctx = try Context.init(std.testing.allocator, 800, 600); defer ctx.deinit(); // Frame 1: Mouse pressed ctx.beginFrame(); ctx.layout.row_height = 30; ctx.input.setMousePos(50, 15); ctx.input.setMouseButton(.left, true); _ = buttonEx(&ctx, "Disabled", .{ .disabled = true }); ctx.endFrame(); // Frame 2: Mouse released ctx.beginFrame(); ctx.layout.row_height = 30; ctx.input.setMousePos(50, 15); ctx.input.setMouseButton(.left, false); const clicked = buttonEx(&ctx, "Disabled", .{ .disabled = true }); try std.testing.expect(!clicked); ctx.endFrame(); } test "buttonStateful transitions" { var ctx = try Context.init(std.testing.allocator, 800, 600); defer ctx.deinit(); var state = ButtonState{}; // Frame 1: Not hovered ctx.beginFrame(); ctx.current_time_ms = 0; ctx.layout.row_height = 30; ctx.input.setMousePos(500, 500); // Outside button _ = buttonStateful(&ctx, &state, "Test"); ctx.endFrame(); // Transition should be at 0 try std.testing.expectEqual(@as(f32, 0.0), state.transition.value); // Frame 2: Hovered ctx.beginFrame(); ctx.current_time_ms = 100; ctx.layout.row_height = 30; ctx.input.setMousePos(50, 15); // Inside button _ = buttonStateful(&ctx, &state, "Test"); ctx.endFrame(); // Transition should have started moving (>0) try std.testing.expect(state.transition.value > 0.0); }