//! Icon Widget - Vector icon system //! //! A lightweight icon system using simple vector drawing. //! Icons are defined as draw commands for resolution independence. 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"); /// Icon size presets pub const Size = enum { small, // 12x12 medium, // 16x16 large, // 24x24 xlarge, // 32x32 pub fn pixels(self: Size) u32 { return switch (self) { .small => 12, .medium => 16, .large => 24, .xlarge => 32, }; } }; /// Built-in icon types pub const IconType = enum { // Navigation arrow_up, arrow_down, arrow_left, arrow_right, chevron_up, chevron_down, chevron_left, chevron_right, home, menu, more_horizontal, more_vertical, // Actions check, close, plus, minus, edit, delete, refresh, search, settings, filter, sort, copy, paste, cut, undo, redo, // Files file, folder, folder_open, document, image_file, download, upload, save, // Status info, warning, error_icon, success, question, star, star_filled, heart, heart_filled, // UI elements eye, eye_off, lock, unlock, user, users, calendar, clock, bell, mail, // Media play, pause, stop, volume, volume_off, // Misc grip, drag, expand, collapse, maximize, minimize, external_link, }; /// Icon configuration pub const Config = struct { /// Icon size size: Size = .medium, /// Custom size (overrides size preset) custom_size: ?u32 = null, /// Stroke width stroke_width: u32 = 2, /// Fill icon filled: bool = false, }; /// Icon colors pub const Colors = struct { foreground: Style.Color = Style.Color.rgba(220, 220, 220, 255), background: ?Style.Color = null, pub fn fromTheme(theme: Style.Theme) Colors { return .{ .foreground = theme.foreground, }; } }; /// Draw an icon pub fn icon(ctx: *Context, icon_type: IconType) void { iconEx(ctx, icon_type, .{}, .{}); } /// Draw an icon with configuration pub fn iconEx( ctx: *Context, icon_type: IconType, config: Config, colors: Colors, ) void { const bounds = ctx.layout.nextRect(); iconRect(ctx, bounds, icon_type, config, colors); } /// Draw an icon in specific rectangle pub fn iconRect( ctx: *Context, bounds: Layout.Rect, icon_type: IconType, config: Config, colors: Colors, ) void { if (bounds.isEmpty()) return; // Background if (colors.background) |bg| { ctx.pushCommand(Command.rect(bounds.x, bounds.y, bounds.w, bounds.h, bg)); } const size = config.custom_size orelse config.size.pixels(); const x = bounds.x + @as(i32, @intCast((bounds.w -| size) / 2)); const y = bounds.y + @as(i32, @intCast((bounds.h -| size) / 2)); const s: i32 = @intCast(size); const sw = config.stroke_width; // Pre-calculated divisions to avoid runtime @divTrunc issues const s2 = @divTrunc(s, 2); const s3 = @divTrunc(s, 3); const s4 = @divTrunc(s, 4); const s5 = @divTrunc(s, 5); const s6 = @divTrunc(s, 6); const s23 = @divTrunc(s * 2, 3); const s34 = @divTrunc(s * 3, 4); const fg = colors.foreground; // Draw icon based on type switch (icon_type) { // Arrows .arrow_up => { drawLine(ctx, x + s2, y + 2, x + 2, y + s - 4, sw, fg); drawLine(ctx, x + s2, y + 2, x + s - 2, y + s - 4, sw, fg); drawLine(ctx, x + s2, y + 2, x + s2, y + s - 2, sw, fg); }, .arrow_down => { drawLine(ctx, x + s2, y + s - 2, x + 2, y + 4, sw, fg); drawLine(ctx, x + s2, y + s - 2, x + s - 2, y + 4, sw, fg); drawLine(ctx, x + s2, y + s - 2, x + s2, y + 2, sw, fg); }, .arrow_left => { drawLine(ctx, x + 2, y + s2, x + s - 4, y + 2, sw, fg); drawLine(ctx, x + 2, y + s2, x + s - 4, y + s - 2, sw, fg); drawLine(ctx, x + 2, y + s2, x + s - 2, y + s2, sw, fg); }, .arrow_right => { drawLine(ctx, x + s - 2, y + s2, x + 4, y + 2, sw, fg); drawLine(ctx, x + s - 2, y + s2, x + 4, y + s - 2, sw, fg); drawLine(ctx, x + s - 2, y + s2, x + 2, y + s2, sw, fg); }, // Chevrons .chevron_up => { drawLine(ctx, x + 2, y + s23, x + s2, y + s3, sw, fg); drawLine(ctx, x + s2, y + s3, x + s - 2, y + s23, sw, fg); }, .chevron_down => { drawLine(ctx, x + 2, y + s3, x + s2, y + s23, sw, fg); drawLine(ctx, x + s2, y + s23, x + s - 2, y + s3, sw, fg); }, .chevron_left => { drawLine(ctx, x + s23, y + 2, x + s3, y + s2, sw, fg); drawLine(ctx, x + s3, y + s2, x + s23, y + s - 2, sw, fg); }, .chevron_right => { drawLine(ctx, x + s3, y + 2, x + s23, y + s2, sw, fg); drawLine(ctx, x + s23, y + s2, x + s3, y + s - 2, sw, fg); }, // Actions .check => { drawLine(ctx, x + 2, y + s2, x + s3, y + s - 4, sw, fg); drawLine(ctx, x + s3, y + s - 4, x + s - 2, y + 3, sw, fg); }, .close => { drawLine(ctx, x + 3, y + 3, x + s - 3, y + s - 3, sw, fg); drawLine(ctx, x + s - 3, y + 3, x + 3, y + s - 3, sw, fg); }, .plus => { drawLine(ctx, x + s2, y + 3, x + s2, y + s - 3, sw, fg); drawLine(ctx, x + 3, y + s2, x + s - 3, y + s2, sw, fg); }, .minus => { drawLine(ctx, x + 3, y + s2, x + s - 3, y + s2, sw, fg); }, // Home .home => { drawLine(ctx, x + 2, y + s2, x + s2, y + 2, sw, fg); drawLine(ctx, x + s2, y + 2, x + s - 2, y + s2, sw, fg); drawLine(ctx, x + 3, y + s2, x + 3, y + s - 2, sw, fg); drawLine(ctx, x + s - 3, y + s2, x + s - 3, y + s - 2, sw, fg); drawLine(ctx, x + 3, y + s - 2, x + s - 3, y + s - 2, sw, fg); }, // Menu .menu => { const bar_y1 = y + s4; const bar_y2 = y + s2; const bar_y3 = y + s34; drawLine(ctx, x + 2, bar_y1, x + s - 2, bar_y1, sw, fg); drawLine(ctx, x + 2, bar_y2, x + s - 2, bar_y2, sw, fg); drawLine(ctx, x + 2, bar_y3, x + s - 2, bar_y3, sw, fg); }, // More (dots) .more_horizontal => { const dot_r: u32 = @max(2, @as(u32, @intCast(s6))); fillCircle(ctx, x + s4, y + s2, dot_r, fg); fillCircle(ctx, x + s2, y + s2, dot_r, fg); fillCircle(ctx, x + s34, y + s2, dot_r, fg); }, .more_vertical => { const dot_r: u32 = @max(2, @as(u32, @intCast(s6))); fillCircle(ctx, x + s2, y + s4, dot_r, fg); fillCircle(ctx, x + s2, y + s2, dot_r, fg); fillCircle(ctx, x + s2, y + s34, dot_r, fg); }, // Search .search => { const r: u32 = @intCast(s3); strokeCircle(ctx, x + s3, y + s3, r, sw, fg); drawLine(ctx, x + s2, y + s2, x + s - 3, y + s - 3, sw, fg); }, // Settings (gear) .settings => { const r: u32 = @intCast(s4); strokeCircle(ctx, x + s2, y + s2, r, sw, fg); drawLine(ctx, x + s2, y + 2, x + s2, y + s4, sw, fg); drawLine(ctx, x + s2, y + s - 2, x + s2, y + s34, sw, fg); drawLine(ctx, x + 2, y + s2, x + s4, y + s2, sw, fg); drawLine(ctx, x + s - 2, y + s2, x + s34, y + s2, sw, fg); }, // File .file => { ctx.pushCommand(Command.rectOutline(x + 2, y + 2, @intCast(s - 4), @intCast(s - 4), fg)); drawLine(ctx, x + s - 6, y + 2, x + s - 2, y + 6, sw, fg); }, // Folder .folder => { ctx.pushCommand(Command.rectOutline(x + 2, y + 4, @intCast(s - 4), @intCast(s - 6), fg)); drawLine(ctx, x + 2, y + 4, x + s3, y + 4, sw, fg); drawLine(ctx, x + s3, y + 4, x + s3 + 2, y + 2, sw, fg); drawLine(ctx, x + s3 + 2, y + 2, x + s2, y + 2, sw, fg); }, // Document .document => { ctx.pushCommand(Command.rectOutline(x + 3, y + 2, @intCast(s - 6), @intCast(s - 4), fg)); drawLine(ctx, x + 5, y + s3, x + s - 5, y + s3, 1, fg); drawLine(ctx, x + 5, y + s2, x + s - 5, y + s2, 1, fg); drawLine(ctx, x + 5, y + s23, x + s - 7, y + s23, 1, fg); }, // Save (floppy) .save => { ctx.pushCommand(Command.rectOutline(x + 2, y + 2, @intCast(s - 4), @intCast(s - 4), fg)); ctx.pushCommand(Command.rect(x + 4, y + s2, @intCast(s - 8), @intCast(s3), fg)); }, // Info .info => { const r: u32 = @intCast(s2 - 2); strokeCircle(ctx, x + s2, y + s2, r, sw, fg); fillCircle(ctx, x + s2, y + s3, 2, fg); drawLine(ctx, x + s2, y + s2 - 1, x + s2, y + s23 + 1, sw, fg); }, // Warning (triangle) .warning => { drawLine(ctx, x + s2, y + 2, x + 2, y + s - 2, sw, fg); drawLine(ctx, x + s2, y + 2, x + s - 2, y + s - 2, sw, fg); drawLine(ctx, x + 2, y + s - 2, x + s - 2, y + s - 2, sw, fg); drawLine(ctx, x + s2, y + s3 + 1, x + s2, y + s2 + 1, sw, fg); fillCircle(ctx, x + s2, y + s23, 2, fg); }, // Error (X in circle) .error_icon => { const r: u32 = @intCast(s2 - 2); strokeCircle(ctx, x + s2, y + s2, r, sw, fg); drawLine(ctx, x + s3, y + s3, x + s23, y + s23, sw, fg); drawLine(ctx, x + s23, y + s3, x + s3, y + s23, sw, fg); }, // Success (checkmark in circle) .success => { const r: u32 = @intCast(s2 - 2); strokeCircle(ctx, x + s2, y + s2, r, sw, fg); drawLine(ctx, x + s4 + 1, y + s2, x + s2 - 1, y + s23, sw, fg); drawLine(ctx, x + s2 - 1, y + s23, x + s34 - 1, y + s3, sw, fg); }, // Question .question => { const r: u32 = @intCast(s2 - 2); strokeCircle(ctx, x + s2, y + s2, r, sw, fg); ctx.pushCommand(Command.text(x + s2 - 3, y + s3, "?", fg)); }, // Star .star => { const s25 = @divTrunc(s * 2, 5); const s35 = @divTrunc(s * 3, 5); drawLine(ctx, x + s2, y + 2, x + s3, y + s25, sw, fg); drawLine(ctx, x + s3, y + s25, x + 2, y + s25, sw, fg); drawLine(ctx, x + 2, y + s25, x + s4, y + s35, sw, fg); drawLine(ctx, x + s4, y + s35, x + s4, y + s - 2, sw, fg); drawLine(ctx, x + s4, y + s - 2, x + s2, y + s34, sw, fg); drawLine(ctx, x + s2, y + s34, x + s34, y + s - 2, sw, fg); drawLine(ctx, x + s34, y + s - 2, x + s34, y + s35, sw, fg); drawLine(ctx, x + s34, y + s35, x + s - 2, y + s25, sw, fg); drawLine(ctx, x + s - 2, y + s25, x + s23, y + s25, sw, fg); drawLine(ctx, x + s23, y + s25, x + s2, y + 2, sw, fg); }, .star_filled => { fillCircle(ctx, x + s2, y + s2, @intCast(s3), fg); }, // Heart .heart => { drawLine(ctx, x + s2, y + s - 3, x + 3, y + s2, sw, fg); drawLine(ctx, x + s2, y + s - 3, x + s - 3, y + s2, sw, fg); fillCircle(ctx, x + s3, y + s3, @intCast(s5), fg); fillCircle(ctx, x + s23, y + s3, @intCast(s5), fg); }, .heart_filled => { fillCircle(ctx, x + s3, y + s3, @intCast(s4), fg); fillCircle(ctx, x + s23, y + s3, @intCast(s4), fg); var dy: i32 = 0; while (dy < s2) : (dy += 1) { const hw = @divTrunc(s2 - dy, 2); ctx.pushCommand(Command.rect(x + s2 - hw, y + s2 + dy, @intCast(hw * 2), 1, fg)); } }, // Eye .eye => { ctx.pushCommand(Command.rectOutline(x + 2, y + s3, @intCast(s - 4), @intCast(s3), fg)); fillCircle(ctx, x + s2, y + s2, @intCast(s6), fg); }, .eye_off => { ctx.pushCommand(Command.rectOutline(x + 2, y + s3, @intCast(s - 4), @intCast(s3), fg)); drawLine(ctx, x + 2, y + s - 3, x + s - 2, y + 3, sw, fg); }, // Lock .lock => { ctx.pushCommand(Command.rectOutline(x + 3, y + s2, @intCast(s - 6), @intCast(s2 - 2), fg)); drawLine(ctx, x + s3, y + s2, x + s3, y + s4, sw, fg); drawLine(ctx, x + s23, y + s2, x + s23, y + s4, sw, fg); drawLine(ctx, x + s3, y + s4, x + s23, y + s4, sw, fg); }, .unlock => { ctx.pushCommand(Command.rectOutline(x + 3, y + s2, @intCast(s - 6), @intCast(s2 - 2), fg)); drawLine(ctx, x + s3, y + s2, x + s3, y + s4, sw, fg); drawLine(ctx, x + s3, y + s4, x + s2, y + s4, sw, fg); }, // User .user => { fillCircle(ctx, x + s2, y + s3, @intCast(s5), fg); drawLine(ctx, x + s4, y + s - 2, x + s4, y + s2 + 2, sw, fg); drawLine(ctx, x + s34, y + s - 2, x + s34, y + s2 + 2, sw, fg); drawLine(ctx, x + s4, y + s2 + 2, x + s34, y + s2 + 2, sw, fg); }, .users => { fillCircle(ctx, x + s3, y + s3 + 1, @intCast(s6), fg); fillCircle(ctx, x + s23, y + s4, @intCast(s6), fg); }, // Calendar .calendar => { ctx.pushCommand(Command.rectOutline(x + 2, y + 4, @intCast(s - 4), @intCast(s - 6), fg)); drawLine(ctx, x + 2, y + s3, x + s - 2, y + s3, sw, fg); drawLine(ctx, x + s3, y + 2, x + s3, y + 5, sw, fg); drawLine(ctx, x + s23, y + 2, x + s23, y + 5, sw, fg); }, // Clock .clock => { const r: u32 = @intCast(s2 - 2); strokeCircle(ctx, x + s2, y + s2, r, sw, fg); drawLine(ctx, x + s2, y + s2, x + s2, y + s3, sw, fg); drawLine(ctx, x + s2, y + s2, x + s23, y + s2, sw, fg); }, // Bell .bell => { drawLine(ctx, x + s3, y + s23, x + s3, y + s3, sw, fg); drawLine(ctx, x + s23, y + s23, x + s23, y + s3, sw, fg); drawLine(ctx, x + s3, y + s3, x + s23, y + s3, sw, fg); drawLine(ctx, x + 3, y + s23, x + s - 3, y + s23, sw, fg); fillCircle(ctx, x + s2, y + s - 3, 2, fg); }, // Mail .mail => { ctx.pushCommand(Command.rectOutline(x + 2, y + s4, @intCast(s - 4), @intCast(s2), fg)); drawLine(ctx, x + 2, y + s4, x + s2, y + s2, sw, fg); drawLine(ctx, x + s - 2, y + s4, x + s2, y + s2, sw, fg); }, // Play .play => { drawLine(ctx, x + s3, y + 3, x + s3, y + s - 3, sw, fg); drawLine(ctx, x + s3, y + 3, x + s23 + 1, y + s2, sw, fg); drawLine(ctx, x + s3, y + s - 3, x + s23 + 1, y + s2, sw, fg); }, // Pause .pause => { ctx.pushCommand(Command.rect(x + s4, y + 3, @intCast(s5), @intCast(s - 6), fg)); ctx.pushCommand(Command.rect(x + s - s4 - s5, y + 3, @intCast(s5), @intCast(s - 6), fg)); }, // Stop .stop => { ctx.pushCommand(Command.rect(x + 3, y + 3, @intCast(s - 6), @intCast(s - 6), fg)); }, // Volume .volume => { ctx.pushCommand(Command.rect(x + 2, y + s3, 4, @intCast(s3), fg)); drawLine(ctx, x + 6, y + s3, x + s2, y + 3, sw, fg); drawLine(ctx, x + 6, y + s23, x + s2, y + s - 3, sw, fg); drawLine(ctx, x + s23, y + s3, x + s23, y + s23, sw, fg); }, .volume_off => { ctx.pushCommand(Command.rect(x + 2, y + s3, 4, @intCast(s3), fg)); drawLine(ctx, x + 6, y + s3, x + s2, y + 3, sw, fg); drawLine(ctx, x + 6, y + s23, x + s2, y + s - 3, sw, fg); drawLine(ctx, x + s23, y + s3, x + s - 3, y + s23, sw, fg); drawLine(ctx, x + s - 3, y + s3, x + s23, y + s23, sw, fg); }, // Edit .edit => { drawLine(ctx, x + 3, y + s - 5, x + s - 5, y + 3, sw, fg); drawLine(ctx, x + s - 5, y + 3, x + s - 3, y + 5, sw, fg); drawLine(ctx, x + s - 3, y + 5, x + 5, y + s - 3, sw, fg); }, // Delete (trash) .delete => { ctx.pushCommand(Command.rectOutline(x + 4, y + 4, @intCast(s - 8), @intCast(s - 6), fg)); drawLine(ctx, x + 2, y + 4, x + s - 2, y + 4, sw, fg); drawLine(ctx, x + s3, y + 2, x + s23, y + 2, sw, fg); }, // Refresh .refresh => { const r: u32 = @intCast(s3); strokeCircle(ctx, x + s2, y + s2, r, sw, fg); drawLine(ctx, x + s2 + @as(i32, @intCast(r)), y + s2 - 3, x + s2 + @as(i32, @intCast(r)) + 3, y + s2, sw, fg); }, // Filter .filter => { drawLine(ctx, x + 2, y + 3, x + s - 2, y + 3, sw, fg); drawLine(ctx, x + 2, y + 3, x + s2, y + s2, sw, fg); drawLine(ctx, x + s - 2, y + 3, x + s2, y + s2, sw, fg); drawLine(ctx, x + s2, y + s2, x + s2, y + s - 3, sw, fg); }, // Sort .sort => { drawLine(ctx, x + 3, y + s4, x + s - 3, y + s4, sw, fg); drawLine(ctx, x + 5, y + s2, x + s - 5, y + s2, sw, fg); drawLine(ctx, x + 7, y + s34, x + s - 7, y + s34, sw, fg); }, // Copy .copy => { ctx.pushCommand(Command.rectOutline(x + 2, y + 4, @intCast(s - 6), @intCast(s - 6), fg)); ctx.pushCommand(Command.rectOutline(x + 4, y + 2, @intCast(s - 6), @intCast(s - 6), fg)); }, // Paste .paste => { ctx.pushCommand(Command.rectOutline(x + 3, y + 3, @intCast(s - 6), @intCast(s - 5), fg)); ctx.pushCommand(Command.rect(x + s3, y + 2, @intCast(s3), 3, fg)); }, // Cut .cut => { fillCircle(ctx, x + s3, y + s23, @intCast(s6), fg); fillCircle(ctx, x + s23, y + s23, @intCast(s6), fg); drawLine(ctx, x + s3, y + s2, x + s2, y + 3, sw, fg); drawLine(ctx, x + s23, y + s2, x + s2, y + 3, sw, fg); }, // Undo .undo => { drawLine(ctx, x + 3, y + s3, x + s3, y + 3, sw, fg); drawLine(ctx, x + 3, y + s3, x + s3, y + s2, sw, fg); drawLine(ctx, x + 3, y + s3, x + s - 3, y + s3, sw, fg); drawLine(ctx, x + s - 3, y + s3, x + s - 3, y + s23, sw, fg); }, // Redo .redo => { drawLine(ctx, x + s - 3, y + s3, x + s23, y + 3, sw, fg); drawLine(ctx, x + s - 3, y + s3, x + s23, y + s2, sw, fg); drawLine(ctx, x + s - 3, y + s3, x + 3, y + s3, sw, fg); drawLine(ctx, x + 3, y + s3, x + 3, y + s23, sw, fg); }, // Folder open .folder_open => { ctx.pushCommand(Command.rectOutline(x + 2, y + 4, @intCast(s - 4), @intCast(s - 6), fg)); drawLine(ctx, x + 2, y + s2, x + s3, y + s2, sw, fg); drawLine(ctx, x + s3, y + s2, x + s2, y + s3, sw, fg); }, // Image file .image_file => { ctx.pushCommand(Command.rectOutline(x + 2, y + 2, @intCast(s - 4), @intCast(s - 4), fg)); drawLine(ctx, x + 4, y + s23, x + s3, y + s2, sw, fg); drawLine(ctx, x + s3, y + s2, x + s23, y + s23, sw, fg); fillCircle(ctx, x + s23, y + s3, 2, fg); }, // Download .download => { drawLine(ctx, x + s2, y + 3, x + s2, y + s23, sw, fg); drawLine(ctx, x + s2, y + s23, x + s3, y + s2, sw, fg); drawLine(ctx, x + s2, y + s23, x + s23, y + s2, sw, fg); drawLine(ctx, x + 3, y + s - 3, x + s - 3, y + s - 3, sw, fg); }, // Upload .upload => { drawLine(ctx, x + s2, y + s23, x + s2, y + 3, sw, fg); drawLine(ctx, x + s2, y + 3, x + s3, y + s4 + 1, sw, fg); drawLine(ctx, x + s2, y + 3, x + s23, y + s4 + 1, sw, fg); drawLine(ctx, x + 3, y + s - 3, x + s - 3, y + s - 3, sw, fg); }, // Grip (drag handle) .grip => { const dot_r: u32 = 1; fillCircle(ctx, x + s3, y + s3, dot_r, fg); fillCircle(ctx, x + s23, y + s3, dot_r, fg); fillCircle(ctx, x + s3, y + s2, dot_r, fg); fillCircle(ctx, x + s23, y + s2, dot_r, fg); fillCircle(ctx, x + s3, y + s23, dot_r, fg); fillCircle(ctx, x + s23, y + s23, dot_r, fg); }, // Drag .drag => { drawLine(ctx, x + s2, y + 2, x + s2, y + s - 2, sw, fg); drawLine(ctx, x + 2, y + s2, x + s - 2, y + s2, sw, fg); drawLine(ctx, x + s2, y + 2, x + s3, y + s4, sw, fg); drawLine(ctx, x + s2, y + 2, x + s23, y + s4, sw, fg); }, // Expand .expand => { drawLine(ctx, x + 2, y + 2, x + s3, y + 2, sw, fg); drawLine(ctx, x + 2, y + 2, x + 2, y + s3, sw, fg); drawLine(ctx, x + s - 2, y + 2, x + s23, y + 2, sw, fg); drawLine(ctx, x + s - 2, y + 2, x + s - 2, y + s3, sw, fg); drawLine(ctx, x + 2, y + s - 2, x + s3, y + s - 2, sw, fg); drawLine(ctx, x + 2, y + s - 2, x + 2, y + s23, sw, fg); drawLine(ctx, x + s - 2, y + s - 2, x + s23, y + s - 2, sw, fg); drawLine(ctx, x + s - 2, y + s - 2, x + s - 2, y + s23, sw, fg); }, // Collapse .collapse => { drawLine(ctx, x + s3, y + s3, x + 2, y + s3, sw, fg); drawLine(ctx, x + s3, y + s3, x + s3, y + 2, sw, fg); drawLine(ctx, x + s23, y + s3, x + s - 2, y + s3, sw, fg); drawLine(ctx, x + s23, y + s3, x + s23, y + 2, sw, fg); drawLine(ctx, x + s3, y + s23, x + 2, y + s23, sw, fg); drawLine(ctx, x + s3, y + s23, x + s3, y + s - 2, sw, fg); drawLine(ctx, x + s23, y + s23, x + s - 2, y + s23, sw, fg); drawLine(ctx, x + s23, y + s23, x + s23, y + s - 2, sw, fg); }, // Maximize .maximize => { ctx.pushCommand(Command.rectOutline(x + 3, y + 3, @intCast(s - 6), @intCast(s - 6), fg)); drawLine(ctx, x + 3, y + 5, x + s - 3, y + 5, sw, fg); }, // Minimize .minimize => { drawLine(ctx, x + 3, y + s - 4, x + s - 3, y + s - 4, sw, fg); }, // External link .external_link => { ctx.pushCommand(Command.rectOutline(x + 2, y + 4, @intCast(s - 6), @intCast(s - 6), fg)); drawLine(ctx, x + s2, y + 2, x + s - 2, y + 2, sw, fg); drawLine(ctx, x + s - 2, y + 2, x + s - 2, y + s2, sw, fg); drawLine(ctx, x + s - 2, y + 2, x + s2, y + s2, sw, fg); }, } } // ============================================================================= // Helper functions // ============================================================================= /// Draw a line with thickness fn drawLine(ctx: *Context, x1: i32, y1: i32, x2: i32, y2: i32, thickness: u32, color: Style.Color) void { if (thickness <= 1) { ctx.pushCommand(Command.line(x1, y1, x2, y2, color)); } else { const dx = x2 - x1; const dy = y2 - y1; const len = @sqrt(@as(f32, @floatFromInt(dx * dx + dy * dy))); if (len < 1) { ctx.pushCommand(Command.rect(x1, y1, thickness, thickness, color)); return; } const nx = -@as(f32, @floatFromInt(dy)) / len; const ny = @as(f32, @floatFromInt(dx)) / len; const half = @as(f32, @floatFromInt(thickness)) / 2.0; var offset: f32 = -half; while (offset < half) : (offset += 1.0) { const ox = @as(i32, @intFromFloat(nx * offset)); const oy = @as(i32, @intFromFloat(ny * offset)); ctx.pushCommand(Command.line(x1 + ox, y1 + oy, x2 + ox, y2 + oy, color)); } } } /// Fill a circle fn fillCircle(ctx: *Context, cx: i32, cy: i32, radius: u32, color: Style.Color) void { if (radius == 0) { ctx.pushCommand(Command.rect(cx, cy, 1, 1, color)); return; } const r = @as(i32, @intCast(radius)); var dy: i32 = -r; while (dy <= r) : (dy += 1) { const dy_f = @as(f32, @floatFromInt(dy)); const r_f = @as(f32, @floatFromInt(r)); const dx = @as(i32, @intFromFloat(@sqrt(r_f * r_f - dy_f * dy_f))); ctx.pushCommand(Command.rect(cx - dx, cy + dy, @intCast(dx * 2 + 1), 1, color)); } } /// Stroke a circle fn strokeCircle(ctx: *Context, cx: i32, cy: i32, radius: u32, thickness: u32, color: Style.Color) void { if (radius == 0) return; const r = @as(i32, @intCast(radius)); var px: i32 = 0; var py: i32 = r; var d: i32 = 3 - 2 * r; while (px <= py) { setPixelThick(ctx, cx + px, cy + py, thickness, color); setPixelThick(ctx, cx - px, cy + py, thickness, color); setPixelThick(ctx, cx + px, cy - py, thickness, color); setPixelThick(ctx, cx - px, cy - py, thickness, color); setPixelThick(ctx, cx + py, cy + px, thickness, color); setPixelThick(ctx, cx - py, cy + px, thickness, color); setPixelThick(ctx, cx + py, cy - px, thickness, color); setPixelThick(ctx, cx - py, cy - px, thickness, color); if (d < 0) { d = d + 4 * px + 6; } else { d = d + 4 * (px - py) + 10; py -= 1; } px += 1; } } fn setPixelThick(ctx: *Context, pixel_x: i32, pixel_y: i32, thickness: u32, color: Style.Color) void { if (thickness <= 1) { ctx.pushCommand(Command.rect(pixel_x, pixel_y, 1, 1, color)); } else { const half = @as(i32, @intCast(thickness / 2)); ctx.pushCommand(Command.rect(pixel_x - half, pixel_y - half, thickness, thickness, color)); } } // ============================================================================= // Tests // ============================================================================= test "Size pixels" { try std.testing.expectEqual(@as(u32, 12), Size.small.pixels()); try std.testing.expectEqual(@as(u32, 16), Size.medium.pixels()); try std.testing.expectEqual(@as(u32, 24), Size.large.pixels()); try std.testing.expectEqual(@as(u32, 32), Size.xlarge.pixels()); } test "icon generates commands" { var ctx = try Context.init(std.testing.allocator, 800, 600); defer ctx.deinit(); ctx.beginFrame(); ctx.layout.row_height = 32; icon(&ctx, .check); try std.testing.expect(ctx.commands.items.len >= 1); ctx.endFrame(); } test "iconEx with config" { var ctx = try Context.init(std.testing.allocator, 800, 600); defer ctx.deinit(); ctx.beginFrame(); ctx.layout.row_height = 48; iconEx(&ctx, .home, .{ .size = .large, .stroke_width = 3 }, .{}); try std.testing.expect(ctx.commands.items.len >= 1); ctx.endFrame(); } test "multiple icons" { var ctx = try Context.init(std.testing.allocator, 800, 600); defer ctx.deinit(); ctx.beginFrame(); ctx.layout.row_height = 24; const icons = [_]IconType{ .check, .close, .plus, .minus, .search }; for (icons) |i| { icon(&ctx, i); } try std.testing.expect(ctx.commands.items.len >= 5); ctx.endFrame(); }