//! Checkbox Widget - Boolean toggle //! //! A checkbox that toggles between checked and unchecked states. //! Returns true when the state changes. 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"); /// Checkbox configuration pub const CheckboxConfig = struct { /// Label text label: []const u8 = "", /// Disabled state disabled: bool = false, /// Size of the checkbox box box_size: u32 = 16, /// Gap between box and label gap: u32 = 8, }; /// Draw a checkbox and return true if state changed pub fn checkbox(ctx: *Context, checked: *bool, label_text: []const u8) bool { return checkboxEx(ctx, checked, .{ .label = label_text }); } /// Draw a checkbox with custom configuration pub fn checkboxEx(ctx: *Context, checked: *bool, config: CheckboxConfig) bool { const bounds = ctx.layout.nextRect(); return checkboxRect(ctx, bounds, checked, config); } /// Draw a checkbox in a specific rectangle pub fn checkboxRect( ctx: *Context, bounds: Layout.Rect, checked: *bool, config: CheckboxConfig, ) bool { if (bounds.isEmpty()) return false; const id = ctx.getId(config.label); _ = id; // Check mouse interaction const mouse = ctx.input.mousePos(); const hovered = bounds.contains(mouse.x, mouse.y) and !config.disabled; const clicked = hovered and ctx.input.mouseReleased(.left); // Toggle on click var changed = false; if (clicked) { checked.* = !checked.*; changed = true; } // Theme colors const theme = Style.Theme.dark; // Calculate box position (vertically centered) const box_y = bounds.y + @as(i32, @intCast((bounds.h -| config.box_size) / 2)); // Determine box colors const box_bg = if (config.disabled) theme.secondary.darken(20) else if (checked.*) theme.primary else if (hovered) theme.input_bg.lighten(10) else theme.input_bg; const box_border = if (config.disabled) theme.border.darken(20) else if (hovered) theme.primary else theme.border; // Draw checkbox box ctx.pushCommand(Command.rect( bounds.x, box_y, config.box_size, config.box_size, box_bg, )); ctx.pushCommand(Command.rectOutline( bounds.x, box_y, config.box_size, config.box_size, box_border, )); // Draw checkmark if checked if (checked.*) { const check_margin: u32 = 4; const check_size = config.box_size -| (check_margin * 2); const check_x = bounds.x + @as(i32, @intCast(check_margin)); const check_y = box_y + @as(i32, @intCast(check_margin)); // Simple checkmark: draw two lines const check_color = Style.Color.white; // Line 1: bottom-left to middle-bottom ctx.pushCommand(Command.line( check_x + 2, check_y + @as(i32, @intCast(check_size / 2)), check_x + @as(i32, @intCast(check_size / 2)), check_y + @as(i32, @intCast(check_size)) - 2, check_color, )); // Line 2: middle-bottom to top-right ctx.pushCommand(Command.line( check_x + @as(i32, @intCast(check_size / 2)), check_y + @as(i32, @intCast(check_size)) - 2, check_x + @as(i32, @intCast(check_size)) - 2, check_y + 2, check_color, )); } // Draw label if present if (config.label.len > 0) { const label_x = bounds.x + @as(i32, @intCast(config.box_size + config.gap)); const char_height: u32 = 8; const label_y = bounds.y + @as(i32, @intCast((bounds.h -| char_height) / 2)); const label_color = if (config.disabled) theme.foreground.darken(40) else theme.foreground; ctx.pushCommand(Command.text(label_x, label_y, config.label, label_color)); } return changed; } // ============================================================================= // Tests // ============================================================================= test "checkbox toggle" { var ctx = try Context.init(std.testing.allocator, 800, 600); defer ctx.deinit(); var checked = false; // Frame 1: Click inside checkbox ctx.beginFrame(); ctx.layout.row_height = 24; ctx.input.setMousePos(8, 12); ctx.input.setMouseButton(.left, true); _ = checkbox(&ctx, &checked, "Option"); ctx.endFrame(); // Frame 2: Release inside checkbox ctx.beginFrame(); ctx.layout.row_height = 24; ctx.input.setMousePos(8, 12); ctx.input.setMouseButton(.left, false); const changed = checkbox(&ctx, &checked, "Option"); ctx.endFrame(); try std.testing.expect(changed); try std.testing.expect(checked); } test "checkbox generates commands" { var ctx = try Context.init(std.testing.allocator, 800, 600); defer ctx.deinit(); var checked = true; ctx.beginFrame(); ctx.layout.row_height = 24; _ = checkbox(&ctx, &checked, "With label"); // Should generate: rect (box) + rect_outline (border) + 2 lines (checkmark) + text (label) try std.testing.expect(ctx.commands.items.len >= 4); ctx.endFrame(); } test "checkbox disabled no toggle" { var ctx = try Context.init(std.testing.allocator, 800, 600); defer ctx.deinit(); var checked = false; // Frame 1: Click ctx.beginFrame(); ctx.layout.row_height = 24; ctx.input.setMousePos(8, 12); ctx.input.setMouseButton(.left, true); _ = checkboxEx(&ctx, &checked, .{ .label = "Disabled", .disabled = true }); ctx.endFrame(); // Frame 2: Release ctx.beginFrame(); ctx.layout.row_height = 24; ctx.input.setMousePos(8, 12); ctx.input.setMouseButton(.left, false); const changed = checkboxEx(&ctx, &checked, .{ .label = "Disabled", .disabled = true }); ctx.endFrame(); try std.testing.expect(!changed); try std.testing.expect(!checked); }