this repo has no description
13
fork

Configure Feed

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

at 3943a6f42f7f4c73ab9493dfb0a4e123be15d302 508 lines 18 kB view raw
1const std = @import("std"); 2const vaxis = @import("../main.zig"); 3 4const Allocator = std.mem.Allocator; 5 6const vxfw = @import("vxfw.zig"); 7 8const Text = @This(); 9 10text: []const u8, 11style: vaxis.Style = .{}, 12text_align: enum { left, center, right } = .left, 13softwrap: bool = true, 14overflow: enum { ellipsis, clip } = .ellipsis, 15width_basis: enum { parent, longest_line } = .longest_line, 16 17pub fn widget(self: *const Text) vxfw.Widget { 18 return .{ 19 .userdata = @constCast(self), 20 .drawFn = typeErasedDrawFn, 21 }; 22} 23 24fn typeErasedDrawFn(ptr: *anyopaque, ctx: vxfw.DrawContext) Allocator.Error!vxfw.Surface { 25 const self: *const Text = @ptrCast(@alignCast(ptr)); 26 return self.draw(ctx); 27} 28 29pub fn draw(self: *const Text, ctx: vxfw.DrawContext) Allocator.Error!vxfw.Surface { 30 if (ctx.max.width != null and ctx.max.width.? == 0) { 31 return .{ 32 .size = ctx.min, 33 .widget = self.widget(), 34 .buffer = &.{}, 35 .children = &.{}, 36 }; 37 } 38 const container_size = self.findContainerSize(ctx); 39 40 // Create a surface of target width and max height. We'll trim the result after drawing 41 const surface = try vxfw.Surface.init( 42 ctx.arena, 43 self.widget(), 44 container_size, 45 ); 46 const base_style: vaxis.Style = .{ 47 .fg = self.style.fg, 48 .bg = self.style.bg, 49 .reverse = self.style.reverse, 50 }; 51 const base: vaxis.Cell = .{ .style = base_style }; 52 @memset(surface.buffer, base); 53 54 var row: u16 = 0; 55 if (self.softwrap) { 56 var iter = SoftwrapIterator.init(self.text, ctx); 57 while (iter.next()) |line| { 58 if (row >= container_size.height) break; 59 defer row += 1; 60 var col: u16 = switch (self.text_align) { 61 .left => 0, 62 .center => (container_size.width - line.width) / 2, 63 .right => container_size.width - line.width, 64 }; 65 var char_iter = ctx.graphemeIterator(line.bytes); 66 while (char_iter.next()) |char| { 67 const grapheme = char.bytes(line.bytes); 68 if (std.mem.eql(u8, grapheme, "\t")) { 69 for (0..8) |i| { 70 surface.writeCell(@intCast(col + i), row, .{ 71 .char = .{ .grapheme = " ", .width = 1 }, 72 .style = self.style, 73 }); 74 } 75 col += 8; 76 continue; 77 } 78 const grapheme_width: u8 = @intCast(ctx.stringWidth(grapheme)); 79 surface.writeCell(col, row, .{ 80 .char = .{ .grapheme = grapheme, .width = grapheme_width }, 81 .style = self.style, 82 }); 83 col += grapheme_width; 84 } 85 } 86 } else { 87 var line_iter: LineIterator = .{ .buf = self.text }; 88 while (line_iter.next()) |line| { 89 if (row >= container_size.height) break; 90 // \t is default 1 wide. We add 7x the count of tab characters to get the full width 91 const line_width = ctx.stringWidth(line) + 7 * std.mem.count(u8, line, "\t"); 92 defer row += 1; 93 const resolved_line_width = @min(container_size.width, line_width); 94 var col: u16 = switch (self.text_align) { 95 .left => 0, 96 .center => (container_size.width - resolved_line_width) / 2, 97 .right => container_size.width - resolved_line_width, 98 }; 99 var char_iter = ctx.graphemeIterator(line); 100 while (char_iter.next()) |char| { 101 if (col >= container_size.width) break; 102 const grapheme = char.bytes(line); 103 const grapheme_width: u8 = @intCast(ctx.stringWidth(grapheme)); 104 105 if (col + grapheme_width >= container_size.width and 106 line_width > container_size.width and 107 self.overflow == .ellipsis) 108 { 109 surface.writeCell(col, row, .{ 110 .char = .{ .grapheme = "", .width = 1 }, 111 .style = self.style, 112 }); 113 col = container_size.width; 114 } else { 115 surface.writeCell(col, row, .{ 116 .char = .{ .grapheme = grapheme, .width = grapheme_width }, 117 .style = self.style, 118 }); 119 col += @intCast(grapheme_width); 120 } 121 } 122 } 123 } 124 return surface.trimHeight(@max(row, ctx.min.height)); 125} 126 127/// Determines the container size by finding the widest line in the viewable area 128fn findContainerSize(self: Text, ctx: vxfw.DrawContext) vxfw.Size { 129 var row: u16 = 0; 130 var max_width: u16 = ctx.min.width; 131 if (self.softwrap) { 132 var iter = SoftwrapIterator.init(self.text, ctx); 133 while (iter.next()) |line| { 134 if (ctx.max.outsideHeight(row)) 135 break; 136 137 defer row += 1; 138 max_width = @max(max_width, line.width); 139 } 140 } else { 141 var line_iter: LineIterator = .{ .buf = self.text }; 142 while (line_iter.next()) |line| { 143 if (ctx.max.outsideHeight(row)) 144 break; 145 const line_width: u16 = @truncate(ctx.stringWidth(line)); 146 defer row += 1; 147 const resolved_line_width = if (ctx.max.width) |max| 148 @min(max, line_width) 149 else 150 line_width; 151 max_width = @max(max_width, resolved_line_width); 152 } 153 } 154 const result_width = switch (self.width_basis) { 155 .longest_line => blk: { 156 if (ctx.max.width) |max| 157 break :blk @min(max, max_width) 158 else 159 break :blk max_width; 160 }, 161 .parent => blk: { 162 std.debug.assert(ctx.max.width != null); 163 break :blk ctx.max.width.?; 164 }, 165 }; 166 return .{ .width = result_width, .height = @max(row, ctx.min.height) }; 167} 168 169/// Iterates a slice of bytes by linebreaks. Lines are split by '\r', '\n', or '\r\n' 170pub const LineIterator = struct { 171 buf: []const u8, 172 index: usize = 0, 173 174 fn next(self: *LineIterator) ?[]const u8 { 175 if (self.index >= self.buf.len) return null; 176 177 const start = self.index; 178 const end = std.mem.indexOfAnyPos(u8, self.buf, self.index, "\r\n") orelse { 179 self.index = self.buf.len; 180 return self.buf[start..]; 181 }; 182 183 self.index = end; 184 self.consumeCR(); 185 self.consumeLF(); 186 return self.buf[start..end]; 187 } 188 189 // consumes a \n byte 190 fn consumeLF(self: *LineIterator) void { 191 if (self.index >= self.buf.len) return; 192 if (self.buf[self.index] == '\n') self.index += 1; 193 } 194 195 // consumes a \r byte 196 fn consumeCR(self: *LineIterator) void { 197 if (self.index >= self.buf.len) return; 198 if (self.buf[self.index] == '\r') self.index += 1; 199 } 200}; 201 202pub const SoftwrapIterator = struct { 203 ctx: vxfw.DrawContext, 204 line: []const u8 = "", 205 index: usize = 0, 206 hard_iter: LineIterator, 207 208 pub const Line = struct { 209 width: u16, 210 bytes: []const u8, 211 }; 212 213 const soft_breaks = " \t"; 214 215 fn init(buf: []const u8, ctx: vxfw.DrawContext) SoftwrapIterator { 216 return .{ 217 .ctx = ctx, 218 .hard_iter = .{ .buf = buf }, 219 }; 220 } 221 222 fn next(self: *SoftwrapIterator) ?Line { 223 // Advance the hard iterator 224 if (self.index == self.line.len) { 225 self.line = self.hard_iter.next() orelse return null; 226 self.line = std.mem.trimRight(u8, self.line, " \t"); 227 self.index = 0; 228 } 229 230 const start = self.index; 231 var cur_width: u16 = 0; 232 while (self.index < self.line.len) { 233 const idx = self.nextWrap(); 234 const word = self.line[self.index..idx]; 235 const next_width = self.ctx.stringWidth(word); 236 237 if (self.ctx.max.width) |max| { 238 if (cur_width + next_width > max) { 239 // Trim the word to see if it can fit on a line by itself 240 const trimmed = std.mem.trimLeft(u8, word, " \t"); 241 const trimmed_bytes = word.len - trimmed.len; 242 // The number of bytes we trimmed is equal to the reduction in length 243 const trimmed_width = next_width - trimmed_bytes; 244 if (trimmed_width > max) { 245 // Won't fit on line by itself, so fit as much on this line as we can 246 var iter = self.ctx.graphemeIterator(word); 247 while (iter.next()) |item| { 248 const grapheme = item.bytes(word); 249 const w = self.ctx.stringWidth(grapheme); 250 if (cur_width + w > max) { 251 const end = self.index; 252 return .{ .width = cur_width, .bytes = self.line[start..end] }; 253 } 254 cur_width += @intCast(w); 255 self.index += grapheme.len; 256 } 257 } 258 // We are softwrapping, advance index to the start of the next word 259 const end = self.index; 260 self.index = std.mem.indexOfNonePos(u8, self.line, self.index, soft_breaks) orelse self.line.len; 261 return .{ .width = cur_width, .bytes = self.line[start..end] }; 262 } 263 } 264 265 self.index = idx; 266 cur_width += @intCast(next_width); 267 } 268 return .{ .width = cur_width, .bytes = self.line[start..] }; 269 } 270 271 /// Determines the index of the end of the next word 272 fn nextWrap(self: *SoftwrapIterator) usize { 273 // Find the first linear whitespace char 274 const start_pos = std.mem.indexOfNonePos(u8, self.line, self.index, soft_breaks) orelse 275 return self.line.len; 276 if (std.mem.indexOfAnyPos(u8, self.line, start_pos, soft_breaks)) |idx| { 277 return idx; 278 } 279 return self.line.len; 280 } 281 282 // consumes a \n byte 283 fn consumeLF(self: *SoftwrapIterator) void { 284 if (self.index >= self.buf.len) return; 285 if (self.buf[self.index] == '\n') self.index += 1; 286 } 287 288 // consumes a \r byte 289 fn consumeCR(self: *SoftwrapIterator) void { 290 if (self.index >= self.buf.len) return; 291 if (self.buf[self.index] == '\r') self.index += 1; 292 } 293}; 294 295test "SoftwrapIterator: LF breaks" { 296 vxfw.DrawContext.init(.unicode); 297 var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 298 defer arena.deinit(); 299 300 const ctx: vxfw.DrawContext = .{ 301 .min = .{ .width = 0, .height = 0 }, 302 .max = .{ .width = 20, .height = 10 }, 303 .arena = arena.allocator(), 304 .cell_size = .{ .width = 10, .height = 20 }, 305 }; 306 var iter = SoftwrapIterator.init("Hello, \n world", ctx); 307 const first = iter.next(); 308 try std.testing.expect(first != null); 309 try std.testing.expectEqualStrings("Hello,", first.?.bytes); 310 try std.testing.expectEqual(6, first.?.width); 311 312 const second = iter.next(); 313 try std.testing.expect(second != null); 314 try std.testing.expectEqualStrings(" world", second.?.bytes); 315 try std.testing.expectEqual(6, second.?.width); 316 317 const end = iter.next(); 318 try std.testing.expect(end == null); 319} 320 321test "SoftwrapIterator: soft breaks that fit" { 322 vxfw.DrawContext.init(.unicode); 323 var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 324 defer arena.deinit(); 325 326 const ctx: vxfw.DrawContext = .{ 327 .min = .{ .width = 0, .height = 0 }, 328 .max = .{ .width = 6, .height = 10 }, 329 .arena = arena.allocator(), 330 .cell_size = .{ .width = 10, .height = 20 }, 331 }; 332 var iter = SoftwrapIterator.init("Hello, \nworld", ctx); 333 const first = iter.next(); 334 try std.testing.expect(first != null); 335 try std.testing.expectEqualStrings("Hello,", first.?.bytes); 336 try std.testing.expectEqual(6, first.?.width); 337 338 const second = iter.next(); 339 try std.testing.expect(second != null); 340 try std.testing.expectEqualStrings("world", second.?.bytes); 341 try std.testing.expectEqual(5, second.?.width); 342 343 const end = iter.next(); 344 try std.testing.expect(end == null); 345} 346 347test "SoftwrapIterator: soft breaks that are longer than width" { 348 vxfw.DrawContext.init(.unicode); 349 var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 350 defer arena.deinit(); 351 352 const ctx: vxfw.DrawContext = .{ 353 .min = .{ .width = 0, .height = 0 }, 354 .max = .{ .width = 6, .height = 10 }, 355 .arena = arena.allocator(), 356 .cell_size = .{ .width = 10, .height = 20 }, 357 }; 358 var iter = SoftwrapIterator.init("very-long-word \nworld", ctx); 359 const first = iter.next(); 360 try std.testing.expect(first != null); 361 try std.testing.expectEqualStrings("very-l", first.?.bytes); 362 try std.testing.expectEqual(6, first.?.width); 363 364 const second = iter.next(); 365 try std.testing.expect(second != null); 366 try std.testing.expectEqualStrings("ong-wo", second.?.bytes); 367 try std.testing.expectEqual(6, second.?.width); 368 369 const third = iter.next(); 370 try std.testing.expect(third != null); 371 try std.testing.expectEqualStrings("rd", third.?.bytes); 372 try std.testing.expectEqual(2, third.?.width); 373 374 const fourth = iter.next(); 375 try std.testing.expect(fourth != null); 376 try std.testing.expectEqualStrings("world", fourth.?.bytes); 377 try std.testing.expectEqual(5, fourth.?.width); 378 379 const end = iter.next(); 380 try std.testing.expect(end == null); 381} 382 383test "SoftwrapIterator: soft breaks with leading spaces" { 384 vxfw.DrawContext.init(.unicode); 385 var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 386 defer arena.deinit(); 387 388 const ctx: vxfw.DrawContext = .{ 389 .min = .{ .width = 0, .height = 0 }, 390 .max = .{ .width = 6, .height = 10 }, 391 .arena = arena.allocator(), 392 .cell_size = .{ .width = 10, .height = 20 }, 393 }; 394 var iter = SoftwrapIterator.init("Hello, \n world", ctx); 395 const first = iter.next(); 396 try std.testing.expect(first != null); 397 try std.testing.expectEqualStrings("Hello,", first.?.bytes); 398 try std.testing.expectEqual(6, first.?.width); 399 400 const second = iter.next(); 401 try std.testing.expect(second != null); 402 try std.testing.expectEqualStrings(" world", second.?.bytes); 403 try std.testing.expectEqual(6, second.?.width); 404 405 const end = iter.next(); 406 try std.testing.expect(end == null); 407} 408 409test "LineIterator: LF breaks" { 410 const input = "Hello, \n world"; 411 var iter: LineIterator = .{ .buf = input }; 412 const first = iter.next(); 413 try std.testing.expect(first != null); 414 try std.testing.expectEqualStrings("Hello, ", first.?); 415 416 const second = iter.next(); 417 try std.testing.expect(second != null); 418 try std.testing.expectEqualStrings(" world", second.?); 419 420 const end = iter.next(); 421 try std.testing.expect(end == null); 422} 423 424test "LineIterator: CR breaks" { 425 const input = "Hello, \r world"; 426 var iter: LineIterator = .{ .buf = input }; 427 const first = iter.next(); 428 try std.testing.expect(first != null); 429 try std.testing.expectEqualStrings("Hello, ", first.?); 430 431 const second = iter.next(); 432 try std.testing.expect(second != null); 433 try std.testing.expectEqualStrings(" world", second.?); 434 435 const end = iter.next(); 436 try std.testing.expect(end == null); 437} 438 439test "LineIterator: CRLF breaks" { 440 const input = "Hello, \r\n world"; 441 var iter: LineIterator = .{ .buf = input }; 442 const first = iter.next(); 443 try std.testing.expect(first != null); 444 try std.testing.expectEqualStrings("Hello, ", first.?); 445 446 const second = iter.next(); 447 try std.testing.expect(second != null); 448 try std.testing.expectEqualStrings(" world", second.?); 449 450 const end = iter.next(); 451 try std.testing.expect(end == null); 452} 453 454test "LineIterator: CRLF breaks with empty line" { 455 const input = "Hello, \r\n\r\n world"; 456 var iter: LineIterator = .{ .buf = input }; 457 const first = iter.next(); 458 try std.testing.expect(first != null); 459 try std.testing.expectEqualStrings("Hello, ", first.?); 460 461 const second = iter.next(); 462 try std.testing.expect(second != null); 463 try std.testing.expectEqualStrings("", second.?); 464 465 const third = iter.next(); 466 try std.testing.expect(third != null); 467 try std.testing.expectEqualStrings(" world", third.?); 468 469 const end = iter.next(); 470 try std.testing.expect(end == null); 471} 472 473test Text { 474 var text: Text = .{ .text = "Hello, world" }; 475 const text_widget = text.widget(); 476 477 var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 478 defer arena.deinit(); 479 vxfw.DrawContext.init(.unicode); 480 481 // Center expands to the max size. It must therefore have non-null max width and max height. 482 // These values are asserted in draw 483 const ctx: vxfw.DrawContext = .{ 484 .arena = arena.allocator(), 485 .min = .{}, 486 .max = .{ .width = 7, .height = 2 }, 487 .cell_size = .{ .width = 10, .height = 20 }, 488 }; 489 490 { 491 // Text softwraps by default 492 const surface = try text_widget.draw(ctx); 493 try std.testing.expectEqual(@as(vxfw.Size, .{ .width = 6, .height = 2 }), surface.size); 494 } 495 496 { 497 text.softwrap = false; 498 text.overflow = .ellipsis; 499 const surface = try text_widget.draw(ctx); 500 try std.testing.expectEqual(@as(vxfw.Size, .{ .width = 7, .height = 1 }), surface.size); 501 // The last character will be an ellipsis 502 try std.testing.expectEqualStrings("", surface.buffer[surface.buffer.len - 1].char.grapheme); 503 } 504} 505 506test "refAllDecls" { 507 std.testing.refAllDecls(@This()); 508}