this repo has no description
13
fork

Configure Feed

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

readme: update with vxfw example

+308 -5
+167 -5
README.md
··· 9 9 Libvaxis _does not use terminfo_. Support for vt features is detected through 10 10 terminal queries. 11 11 12 - Contributions are welcome. 13 - 14 12 Vaxis uses zig `0.13.0`. 15 13 16 14 ## Features ··· 37 35 38 36 [Documentation](https://rockorager.github.io/libvaxis/#vaxis.Vaxis) 39 37 40 - [Starter repo](https://github.com/rockorager/libvaxis-starter) 38 + The library provides both a low level API suitable for making applications of 39 + any sort as well as a higher level framework. The low level API is suitable for 40 + making applications of any type, providing your own event loop, and gives you 41 + full control over each cell on the screen. 42 + 43 + The high level API, called `vxfw` (Vaxis framework), provides a Flutter-like 44 + style of API. The framework provides an application runtime which handles the 45 + event loop, focus management, mouse handling, and more. Several widgets are 46 + provided, and custom widgets are easy to build. This API is most likely what you 47 + want to use for typical TUI applications. 48 + 49 + ### vxfw (Vaxis framework) 50 + 51 + Let's build a simple button counter application. This example can be run using 52 + the command `zig build example -Dexample=counter`. The below application has 53 + full mouse support: the button *and mouse shape* will change style on hover, on 54 + click, and has enough logic to cancel a press if the release does not occur over 55 + the button. Try it! Click the button, move the mouse off the button and release. 56 + All of this logic is baked into the base `Button` widget. 57 + 58 + ```zig 59 + const std = @import("std"); 60 + const vaxis = @import("vaxis"); 61 + const vxfw = vaxis.vxfw; 62 + 63 + /// Our main application state 64 + const Model = struct { 65 + /// State of the counter 66 + count: u32 = 0, 67 + /// The button. This widget is stateful and must live between frames 68 + button: vxfw.Button, 69 + 70 + /// Helper function to return a vxfw.Widget struct 71 + pub fn widget(self: *Model) vxfw.Widget { 72 + return .{ 73 + .userdata = self, 74 + .eventHandler = Model.typeErasedEventHandler, 75 + .drawFn = Model.typeErasedDrawFn, 76 + }; 77 + } 78 + 79 + /// This function will be called from the vxfw runtime. 80 + fn typeErasedEventHandler(ptr: *anyopaque, ctx: *vxfw.EventContext, event: vxfw.Event) anyerror!void { 81 + const self: *Model = @ptrCast(@alignCast(ptr)); 82 + switch (event) { 83 + // The root widget is always sent an init event as the first event. Users of the 84 + // library can also send this event to other widgets they create if they need to do 85 + // some initialization. 86 + .init => return ctx.requestFocus(self.button.widget()), 87 + .key_press => |key| { 88 + if (key.matches('c', .{ .ctrl = true })) { 89 + ctx.quit = true; 90 + return; 91 + } 92 + }, 93 + // We can request a specific widget gets focus. In this case, we always want to focus 94 + // our button. Having focus means that key events will be sent up the widget tree to 95 + // the focused widget, and then bubble back down the tree to the root. Users can tell 96 + // the runtime the event was handled and the capture or bubble phase will stop 97 + .focus_in => return ctx.requestFocus(self.button.widget()), 98 + else => {}, 99 + } 100 + } 101 + 102 + /// This function is called from the vxfw runtime. It will be called on a regular interval, and 103 + /// only when any event handler has marked the redraw flag in EventContext as true. By 104 + /// explicitly requiring setting the redraw flag, vxfw can prevent excessive redraws for events 105 + /// which don't change state (ie mouse motion, unhandled key events, etc) 106 + fn typeErasedDrawFn(ptr: *anyopaque, ctx: vxfw.DrawContext) std.mem.Allocator.Error!vxfw.Surface { 107 + const self: *Model = @ptrCast(@alignCast(ptr)); 108 + // The DrawContext is inspired from Flutter. Each widget will receive a minimum and maximum 109 + // constraint. The minimum constraint will always be set, even if it is set to 0x0. The 110 + // maximum constraint can have null width and/or height - meaning there is no constraint in 111 + // that direction and the widget should take up as much space as it needs. By calling size() 112 + // on the max, we assert that it has some constrained size. This is *always* the case for 113 + // the root widget - the maximum size will always be the size of the terminal screen. 114 + const max_size = ctx.max.size(); 115 + 116 + // The DrawContext also contains an arena allocator that can be used for each frame. The 117 + // lifetime of this allocation is until the next time we draw a frame. This is useful for 118 + // temporary allocations such as the one below: we have an integer we want to print as text. 119 + // We can safely allocate this with the ctx arena since we only need it for this frame. 120 + const count_text = try std.fmt.allocPrint(ctx.arena, "{d}", .{self.count}); 121 + const text: vxfw.Text = .{ .text = count_text }; 122 + 123 + // Each widget returns a Surface from it's draw function. A Surface contains the rectangular 124 + // area of the widget, as well as some information about the surface or widget: can we focus 125 + // it? does it handle the mouse? 126 + // 127 + // It DOES NOT contain the location it should be within it's parent. Only the parent can set 128 + // this via a SubSurface. Here, we will return a Surface for the root widget (Model), which 129 + // has two SubSurfaces: one for the text and one for the button. A SubSurface is a Surface 130 + // with an offset and a z-index - the offset can be negative. This lets a parent draw a 131 + // child and place it within itself 132 + const text_child: vxfw.SubSurface = .{ 133 + .origin = .{ .row = 0, .col = 0 }, 134 + .surface = try text.draw(ctx), 135 + }; 136 + 137 + const button_child: vxfw.SubSurface = .{ 138 + .origin = .{ .row = 2, .col = 0 }, 139 + .surface = try self.button.draw(ctx.withConstraints( 140 + ctx.min, 141 + // Here we explicitly set a new maximum size constraint for the Button. A Button will 142 + // expand to fill it's area and must have some hard limit in the maximum constraint 143 + .{ .width = 16, .height = 3 }, 144 + )), 145 + }; 146 + 147 + // We also can use our arena to allocate the slice for our SubSurfaces. This slice only 148 + // needs to live until the next frame, making this safe. 149 + const children = try ctx.arena.alloc(vxfw.SubSurface, 2); 150 + children[0] = text_child; 151 + children[1] = button_child; 152 + 153 + return .{ 154 + // A Surface must have a size. Our root widget is the size of the screen 155 + .size = max_size, 156 + .widget = self.widget(), 157 + .focusable = false, 158 + // We didn't actually need to draw anything for the root. In this case, we can set 159 + // buffer to a zero length slice. If this slice is *not zero length*, the runtime will 160 + // assert that it's length is equal to the size.width * size.height. 161 + .buffer = &.{}, 162 + .children = children, 163 + }; 164 + } 165 + 166 + /// The onClick callback for our button. This is also called if we press enter while the button 167 + /// has focus 168 + fn onClick(maybe_ptr: ?*anyopaque, ctx: *vxfw.EventContext) anyerror!void { 169 + const ptr = maybe_ptr orelse return; 170 + const self: *Model = @ptrCast(@alignCast(ptr)); 171 + self.count +|= 1; 172 + return ctx.consumeAndRedraw(); 173 + } 174 + }; 175 + 176 + pub fn main() !void { 177 + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; 178 + defer _ = gpa.deinit(); 179 + 180 + const allocator = gpa.allocator(); 181 + 182 + var app = try vxfw.App.init(allocator); 183 + defer app.deinit(); 184 + 185 + // We heap allocate our model because we will require a stable pointer to it in our Button 186 + // widget 187 + const model = try allocator.create(Model); 188 + defer allocator.destroy(model); 189 + 190 + // Set the initial state of our button 191 + model.* = .{ 192 + .count = 0, 193 + .button = .{ 194 + .label = "Click me!", 195 + .onClick = Model.onClick, 196 + .userdata = model, 197 + }, 198 + }; 199 + 200 + try app.run(model.widget(), .{}); 201 + } 202 + ``` 203 + 204 + ### Low level API 41 205 42 206 Vaxis requires three basic primitives to operate: 43 207 ··· 55 219 also handle these query responses and update the Vaxis.caps struct accordingly. 56 220 See the `Loop` implementation to see how this is done if writing your own event 57 221 loop. 58 - 59 - ## Example 60 222 61 223 ```zig 62 224 const std = @import("std");
+1
build.zig
··· 29 29 // Examples 30 30 const Example = enum { 31 31 cli, 32 + counter, 32 33 fuzzy, 33 34 image, 34 35 main,
+140
examples/counter.zig
··· 1 + const std = @import("std"); 2 + const vaxis = @import("vaxis"); 3 + const vxfw = vaxis.vxfw; 4 + 5 + /// Our main application state 6 + const 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 + .focusable = false, 97 + // We didn't actually need to draw anything for the root. In this case, we can set 98 + // buffer to a zero length slice. If this slice is *not zero length*, the runtime will 99 + // assert that it's length is equal to the size.width * size.height. 100 + .buffer = &.{}, 101 + .children = children, 102 + }; 103 + } 104 + 105 + /// The onClick callback for our button. This is also called if we press enter while the button 106 + /// has focus 107 + fn onClick(maybe_ptr: ?*anyopaque, ctx: *vxfw.EventContext) anyerror!void { 108 + const ptr = maybe_ptr orelse return; 109 + const self: *Model = @ptrCast(@alignCast(ptr)); 110 + self.count +|= 1; 111 + return ctx.consumeAndRedraw(); 112 + } 113 + }; 114 + 115 + pub fn main() !void { 116 + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; 117 + defer _ = gpa.deinit(); 118 + 119 + const allocator = gpa.allocator(); 120 + 121 + var app = try vxfw.App.init(allocator); 122 + defer app.deinit(); 123 + 124 + // We heap allocate our model because we will require a stable pointer to it in our Button 125 + // widget 126 + const model = try allocator.create(Model); 127 + defer allocator.destroy(model); 128 + 129 + // Set the initial state of our button 130 + model.* = .{ 131 + .count = 0, 132 + .button = .{ 133 + .label = "Click me!", 134 + .onClick = Model.onClick, 135 + .userdata = model, 136 + }, 137 + }; 138 + 139 + try app.run(model.widget(), .{}); 140 + }