@@ -5,9 +5,10 @@ const font = @import("../font/main.zig");
55const terminal = @import ("../terminal/main.zig" );
66const renderer = @import ("../renderer.zig" );
77const shaderpkg = renderer .Renderer .API .shaders ;
8- const ArrayListCollection = @import ("../datastruct/array_list_collection.zig" ).ArrayListCollection ;
98const symbols = @import ("../unicode/symbols_table.zig" ).table ;
109
10+ const CellTextRow = std .ArrayListUnmanaged (shaderpkg .CellText );
11+
1112/// The possible cell content keys that exist.
1213pub const Key = enum {
1314 bg ,
@@ -47,11 +48,11 @@ pub const Contents = struct {
4748 ///
4849 /// Prefer accessing with `Contents.bgCell(row, col).*` instead
4950 /// of directly indexing in order to avoid integer size bugs.
50- bg_cells : []shaderpkg.CellBg = undefined ,
51+ bg_cells : []shaderpkg.CellBg = &.{} ,
5152
52- /// The ArrayListCollection which holds all of the foreground cells. When
53- /// sized with Contents.resize the individual ArrayLists are given enough
54- /// room that they can hold a single row with #cols glyphs, underlines, and
53+ /// The lists which hold all of the foreground cells. When sized with
54+ /// Contents.resize the individual ArrayLists are given enough room that
55+ /// they can hold a single row with #cols glyphs, underlines, and
5556 /// strikethroughs; however, appendAssumeCapacity MUST NOT be used since
5657 /// it is possible to exceed this with combining glyphs that add a glyph
5758 /// but take up no column since they combine with the previous one, as
@@ -70,11 +71,12 @@ pub const Contents = struct {
7071 ///
7172 /// Must be initialized by calling resize on the Contents struct before
7273 /// calling any operations.
73- fg_rows : ArrayListCollection ( shaderpkg . CellText ) = .{ . lists = &.{} },
74+ fg_rows : [] CellTextRow = &.{},
7475
7576 pub fn deinit (self : * Contents , alloc : Allocator ) void {
7677 alloc .free (self .bg_cells );
77- self .fg_rows .deinit (alloc );
78+ for (self .fg_rows ) | * row | row .deinit (alloc );
79+ alloc .free (self .fg_rows );
7880 }
7981
8082 /// Resize the cell contents for the given grid size. This will
@@ -84,55 +86,50 @@ pub const Contents = struct {
8486 alloc : Allocator ,
8587 size : renderer.GridSize ,
8688 ) Allocator.Error ! void {
87- self .size = size ;
89+ const row_count : usize = size .rows ;
90+
91+ // The two extra lists hold cursor cells: index 0 is drawn before the
92+ // row contents, and index row_count + 1 is drawn after them.
93+ const fg_rows = try alloc .alloc (CellTextRow , row_count + 2 );
94+ @memset (fg_rows , .empty );
95+ errdefer {
96+ for (fg_rows ) | * row | row .deinit (alloc );
97+ alloc .free (fg_rows );
98+ }
8899
89- const cell_count = @as (usize , size .columns ) * @as (usize , size .rows );
90-
91- const bg_cells = try alloc .alloc (shaderpkg .CellBg , cell_count );
92- errdefer alloc .free (bg_cells );
93- @memset (bg_cells , .{ 0 , 0 , 0 , 0 });
94-
95- // The foreground lists can hold 3 types of items:
96- // - Glyphs
97- // - Underlines
98- // - Strikethroughs
99- // So we give them an initial capacity of size.columns * 3, which will
100- // avoid any further allocations in the vast majority of cases. Sadly
101- // we can not assume capacity though, since with combining glyphs that
102- // form a single grapheme, and multi-substitutions in fonts, the number
103- // of glyphs in a row is theoretically unlimited.
104- //
105- // We have size.rows + 2 lists because indexes 0 and size.rows - 1 are
106- // used for special lists containing the cursor cell which need to
107- // be first and last in the buffer, respectively.
108- var fg_rows : ArrayListCollection (shaderpkg .CellText ) = try .init (
109- alloc ,
110- size .rows + 2 ,
111- size .columns * 3 ,
112- );
113- errdefer fg_rows .deinit (alloc );
100+ // Foreground rows hold glyphs plus underlines, overlines, and
101+ // strikethroughs. Three entries per column cover the common cases
102+ // without reserving space for every decoration combination. We can't
103+ // assume this capacity because combining glyphs and font substitutions
104+ // can produce arbitrarily many glyphs in one column.
105+ const fg_row_capacity = @as (usize , size .columns ) * 3 ;
106+
107+ // The cursor lists need just one cell. The rest get the full capacity.
108+ fg_rows [0 ] = try .initCapacity (alloc , 1 );
109+ fg_rows [row_count + 1 ] = try .initCapacity (alloc , 1 );
110+ for (fg_rows [1 .. row_count + 1 ]) | * row | {
111+ row .* = try .initCapacity (alloc , fg_row_capacity );
112+ }
114113
115- // We don't need 3*cols worth of cells for the cursor lists, so we can
116- // replace them with smaller lists. This is technically a tiny bit of
117- // extra work but resize is not a hot function so it's worth it to not
118- // waste the memory.
119- fg_rows .lists [0 ].deinit (alloc );
120- fg_rows .lists [0 ] = try .initCapacity (alloc , 1 );
121- fg_rows .lists [size .rows + 1 ].deinit (alloc );
122- fg_rows .lists [size .rows + 1 ] = try .initCapacity (alloc , 1 );
114+ const bg_cells = try alloc .realloc (
115+ self .bg_cells ,
116+ row_count * @as (usize , size .columns ),
117+ );
123118
124119 // Perform the swap, no going back from here.
125120 errdefer comptime unreachable ;
126- alloc .free (self .bg_cells );
127- self .fg_rows .deinit (alloc );
121+ for (self .fg_rows ) | * row | row .deinit (alloc );
122+ alloc .free (self .fg_rows );
123+ self .size = size ;
128124 self .bg_cells = bg_cells ;
129125 self .fg_rows = fg_rows ;
126+ self .reset ();
130127 }
131128
132129 /// Reset the cell contents to an empty state without resizing.
133130 pub fn reset (self : * Contents ) void {
134131 @memset (self .bg_cells , .{ 0 , 0 , 0 , 0 });
135- self .fg_rows . reset ();
132+ for ( self .fg_rows ) | * row | row . clearRetainingCapacity ();
136133 }
137134
138135 /// Set the cursor value. If the value is null then the cursor is hidden.
@@ -142,28 +139,28 @@ pub const Contents = struct {
142139 cursor_style : ? renderer.CursorStyle ,
143140 ) void {
144141 if (self .size .rows == 0 ) return ;
145- self .fg_rows . lists [0 ].clearRetainingCapacity ();
146- self .fg_rows . lists [self .size .rows + 1 ].clearRetainingCapacity ();
142+ self .fg_rows [0 ].clearRetainingCapacity ();
143+ self .fg_rows [self .size .rows + 1 ].clearRetainingCapacity ();
147144
148145 const cell = v orelse return ;
149146 const style = cursor_style orelse return ;
150147
151148 switch (style ) {
152149 // Block cursors should be drawn first
153- .block = > self .fg_rows . lists [0 ].appendAssumeCapacity (cell ),
150+ .block = > self .fg_rows [0 ].appendAssumeCapacity (cell ),
154151 // Other cursor styles should be drawn last
155- .block_hollow , .bar , .underline , .lock = > self .fg_rows . lists [self .size .rows + 1 ].appendAssumeCapacity (cell ),
152+ .block_hollow , .bar , .underline , .lock = > self .fg_rows [self .size .rows + 1 ].appendAssumeCapacity (cell ),
156153 }
157154 }
158155
159156 /// Returns the current cursor glyph if present, checking both cursor lists.
160157 pub fn getCursorGlyph (self : * Contents ) ? shaderpkg.CellText {
161158 if (self .size .rows == 0 ) return null ;
162- if (self .fg_rows . lists [0 ].items .len > 0 ) {
163- return self .fg_rows . lists [0 ].items [0 ];
159+ if (self .fg_rows [0 ].items .len > 0 ) {
160+ return self .fg_rows [0 ].items [0 ];
164161 }
165- if (self .fg_rows . lists [self .size .rows + 1 ].items .len > 0 ) {
166- return self .fg_rows . lists [self .size .rows + 1 ].items [0 ];
162+ if (self .fg_rows [self .size .rows + 1 ].items .len > 0 ) {
163+ return self .fg_rows [self .size .rows + 1 ].items [0 ];
167164 }
168165 return null ;
169166 }
@@ -201,7 +198,7 @@ pub const Contents = struct {
201198 // We have a special list containing the cursor cell at the start
202199 // of our fg row collection, so we need to add 1 to the y to get
203200 // the correct index.
204- = > try self .fg_rows . lists [y + 1 ].append (alloc , cell ),
201+ = > try self .fg_rows [y + 1 ].append (alloc , cell ),
205202 }
206203 }
207204
@@ -214,7 +211,7 @@ pub const Contents = struct {
214211 // We have a special list containing the cursor cell at the start
215212 // of our fg row collection, so we need to add 1 to the y to get
216213 // the correct index.
217- self .fg_rows . lists [y + 1 ].clearRetainingCapacity ();
214+ self .fg_rows [y + 1 ].clearRetainingCapacity ();
218215 }
219216};
220217
@@ -362,14 +359,14 @@ test Contents {
362359
363360 // We should start off empty after resizing.
364361 for (0.. rows ) | y | {
365- try testing .expect (c .fg_rows . lists [y + 1 ].items .len == 0 );
362+ try testing .expect (c .fg_rows [y + 1 ].items .len == 0 );
366363 for (0.. cols ) | x | {
367364 try testing .expectEqual (.{ 0 , 0 , 0 , 0 }, c .bgCell (y , x ).* );
368365 }
369366 }
370367 // And the cursor row should have a capacity of 1 and also be empty.
371- try testing .expect (c .fg_rows . lists [0 ].capacity == 1 );
372- try testing .expect (c .fg_rows . lists [0 ].items .len == 0 );
368+ try testing .expect (c .fg_rows [0 ].capacity == 1 );
369+ try testing .expect (c .fg_rows [0 ].items .len == 0 );
373370
374371 // Add some contents.
375372 const bg_cell : shaderpkg.CellBg = .{ 0 , 0 , 0 , 1 };
@@ -382,12 +379,12 @@ test Contents {
382379 try c .add (alloc , .text , fg_cell );
383380 try testing .expectEqual (bg_cell , c .bgCell (1 , 4 ).* );
384381 // The fg row index is offset by 1 because of the cursor list.
385- try testing .expectEqual (fg_cell , c .fg_rows . lists [2 ].items [0 ]);
382+ try testing .expectEqual (fg_cell , c .fg_rows [2 ].items [0 ]);
386383
387384 // And we should be able to clear it.
388385 c .clear (1 );
389386 for (0.. rows ) | y | {
390- try testing .expect (c .fg_rows . lists [y + 1 ].items .len == 0 );
387+ try testing .expect (c .fg_rows [y + 1 ].items .len == 0 );
391388 for (0.. cols ) | x | {
392389 try testing .expectEqual (.{ 0 , 0 , 0 , 0 }, c .bgCell (y , x ).* );
393390 }
@@ -401,20 +398,49 @@ test Contents {
401398 .color = .{ 0 , 0 , 0 , 1 },
402399 };
403400 c .setCursor (cursor_cell , .block );
404- try testing .expectEqual (cursor_cell , c .fg_rows . lists [0 ].items [0 ]);
401+ try testing .expectEqual (cursor_cell , c .fg_rows [0 ].items [0 ]);
405402 try testing .expectEqual (cursor_cell , c .getCursorGlyph ().? );
406403
407404 // And remove it.
408405 c .setCursor (null , null );
409- try testing .expectEqual (0 , c .fg_rows . lists [0 ].items .len );
406+ try testing .expectEqual (0 , c .fg_rows [0 ].items .len );
410407 try testing .expect (c .getCursorGlyph () == null );
411408
412409 // Add a hollow cursor.
413410 c .setCursor (cursor_cell , .block_hollow );
414- try testing .expectEqual (cursor_cell , c .fg_rows . lists [rows + 1 ].items [0 ]);
411+ try testing .expectEqual (cursor_cell , c .fg_rows [rows + 1 ].items [0 ]);
415412 try testing .expectEqual (cursor_cell , c .getCursorGlyph ().? );
416413}
417414
415+ test "Contents resize grows and shrinks" {
416+ const testing = std .testing ;
417+ const alloc = testing .allocator ;
418+
419+ var c : Contents = .{};
420+ defer c .deinit (alloc );
421+
422+ try c .resize (alloc , .{ .rows = 2 , .columns = 2 });
423+ var cell : shaderpkg.CellText = .{
424+ .atlas = .grayscale ,
425+ .grid_pos = .{ 1 , 1 },
426+ .color = .{ 0 , 0 , 0 , 1 },
427+ };
428+ try c .add (alloc , .text , cell );
429+ c .bgCell (1 , 1 ).* = .{ 0 , 0 , 0 , 1 };
430+ c .setCursor (cell , .bar );
431+
432+ try c .resize (alloc , .{ .rows = 3 , .columns = 4 });
433+ try testing .expectEqual (.{ 0 , 0 , 0 , 0 }, c .bgCell (1 , 1 ).* );
434+ try testing .expectEqual (@as (usize , 0 ), c .fg_rows [2 ].items .len );
435+ try testing .expectEqual (@as (? shaderpkg .CellText , null ), c .getCursorGlyph ());
436+ cell .grid_pos = .{ 3 , 2 };
437+ try c .add (alloc , .text , cell );
438+
439+ try c .resize (alloc , .{ .rows = 1 , .columns = 1 });
440+ cell .grid_pos = .{ 0 , 0 };
441+ try c .add (alloc , .text , cell );
442+ }
443+
418444test "Contents clear retains other content" {
419445 const testing = std .testing ;
420446 const alloc = testing .allocator ;
@@ -452,7 +478,7 @@ test "Contents clear retains other content" {
452478 // Row 2 should still contain its cells.
453479 try testing .expectEqual (bg_cell_2 , c .bgCell (2 , 4 ).* );
454480 // Fg row index is +1 because of cursor list at start
455- try testing .expectEqual (fg_cell_2 , c .fg_rows . lists [3 ].items [0 ]);
481+ try testing .expectEqual (fg_cell_2 , c .fg_rows [3 ].items [0 ]);
456482}
457483
458484test "Contents clear last added content" {
@@ -492,7 +518,7 @@ test "Contents clear last added content" {
492518 // Row 1 should still contain its cells.
493519 try testing .expectEqual (bg_cell_1 , c .bgCell (1 , 4 ).* );
494520 // Fg row index is +1 because of cursor list at start
495- try testing .expectEqual (fg_cell_1 , c .fg_rows . lists [2 ].items [0 ]);
521+ try testing .expectEqual (fg_cell_1 , c .fg_rows [2 ].items [0 ]);
496522}
497523
498524test "Contents with zero-sized screen" {
0 commit comments