zcatgui/src/widgets/icon.zig
reugenio bb5b201203 feat: zcatgui v0.11.0 - Phase 5 Data Visualization
New Widgets (3):

Canvas - Drawing primitives widget
- Point, fillRect, strokeRect, line, text
- fillCircle, strokeCircle (Bresenham algorithm)
- fillArc, fillTriangle (scanline fill)
- strokePolygon, fillRoundedRect
- horizontalGradient, verticalGradient
- Color interpolation (lerpColor)

Chart - Data visualization widgets
- LineChart: Points, grid, axis labels, fill under line
- BarChart: Vertical bars, value display, labels
- PieChart: Slices with colors, donut mode
- DataPoint and DataSeries for multi-series
- 8-color default palette
- Scanline fill for triangles and quads

Icon - Vector icon system (60+ icons)
- Size presets: small(12), medium(16), large(24), xlarge(32)
- Categories: Navigation, Actions, Files, Status, UI, Media
- Stroke-based drawing with configurable thickness
- All icons resolution-independent

Widget count: 34 widget files
All tests passing

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-09 13:37:27 +01:00

805 lines
27 KiB
Zig

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