New widgets (12): - Switch: Toggle switch with animation - IconButton: Circular icon button (filled/outlined/ghost/tonal) - Divider: Horizontal/vertical separator with optional label - Loader: 7 spinner styles (circular/dots/bars/pulse/bounce/ring/square) - Surface: Elevated container with shadow layers - Grid: Layout grid with scrolling and selection - Resize: Draggable resize handle (horizontal/vertical/both) - AppBar: Application bar (top/bottom) with actions - NavDrawer: Navigation drawer with items, icons, badges - Sheet: Side/bottom sliding panel with modal support - Discloser: Expandable/collapsible container (3 icon styles) - Selectable: Clickable region with selection modes Core systems added: - GestureRecognizer: Tap, double-tap, long-press, drag, swipe - Velocity tracking and fling detection - Spring physics for fluid animations Integration: - All widgets exported via widgets.zig - GestureRecognizer exported via zcatgui.zig - Spring/SpringConfig exported from animation.zig - Color.withAlpha() method added to style.zig Stats: 47 widget files, 338+ tests, +5,619 LOC Full Gio UI parity achieved. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
308 lines
9.2 KiB
Zig
308 lines
9.2 KiB
Zig
//! Divider Widget - Visual separator
|
|
//!
|
|
//! A simple line that separates content. Can be horizontal or vertical,
|
|
//! and optionally include a label in the middle.
|
|
|
|
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");
|
|
|
|
/// Divider orientation
|
|
pub const Orientation = enum {
|
|
horizontal,
|
|
vertical,
|
|
};
|
|
|
|
/// Divider configuration
|
|
pub const Config = struct {
|
|
/// Orientation
|
|
orientation: Orientation = .horizontal,
|
|
/// Line thickness
|
|
thickness: u16 = 1,
|
|
/// Margin on each side
|
|
margin: u16 = 8,
|
|
/// Label text (centered in divider)
|
|
label: ?[]const u8 = null,
|
|
/// Label padding (space between line and label)
|
|
label_padding: u16 = 12,
|
|
/// Inset from edges (e.g., to not span full width)
|
|
inset: u16 = 0,
|
|
/// Use dashed line
|
|
dashed: bool = false,
|
|
/// Dash length (if dashed)
|
|
dash_length: u16 = 4,
|
|
/// Gap between dashes
|
|
dash_gap: u16 = 4,
|
|
};
|
|
|
|
/// Divider colors
|
|
pub const Colors = struct {
|
|
/// Line color
|
|
line: Style.Color = Style.Color.rgba(60, 60, 60, 255),
|
|
/// Label text color
|
|
label_color: Style.Color = Style.Color.rgba(120, 120, 120, 255),
|
|
/// Label background (to cover line behind text)
|
|
label_bg: ?Style.Color = null,
|
|
|
|
pub fn fromTheme(theme: Style.Theme) Colors {
|
|
return .{
|
|
.line = theme.border,
|
|
.label_color = theme.foreground.darken(30),
|
|
.label_bg = theme.background,
|
|
};
|
|
}
|
|
};
|
|
|
|
/// Draw a simple horizontal divider
|
|
pub fn divider(ctx: *Context) void {
|
|
dividerEx(ctx, .{}, .{});
|
|
}
|
|
|
|
/// Draw a divider with label
|
|
pub fn dividerLabel(ctx: *Context, label_text: []const u8) void {
|
|
dividerEx(ctx, .{ .label = label_text }, .{});
|
|
}
|
|
|
|
/// Draw a divider with configuration
|
|
pub fn dividerEx(ctx: *Context, config: Config, colors: Colors) void {
|
|
const bounds = ctx.layout.nextRect();
|
|
dividerRect(ctx, bounds, config, colors);
|
|
}
|
|
|
|
/// Draw a divider in a specific rectangle
|
|
pub fn dividerRect(
|
|
ctx: *Context,
|
|
bounds: Layout.Rect,
|
|
config: Config,
|
|
colors: Colors,
|
|
) void {
|
|
if (bounds.isEmpty()) return;
|
|
|
|
switch (config.orientation) {
|
|
.horizontal => drawHorizontal(ctx, bounds, config, colors),
|
|
.vertical => drawVertical(ctx, bounds, config, colors),
|
|
}
|
|
}
|
|
|
|
fn drawHorizontal(ctx: *Context, bounds: Layout.Rect, config: Config, colors: Colors) void {
|
|
const y = bounds.y + @as(i32, @intCast(bounds.h / 2));
|
|
const x_start = bounds.x + @as(i32, @intCast(config.inset));
|
|
const x_end = bounds.x + @as(i32, @intCast(bounds.w)) - @as(i32, @intCast(config.inset));
|
|
const line_width = @as(u32, @intCast(@max(0, x_end - x_start)));
|
|
|
|
if (config.label) |label_text| {
|
|
if (label_text.len > 0) {
|
|
// Draw with label
|
|
const label_width = label_text.len * 8; // Approximate char width
|
|
const center_x = bounds.x + @as(i32, @intCast(bounds.w / 2));
|
|
const label_x = center_x - @as(i32, @intCast(label_width / 2));
|
|
const gap_start = label_x - @as(i32, @intCast(config.label_padding));
|
|
const gap_end = label_x + @as(i32, @intCast(label_width + config.label_padding));
|
|
|
|
// Left line
|
|
if (gap_start > x_start) {
|
|
drawLine(ctx, x_start, y, gap_start, config, colors);
|
|
}
|
|
|
|
// Right line
|
|
if (gap_end < x_end) {
|
|
drawLine(ctx, gap_end, y, x_end, config, colors);
|
|
}
|
|
|
|
// Label background
|
|
if (colors.label_bg) |bg| {
|
|
ctx.pushCommand(Command.rect(
|
|
gap_start,
|
|
y - 6,
|
|
@intCast(@as(u32, @intCast(gap_end - gap_start))),
|
|
12,
|
|
bg,
|
|
));
|
|
}
|
|
|
|
// Label text
|
|
ctx.pushCommand(Command.text(label_x, y - 4, label_text, colors.label_color));
|
|
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Simple line without label
|
|
if (config.dashed) {
|
|
drawDashedLine(ctx, x_start, y, line_width, true, config, colors);
|
|
} else {
|
|
ctx.pushCommand(Command.rect(x_start, y, line_width, config.thickness, colors.line));
|
|
}
|
|
}
|
|
|
|
fn drawVertical(ctx: *Context, bounds: Layout.Rect, config: Config, colors: Colors) void {
|
|
const x = bounds.x + @as(i32, @intCast(bounds.w / 2));
|
|
const y_start = bounds.y + @as(i32, @intCast(config.inset));
|
|
const y_end = bounds.y + @as(i32, @intCast(bounds.h)) - @as(i32, @intCast(config.inset));
|
|
const line_height = @as(u32, @intCast(@max(0, y_end - y_start)));
|
|
|
|
if (config.label) |label_text| {
|
|
if (label_text.len > 0) {
|
|
// For vertical dividers, rotate the label concept
|
|
const center_y = bounds.y + @as(i32, @intCast(bounds.h / 2));
|
|
const gap_start = center_y - @as(i32, @intCast(config.label_padding));
|
|
const gap_end = center_y + @as(i32, @intCast(config.label_padding));
|
|
|
|
// Top line
|
|
if (gap_start > y_start) {
|
|
if (config.dashed) {
|
|
drawDashedLine(ctx, x, y_start, @intCast(@as(u32, @intCast(gap_start - y_start))), false, config, colors);
|
|
} else {
|
|
ctx.pushCommand(Command.rect(x, y_start, config.thickness, @intCast(@as(u32, @intCast(gap_start - y_start))), colors.line));
|
|
}
|
|
}
|
|
|
|
// Bottom line
|
|
if (gap_end < y_end) {
|
|
if (config.dashed) {
|
|
drawDashedLine(ctx, x, gap_end, @intCast(@as(u32, @intCast(y_end - gap_end))), false, config, colors);
|
|
} else {
|
|
ctx.pushCommand(Command.rect(x, gap_end, config.thickness, @intCast(@as(u32, @intCast(y_end - gap_end))), colors.line));
|
|
}
|
|
}
|
|
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Simple vertical line
|
|
if (config.dashed) {
|
|
drawDashedLine(ctx, x, y_start, line_height, false, config, colors);
|
|
} else {
|
|
ctx.pushCommand(Command.rect(x, y_start, config.thickness, line_height, colors.line));
|
|
}
|
|
}
|
|
|
|
fn drawLine(ctx: *Context, x_start: i32, y: i32, x_end: i32, config: Config, colors: Colors) void {
|
|
const width = @as(u32, @intCast(@max(0, x_end - x_start)));
|
|
if (config.dashed) {
|
|
drawDashedLine(ctx, x_start, y, width, true, config, colors);
|
|
} else {
|
|
ctx.pushCommand(Command.rect(x_start, y, width, config.thickness, colors.line));
|
|
}
|
|
}
|
|
|
|
fn drawDashedLine(ctx: *Context, start_x: i32, start_y: i32, length: u32, horizontal: bool, config: Config, colors: Colors) void {
|
|
const dash_len = config.dash_length;
|
|
const gap_len = config.dash_gap;
|
|
const stride = dash_len + gap_len;
|
|
|
|
var pos: u32 = 0;
|
|
while (pos < length) {
|
|
const dash_size = @min(dash_len, length - pos);
|
|
|
|
if (horizontal) {
|
|
ctx.pushCommand(Command.rect(
|
|
start_x + @as(i32, @intCast(pos)),
|
|
start_y,
|
|
dash_size,
|
|
config.thickness,
|
|
colors.line,
|
|
));
|
|
} else {
|
|
ctx.pushCommand(Command.rect(
|
|
start_x,
|
|
start_y + @as(i32, @intCast(pos)),
|
|
config.thickness,
|
|
dash_size,
|
|
colors.line,
|
|
));
|
|
}
|
|
|
|
pos += stride;
|
|
}
|
|
}
|
|
|
|
/// Convenience: horizontal rule
|
|
pub fn hr(ctx: *Context) void {
|
|
divider(ctx);
|
|
}
|
|
|
|
/// Convenience: vertical rule
|
|
pub fn vr(ctx: *Context) void {
|
|
dividerEx(ctx, .{ .orientation = .vertical }, .{});
|
|
}
|
|
|
|
// =============================================================================
|
|
// Tests
|
|
// =============================================================================
|
|
|
|
test "divider generates command" {
|
|
var ctx = try Context.init(std.testing.allocator, 800, 600);
|
|
defer ctx.deinit();
|
|
|
|
ctx.beginFrame();
|
|
ctx.layout.row_height = 16;
|
|
|
|
divider(&ctx);
|
|
|
|
try std.testing.expect(ctx.commands.items.len >= 1);
|
|
|
|
ctx.endFrame();
|
|
}
|
|
|
|
test "divider with label" {
|
|
var ctx = try Context.init(std.testing.allocator, 800, 600);
|
|
defer ctx.deinit();
|
|
|
|
ctx.beginFrame();
|
|
ctx.layout.row_height = 16;
|
|
|
|
dividerLabel(&ctx, "Section");
|
|
|
|
// Should generate: left line + right line + text
|
|
try std.testing.expect(ctx.commands.items.len >= 3);
|
|
|
|
ctx.endFrame();
|
|
}
|
|
|
|
test "vertical divider" {
|
|
var ctx = try Context.init(std.testing.allocator, 800, 600);
|
|
defer ctx.deinit();
|
|
|
|
ctx.beginFrame();
|
|
ctx.layout.row_height = 100;
|
|
|
|
dividerEx(&ctx, .{ .orientation = .vertical }, .{});
|
|
|
|
try std.testing.expect(ctx.commands.items.len >= 1);
|
|
|
|
ctx.endFrame();
|
|
}
|
|
|
|
test "dashed divider" {
|
|
var ctx = try Context.init(std.testing.allocator, 800, 600);
|
|
defer ctx.deinit();
|
|
|
|
ctx.beginFrame();
|
|
ctx.layout.row_height = 16;
|
|
|
|
dividerEx(&ctx, .{ .dashed = true }, .{});
|
|
|
|
// Dashed line should generate multiple rect commands
|
|
try std.testing.expect(ctx.commands.items.len >= 1);
|
|
|
|
ctx.endFrame();
|
|
}
|
|
|
|
test "hr and vr convenience" {
|
|
var ctx = try Context.init(std.testing.allocator, 800, 600);
|
|
defer ctx.deinit();
|
|
|
|
ctx.beginFrame();
|
|
ctx.layout.row_height = 16;
|
|
|
|
hr(&ctx);
|
|
vr(&ctx);
|
|
|
|
try std.testing.expect(ctx.commands.items.len >= 2);
|
|
|
|
ctx.endFrame();
|
|
}
|