a modern tui library written in zig
at main 14 kB view raw
1const std = @import("std"); 2const vaxis = @import("vaxis"); 3const vxfw = vaxis.vxfw; 4 5const ModelRow = struct { 6 text: []const u8, 7 idx: usize, 8 wrap_lines: bool = true, 9 10 pub fn widget(self: *ModelRow) vxfw.Widget { 11 return .{ 12 .userdata = self, 13 .drawFn = ModelRow.typeErasedDrawFn, 14 }; 15 } 16 17 fn typeErasedDrawFn(ptr: *anyopaque, ctx: vxfw.DrawContext) std.mem.Allocator.Error!vxfw.Surface { 18 const self: *ModelRow = @ptrCast(@alignCast(ptr)); 19 20 const idx_text = try std.fmt.allocPrint(ctx.arena, "{d: >4}", .{self.idx}); 21 const idx_widget: vxfw.Text = .{ .text = idx_text }; 22 23 const idx_surf: vxfw.SubSurface = .{ 24 .origin = .{ .row = 0, .col = 0 }, 25 .surface = try idx_widget.draw(ctx.withConstraints( 26 // We're only interested in constraining the width, and we know the height will 27 // always be 1 row. 28 .{ .width = 1, .height = 1 }, 29 .{ .width = 4, .height = 1 }, 30 )), 31 }; 32 33 const text_widget: vxfw.Text = .{ .text = self.text, .softwrap = self.wrap_lines }; 34 const text_surf: vxfw.SubSurface = .{ 35 .origin = .{ .row = 0, .col = 6 }, 36 .surface = try text_widget.draw(ctx.withConstraints( 37 ctx.min, 38 // We've shifted the origin over 6 columns so we need to take that into account or 39 // we'll draw outside the window. 40 if (self.wrap_lines) 41 .{ .width = ctx.min.width -| 6, .height = ctx.max.height } 42 else 43 .{ .width = if (ctx.max.width) |w| w - 6 else null, .height = ctx.max.height }, 44 )), 45 }; 46 47 const children = try ctx.arena.alloc(vxfw.SubSurface, 2); 48 children[0] = idx_surf; 49 children[1] = text_surf; 50 51 return .{ 52 .size = .{ 53 .width = 6 + text_surf.surface.size.width, 54 .height = @max(idx_surf.surface.size.height, text_surf.surface.size.height), 55 }, 56 .widget = self.widget(), 57 .buffer = &.{}, 58 .children = children, 59 }; 60 } 61}; 62 63const Model = struct { 64 scroll_bars: vxfw.ScrollBars, 65 rows: std.ArrayList(ModelRow), 66 67 pub fn widget(self: *Model) vxfw.Widget { 68 return .{ 69 .userdata = self, 70 .eventHandler = Model.typeErasedEventHandler, 71 .drawFn = Model.typeErasedDrawFn, 72 }; 73 } 74 75 fn typeErasedEventHandler(ptr: *anyopaque, ctx: *vxfw.EventContext, event: vxfw.Event) anyerror!void { 76 const self: *Model = @ptrCast(@alignCast(ptr)); 77 switch (event) { 78 .key_press => |key| { 79 if (key.matches('c', .{ .ctrl = true })) { 80 ctx.quit = true; 81 return; 82 } 83 if (key.matches('w', .{ .ctrl = true })) { 84 for (self.rows.items) |*row| { 85 row.wrap_lines = !row.wrap_lines; 86 } 87 self.scroll_bars.estimated_content_height = 88 if (self.scroll_bars.estimated_content_height == 800) 89 @intCast(self.rows.items.len) 90 else 91 800; 92 93 return ctx.consumeAndRedraw(); 94 } 95 if (key.matches('e', .{ .ctrl = true })) { 96 if (self.scroll_bars.estimated_content_height == null) 97 self.scroll_bars.estimated_content_height = 800 98 else 99 self.scroll_bars.estimated_content_height = null; 100 101 return ctx.consumeAndRedraw(); 102 } 103 if (key.matches(vaxis.Key.tab, .{})) { 104 self.scroll_bars.scroll_view.draw_cursor = !self.scroll_bars.scroll_view.draw_cursor; 105 return ctx.consumeAndRedraw(); 106 } 107 if (key.matches('v', .{ .ctrl = true })) { 108 self.scroll_bars.draw_vertical_scrollbar = !self.scroll_bars.draw_vertical_scrollbar; 109 return ctx.consumeAndRedraw(); 110 } 111 if (key.matches('h', .{ .ctrl = true })) { 112 self.scroll_bars.draw_horizontal_scrollbar = !self.scroll_bars.draw_horizontal_scrollbar; 113 return ctx.consumeAndRedraw(); 114 } 115 if (key.matches(vaxis.Key.tab, .{ .shift = true })) { 116 self.scroll_bars.draw_vertical_scrollbar = !self.scroll_bars.draw_vertical_scrollbar; 117 self.scroll_bars.draw_horizontal_scrollbar = !self.scroll_bars.draw_horizontal_scrollbar; 118 return ctx.consumeAndRedraw(); 119 } 120 return self.scroll_bars.scroll_view.handleEvent(ctx, event); 121 }, 122 else => {}, 123 } 124 } 125 126 fn typeErasedDrawFn(ptr: *anyopaque, ctx: vxfw.DrawContext) std.mem.Allocator.Error!vxfw.Surface { 127 const self: *Model = @ptrCast(@alignCast(ptr)); 128 const max = ctx.max.size(); 129 130 const scroll_view: vxfw.SubSurface = .{ 131 .origin = .{ .row = 0, .col = 0 }, 132 .surface = try self.scroll_bars.draw(ctx), 133 }; 134 135 const children = try ctx.arena.alloc(vxfw.SubSurface, 1); 136 children[0] = scroll_view; 137 138 return .{ 139 .size = max, 140 .widget = self.widget(), 141 .buffer = &.{}, 142 .children = children, 143 }; 144 } 145 146 fn widgetBuilder(ptr: *const anyopaque, idx: usize, _: usize) ?vxfw.Widget { 147 const self: *const Model = @ptrCast(@alignCast(ptr)); 148 if (idx >= self.rows.items.len) return null; 149 150 return self.rows.items[idx].widget(); 151 } 152}; 153 154pub fn main() !void { 155 var gpa = std.heap.GeneralPurposeAllocator(.{}){}; 156 defer _ = gpa.deinit(); 157 158 const allocator = gpa.allocator(); 159 160 var app = try vxfw.App.init(allocator); 161 errdefer app.deinit(); 162 163 var arena = std.heap.ArenaAllocator.init(allocator); 164 defer arena.deinit(); 165 166 const model = try allocator.create(Model); 167 defer allocator.destroy(model); 168 model.* = .{ 169 .scroll_bars = .{ 170 .scroll_view = .{ 171 .children = .{ 172 .builder = .{ 173 .userdata = model, 174 .buildFn = Model.widgetBuilder, 175 }, 176 }, 177 }, 178 // NOTE: This is not the actual content height, but rather an estimate. In reality 179 // you would want to do some calculations to keep this up to date and as close to 180 // the real value as possible, but this suffices for the sake of the example. Try 181 // playing around with the value to see how it affects the scrollbar. Try removing 182 // it as well to see what that does. 183 .estimated_content_height = 800, 184 }, 185 .rows = std.ArrayList(ModelRow).empty, 186 }; 187 defer model.rows.deinit(allocator); 188 189 var lipsum = std.ArrayList([]const u8).empty; 190 defer lipsum.deinit(allocator); 191 192 try lipsum.append(allocator, " Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc sit amet nunc porta, commodo tellus eu, blandit lectus. Aliquam dignissim rhoncus mi eu ultrices. Suspendisse lectus massa, bibendum sed lorem sit amet, egestas aliquam ante. Mauris venenatis nibh neque. Nulla a mi eget purus porttitor malesuada. Sed ac porta felis. Morbi ultricies urna nisi, et maximus elit convallis a. Morbi ut felis nec orci euismod congue efficitur egestas ex. Quisque eu feugiat magna. Pellentesque porttitor tortor ut iaculis dictum. Nulla erat neque, sollicitudin vitae enim nec, pharetra blandit tortor. Sed orci ante, condimentum vitae sodales in, sodales ut nulla. Suspendisse quam felis, aliquet ut neque a, lacinia sagittis turpis. Vivamus nec dui purus. Proin tempor nisl et porttitor consequat."); 193 try lipsum.append(allocator, " Vivamus elit massa, commodo in laoreet nec, scelerisque ac orci. Donec nec ante sit amet nisi ullamcorper dictum quis non enim. Proin ante libero, consequat sit amet semper a, vulputate non odio. Mauris ut suscipit lacus. Mauris nec dolor id ex mollis tempor at quis ligula. Integer varius commodo ipsum id gravida. Sed ut lobortis est, id egestas nunc. In fringilla ullamcorper porttitor. Donec quis dignissim arcu, vitae sagittis tortor. Sed tempor porttitor arcu, sit amet elementum est ornare id. Morbi rhoncus, ipsum eget tincidunt volutpat, mauris enim vestibulum nibh, mollis iaculis ante enim quis enim. Donec pharetra odio vel ex fringilla, ut laoreet ipsum commodo. Praesent tempus, leo a pellentesque sodales, erat ipsum pretium nulla, id faucibus sem turpis at nibh. Aenean ut dui luctus, vehicula felis vel, aliquam nulla."); 194 try lipsum.append(allocator, " Cras interdum mattis elit non varius. In condimentum velit a tellus sollicitudin interdum. Etiam pulvinar semper ex, eget congue ante tristique ut. Phasellus commodo magna magna, at fermentum tortor porttitor ac. Fusce a efficitur diam, a congue ante. Mauris maximus ultrices leo, non viverra ex hendrerit eu. Donec laoreet turpis nulla, eget imperdiet tortor mollis aliquam. Donec a est eget ante consequat rhoncus."); 195 try lipsum.append(allocator, " Morbi facilisis libero nec viverra imperdiet. Ut dictum faucibus bibendum. Vestibulum ut nisl eu magna sollicitudin elementum vel eu ante. Phasellus euismod ligula massa, vel rutrum elit hendrerit ut. Vivamus id luctus lectus, at ullamcorper leo. Pellentesque in risus finibus, viverra ligula sed, porta nisl. Aliquam pretium accumsan placerat. Etiam a elit posuere, varius erat sed, aliquet quam. Morbi finibus gravida erat, non imperdiet dolor sollicitudin dictum. Aenean eget ullamcorper lacus, et hendrerit lorem. Quisque sed varius mauris."); 196 try lipsum.append(allocator, " Nullam vitae euismod mauris, eu gravida dolor. Nunc vel urna laoreet justo faucibus tempus. Vestibulum tincidunt sagittis metus ac dignissim. Curabitur eleifend dolor consequat malesuada posuere. In hac habitasse platea dictumst. Fusce eget ipsum tincidunt, placerat orci ut, malesuada ante. Vivamus ultrices purus vel orci posuere, sed posuere eros porta. Vestibulum a tellus et tortor scelerisque varius. Pellentesque vel leo sed est semper bibendum. Mauris tellus ante, cursus et nunc vitae, dictum pellentesque ex. In tristique purus felis, non efficitur ante mollis id. Nulla quam nisi, suscipit sit amet mattis vel, placerat sit amet lectus. Vestibulum cursus auctor quam, at convallis felis euismod non. Sed nec magna nisi. Morbi scelerisque accumsan nunc, sed sagittis sem varius sit amet. Maecenas arcu dui, euismod et sem quis, condimentum blandit tellus."); 197 try lipsum.append(allocator, " Nullam auctor lobortis libero non viverra. Mauris a imperdiet eros, a luctus est. Integer pellentesque eros et metus rhoncus egestas. Suspendisse eu risus mauris. Mauris posuere nulla in justo pharetra molestie. Maecenas sagittis at nunc et finibus. Vestibulum quis leo ac mauris malesuada vestibulum vitae eu enim. Ut et maximus elit. Pellentesque lorem felis, tristique vitae posuere vitae, auctor tempus magna. Fusce cursus purus sit amet risus pulvinar, non egestas ligula imperdiet."); 198 try lipsum.append(allocator, " Proin rhoncus tincidunt congue. Curabitur pretium mauris eu erat iaculis semper. Vestibulum augue tortor, vehicula id maximus at, semper eu leo. Vivamus feugiat at purus eu dapibus. Mauris luctus sollicitudin nibh, in placerat est mattis vitae. Morbi ut risus felis. Etiam lobortis mollis diam, id tempor odio sollicitudin a. Morbi congue, lacus ac accumsan consequat, ipsum eros facilisis est, in congue metus ex nec ligula. Vestibulum dolor ligula, interdum nec iaculis vel, interdum a diam. Curabitur mattis, risus at rhoncus gravida, diam est viverra diam, ut mattis augue nulla sed lacus."); 199 try lipsum.append(allocator, " Duis rutrum orci sit amet dui imperdiet porta. In pulvinar imperdiet enim nec tristique. Etiam egestas pulvinar arcu, viverra mollis ipsum. Ut sit amet sapien nibh. Maecenas ut velit egestas, suscipit dolor vel, interdum tellus. Pellentesque faucibus euismod risus, ac vehicula erat sodales a. Aliquam egestas sit amet enim ac posuere. In id venenatis eros, et pharetra neque. Proin facilisis, odio id vehicula elementum, sapien ligula interdum dui, quis vestibulum est quam sit amet nisl. Aliquam in orci et felis aliquet tempus quis id magna. Sed interdum malesuada sem. Proin sagittis est metus, eu vestibulum nunc lacinia in. Vestibulum enim erat, cursus at justo at, porta feugiat quam. Phasellus vestibulum finibus nulla, at egestas augue imperdiet dapibus. Nunc in felis at ante congue interdum ut nec sapien."); 200 try lipsum.append(allocator, " Etiam lacinia ornare mauris, ut lacinia elit sollicitudin non. Morbi cursus dictum enim, et vulputate mi sollicitudin vel. Fusce rutrum augue justo. Phasellus et mauris tincidunt erat lacinia bibendum sed eu orci. Sed nunc lectus, dignissim sit amet ultricies sit amet, efficitur eu urna. Fusce feugiat malesuada ipsum nec congue. Praesent ultrices metus eu pulvinar laoreet. Maecenas pellentesque, metus ac lobortis rhoncus, ligula eros consequat urna, eget dictum lectus sem ut orci. Donec lobortis, lacus sed bibendum auctor, odio turpis suscipit odio, vitae feugiat leo metus ac lectus. Curabitur sed sem arcu."); 201 try lipsum.append(allocator, " Mauris nisi tortor, auctor venenatis turpis a, finibus condimentum lectus. Donec id velit odio. Curabitur ac varius lorem. Nam cursus quam in velit gravida, in bibendum purus fermentum. Sed non rutrum dui, nec ultrices ligula. Integer lacinia blandit nisl non sollicitudin. Praesent nec malesuada eros, sit amet tincidunt nunc."); 202 203 // Try playing around with the amount of items in the scroll view to see how the scrollbar 204 // reacts. 205 for (0..10) |i| { 206 for (lipsum.items, 0..) |paragraph, j| { 207 const number = i * 10 + j; 208 try model.rows.append(allocator, .{ .idx = number, .text = paragraph }); 209 } 210 } 211 212 try app.run(model.widget(), .{}); 213 app.deinit(); 214}