this repo has no description
13
fork

Configure Feed

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

at main 425 lines 14 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 RichText = @This(); 9 10pub const TextSpan = vaxis.Segment; 11 12text: []const TextSpan, 13text_align: enum { left, center, right } = .left, 14base_style: vaxis.Style = .{}, 15softwrap: bool = true, 16overflow: enum { ellipsis, clip } = .ellipsis, 17width_basis: enum { parent, longest_line } = .longest_line, 18 19pub fn widget(self: *const RichText) vxfw.Widget { 20 return .{ 21 .userdata = @constCast(self), 22 .drawFn = typeErasedDrawFn, 23 }; 24} 25 26fn typeErasedDrawFn(ptr: *anyopaque, ctx: vxfw.DrawContext) Allocator.Error!vxfw.Surface { 27 const self: *const RichText = @ptrCast(@alignCast(ptr)); 28 return self.draw(ctx); 29} 30 31pub fn draw(self: *const RichText, ctx: vxfw.DrawContext) Allocator.Error!vxfw.Surface { 32 if (ctx.max.width != null and ctx.max.width.? == 0) { 33 return .{ 34 .size = ctx.min, 35 .widget = self.widget(), 36 .buffer = &.{}, 37 .children = &.{}, 38 }; 39 } 40 var iter = try SoftwrapIterator.init(self.text, ctx); 41 const container_size = self.findContainerSize(&iter); 42 43 // Create a surface of target width and max height. We'll trim the result after drawing 44 const surface = try vxfw.Surface.init( 45 ctx.arena, 46 self.widget(), 47 container_size, 48 ); 49 const base: vaxis.Cell = .{ .style = self.base_style }; 50 @memset(surface.buffer, base); 51 52 var row: u16 = 0; 53 if (self.softwrap) { 54 while (iter.next()) |line| { 55 if (ctx.max.outsideHeight(row)) break; 56 defer row += 1; 57 var col: u16 = switch (self.text_align) { 58 .left => 0, 59 .center => (container_size.width - line.width) / 2, 60 .right => container_size.width - line.width, 61 }; 62 for (line.cells) |cell| { 63 surface.writeCell(col, row, cell); 64 col += cell.char.width; 65 } 66 } 67 } else { 68 while (iter.nextHardBreak()) |line| { 69 if (ctx.max.outsideHeight(row)) break; 70 const line_width = blk: { 71 var w: u16 = 0; 72 for (line) |cell| { 73 w +|= cell.char.width; 74 } 75 break :blk w; 76 }; 77 defer row += 1; 78 var col: u16 = switch (self.text_align) { 79 .left => 0, 80 .center => (container_size.width -| line_width) / 2, 81 .right => container_size.width -| line_width, 82 }; 83 for (line) |cell| { 84 if (col + cell.char.width >= container_size.width and 85 line_width > container_size.width and 86 self.overflow == .ellipsis) 87 { 88 surface.writeCell(col, row, .{ 89 .char = .{ .grapheme = "", .width = 1 }, 90 .style = cell.style, 91 }); 92 col = container_size.width; 93 continue; 94 } else { 95 surface.writeCell(col, row, cell); 96 col += @intCast(cell.char.width); 97 } 98 } 99 } 100 } 101 return surface.trimHeight(@max(row, ctx.min.height)); 102} 103 104/// Finds the widest line within the viewable portion of ctx 105fn findContainerSize(self: RichText, iter: *SoftwrapIterator) vxfw.Size { 106 defer iter.reset(); 107 var row: u16 = 0; 108 var max_width: u16 = iter.ctx.min.width; 109 if (self.softwrap) { 110 while (iter.next()) |line| { 111 if (iter.ctx.max.outsideHeight(row)) break; 112 defer row += 1; 113 max_width = @max(max_width, line.width); 114 } 115 } else { 116 while (iter.nextHardBreak()) |line| { 117 if (iter.ctx.max.outsideHeight(row)) break; 118 defer row += 1; 119 var w: u16 = 0; 120 for (line) |cell| { 121 w +|= cell.char.width; 122 } 123 max_width = @max(max_width, w); 124 } 125 } 126 const result_width = switch (self.width_basis) { 127 .longest_line => blk: { 128 if (iter.ctx.max.width) |max| 129 break :blk @min(max, max_width) 130 else 131 break :blk max_width; 132 }, 133 .parent => blk: { 134 std.debug.assert(iter.ctx.max.width != null); 135 break :blk iter.ctx.max.width.?; 136 }, 137 }; 138 return .{ .width = result_width, .height = @max(row, iter.ctx.min.height) }; 139} 140 141pub const SoftwrapIterator = struct { 142 arena: std.heap.ArenaAllocator, 143 ctx: vxfw.DrawContext, 144 text: []const vaxis.Cell, 145 line: []const vaxis.Cell, 146 index: usize = 0, 147 // Index of the hard iterator 148 hard_index: usize = 0, 149 150 const soft_breaks = " \t"; 151 152 pub const Line = struct { 153 width: u16, 154 cells: []const vaxis.Cell, 155 }; 156 157 fn init(spans: []const TextSpan, ctx: vxfw.DrawContext) Allocator.Error!SoftwrapIterator { 158 // Estimate the number of cells we need 159 var len: usize = 0; 160 for (spans) |span| { 161 len += span.text.len; 162 } 163 var arena = std.heap.ArenaAllocator.init(ctx.arena); 164 const alloc = arena.allocator(); 165 var list: std.ArrayList(vaxis.Cell) = try .initCapacity(alloc, len); 166 167 for (spans) |span| { 168 var iter = ctx.graphemeIterator(span.text); 169 while (iter.next()) |grapheme| { 170 const char = grapheme.bytes(span.text); 171 if (std.mem.eql(u8, char, "\t")) { 172 const cell: vaxis.Cell = .{ 173 .char = .{ .grapheme = " ", .width = 1 }, 174 .style = span.style, 175 .link = span.link, 176 }; 177 for (0..8) |_| { 178 try list.append(alloc, cell); 179 } 180 continue; 181 } 182 const width = ctx.stringWidth(char); 183 const cell: vaxis.Cell = .{ 184 .char = .{ .grapheme = char, .width = @intCast(width) }, 185 .style = span.style, 186 .link = span.link, 187 }; 188 try list.append(alloc, cell); 189 } 190 } 191 return .{ 192 .arena = arena, 193 .ctx = ctx, 194 .text = list.items, 195 .line = &.{}, 196 }; 197 } 198 199 fn reset(self: *SoftwrapIterator) void { 200 self.index = 0; 201 self.hard_index = 0; 202 self.line = &.{}; 203 } 204 205 fn deinit(self: *SoftwrapIterator) void { 206 self.arena.deinit(); 207 } 208 209 fn nextHardBreak(self: *SoftwrapIterator) ?[]const vaxis.Cell { 210 if (self.hard_index >= self.text.len) return null; 211 const start = self.hard_index; 212 var saw_cr: bool = false; 213 while (self.hard_index < self.text.len) : (self.hard_index += 1) { 214 const cell = self.text[self.hard_index]; 215 if (std.mem.eql(u8, cell.char.grapheme, "\r")) { 216 saw_cr = true; 217 } 218 if (std.mem.eql(u8, cell.char.grapheme, "\n")) { 219 self.hard_index += 1; 220 if (saw_cr) { 221 return self.text[start .. self.hard_index - 2]; 222 } 223 return self.text[start .. self.hard_index - 1]; 224 } 225 if (saw_cr) { 226 // back up one 227 self.hard_index -= 1; 228 return self.text[start .. self.hard_index - 1]; 229 } 230 } else return self.text[start..]; 231 } 232 233 fn trimWSPRight(text: []const vaxis.Cell) []const vaxis.Cell { 234 // trim linear whitespace 235 var i: usize = text.len; 236 while (i > 0) : (i -= 1) { 237 if (std.mem.eql(u8, text[i - 1].char.grapheme, " ") or 238 std.mem.eql(u8, text[i - 1].char.grapheme, "\t")) 239 { 240 continue; 241 } 242 break; 243 } 244 return text[0..i]; 245 } 246 247 fn trimWSPLeft(text: []const vaxis.Cell) []const vaxis.Cell { 248 // trim linear whitespace 249 var i: usize = 0; 250 while (i < text.len) : (i += 1) { 251 if (std.mem.eql(u8, text[i].char.grapheme, " ") or 252 std.mem.eql(u8, text[i].char.grapheme, "\t")) 253 { 254 continue; 255 } 256 break; 257 } 258 return text[i..]; 259 } 260 261 fn next(self: *SoftwrapIterator) ?Line { 262 // Advance the hard iterator 263 if (self.index == self.line.len) { 264 self.line = self.nextHardBreak() orelse return null; 265 // trim linear whitespace 266 self.line = trimWSPRight(self.line); 267 self.index = 0; 268 } 269 270 const max_width = self.ctx.max.width orelse { 271 var width: u16 = 0; 272 for (self.line) |cell| { 273 width += cell.char.width; 274 } 275 self.index = self.line.len; 276 return .{ 277 .width = width, 278 .cells = self.line, 279 }; 280 }; 281 282 const start = self.index; 283 var cur_width: u16 = 0; 284 while (self.index < self.line.len) { 285 // Find the width from current position to next word break 286 const idx = self.nextWrap(); 287 const word = self.line[self.index..idx]; 288 const next_width = blk: { 289 var w: usize = 0; 290 for (word) |ch| { 291 w += ch.char.width; 292 } 293 break :blk w; 294 }; 295 296 if (cur_width + next_width > max_width) { 297 // Trim the word to see if it can fit on a line by itself 298 const trimmed = trimWSPLeft(word); 299 // New width is the previous width minus the number of cells we trimmed because we 300 // are only trimming cells that would have been 1 wide (' ' and '\t' both measure as 301 // 1 wide) 302 const trimmed_width = next_width -| (word.len - trimmed.len); 303 if (trimmed_width > max_width) { 304 // Won't fit on line by itself, so fit as much on this line as we can 305 for (word) |cell| { 306 if (cur_width + cell.char.width > max_width) { 307 const end = self.index; 308 return .{ .width = cur_width, .cells = self.line[start..end] }; 309 } 310 cur_width += @intCast(cell.char.width); 311 self.index += 1; 312 } 313 } 314 const end = self.index; 315 // We are softwrapping, advance index to the start of the next word. This is equal 316 // to the difference in our word length and trimmed word length 317 self.index += (word.len - trimmed.len); 318 return .{ .width = cur_width, .cells = self.line[start..end] }; 319 } 320 321 self.index = idx; 322 cur_width += @intCast(next_width); 323 } 324 return .{ .width = cur_width, .cells = self.line[start..] }; 325 } 326 327 fn nextWrap(self: *SoftwrapIterator) usize { 328 var i: usize = self.index; 329 330 // Find the first non-whitespace character 331 while (i < self.line.len) : (i += 1) { 332 if (std.mem.eql(u8, self.line[i].char.grapheme, " ") or 333 std.mem.eql(u8, self.line[i].char.grapheme, "\t")) 334 { 335 continue; 336 } 337 break; 338 } 339 340 // Now find the first whitespace 341 while (i < self.line.len) : (i += 1) { 342 if (std.mem.eql(u8, self.line[i].char.grapheme, " ") or 343 std.mem.eql(u8, self.line[i].char.grapheme, "\t")) 344 { 345 return i; 346 } 347 continue; 348 } 349 350 return self.line.len; 351 } 352}; 353 354test RichText { 355 var rich_text: RichText = .{ 356 .text = &.{ 357 .{ .text = "Hello, " }, 358 .{ .text = "World", .style = .{ .bold = true } }, 359 }, 360 }; 361 362 const rich_widget = rich_text.widget(); 363 364 var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 365 defer arena.deinit(); 366 367 vxfw.DrawContext.init(.unicode); 368 369 // Center expands to the max size. It must therefore have non-null max width and max height. 370 // These values are asserted in draw 371 const ctx: vxfw.DrawContext = .{ 372 .arena = arena.allocator(), 373 .min = .{}, 374 .max = .{ .width = 7, .height = 2 }, 375 .cell_size = .{ .width = 10, .height = 20 }, 376 }; 377 378 { 379 // RichText softwraps by default 380 const surface = try rich_widget.draw(ctx); 381 try std.testing.expectEqual(@as(vxfw.Size, .{ .width = 6, .height = 2 }), surface.size); 382 } 383 384 { 385 rich_text.softwrap = false; 386 rich_text.overflow = .ellipsis; 387 const surface = try rich_widget.draw(ctx); 388 try std.testing.expectEqual(@as(vxfw.Size, .{ .width = 7, .height = 1 }), surface.size); 389 // The last character will be an ellipsis 390 try std.testing.expectEqualStrings("", surface.buffer[surface.buffer.len - 1].char.grapheme); 391 } 392} 393 394test "long word wrapping" { 395 var rich_text: RichText = .{ 396 .text = &.{ 397 .{ .text = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" }, 398 }, 399 }; 400 401 const rich_widget = rich_text.widget(); 402 403 var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 404 defer arena.deinit(); 405 406 vxfw.DrawContext.init(.unicode); 407 408 const len = rich_text.text[0].text.len; 409 const width: u16 = 8; 410 411 const ctx: vxfw.DrawContext = .{ 412 .arena = arena.allocator(), 413 .min = .{}, 414 .max = .{ .width = width, .height = null }, 415 .cell_size = .{ .width = 10, .height = 20 }, 416 }; 417 418 const surface = try rich_widget.draw(ctx); 419 // Height should be length / width 420 try std.testing.expectEqual(len / width, surface.size.height); 421} 422 423test "refAllDecls" { 424 std.testing.refAllDecls(@This()); 425}