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