this repo has no description
13
fork

Configure Feed

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

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