zcatgui/src/widgets/progress/spinner.zig
reugenio 59935aeb2b refactor: Split textarea.zig and progress.zig into modular structures
Split large widget files for better maintainability (~500-600 lines per file):

textarea/ (was 882 lines):
- types.zig: TextAreaConfig, TextAreaColors, TextAreaResult
- state.zig: TextAreaState with cursor/selection methods
- render.zig: drawLineNumber, drawLineText, drawLineSelection
- textarea.zig: Main API with re-exports and tests

progress/ (was 806 lines):
- render.zig: Shared drawing helpers (stripes, gradients, arcs)
- bar.zig: ProgressBar widget
- circle.zig: ProgressCircle widget
- spinner.zig: Spinner widget with animation state
- progress.zig: Main API with re-exports and tests

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-11 23:21:06 +01:00

141 lines
3.9 KiB
Zig

//! Spinner Widget - Animated loading indicator
//!
//! Part of the progress widget module.
const std = @import("std");
const Context = @import("../../core/context.zig").Context;
const Layout = @import("../../core/layout.zig");
const Style = @import("../../core/style.zig");
const Rect = Layout.Rect;
const Color = Style.Color;
const render = @import("render.zig");
// =============================================================================
// Types
// =============================================================================
/// Spinner style
pub const SpinnerStyle = enum {
/// Rotating arc
circular,
/// Pulsing dots
dots,
/// Bouncing bars (equalizer)
bars,
/// Ring with gap
ring,
};
/// Spinner configuration
pub const SpinnerConfig = struct {
/// Visual style
style: SpinnerStyle = .circular,
/// Size in pixels
size: u16 = 24,
/// Animation speed multiplier
speed: f32 = 1.0,
/// Optional label below spinner
label: ?[]const u8 = null,
/// Primary color
color: ?Color = null,
/// Number of elements (dots or bars)
elements: u8 = 8,
};
/// Spinner state (for animation)
pub const SpinnerState = struct {
/// Animation progress (0-1, loops)
animation: f32 = 0,
/// Last update timestamp
last_update: i64 = 0,
pub fn update(self: *SpinnerState, speed: f32) void {
const now = std.time.milliTimestamp();
if (self.last_update == 0) {
self.last_update = now;
return;
}
const delta_ms = now - self.last_update;
self.last_update = now;
// Advance animation
const delta_f: f32 = @floatFromInt(delta_ms);
self.animation += (delta_f / 1000.0) * speed;
if (self.animation >= 1.0) {
self.animation -= 1.0;
}
}
};
/// Spinner result
pub const SpinnerResult = struct {
bounds: Rect,
};
// =============================================================================
// Public API
// =============================================================================
/// Simple spinner
pub fn spinner(ctx: *Context, state: *SpinnerState) SpinnerResult {
return spinnerEx(ctx, state, .{});
}
/// Spinner with configuration
pub fn spinnerEx(ctx: *Context, state: *SpinnerState, config: SpinnerConfig) SpinnerResult {
const theme = Style.currentTheme();
const color = config.color orelse theme.primary;
// Update animation
state.update(config.speed);
// Get bounds
const layout_rect = ctx.layout.area;
const bounds = Rect.init(
layout_rect.x,
layout_rect.y,
config.size,
config.size,
);
const center_x = bounds.x + @as(i32, @intCast(bounds.w / 2));
const center_y = bounds.y + @as(i32, @intCast(bounds.h / 2));
switch (config.style) {
.circular => {
render.drawRotatingArc(ctx, center_x, center_y, config.size / 2 - 2, color, state.animation);
},
.dots => {
render.drawPulsingDots(ctx, center_x, center_y, config.size, color, config.elements, state.animation);
},
.bars => {
render.drawBouncingBars(ctx, bounds, color, config.elements, state.animation);
},
.ring => {
render.drawRingWithGap(ctx, center_x, center_y, config.size / 2 - 2, color, state.animation);
},
}
// Draw label if present
if (config.label) |label| {
const text_x = bounds.x;
const text_y = bounds.y + @as(i32, @intCast(bounds.h)) + 4;
ctx.pushCommand(.{
.text = .{
.x = text_x,
.y = text_y,
.text = label,
.color = theme.text_secondary,
},
});
}
ctx.countWidget();
return .{
.bounds = bounds,
};
}