//! 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(); }