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>
217 lines
6.1 KiB
Zig
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);
|
|
}
|