zcatgui/src/widgets/checkbox.zig
reugenio 8adc93a345 feat: zcatgui v0.6.0 - Phase 1 Optimization Complete
Performance Infrastructure:
- FrameArena: O(1) per-frame allocator with automatic reset
- ObjectPool: Generic object pool for frequently allocated types
- CommandPool: Specialized pool for draw commands
- RingBuffer: Circular buffer for streaming data
- ScopedArena: RAII pattern for temporary allocations

Dirty Rectangle System:
- Context now tracks dirty regions for partial redraws
- Automatic rect merging to reduce overdraw
- invalidateRect(), needsRedraw(), getDirtyRects() API
- Falls back to full redraw when > 32 dirty rects

Benchmark Suite:
- Timer: High-resolution timing
- Benchmark: Stats collection (avg, min, max, stddev, median)
- FrameTimer: FPS and frame time tracking
- AllocationTracker: Memory usage monitoring
- Pre-built benchmarks for arena, pool, and commands

Context Improvements:
- Integrated FrameArena for zero-allocation hot paths
- frameAllocator() for per-frame widget allocations
- FrameStats for performance monitoring
- Context.init() now returns error union (breaking change)

New Widgets (from previous session):
- Slider: Horizontal/vertical with customization
- ScrollArea: Scrollable content region
- Tabs: Tab container with keyboard navigation
- RadioButton: Radio button groups
- Menu: Dropdown menus (foundation)

Theme System Expansion:
- 5 built-in themes: dark, light, high_contrast, nord, dracula
- ThemeManager with runtime switching
- TTF font support via stb_truetype

Documentation:
- DEVELOPMENT_PLAN.md: 9-phase roadmap to DVUI/Gio parity
- Updated WIDGET_COMPARISON.md with detailed analysis
- Lego Panels architecture documented

Stats: 17 widgets, 123 tests, 5 themes

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-09 12:45:00 +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
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);
}