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>
805 lines
27 KiB
Zig
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();
|
|
}
|