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