this repo has no description
13
fork

Configure Feed

Select the types of activity you want to include in your feed.

vxfw: add Spinner widget

+137
+136
src/vxfw/Spinner.zig
··· 1 + const std = @import("std"); 2 + const vaxis = @import("../main.zig"); 3 + 4 + const vxfw = @import("vxfw.zig"); 5 + 6 + const Allocator = std.mem.Allocator; 7 + 8 + const Spinner = @This(); 9 + 10 + const frames: []const []const u8 = &.{ "⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏" }; 11 + const time_lapse: u32 = std.time.ms_per_s / 12; // 12 fps 12 + 13 + count: std.atomic.Value(u16) = .{ .raw = 0 }, 14 + style: vaxis.Style = .{}, 15 + /// The frame index 16 + frame: u4 = 0, 17 + 18 + /// Start, or add one, to the spinner counter. Thread safe. 19 + pub fn start(self: *Spinner) ?vxfw.Command { 20 + const count = self.count.fetchAdd(1, .monotonic); 21 + if (count == 0) { 22 + return vxfw.Tick.in(time_lapse, self.widget()); 23 + } 24 + return null; 25 + } 26 + 27 + /// Reduce one from the spinner counter. The spinner will stop when it reaches 0. Thread safe 28 + pub fn stop(self: *Spinner) void { 29 + self.count.store(self.count.load(.unordered) -| 1, .unordered); 30 + } 31 + 32 + pub fn widget(self: *Spinner) vxfw.Widget { 33 + return .{ 34 + .userdata = self, 35 + .eventHandler = typeErasedEventHandler, 36 + .drawFn = typeErasedDrawFn, 37 + }; 38 + } 39 + 40 + fn typeErasedEventHandler(ptr: *anyopaque, ctx: *vxfw.EventContext, event: vxfw.Event) anyerror!void { 41 + const self: *Spinner = @ptrCast(@alignCast(ptr)); 42 + return self.handleEvent(ctx, event); 43 + } 44 + 45 + pub fn handleEvent(self: *Spinner, ctx: *vxfw.EventContext, event: vxfw.Event) Allocator.Error!void { 46 + switch (event) { 47 + .tick => { 48 + const count = self.count.load(.unordered); 49 + 50 + if (count == 0) return; 51 + // Update frame 52 + self.frame += 1; 53 + if (self.frame >= frames.len) self.frame = 0; 54 + 55 + // Update rearm 56 + try ctx.tick(time_lapse, self.widget()); 57 + }, 58 + else => {}, 59 + } 60 + } 61 + 62 + fn typeErasedDrawFn(ptr: *anyopaque, ctx: vxfw.DrawContext) Allocator.Error!vxfw.Surface { 63 + const self: *Spinner = @ptrCast(@alignCast(ptr)); 64 + return self.draw(ctx); 65 + } 66 + 67 + pub fn draw(self: *Spinner, ctx: vxfw.DrawContext) Allocator.Error!vxfw.Surface { 68 + const size: vxfw.Size = .{ 69 + .width = @max(1, ctx.min.width), 70 + .height = @max(1, ctx.min.height), 71 + }; 72 + 73 + const surface = try vxfw.Surface.init(ctx.arena, self.widget(), size); 74 + @memset(surface.buffer, .{ .style = self.style }); 75 + 76 + if (self.count.load(.unordered) == 0) return surface; 77 + 78 + surface.writeCell(0, 0, .{ 79 + .char = .{ 80 + .grapheme = frames[self.frame], 81 + .width = 1, 82 + }, 83 + .style = self.style, 84 + }); 85 + return surface; 86 + } 87 + 88 + test Spinner { 89 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 90 + defer arena.deinit(); 91 + // Create a spinner 92 + var spinner: Spinner = .{}; 93 + // Get our widget interface 94 + const spinner_widget = spinner.widget(); 95 + 96 + // Start the spinner. This (maybe) returns a Tick command to schedule the next frame. If the 97 + // spinner is already running, no command is returned. Calling start is thread safe. The 98 + // returned command can be added to an EventContext to schedule the frame 99 + const maybe_cmd = spinner.start(); 100 + try std.testing.expect(maybe_cmd != null); 101 + try std.testing.expect(maybe_cmd.? == .tick); 102 + try std.testing.expectEqual(1, spinner.count.load(.unordered)); 103 + 104 + // If we call start again, we won't get another command but our counter will go up 105 + const maybe_cmd2 = spinner.start(); 106 + try std.testing.expect(maybe_cmd2 == null); 107 + try std.testing.expectEqual(2, spinner.count.load(.unordered)); 108 + 109 + // We are about to deliver the tick to the widget. We need an EventContext (the engine will 110 + // provide this) 111 + var ctx: vxfw.EventContext = .{ .cmds = vxfw.CommandList.init(arena.allocator()) }; 112 + 113 + // The event loop handles the tick event and calls us back with a .tick event. If we should keep 114 + // running, we will add a new tick event 115 + try spinner_widget.handleEvent(&ctx, .tick); 116 + 117 + // Receiving a .tick advances the frame 118 + try std.testing.expectEqual(1, spinner.frame); 119 + 120 + // Simulate a draw 121 + const surface = try spinner_widget.draw(.{ .arena = arena.allocator(), .min = .{}, .max = .{} }); 122 + 123 + // Spinner will try to be 1x1 124 + try std.testing.expectEqual(1, surface.size.width); 125 + try std.testing.expectEqual(1, surface.size.height); 126 + 127 + // Stopping the spinner decrements our counter 128 + spinner.stop(); 129 + try std.testing.expectEqual(1, spinner.count.load(.unordered)); 130 + spinner.stop(); 131 + try std.testing.expectEqual(0, spinner.count.load(.unordered)); 132 + } 133 + 134 + test "refAllDecls" { 135 + std.testing.refAllDecls(@This()); 136 + }
+1
src/vxfw/vxfw.zig
··· 19 19 pub const Padding = @import("Padding.zig"); 20 20 pub const RichText = @import("RichText.zig"); 21 21 pub const SizedBox = @import("SizedBox.zig"); 22 + pub const Spinner = @import("Spinner.zig"); 22 23 pub const Text = @import("Text.zig"); 23 24 pub const TextField = @import("TextField.zig"); 24 25