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>
This commit is contained in:
parent
34dfcfce18
commit
bb5b201203
4 changed files with 1990 additions and 0 deletions
494
src/widgets/canvas.zig
Normal file
494
src/widgets/canvas.zig
Normal file
|
|
@ -0,0 +1,494 @@
|
||||||
|
//! Canvas Widget - Drawing primitives
|
||||||
|
//!
|
||||||
|
//! A canvas for drawing shapes, lines, and custom graphics.
|
||||||
|
//! Provides an immediate-mode drawing API.
|
||||||
|
|
||||||
|
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");
|
||||||
|
|
||||||
|
/// Point in 2D space
|
||||||
|
pub const Point = struct {
|
||||||
|
x: i32,
|
||||||
|
y: i32,
|
||||||
|
|
||||||
|
pub fn init(x: i32, y: i32) Point {
|
||||||
|
return .{ .x = x, .y = y };
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add(self: Point, other: Point) Point {
|
||||||
|
return .{ .x = self.x + other.x, .y = self.y + other.y };
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sub(self: Point, other: Point) Point {
|
||||||
|
return .{ .x = self.x - other.x, .y = self.y - other.y };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Canvas state for drawing operations
|
||||||
|
pub const Canvas = struct {
|
||||||
|
ctx: *Context,
|
||||||
|
bounds: Layout.Rect,
|
||||||
|
offset: Point,
|
||||||
|
|
||||||
|
const Self = @This();
|
||||||
|
|
||||||
|
/// Begin canvas operations in a region
|
||||||
|
pub fn begin(ctx: *Context) Self {
|
||||||
|
const bounds = ctx.layout.nextRect();
|
||||||
|
return .{
|
||||||
|
.ctx = ctx,
|
||||||
|
.bounds = bounds,
|
||||||
|
.offset = Point.init(bounds.x, bounds.y),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Begin canvas in specific rectangle
|
||||||
|
pub fn beginRect(ctx: *Context, bounds: Layout.Rect) Self {
|
||||||
|
return .{
|
||||||
|
.ctx = ctx,
|
||||||
|
.bounds = bounds,
|
||||||
|
.offset = Point.init(bounds.x, bounds.y),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Basic shapes
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
/// Draw a filled rectangle
|
||||||
|
pub fn fillRect(self: *Self, x: i32, y: i32, w: u32, h: u32, color: Style.Color) void {
|
||||||
|
self.ctx.pushCommand(Command.rect(
|
||||||
|
self.offset.x + x,
|
||||||
|
self.offset.y + y,
|
||||||
|
w,
|
||||||
|
h,
|
||||||
|
color,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draw a rectangle outline
|
||||||
|
pub fn strokeRect(self: *Self, x: i32, y: i32, w: u32, h: u32, color: Style.Color) void {
|
||||||
|
self.ctx.pushCommand(Command.rectOutline(
|
||||||
|
self.offset.x + x,
|
||||||
|
self.offset.y + y,
|
||||||
|
w,
|
||||||
|
h,
|
||||||
|
color,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draw a line
|
||||||
|
pub fn line(self: *Self, x1: i32, y1: i32, x2: i32, y2: i32, color: Style.Color) void {
|
||||||
|
self.ctx.pushCommand(Command.line(
|
||||||
|
self.offset.x + x1,
|
||||||
|
self.offset.y + y1,
|
||||||
|
self.offset.x + x2,
|
||||||
|
self.offset.y + y2,
|
||||||
|
color,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draw text
|
||||||
|
pub fn text(self: *Self, x: i32, y: i32, str: []const u8, color: Style.Color) void {
|
||||||
|
self.ctx.pushCommand(Command.text(
|
||||||
|
self.offset.x + x,
|
||||||
|
self.offset.y + y,
|
||||||
|
str,
|
||||||
|
color,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Circle drawing (using rectangles approximation)
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
/// Draw a filled circle (approximated with octagon/rects)
|
||||||
|
pub fn fillCircle(self: *Self, cx: i32, cy: i32, radius: u32, color: Style.Color) void {
|
||||||
|
if (radius == 0) return;
|
||||||
|
|
||||||
|
const r = @as(i32, @intCast(radius));
|
||||||
|
const x = self.offset.x + cx;
|
||||||
|
const y = self.offset.y + cy;
|
||||||
|
|
||||||
|
// Draw circle using horizontal lines at different heights
|
||||||
|
var dy: i32 = -r;
|
||||||
|
while (dy <= r) : (dy += 1) {
|
||||||
|
// Calculate width at this height using circle equation
|
||||||
|
const dy_f = @as(f32, @floatFromInt(dy));
|
||||||
|
const r_f = @as(f32, @floatFromInt(r));
|
||||||
|
const dx_f = @sqrt(r_f * r_f - dy_f * dy_f);
|
||||||
|
const dx = @as(i32, @intFromFloat(dx_f));
|
||||||
|
|
||||||
|
self.ctx.pushCommand(Command.rect(
|
||||||
|
x - dx,
|
||||||
|
y + dy,
|
||||||
|
@intCast(dx * 2 + 1),
|
||||||
|
1,
|
||||||
|
color,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draw a circle outline
|
||||||
|
pub fn strokeCircle(self: *Self, cx: i32, cy: i32, radius: u32, color: Style.Color) void {
|
||||||
|
if (radius == 0) return;
|
||||||
|
|
||||||
|
const r = @as(i32, @intCast(radius));
|
||||||
|
const x = self.offset.x + cx;
|
||||||
|
const y = self.offset.y + cy;
|
||||||
|
|
||||||
|
// Draw circle outline using Bresenham's algorithm
|
||||||
|
var px: i32 = 0;
|
||||||
|
var py: i32 = r;
|
||||||
|
var d: i32 = 3 - 2 * r;
|
||||||
|
|
||||||
|
while (px <= py) {
|
||||||
|
// Draw 8 symmetric points
|
||||||
|
self.setPixel(x + px, y + py, color);
|
||||||
|
self.setPixel(x - px, y + py, color);
|
||||||
|
self.setPixel(x + px, y - py, color);
|
||||||
|
self.setPixel(x - px, y - py, color);
|
||||||
|
self.setPixel(x + py, y + px, color);
|
||||||
|
self.setPixel(x - py, y + px, color);
|
||||||
|
self.setPixel(x + py, y - px, color);
|
||||||
|
self.setPixel(x - py, y - px, color);
|
||||||
|
|
||||||
|
if (d < 0) {
|
||||||
|
d = d + 4 * px + 6;
|
||||||
|
} else {
|
||||||
|
d = d + 4 * (px - py) + 10;
|
||||||
|
py -= 1;
|
||||||
|
}
|
||||||
|
px += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set a single pixel
|
||||||
|
fn setPixel(self: *Self, x: i32, y: i32, color: Style.Color) void {
|
||||||
|
self.ctx.pushCommand(Command.rect(x, y, 1, 1, color));
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Arc and pie
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
/// Draw a filled arc/pie segment
|
||||||
|
pub fn fillArc(
|
||||||
|
self: *Self,
|
||||||
|
cx: i32,
|
||||||
|
cy: i32,
|
||||||
|
radius: u32,
|
||||||
|
start_angle: f32,
|
||||||
|
end_angle: f32,
|
||||||
|
color: Style.Color,
|
||||||
|
) void {
|
||||||
|
if (radius == 0) return;
|
||||||
|
|
||||||
|
const r = @as(f32, @floatFromInt(radius));
|
||||||
|
const x = self.offset.x + cx;
|
||||||
|
const y = self.offset.y + cy;
|
||||||
|
|
||||||
|
// Draw arc using line segments
|
||||||
|
const segments: u32 = @max(8, radius / 2);
|
||||||
|
const angle_step = (end_angle - start_angle) / @as(f32, @floatFromInt(segments));
|
||||||
|
|
||||||
|
var angle = start_angle;
|
||||||
|
var i: u32 = 0;
|
||||||
|
while (i < segments) : (i += 1) {
|
||||||
|
const next_angle = angle + angle_step;
|
||||||
|
|
||||||
|
// Calculate points
|
||||||
|
const x1 = x + @as(i32, @intFromFloat(@cos(angle) * r));
|
||||||
|
const y1 = y + @as(i32, @intFromFloat(@sin(angle) * r));
|
||||||
|
const x2 = x + @as(i32, @intFromFloat(@cos(next_angle) * r));
|
||||||
|
const y2 = y + @as(i32, @intFromFloat(@sin(next_angle) * r));
|
||||||
|
|
||||||
|
// Draw triangle from center to arc segment
|
||||||
|
self.fillTriangle(x, y, x1, y1, x2, y2, color);
|
||||||
|
|
||||||
|
angle = next_angle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draw a triangle (filled)
|
||||||
|
pub fn fillTriangle(
|
||||||
|
self: *Self,
|
||||||
|
x1: i32,
|
||||||
|
y1: i32,
|
||||||
|
x2: i32,
|
||||||
|
y2: i32,
|
||||||
|
x3: i32,
|
||||||
|
y3: i32,
|
||||||
|
color: Style.Color,
|
||||||
|
) void {
|
||||||
|
// Sort vertices by y
|
||||||
|
var v1 = Point.init(x1, y1);
|
||||||
|
var v2 = Point.init(x2, y2);
|
||||||
|
var v3 = Point.init(x3, y3);
|
||||||
|
|
||||||
|
if (v1.y > v2.y) std.mem.swap(Point, &v1, &v2);
|
||||||
|
if (v1.y > v3.y) std.mem.swap(Point, &v1, &v3);
|
||||||
|
if (v2.y > v3.y) std.mem.swap(Point, &v2, &v3);
|
||||||
|
|
||||||
|
// Fill using horizontal scanlines
|
||||||
|
const total_height = v3.y - v1.y;
|
||||||
|
if (total_height == 0) return;
|
||||||
|
|
||||||
|
var py = v1.y;
|
||||||
|
while (py <= v3.y) : (py += 1) {
|
||||||
|
const second_half = py > v2.y or v2.y == v1.y;
|
||||||
|
const segment_height = if (second_half) v3.y - v2.y else v2.y - v1.y;
|
||||||
|
|
||||||
|
if (segment_height == 0) continue;
|
||||||
|
|
||||||
|
const alpha = @as(f32, @floatFromInt(py - v1.y)) / @as(f32, @floatFromInt(total_height));
|
||||||
|
const beta = @as(f32, @floatFromInt(py - (if (second_half) v2.y else v1.y))) /
|
||||||
|
@as(f32, @floatFromInt(segment_height));
|
||||||
|
|
||||||
|
var ax = v1.x + @as(i32, @intFromFloat(@as(f32, @floatFromInt(v3.x - v1.x)) * alpha));
|
||||||
|
var bx: i32 = undefined;
|
||||||
|
|
||||||
|
if (second_half) {
|
||||||
|
bx = v2.x + @as(i32, @intFromFloat(@as(f32, @floatFromInt(v3.x - v2.x)) * beta));
|
||||||
|
} else {
|
||||||
|
bx = v1.x + @as(i32, @intFromFloat(@as(f32, @floatFromInt(v2.x - v1.x)) * beta));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ax > bx) std.mem.swap(i32, &ax, &bx);
|
||||||
|
|
||||||
|
self.ctx.pushCommand(Command.rect(ax, py, @intCast(bx - ax + 1), 1, color));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Polygon
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
/// Draw a polygon outline
|
||||||
|
pub fn strokePolygon(self: *Self, points: []const Point, color: Style.Color) void {
|
||||||
|
if (points.len < 2) return;
|
||||||
|
|
||||||
|
var i: usize = 0;
|
||||||
|
while (i < points.len) : (i += 1) {
|
||||||
|
const p1 = points[i];
|
||||||
|
const p2 = points[(i + 1) % points.len];
|
||||||
|
self.line(p1.x, p1.y, p2.x, p2.y, color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Rounded rectangle
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
/// Draw a filled rounded rectangle
|
||||||
|
pub fn fillRoundedRect(
|
||||||
|
self: *Self,
|
||||||
|
x: i32,
|
||||||
|
y: i32,
|
||||||
|
w: u32,
|
||||||
|
h: u32,
|
||||||
|
radius: u32,
|
||||||
|
color: Style.Color,
|
||||||
|
) void {
|
||||||
|
if (w == 0 or h == 0) return;
|
||||||
|
|
||||||
|
const r = @min(radius, @min(w / 2, h / 2));
|
||||||
|
const ri = @as(i32, @intCast(r));
|
||||||
|
|
||||||
|
// Center rectangle
|
||||||
|
self.fillRect(x + ri, y, w - r * 2, h, color);
|
||||||
|
|
||||||
|
// Left rectangle
|
||||||
|
self.fillRect(x, y + ri, r, h - r * 2, color);
|
||||||
|
|
||||||
|
// Right rectangle
|
||||||
|
self.fillRect(x + @as(i32, @intCast(w)) - ri, y + ri, r, h - r * 2, color);
|
||||||
|
|
||||||
|
// Four corners
|
||||||
|
if (r > 0) {
|
||||||
|
self.fillCorner(x + ri, y + ri, r, 2, color); // Top-left
|
||||||
|
self.fillCorner(x + @as(i32, @intCast(w)) - ri - 1, y + ri, r, 1, color); // Top-right
|
||||||
|
self.fillCorner(x + ri, y + @as(i32, @intCast(h)) - ri - 1, r, 3, color); // Bottom-left
|
||||||
|
self.fillCorner(x + @as(i32, @intCast(w)) - ri - 1, y + @as(i32, @intCast(h)) - ri - 1, r, 0, color); // Bottom-right
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fill a quarter circle (corner)
|
||||||
|
fn fillCorner(self: *Self, cx: i32, cy: i32, radius: u32, quadrant: u8, color: Style.Color) void {
|
||||||
|
const r = @as(i32, @intCast(radius));
|
||||||
|
const ox = self.offset.x + cx;
|
||||||
|
const oy = self.offset.y + cy;
|
||||||
|
|
||||||
|
var dy: i32 = 0;
|
||||||
|
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)));
|
||||||
|
|
||||||
|
switch (quadrant) {
|
||||||
|
0 => { // Bottom-right
|
||||||
|
self.ctx.pushCommand(Command.rect(ox, oy + dy, @intCast(dx + 1), 1, color));
|
||||||
|
},
|
||||||
|
1 => { // Top-right
|
||||||
|
self.ctx.pushCommand(Command.rect(ox, oy - dy, @intCast(dx + 1), 1, color));
|
||||||
|
},
|
||||||
|
2 => { // Top-left
|
||||||
|
self.ctx.pushCommand(Command.rect(ox - dx, oy - dy, @intCast(dx + 1), 1, color));
|
||||||
|
},
|
||||||
|
3 => { // Bottom-left
|
||||||
|
self.ctx.pushCommand(Command.rect(ox - dx, oy + dy, @intCast(dx + 1), 1, color));
|
||||||
|
},
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Gradient
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
/// Draw a horizontal gradient
|
||||||
|
pub fn horizontalGradient(
|
||||||
|
self: *Self,
|
||||||
|
x: i32,
|
||||||
|
y: i32,
|
||||||
|
w: u32,
|
||||||
|
h: u32,
|
||||||
|
start: Style.Color,
|
||||||
|
end: Style.Color,
|
||||||
|
) void {
|
||||||
|
if (w == 0) return;
|
||||||
|
|
||||||
|
var px: u32 = 0;
|
||||||
|
while (px < w) : (px += 1) {
|
||||||
|
const t = @as(f32, @floatFromInt(px)) / @as(f32, @floatFromInt(w - 1));
|
||||||
|
const color = lerpColor(start, end, t);
|
||||||
|
self.ctx.pushCommand(Command.rect(
|
||||||
|
self.offset.x + x + @as(i32, @intCast(px)),
|
||||||
|
self.offset.y + y,
|
||||||
|
1,
|
||||||
|
h,
|
||||||
|
color,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draw a vertical gradient
|
||||||
|
pub fn verticalGradient(
|
||||||
|
self: *Self,
|
||||||
|
x: i32,
|
||||||
|
y: i32,
|
||||||
|
w: u32,
|
||||||
|
h: u32,
|
||||||
|
start: Style.Color,
|
||||||
|
end: Style.Color,
|
||||||
|
) void {
|
||||||
|
if (h == 0) return;
|
||||||
|
|
||||||
|
var py: u32 = 0;
|
||||||
|
while (py < h) : (py += 1) {
|
||||||
|
const t = @as(f32, @floatFromInt(py)) / @as(f32, @floatFromInt(h - 1));
|
||||||
|
const color = lerpColor(start, end, t);
|
||||||
|
self.ctx.pushCommand(Command.rect(
|
||||||
|
self.offset.x + x,
|
||||||
|
self.offset.y + y + @as(i32, @intCast(py)),
|
||||||
|
w,
|
||||||
|
1,
|
||||||
|
color,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clear the canvas area
|
||||||
|
pub fn clear(self: *Self, color: Style.Color) void {
|
||||||
|
self.ctx.pushCommand(Command.rect(
|
||||||
|
self.bounds.x,
|
||||||
|
self.bounds.y,
|
||||||
|
self.bounds.w,
|
||||||
|
self.bounds.h,
|
||||||
|
color,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get canvas width
|
||||||
|
pub fn width(self: Self) u32 {
|
||||||
|
return self.bounds.w;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get canvas height
|
||||||
|
pub fn height(self: Self) u32 {
|
||||||
|
return self.bounds.h;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Linear interpolation between colors
|
||||||
|
fn lerpColor(a: Style.Color, b: Style.Color, t: f32) Style.Color {
|
||||||
|
const t_clamped = @max(0.0, @min(1.0, t));
|
||||||
|
return Style.Color.rgba(
|
||||||
|
@intFromFloat(@as(f32, @floatFromInt(a.r)) * (1 - t_clamped) + @as(f32, @floatFromInt(b.r)) * t_clamped),
|
||||||
|
@intFromFloat(@as(f32, @floatFromInt(a.g)) * (1 - t_clamped) + @as(f32, @floatFromInt(b.g)) * t_clamped),
|
||||||
|
@intFromFloat(@as(f32, @floatFromInt(a.b)) * (1 - t_clamped) + @as(f32, @floatFromInt(b.b)) * t_clamped),
|
||||||
|
@intFromFloat(@as(f32, @floatFromInt(a.a)) * (1 - t_clamped) + @as(f32, @floatFromInt(b.a)) * t_clamped),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Tests
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
test "Point operations" {
|
||||||
|
const p1 = Point.init(10, 20);
|
||||||
|
const p2 = Point.init(5, 10);
|
||||||
|
|
||||||
|
const sum = p1.add(p2);
|
||||||
|
try std.testing.expectEqual(@as(i32, 15), sum.x);
|
||||||
|
try std.testing.expectEqual(@as(i32, 30), sum.y);
|
||||||
|
|
||||||
|
const diff = p1.sub(p2);
|
||||||
|
try std.testing.expectEqual(@as(i32, 5), diff.x);
|
||||||
|
try std.testing.expectEqual(@as(i32, 10), diff.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "Canvas basic drawing" {
|
||||||
|
var ctx = try Context.init(std.testing.allocator, 800, 600);
|
||||||
|
defer ctx.deinit();
|
||||||
|
|
||||||
|
ctx.beginFrame();
|
||||||
|
ctx.layout.row_height = 200;
|
||||||
|
|
||||||
|
var canvas = Canvas.begin(&ctx);
|
||||||
|
canvas.fillRect(10, 10, 50, 50, Style.Color.rgba(255, 0, 0, 255));
|
||||||
|
canvas.strokeRect(70, 10, 50, 50, Style.Color.rgba(0, 255, 0, 255));
|
||||||
|
canvas.line(10, 100, 100, 150, Style.Color.rgba(0, 0, 255, 255));
|
||||||
|
|
||||||
|
try std.testing.expect(ctx.commands.items.len >= 3);
|
||||||
|
|
||||||
|
ctx.endFrame();
|
||||||
|
}
|
||||||
|
|
||||||
|
test "Canvas circle" {
|
||||||
|
var ctx = try Context.init(std.testing.allocator, 800, 600);
|
||||||
|
defer ctx.deinit();
|
||||||
|
|
||||||
|
ctx.beginFrame();
|
||||||
|
ctx.layout.row_height = 200;
|
||||||
|
|
||||||
|
var canvas = Canvas.begin(&ctx);
|
||||||
|
canvas.fillCircle(100, 100, 50, Style.Color.rgba(255, 255, 0, 255));
|
||||||
|
|
||||||
|
try std.testing.expect(ctx.commands.items.len >= 1);
|
||||||
|
|
||||||
|
ctx.endFrame();
|
||||||
|
}
|
||||||
|
|
||||||
|
test "lerpColor" {
|
||||||
|
const black = Style.Color.rgba(0, 0, 0, 255);
|
||||||
|
const white = Style.Color.rgba(255, 255, 255, 255);
|
||||||
|
|
||||||
|
const mid = lerpColor(black, white, 0.5);
|
||||||
|
try std.testing.expect(mid.r > 120 and mid.r < 130);
|
||||||
|
try std.testing.expect(mid.g > 120 and mid.g < 130);
|
||||||
|
}
|
||||||
666
src/widgets/chart.zig
Normal file
666
src/widgets/chart.zig
Normal file
|
|
@ -0,0 +1,666 @@
|
||||||
|
//! Chart Widgets - Data visualization
|
||||||
|
//!
|
||||||
|
//! Line chart, bar chart, and pie chart for data visualization.
|
||||||
|
//! All charts auto-scale to fit data.
|
||||||
|
|
||||||
|
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");
|
||||||
|
const canvas = @import("canvas.zig");
|
||||||
|
|
||||||
|
/// Data point for charts
|
||||||
|
pub const DataPoint = struct {
|
||||||
|
value: f64,
|
||||||
|
label: ?[]const u8 = null,
|
||||||
|
color: ?Style.Color = null,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Data series for multi-series charts
|
||||||
|
pub const DataSeries = struct {
|
||||||
|
name: []const u8,
|
||||||
|
data: []const f64,
|
||||||
|
color: Style.Color,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Default chart colors (palette)
|
||||||
|
pub const default_colors = [_]Style.Color{
|
||||||
|
Style.Color.rgba(100, 149, 237, 255), // Cornflower blue
|
||||||
|
Style.Color.rgba(255, 99, 132, 255), // Red
|
||||||
|
Style.Color.rgba(75, 192, 192, 255), // Teal
|
||||||
|
Style.Color.rgba(255, 206, 86, 255), // Yellow
|
||||||
|
Style.Color.rgba(153, 102, 255, 255), // Purple
|
||||||
|
Style.Color.rgba(255, 159, 64, 255), // Orange
|
||||||
|
Style.Color.rgba(54, 162, 235, 255), // Blue
|
||||||
|
Style.Color.rgba(201, 203, 207, 255), // Gray
|
||||||
|
};
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Line Chart
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/// Line chart configuration
|
||||||
|
pub const LineChartConfig = struct {
|
||||||
|
/// Show points
|
||||||
|
show_points: bool = true,
|
||||||
|
/// Point radius
|
||||||
|
point_radius: u32 = 4,
|
||||||
|
/// Show grid
|
||||||
|
show_grid: bool = true,
|
||||||
|
/// Show x labels
|
||||||
|
show_x_labels: bool = true,
|
||||||
|
/// Show y labels
|
||||||
|
show_y_labels: bool = true,
|
||||||
|
/// Padding
|
||||||
|
padding: u32 = 40,
|
||||||
|
/// Fill under line
|
||||||
|
fill: bool = false,
|
||||||
|
/// Smooth line (bezier)
|
||||||
|
smooth: bool = false,
|
||||||
|
/// Y axis min (null = auto)
|
||||||
|
y_min: ?f64 = null,
|
||||||
|
/// Y axis max (null = auto)
|
||||||
|
y_max: ?f64 = null,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Line chart colors
|
||||||
|
pub const LineChartColors = struct {
|
||||||
|
background: ?Style.Color = null,
|
||||||
|
grid: Style.Color = Style.Color.rgba(60, 60, 60, 255),
|
||||||
|
axis: Style.Color = Style.Color.rgba(100, 100, 100, 255),
|
||||||
|
label: Style.Color = Style.Color.rgba(180, 180, 180, 255),
|
||||||
|
line: Style.Color = Style.Color.rgba(100, 149, 237, 255),
|
||||||
|
point: Style.Color = Style.Color.rgba(100, 149, 237, 255),
|
||||||
|
fill_color: Style.Color = Style.Color.rgba(100, 149, 237, 80),
|
||||||
|
|
||||||
|
pub fn fromTheme(theme: Style.Theme) LineChartColors {
|
||||||
|
return .{
|
||||||
|
.grid = theme.secondary.darken(30),
|
||||||
|
.axis = theme.secondary,
|
||||||
|
.label = theme.foreground,
|
||||||
|
.line = theme.primary,
|
||||||
|
.point = theme.primary,
|
||||||
|
.fill_color = theme.primary.withAlpha(80),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Draw a line chart
|
||||||
|
pub fn lineChart(ctx: *Context, data: []const f64) void {
|
||||||
|
lineChartEx(ctx, data, .{}, .{});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draw a line chart with configuration
|
||||||
|
pub fn lineChartEx(
|
||||||
|
ctx: *Context,
|
||||||
|
data: []const f64,
|
||||||
|
config: LineChartConfig,
|
||||||
|
colors: LineChartColors,
|
||||||
|
) void {
|
||||||
|
const bounds = ctx.layout.nextRect();
|
||||||
|
lineChartRect(ctx, bounds, data, config, colors);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draw a line chart in specific rectangle
|
||||||
|
pub fn lineChartRect(
|
||||||
|
ctx: *Context,
|
||||||
|
bounds: Layout.Rect,
|
||||||
|
data: []const f64,
|
||||||
|
config: LineChartConfig,
|
||||||
|
colors: LineChartColors,
|
||||||
|
) void {
|
||||||
|
if (bounds.isEmpty() or data.len < 2) return;
|
||||||
|
|
||||||
|
// Background
|
||||||
|
if (colors.background) |bg| {
|
||||||
|
ctx.pushCommand(Command.rect(bounds.x, bounds.y, bounds.w, bounds.h, bg));
|
||||||
|
}
|
||||||
|
|
||||||
|
const padding = config.padding;
|
||||||
|
const chart_x = bounds.x + @as(i32, @intCast(padding));
|
||||||
|
const chart_y = bounds.y + @as(i32, @intCast(padding / 2));
|
||||||
|
const chart_w = bounds.w -| (padding * 2);
|
||||||
|
const chart_h = bounds.h -| padding;
|
||||||
|
|
||||||
|
if (chart_w < 10 or chart_h < 10) return;
|
||||||
|
|
||||||
|
// Find data range
|
||||||
|
var min_val: f64 = config.y_min orelse data[0];
|
||||||
|
var max_val: f64 = config.y_max orelse data[0];
|
||||||
|
|
||||||
|
if (config.y_min == null or config.y_max == null) {
|
||||||
|
for (data) |v| {
|
||||||
|
if (config.y_min == null and v < min_val) min_val = v;
|
||||||
|
if (config.y_max == null and v > max_val) max_val = v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add padding to range
|
||||||
|
const range = max_val - min_val;
|
||||||
|
if (range == 0) {
|
||||||
|
min_val -= 1;
|
||||||
|
max_val += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw grid
|
||||||
|
if (config.show_grid) {
|
||||||
|
const grid_lines: u32 = 5;
|
||||||
|
var i: u32 = 0;
|
||||||
|
while (i <= grid_lines) : (i += 1) {
|
||||||
|
const y_pos = chart_y + @as(i32, @intCast(chart_h * i / grid_lines));
|
||||||
|
ctx.pushCommand(Command.rect(chart_x, y_pos, chart_w, 1, colors.grid));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw y labels
|
||||||
|
if (config.show_y_labels) {
|
||||||
|
const label_count: u32 = 5;
|
||||||
|
var i: u32 = 0;
|
||||||
|
while (i <= label_count) : (i += 1) {
|
||||||
|
const val = max_val - (max_val - min_val) * @as(f64, @floatFromInt(i)) / @as(f64, @floatFromInt(label_count));
|
||||||
|
const y_pos = chart_y + @as(i32, @intCast(chart_h * i / label_count));
|
||||||
|
|
||||||
|
// Format value
|
||||||
|
var buf: [16]u8 = undefined;
|
||||||
|
const label = std.fmt.bufPrint(&buf, "{d:.1}", .{val}) catch continue;
|
||||||
|
ctx.pushCommand(Command.text(bounds.x + 4, y_pos - 4, label, colors.label));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate point positions
|
||||||
|
const x_step = @as(f64, @floatFromInt(chart_w)) / @as(f64, @floatFromInt(data.len - 1));
|
||||||
|
const y_scale = @as(f64, @floatFromInt(chart_h)) / (max_val - min_val);
|
||||||
|
|
||||||
|
// Draw fill if enabled
|
||||||
|
if (config.fill) {
|
||||||
|
var xi: usize = 0;
|
||||||
|
while (xi < data.len - 1) : (xi += 1) {
|
||||||
|
const x1 = chart_x + @as(i32, @intFromFloat(@as(f64, @floatFromInt(xi)) * x_step));
|
||||||
|
const x2 = chart_x + @as(i32, @intFromFloat(@as(f64, @floatFromInt(xi + 1)) * x_step));
|
||||||
|
const y1 = chart_y + @as(i32, @intFromFloat((max_val - data[xi]) * y_scale));
|
||||||
|
const y2 = chart_y + @as(i32, @intFromFloat((max_val - data[xi + 1]) * y_scale));
|
||||||
|
const bottom = chart_y + @as(i32, @intCast(chart_h));
|
||||||
|
|
||||||
|
// Draw quad as two triangles
|
||||||
|
drawFilledQuad(ctx, x1, y1, x2, y2, x2, bottom, x1, bottom, colors.fill_color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw lines
|
||||||
|
var xi: usize = 0;
|
||||||
|
while (xi < data.len - 1) : (xi += 1) {
|
||||||
|
const x1 = chart_x + @as(i32, @intFromFloat(@as(f64, @floatFromInt(xi)) * x_step));
|
||||||
|
const x2 = chart_x + @as(i32, @intFromFloat(@as(f64, @floatFromInt(xi + 1)) * x_step));
|
||||||
|
const y1 = chart_y + @as(i32, @intFromFloat((max_val - data[xi]) * y_scale));
|
||||||
|
const y2 = chart_y + @as(i32, @intFromFloat((max_val - data[xi + 1]) * y_scale));
|
||||||
|
|
||||||
|
ctx.pushCommand(Command.line(x1, y1, x2, y2, colors.line));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw points
|
||||||
|
if (config.show_points) {
|
||||||
|
xi = 0;
|
||||||
|
while (xi < data.len) : (xi += 1) {
|
||||||
|
const px = chart_x + @as(i32, @intFromFloat(@as(f64, @floatFromInt(xi)) * x_step));
|
||||||
|
const py = chart_y + @as(i32, @intFromFloat((max_val - data[xi]) * y_scale));
|
||||||
|
const r = config.point_radius;
|
||||||
|
|
||||||
|
// Draw filled circle (simple approximation)
|
||||||
|
var dy: i32 = -@as(i32, @intCast(r));
|
||||||
|
while (dy <= @as(i32, @intCast(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(px - dx, py + dy, @intCast(dx * 2 + 1), 1, colors.point));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helper to draw filled quad
|
||||||
|
fn drawFilledQuad(
|
||||||
|
ctx: *Context,
|
||||||
|
x1: i32,
|
||||||
|
y1: i32,
|
||||||
|
x2: i32,
|
||||||
|
y2: i32,
|
||||||
|
x3: i32,
|
||||||
|
y3: i32,
|
||||||
|
x4: i32,
|
||||||
|
y4: i32,
|
||||||
|
color: Style.Color,
|
||||||
|
) void {
|
||||||
|
// Find y range
|
||||||
|
const min_y = @min(@min(y1, y2), @min(y3, y4));
|
||||||
|
const max_y = @max(@max(y1, y2), @max(y3, y4));
|
||||||
|
|
||||||
|
// Scanline fill
|
||||||
|
var y = min_y;
|
||||||
|
while (y <= max_y) : (y += 1) {
|
||||||
|
var min_x: i32 = std.math.maxInt(i32);
|
||||||
|
var max_x: i32 = std.math.minInt(i32);
|
||||||
|
|
||||||
|
// Check intersections with all edges
|
||||||
|
const edges = [_][4]i32{
|
||||||
|
.{ x1, y1, x2, y2 },
|
||||||
|
.{ x2, y2, x3, y3 },
|
||||||
|
.{ x3, y3, x4, y4 },
|
||||||
|
.{ x4, y4, x1, y1 },
|
||||||
|
};
|
||||||
|
|
||||||
|
for (edges) |edge| {
|
||||||
|
const ex1 = edge[0];
|
||||||
|
const ey1 = edge[1];
|
||||||
|
const ex2 = edge[2];
|
||||||
|
const ey2 = edge[3];
|
||||||
|
|
||||||
|
if ((ey1 <= y and y < ey2) or (ey2 <= y and y < ey1)) {
|
||||||
|
const x = ex1 + @divTrunc((y - ey1) * (ex2 - ex1), (ey2 - ey1));
|
||||||
|
min_x = @min(min_x, x);
|
||||||
|
max_x = @max(max_x, x);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (min_x <= max_x) {
|
||||||
|
ctx.pushCommand(Command.rect(min_x, y, @intCast(max_x - min_x + 1), 1, color));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Bar Chart
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/// Bar chart configuration
|
||||||
|
pub const BarChartConfig = struct {
|
||||||
|
/// Horizontal bars
|
||||||
|
horizontal: bool = false,
|
||||||
|
/// Show values on bars
|
||||||
|
show_values: bool = true,
|
||||||
|
/// Bar spacing (0.0-1.0)
|
||||||
|
spacing: f32 = 0.2,
|
||||||
|
/// Show grid
|
||||||
|
show_grid: bool = true,
|
||||||
|
/// Padding
|
||||||
|
padding: u32 = 40,
|
||||||
|
/// Stacked bars
|
||||||
|
stacked: bool = false,
|
||||||
|
/// Y axis min (null = auto, 0 for bar charts)
|
||||||
|
y_min: ?f64 = 0,
|
||||||
|
/// Y axis max (null = auto)
|
||||||
|
y_max: ?f64 = null,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Bar chart colors
|
||||||
|
pub const BarChartColors = struct {
|
||||||
|
background: ?Style.Color = null,
|
||||||
|
grid: Style.Color = Style.Color.rgba(60, 60, 60, 255),
|
||||||
|
axis: Style.Color = Style.Color.rgba(100, 100, 100, 255),
|
||||||
|
label: Style.Color = Style.Color.rgba(180, 180, 180, 255),
|
||||||
|
bar: Style.Color = Style.Color.rgba(100, 149, 237, 255),
|
||||||
|
value_text: Style.Color = Style.Color.rgba(255, 255, 255, 255),
|
||||||
|
|
||||||
|
pub fn fromTheme(theme: Style.Theme) BarChartColors {
|
||||||
|
return .{
|
||||||
|
.grid = theme.secondary.darken(30),
|
||||||
|
.axis = theme.secondary,
|
||||||
|
.label = theme.foreground,
|
||||||
|
.bar = theme.primary,
|
||||||
|
.value_text = theme.foreground,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Draw a bar chart
|
||||||
|
pub fn barChart(ctx: *Context, data: []const DataPoint) void {
|
||||||
|
barChartEx(ctx, data, .{}, .{});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draw a bar chart with configuration
|
||||||
|
pub fn barChartEx(
|
||||||
|
ctx: *Context,
|
||||||
|
data: []const DataPoint,
|
||||||
|
config: BarChartConfig,
|
||||||
|
colors: BarChartColors,
|
||||||
|
) void {
|
||||||
|
const bounds = ctx.layout.nextRect();
|
||||||
|
barChartRect(ctx, bounds, data, config, colors);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draw a bar chart in specific rectangle
|
||||||
|
pub fn barChartRect(
|
||||||
|
ctx: *Context,
|
||||||
|
bounds: Layout.Rect,
|
||||||
|
data: []const DataPoint,
|
||||||
|
config: BarChartConfig,
|
||||||
|
colors: BarChartColors,
|
||||||
|
) void {
|
||||||
|
if (bounds.isEmpty() or data.len == 0) return;
|
||||||
|
|
||||||
|
// Background
|
||||||
|
if (colors.background) |bg| {
|
||||||
|
ctx.pushCommand(Command.rect(bounds.x, bounds.y, bounds.w, bounds.h, bg));
|
||||||
|
}
|
||||||
|
|
||||||
|
const padding = config.padding;
|
||||||
|
const chart_x = bounds.x + @as(i32, @intCast(padding));
|
||||||
|
const chart_y = bounds.y + @as(i32, @intCast(padding / 2));
|
||||||
|
const chart_w = bounds.w -| (padding * 2);
|
||||||
|
const chart_h = bounds.h -| padding;
|
||||||
|
|
||||||
|
if (chart_w < 10 or chart_h < 10) return;
|
||||||
|
|
||||||
|
// Find data range
|
||||||
|
const min_val: f64 = config.y_min orelse 0;
|
||||||
|
var max_val: f64 = config.y_max orelse data[0].value;
|
||||||
|
|
||||||
|
if (config.y_max == null) {
|
||||||
|
for (data) |d| {
|
||||||
|
if (d.value > max_val) max_val = d.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add padding
|
||||||
|
if (max_val == min_val) {
|
||||||
|
max_val += 1;
|
||||||
|
}
|
||||||
|
max_val *= 1.1; // 10% padding at top
|
||||||
|
|
||||||
|
// Draw grid
|
||||||
|
if (config.show_grid) {
|
||||||
|
const grid_lines: u32 = 5;
|
||||||
|
var i: u32 = 0;
|
||||||
|
while (i <= grid_lines) : (i += 1) {
|
||||||
|
const y_pos = chart_y + @as(i32, @intCast(chart_h * i / grid_lines));
|
||||||
|
ctx.pushCommand(Command.rect(chart_x, y_pos, chart_w, 1, colors.grid));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate bar dimensions
|
||||||
|
const bar_total_w = @as(f64, @floatFromInt(chart_w)) / @as(f64, @floatFromInt(data.len));
|
||||||
|
const bar_spacing = bar_total_w * config.spacing;
|
||||||
|
const bar_w = bar_total_w - bar_spacing;
|
||||||
|
|
||||||
|
// Draw bars
|
||||||
|
for (data, 0..) |d, i| {
|
||||||
|
const bar_x = chart_x + @as(i32, @intFromFloat(@as(f64, @floatFromInt(i)) * bar_total_w + bar_spacing / 2));
|
||||||
|
const bar_height = @as(u32, @intFromFloat((d.value - min_val) / (max_val - min_val) * @as(f64, @floatFromInt(chart_h))));
|
||||||
|
const bar_y = chart_y + @as(i32, @intCast(chart_h - bar_height));
|
||||||
|
|
||||||
|
const bar_color = d.color orelse colors.bar;
|
||||||
|
ctx.pushCommand(Command.rect(bar_x, bar_y, @intFromFloat(bar_w), bar_height, bar_color));
|
||||||
|
|
||||||
|
// Draw value
|
||||||
|
if (config.show_values and bar_height > 15) {
|
||||||
|
var buf: [16]u8 = undefined;
|
||||||
|
const value_str = std.fmt.bufPrint(&buf, "{d:.0}", .{d.value}) catch continue;
|
||||||
|
const text_x = bar_x + @as(i32, @intFromFloat(bar_w / 2)) - @as(i32, @intCast(value_str.len * 4));
|
||||||
|
ctx.pushCommand(Command.text(text_x, bar_y + 4, value_str, colors.value_text));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw label
|
||||||
|
if (d.label) |lbl| {
|
||||||
|
const label_x = bar_x + @as(i32, @intFromFloat(bar_w / 2)) - @as(i32, @intCast(lbl.len * 4));
|
||||||
|
const label_y = chart_y + @as(i32, @intCast(chart_h)) + 4;
|
||||||
|
ctx.pushCommand(Command.text(label_x, label_y, lbl, colors.label));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Pie Chart
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/// Pie chart configuration
|
||||||
|
pub const PieChartConfig = struct {
|
||||||
|
/// Show labels
|
||||||
|
show_labels: bool = true,
|
||||||
|
/// Show values
|
||||||
|
show_values: bool = true,
|
||||||
|
/// Show percentages
|
||||||
|
show_percentages: bool = true,
|
||||||
|
/// Donut hole radius (0 = no hole)
|
||||||
|
donut_radius: f32 = 0.0,
|
||||||
|
/// Start angle (radians, 0 = right)
|
||||||
|
start_angle: f32 = -std.math.pi / 2.0, // Top
|
||||||
|
/// Padding
|
||||||
|
padding: u32 = 20,
|
||||||
|
/// Explode all slices
|
||||||
|
explode: f32 = 0.0,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Pie chart colors
|
||||||
|
pub const PieChartColors = struct {
|
||||||
|
background: ?Style.Color = null,
|
||||||
|
label: Style.Color = Style.Color.rgba(220, 220, 220, 255),
|
||||||
|
|
||||||
|
pub fn fromTheme(theme: Style.Theme) PieChartColors {
|
||||||
|
return .{
|
||||||
|
.label = theme.foreground,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Draw a pie chart
|
||||||
|
pub fn pieChart(ctx: *Context, data: []const DataPoint) void {
|
||||||
|
pieChartEx(ctx, data, .{}, .{});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draw a pie chart with configuration
|
||||||
|
pub fn pieChartEx(
|
||||||
|
ctx: *Context,
|
||||||
|
data: []const DataPoint,
|
||||||
|
config: PieChartConfig,
|
||||||
|
colors: PieChartColors,
|
||||||
|
) void {
|
||||||
|
const bounds = ctx.layout.nextRect();
|
||||||
|
pieChartRect(ctx, bounds, data, config, colors);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draw a pie chart in specific rectangle
|
||||||
|
pub fn pieChartRect(
|
||||||
|
ctx: *Context,
|
||||||
|
bounds: Layout.Rect,
|
||||||
|
data: []const DataPoint,
|
||||||
|
config: PieChartConfig,
|
||||||
|
colors: PieChartColors,
|
||||||
|
) void {
|
||||||
|
if (bounds.isEmpty() or data.len == 0) return;
|
||||||
|
|
||||||
|
// Background
|
||||||
|
if (colors.background) |bg| {
|
||||||
|
ctx.pushCommand(Command.rect(bounds.x, bounds.y, bounds.w, bounds.h, bg));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate total
|
||||||
|
var total: f64 = 0;
|
||||||
|
for (data) |d| {
|
||||||
|
total += d.value;
|
||||||
|
}
|
||||||
|
if (total == 0) return;
|
||||||
|
|
||||||
|
// Calculate center and radius
|
||||||
|
const padding = config.padding;
|
||||||
|
const chart_w = bounds.w -| (padding * 2);
|
||||||
|
const chart_h = bounds.h -| (padding * 2);
|
||||||
|
const radius = @min(chart_w, chart_h) / 2;
|
||||||
|
const center_x = bounds.x + @as(i32, @intCast(bounds.w / 2));
|
||||||
|
const center_y = bounds.y + @as(i32, @intCast(bounds.h / 2));
|
||||||
|
|
||||||
|
if (radius < 10) return;
|
||||||
|
|
||||||
|
// Draw slices
|
||||||
|
var current_angle = config.start_angle;
|
||||||
|
const r_f = @as(f32, @floatFromInt(radius));
|
||||||
|
|
||||||
|
for (data, 0..) |d, i| {
|
||||||
|
const slice_angle = @as(f32, @floatFromInt(@as(u32, @intFromFloat(d.value / total * std.math.pi * 2.0 * 1000.0)))) / 1000.0;
|
||||||
|
const slice_color = d.color orelse default_colors[i % default_colors.len];
|
||||||
|
|
||||||
|
// Draw arc using triangles
|
||||||
|
const segments: u32 = @max(8, @as(u32, @intFromFloat(slice_angle * r_f / 4)));
|
||||||
|
const angle_step = slice_angle / @as(f32, @floatFromInt(segments));
|
||||||
|
|
||||||
|
var angle = current_angle;
|
||||||
|
var seg: u32 = 0;
|
||||||
|
while (seg < segments) : (seg += 1) {
|
||||||
|
const next_angle = angle + angle_step;
|
||||||
|
|
||||||
|
const x1 = center_x + @as(i32, @intFromFloat(@cos(angle) * r_f));
|
||||||
|
const y1 = center_y + @as(i32, @intFromFloat(@sin(angle) * r_f));
|
||||||
|
const x2 = center_x + @as(i32, @intFromFloat(@cos(next_angle) * r_f));
|
||||||
|
const y2 = center_y + @as(i32, @intFromFloat(@sin(next_angle) * r_f));
|
||||||
|
|
||||||
|
// Draw triangle
|
||||||
|
drawFilledTriangle(ctx, center_x, center_y, x1, y1, x2, y2, slice_color);
|
||||||
|
|
||||||
|
angle = next_angle;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw label
|
||||||
|
if (config.show_labels and d.label != null) {
|
||||||
|
const mid_angle = current_angle + slice_angle / 2;
|
||||||
|
const label_r = r_f * 0.7;
|
||||||
|
const label_x = center_x + @as(i32, @intFromFloat(@cos(mid_angle) * label_r));
|
||||||
|
const label_y = center_y + @as(i32, @intFromFloat(@sin(mid_angle) * label_r));
|
||||||
|
|
||||||
|
if (d.label) |lbl| {
|
||||||
|
const text_x = label_x - @as(i32, @intCast(lbl.len * 4));
|
||||||
|
ctx.pushCommand(Command.text(text_x, label_y - 4, lbl, colors.label));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
current_angle += slice_angle;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw donut hole
|
||||||
|
if (config.donut_radius > 0) {
|
||||||
|
const hole_r = @as(u32, @intFromFloat(r_f * config.donut_radius));
|
||||||
|
if (colors.background) |bg| {
|
||||||
|
// Fill center with background
|
||||||
|
var dy: i32 = -@as(i32, @intCast(hole_r));
|
||||||
|
while (dy <= @as(i32, @intCast(hole_r))) : (dy += 1) {
|
||||||
|
const dy_f = @as(f32, @floatFromInt(dy));
|
||||||
|
const hr_f = @as(f32, @floatFromInt(hole_r));
|
||||||
|
const dx = @as(i32, @intFromFloat(@sqrt(hr_f * hr_f - dy_f * dy_f)));
|
||||||
|
ctx.pushCommand(Command.rect(center_x - dx, center_y + dy, @intCast(dx * 2 + 1), 1, bg));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draw a filled triangle
|
||||||
|
fn drawFilledTriangle(
|
||||||
|
ctx: *Context,
|
||||||
|
x1: i32,
|
||||||
|
y1: i32,
|
||||||
|
x2: i32,
|
||||||
|
y2: i32,
|
||||||
|
x3: i32,
|
||||||
|
y3: i32,
|
||||||
|
color: Style.Color,
|
||||||
|
) void {
|
||||||
|
// Sort vertices by y
|
||||||
|
var v1 = canvas.Point.init(x1, y1);
|
||||||
|
var v2 = canvas.Point.init(x2, y2);
|
||||||
|
var v3 = canvas.Point.init(x3, y3);
|
||||||
|
|
||||||
|
if (v1.y > v2.y) std.mem.swap(canvas.Point, &v1, &v2);
|
||||||
|
if (v1.y > v3.y) std.mem.swap(canvas.Point, &v1, &v3);
|
||||||
|
if (v2.y > v3.y) std.mem.swap(canvas.Point, &v2, &v3);
|
||||||
|
|
||||||
|
const total_height = v3.y - v1.y;
|
||||||
|
if (total_height == 0) return;
|
||||||
|
|
||||||
|
var py = v1.y;
|
||||||
|
while (py <= v3.y) : (py += 1) {
|
||||||
|
const second_half = py > v2.y or v2.y == v1.y;
|
||||||
|
const segment_height = if (second_half) v3.y - v2.y else v2.y - v1.y;
|
||||||
|
|
||||||
|
if (segment_height == 0) continue;
|
||||||
|
|
||||||
|
const alpha = @as(f32, @floatFromInt(py - v1.y)) / @as(f32, @floatFromInt(total_height));
|
||||||
|
const beta = @as(f32, @floatFromInt(py - (if (second_half) v2.y else v1.y))) /
|
||||||
|
@as(f32, @floatFromInt(segment_height));
|
||||||
|
|
||||||
|
var ax = v1.x + @as(i32, @intFromFloat(@as(f32, @floatFromInt(v3.x - v1.x)) * alpha));
|
||||||
|
var bx: i32 = undefined;
|
||||||
|
|
||||||
|
if (second_half) {
|
||||||
|
bx = v2.x + @as(i32, @intFromFloat(@as(f32, @floatFromInt(v3.x - v2.x)) * beta));
|
||||||
|
} else {
|
||||||
|
bx = v1.x + @as(i32, @intFromFloat(@as(f32, @floatFromInt(v2.x - v1.x)) * beta));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ax > bx) std.mem.swap(i32, &ax, &bx);
|
||||||
|
|
||||||
|
ctx.pushCommand(Command.rect(ax, py, @intCast(bx - ax + 1), 1, color));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Tests
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
test "DataPoint creation" {
|
||||||
|
const point = DataPoint{
|
||||||
|
.value = 42.5,
|
||||||
|
.label = "Test",
|
||||||
|
};
|
||||||
|
try std.testing.expectEqual(@as(f64, 42.5), point.value);
|
||||||
|
try std.testing.expectEqualStrings("Test", point.label.?);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "lineChart generates commands" {
|
||||||
|
var ctx = try Context.init(std.testing.allocator, 800, 600);
|
||||||
|
defer ctx.deinit();
|
||||||
|
|
||||||
|
ctx.beginFrame();
|
||||||
|
ctx.layout.row_height = 300;
|
||||||
|
|
||||||
|
const data = [_]f64{ 10, 20, 15, 30, 25 };
|
||||||
|
lineChart(&ctx, &data);
|
||||||
|
|
||||||
|
try std.testing.expect(ctx.commands.items.len >= 1);
|
||||||
|
|
||||||
|
ctx.endFrame();
|
||||||
|
}
|
||||||
|
|
||||||
|
test "barChart generates commands" {
|
||||||
|
var ctx = try Context.init(std.testing.allocator, 800, 600);
|
||||||
|
defer ctx.deinit();
|
||||||
|
|
||||||
|
ctx.beginFrame();
|
||||||
|
ctx.layout.row_height = 300;
|
||||||
|
|
||||||
|
const data = [_]DataPoint{
|
||||||
|
.{ .value = 10, .label = "A" },
|
||||||
|
.{ .value = 20, .label = "B" },
|
||||||
|
.{ .value = 15, .label = "C" },
|
||||||
|
};
|
||||||
|
barChart(&ctx, &data);
|
||||||
|
|
||||||
|
try std.testing.expect(ctx.commands.items.len >= 1);
|
||||||
|
|
||||||
|
ctx.endFrame();
|
||||||
|
}
|
||||||
|
|
||||||
|
test "pieChart generates commands" {
|
||||||
|
var ctx = try Context.init(std.testing.allocator, 800, 600);
|
||||||
|
defer ctx.deinit();
|
||||||
|
|
||||||
|
ctx.beginFrame();
|
||||||
|
ctx.layout.row_height = 300;
|
||||||
|
|
||||||
|
const data = [_]DataPoint{
|
||||||
|
.{ .value = 30, .label = "A" },
|
||||||
|
.{ .value = 50, .label = "B" },
|
||||||
|
.{ .value = 20, .label = "C" },
|
||||||
|
};
|
||||||
|
pieChart(&ctx, &data);
|
||||||
|
|
||||||
|
try std.testing.expect(ctx.commands.items.len >= 1);
|
||||||
|
|
||||||
|
ctx.endFrame();
|
||||||
|
}
|
||||||
805
src/widgets/icon.zig
Normal file
805
src/widgets/icon.zig
Normal file
|
|
@ -0,0 +1,805 @@
|
||||||
|
//! 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();
|
||||||
|
}
|
||||||
|
|
@ -38,6 +38,9 @@ pub const datepicker = @import("datepicker.zig");
|
||||||
pub const numberentry = @import("numberentry.zig");
|
pub const numberentry = @import("numberentry.zig");
|
||||||
pub const richtext = @import("richtext.zig");
|
pub const richtext = @import("richtext.zig");
|
||||||
pub const breadcrumb = @import("breadcrumb.zig");
|
pub const breadcrumb = @import("breadcrumb.zig");
|
||||||
|
pub const canvas = @import("canvas.zig");
|
||||||
|
pub const chart = @import("chart.zig");
|
||||||
|
pub const icon = @import("icon.zig");
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// Re-exports for convenience
|
// Re-exports for convenience
|
||||||
|
|
@ -288,6 +291,28 @@ pub const BreadcrumbConfig = breadcrumb.Config;
|
||||||
pub const BreadcrumbColors = breadcrumb.Colors;
|
pub const BreadcrumbColors = breadcrumb.Colors;
|
||||||
pub const BreadcrumbResult = breadcrumb.Result;
|
pub const BreadcrumbResult = breadcrumb.Result;
|
||||||
|
|
||||||
|
// Canvas
|
||||||
|
pub const Canvas = canvas.Canvas;
|
||||||
|
pub const CanvasPoint = canvas.Point;
|
||||||
|
|
||||||
|
// Chart
|
||||||
|
pub const Chart = chart;
|
||||||
|
pub const DataPoint = chart.DataPoint;
|
||||||
|
pub const DataSeries = chart.DataSeries;
|
||||||
|
pub const LineChartConfig = chart.LineChartConfig;
|
||||||
|
pub const LineChartColors = chart.LineChartColors;
|
||||||
|
pub const BarChartConfig = chart.BarChartConfig;
|
||||||
|
pub const BarChartColors = chart.BarChartColors;
|
||||||
|
pub const PieChartConfig = chart.PieChartConfig;
|
||||||
|
pub const PieChartColors = chart.PieChartColors;
|
||||||
|
|
||||||
|
// Icon
|
||||||
|
pub const Icon = icon;
|
||||||
|
pub const IconType = icon.IconType;
|
||||||
|
pub const IconSize = icon.Size;
|
||||||
|
pub const IconConfig = icon.Config;
|
||||||
|
pub const IconColors = icon.Colors;
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// Tests
|
// Tests
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue