zcatgui/src/widgets/checkbox.zig
R.Eugenio 8f4336f1f6 feat(widgets): Z-Design usar theme dinámico en todos los widgets
Cambio de Style.Theme.dark (hardcoded) a Style.currentTheme().*
en 5 archivos / 7 ocurrencias:

- text_input.zig (línea 282)
- button.zig (líneas 75, 163)
- list.zig (líneas 124, 131)
- checkbox.zig (línea 61)
- select.zig (línea 102)

Ahora todos los widgets usan el ThemeManager global, permitiendo
cambio de tema en runtime.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-29 21:30:11 +01:00

217 lines
6.1 KiB
Zig

//! 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 (Z-Design: usar theme dinámico)
const theme = Style.currentTheme().*;
// 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);
}