a modern tui library written in zig
at main 6.5 kB view raw
1const std = @import("std"); 2const vaxis = @import("vaxis"); 3const vxfw = vaxis.vxfw; 4 5/// Our main application state 6const Model = struct { 7 /// State of the counter 8 count: u32 = 0, 9 /// The button. This widget is stateful and must live between frames 10 button: vxfw.Button, 11 12 /// Helper function to return a vxfw.Widget struct 13 pub fn widget(self: *Model) vxfw.Widget { 14 return .{ 15 .userdata = self, 16 .eventHandler = Model.typeErasedEventHandler, 17 .drawFn = Model.typeErasedDrawFn, 18 }; 19 } 20 21 /// This function will be called from the vxfw runtime. 22 fn typeErasedEventHandler(ptr: *anyopaque, ctx: *vxfw.EventContext, event: vxfw.Event) anyerror!void { 23 const self: *Model = @ptrCast(@alignCast(ptr)); 24 switch (event) { 25 // The root widget is always sent an init event as the first event. Users of the 26 // library can also send this event to other widgets they create if they need to do 27 // some initialization. 28 .init => return ctx.requestFocus(self.button.widget()), 29 .key_press => |key| { 30 if (key.matches('c', .{ .ctrl = true })) { 31 ctx.quit = true; 32 return; 33 } 34 }, 35 // We can request a specific widget gets focus. In this case, we always want to focus 36 // our button. Having focus means that key events will be sent up the widget tree to 37 // the focused widget, and then bubble back down the tree to the root. Users can tell 38 // the runtime the event was handled and the capture or bubble phase will stop 39 .focus_in => return ctx.requestFocus(self.button.widget()), 40 else => {}, 41 } 42 } 43 44 /// This function is called from the vxfw runtime. It will be called on a regular interval, and 45 /// only when any event handler has marked the redraw flag in EventContext as true. By 46 /// explicitly requiring setting the redraw flag, vxfw can prevent excessive redraws for events 47 /// which don't change state (ie mouse motion, unhandled key events, etc) 48 fn typeErasedDrawFn(ptr: *anyopaque, ctx: vxfw.DrawContext) std.mem.Allocator.Error!vxfw.Surface { 49 const self: *Model = @ptrCast(@alignCast(ptr)); 50 // The DrawContext is inspired from Flutter. Each widget will receive a minimum and maximum 51 // constraint. The minimum constraint will always be set, even if it is set to 0x0. The 52 // maximum constraint can have null width and/or height - meaning there is no constraint in 53 // that direction and the widget should take up as much space as it needs. By calling size() 54 // on the max, we assert that it has some constrained size. This is *always* the case for 55 // the root widget - the maximum size will always be the size of the terminal screen. 56 const max_size = ctx.max.size(); 57 58 // The DrawContext also contains an arena allocator that can be used for each frame. The 59 // lifetime of this allocation is until the next time we draw a frame. This is useful for 60 // temporary allocations such as the one below: we have an integer we want to print as text. 61 // We can safely allocate this with the ctx arena since we only need it for this frame. 62 if (self.count > 0) { 63 self.button.label = try std.fmt.allocPrint(ctx.arena, "Clicks: {d}", .{self.count}); 64 } else { 65 self.button.label = "Click me!"; 66 } 67 68 // Each widget returns a Surface from it's draw function. A Surface contains the rectangular 69 // area of the widget, as well as some information about the surface or widget: can we focus 70 // it? does it handle the mouse? 71 // 72 // It DOES NOT contain the location it should be within it's parent. Only the parent can set 73 // this via a SubSurface. Here, we will return a Surface for the root widget (Model), which 74 // has two SubSurfaces: one for the text and one for the button. A SubSurface is a Surface 75 // with an offset and a z-index - the offset can be negative. This lets a parent draw a 76 // child and place it within itself 77 const button_child: vxfw.SubSurface = .{ 78 .origin = .{ .row = 0, .col = 0 }, 79 .surface = try self.button.draw(ctx.withConstraints( 80 ctx.min, 81 // Here we explicitly set a new maximum size constraint for the Button. A Button will 82 // expand to fill it's area and must have some hard limit in the maximum constraint 83 .{ .width = 16, .height = 3 }, 84 )), 85 }; 86 87 // We also can use our arena to allocate the slice for our SubSurfaces. This slice only 88 // needs to live until the next frame, making this safe. 89 const children = try ctx.arena.alloc(vxfw.SubSurface, 1); 90 children[0] = button_child; 91 92 return .{ 93 // A Surface must have a size. Our root widget is the size of the screen 94 .size = max_size, 95 .widget = self.widget(), 96 // We didn't actually need to draw anything for the root. In this case, we can set 97 // buffer to a zero length slice. If this slice is *not zero length*, the runtime will 98 // assert that it's length is equal to the size.width * size.height. 99 .buffer = &.{}, 100 .children = children, 101 }; 102 } 103 104 /// The onClick callback for our button. This is also called if we press enter while the button 105 /// has focus 106 fn onClick(maybe_ptr: ?*anyopaque, ctx: *vxfw.EventContext) anyerror!void { 107 const ptr = maybe_ptr orelse return; 108 const self: *Model = @ptrCast(@alignCast(ptr)); 109 self.count +|= 1; 110 return ctx.consumeAndRedraw(); 111 } 112}; 113 114pub fn main() !void { 115 var gpa = std.heap.GeneralPurposeAllocator(.{}){}; 116 defer _ = gpa.deinit(); 117 118 const allocator = gpa.allocator(); 119 120 var app = try vxfw.App.init(allocator); 121 defer app.deinit(); 122 123 // We heap allocate our model because we will require a stable pointer to it in our Button 124 // widget 125 const model = try allocator.create(Model); 126 defer allocator.destroy(model); 127 128 // Set the initial state of our button 129 model.* = .{ 130 .count = 0, 131 .button = .{ 132 .label = "Click me!", 133 .onClick = Model.onClick, 134 .userdata = model, 135 }, 136 }; 137 138 try app.run(model.widget(), .{}); 139}