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>
115 lines
3.3 KiB
Zig
115 lines
3.3 KiB
Zig
//! Label Widget - Static text display
|
|
//!
|
|
//! A simple widget for displaying text. Supports alignment and styling.
|
|
|
|
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");
|
|
|
|
/// Text alignment
|
|
pub const Alignment = enum {
|
|
left,
|
|
center,
|
|
right,
|
|
};
|
|
|
|
/// Label configuration
|
|
pub const LabelConfig = struct {
|
|
color: Style.Color = Style.Color.foreground,
|
|
alignment: Alignment = .left,
|
|
/// Padding inside the label area
|
|
padding: u32 = 0,
|
|
};
|
|
|
|
/// Draw a label at the current layout position
|
|
pub fn label(ctx: *Context, text: []const u8) void {
|
|
labelEx(ctx, text, .{});
|
|
}
|
|
|
|
/// Draw a label with custom configuration
|
|
pub fn labelEx(ctx: *Context, text: []const u8, config: LabelConfig) void {
|
|
const bounds = ctx.layout.nextRect();
|
|
labelRect(ctx, bounds, text, config);
|
|
}
|
|
|
|
/// Draw a label in a specific rectangle
|
|
pub fn labelRect(ctx: *Context, bounds: Layout.Rect, text: []const u8, config: LabelConfig) void {
|
|
if (bounds.isEmpty()) return;
|
|
|
|
const inner = bounds.shrink(config.padding);
|
|
if (inner.isEmpty()) return;
|
|
|
|
// Calculate text position based on alignment
|
|
// Assume 8 pixels per character (bitmap font)
|
|
const char_width: u32 = 8;
|
|
const text_width = @as(u32, @intCast(text.len)) * char_width;
|
|
|
|
const x: i32 = switch (config.alignment) {
|
|
.left => inner.x,
|
|
.center => inner.x + @as(i32, @intCast((inner.w -| text_width) / 2)),
|
|
.right => inner.x + @as(i32, @intCast(inner.w -| text_width)),
|
|
};
|
|
|
|
// Center vertically (assume 8 pixel font height)
|
|
const char_height: u32 = 8;
|
|
const y = inner.y + @as(i32, @intCast((inner.h -| char_height) / 2));
|
|
|
|
ctx.pushCommand(Command.text(x, y, text, config.color));
|
|
}
|
|
|
|
/// Draw a colored label (convenience function)
|
|
pub fn labelColored(ctx: *Context, text: []const u8, color: Style.Color) void {
|
|
labelEx(ctx, text, .{ .color = color });
|
|
}
|
|
|
|
/// Draw a centered label (convenience function)
|
|
pub fn labelCentered(ctx: *Context, text: []const u8) void {
|
|
labelEx(ctx, text, .{ .alignment = .center });
|
|
}
|
|
|
|
// =============================================================================
|
|
// Tests
|
|
// =============================================================================
|
|
|
|
test "label generates text command" {
|
|
var ctx = try Context.init(std.testing.allocator, 800, 600);
|
|
defer ctx.deinit();
|
|
|
|
ctx.beginFrame();
|
|
ctx.layout.row_height = 20;
|
|
|
|
label(&ctx, "Hello");
|
|
|
|
try std.testing.expectEqual(@as(usize, 1), ctx.commands.items.len);
|
|
switch (ctx.commands.items[0]) {
|
|
.text => |t| {
|
|
try std.testing.expectEqualStrings("Hello", t.text);
|
|
},
|
|
else => unreachable,
|
|
}
|
|
|
|
ctx.endFrame();
|
|
}
|
|
|
|
test "label alignment" {
|
|
var ctx = try Context.init(std.testing.allocator, 800, 600);
|
|
defer ctx.deinit();
|
|
|
|
ctx.beginFrame();
|
|
ctx.layout.row_height = 20;
|
|
|
|
// Left aligned (default)
|
|
labelEx(&ctx, "Left", .{ .alignment = .left });
|
|
|
|
// The text should start at x=0
|
|
switch (ctx.commands.items[0]) {
|
|
.text => |t| {
|
|
try std.testing.expectEqual(@as(i32, 0), t.x);
|
|
},
|
|
else => unreachable,
|
|
}
|
|
|
|
ctx.endFrame();
|
|
}
|