1 module jcli.text.buffer; 2 3 import std, jansi; 4 5 struct TextBufferCell 6 { 7 char[4] ch = [' ', ' ', ' ', ' ']; // Unicode supports a max of 4 bytes to represent a char. 8 ubyte chLen = 1; 9 AnsiStyleSet style; 10 } 11 12 static bool g_jcliTextUseColour = true; 13 14 final class TextBuffer 15 { 16 enum AUTO_GROW = size_t.max; 17 enum ALL = size_t.max-1; 18 alias OnRefreshFunc = void delegate(size_t row, const TextBufferCell[] rowCells); 19 20 private 21 { 22 TextBufferCell[] _cells; 23 OnRefreshFunc _onRefresh; 24 ulong[] _dirtyRowFlags; 25 size_t _width; 26 size_t _height; 27 bool _autoGrowHeight; 28 } 29 30 @safe nothrow pure 31 this(size_t width, size_t height) 32 { 33 assert(width > 0); 34 assert(height > 0); 35 this._width = width; 36 this._height = height; 37 38 if(height == AUTO_GROW) 39 { 40 this._height = 1; 41 this._autoGrowHeight = true; 42 } 43 44 this._cells.length = width * this._height; 45 this._dirtyRowFlags.length = (this._height / 64) + 1; 46 } 47 48 @safe pure 49 void setCell(size_t x, size_t y, const char[] ch, Nullable!AnsiStyleSet style = Nullable!AnsiStyleSet.init) 50 { 51 this.autoGrow(y); 52 enforce(x < this._width, "X is too high."); 53 enforce(y < this._height, "Y is too high."); 54 enforce(ch.length, "No character was given."); 55 this.setRowDirty(y); 56 57 size_t index = 0; 58 decode(ch, index); 59 enforce(index == ch.length, "Too many characters were given, only 1 was expected."); 60 61 scope cell = &this._cells[x+(this._width*y)]; 62 cell.ch = ch[0..index]; 63 cell.chLen = cast(ubyte)index; 64 65 if(!style.isNull) 66 cell.style = style.get; 67 } 68 69 @safe pure 70 void setCellsSingleChar( 71 size_t x, 72 size_t y, 73 size_t width, 74 size_t height, 75 const char[] ch, 76 Nullable!AnsiStyleSet style = Nullable!AnsiStyleSet.init 77 ) 78 { 79 if(width == ALL) width = this._width - x; 80 if(height == ALL) height = this._height - y; 81 this.autoGrow(y + height); 82 enforceSubRect( 83 0, 0, this._width, this._height, 84 x, y, width, height 85 ); 86 enforce(ch.length, "No character was given."); 87 88 size_t index = 0; 89 decode(ch, index); 90 enforce(index == ch.length, "Too many characters were given, only 1 was expected."); 91 92 foreach(i; 0..height) 93 { 94 const rowy = y + i; 95 this.setRowDirty(rowy); 96 97 const rowStart = x + (this._width * rowy); 98 const rowEnd = rowStart + width; 99 auto row = this._cells[rowStart..rowEnd]; 100 101 foreach(ref cell; row) 102 { 103 cell.ch = ch[0..index]; 104 cell.chLen = cast(ubyte)index; 105 if(!style.isNull) 106 cell.style = style.get; 107 } 108 } 109 } 110 111 @safe pure 112 void setCellsString( 113 size_t x, 114 size_t y, 115 size_t width, 116 size_t height, 117 const char[] ch, 118 out size_t stopX, 119 out size_t stopY, 120 Nullable!AnsiStyleSet style = Nullable!AnsiStyleSet.init 121 ) 122 { 123 bool autoGrowHeight = false; 124 125 if(width == ALL) width = this._width - x; 126 if(height == ALL) height = this._height - y; 127 if(height == AUTO_GROW) { height = 1; autoGrowHeight = true; } 128 this.autoGrow(y + height); 129 enforceSubRect( 130 0, 0, this._width, this._height, 131 x, y, width, height 132 ); 133 134 stopX = x; 135 stopY = y; 136 137 size_t cursor; 138 for(auto i = 0; i < height; i++) 139 { 140 const rowy = y + i; 141 this.setRowDirty(rowy); 142 143 const rowStart = x + (this._width * rowy); 144 const rowEnd = rowStart + width; 145 auto row = this._cells[rowStart..rowEnd]; 146 147 foreach(j, ref cell; row) 148 { 149 NextChar: 150 if(cursor < ch.length) 151 { 152 const cursorStart = cursor; 153 auto chCopy = ch; 154 decode(chCopy, cursor); 155 const chSize = cursor - cursorStart; 156 157 if(ch[cursorStart..cursor] == "\n") 158 { 159 cell.ch[0] = ' '; 160 cell.chLen = 1; 161 stopY = rowy + 1; 162 stopX = rowStart + j; 163 break; 164 } 165 166 if(j == 0 && ch[cursorStart..cursor] == " ") 167 goto NextChar; // ewwwwwwwwww 168 169 cell.ch[0..(cursor - cursorStart)] = ch[cursorStart..cursor]; 170 cell.chLen = cast(ubyte)chSize; 171 172 stopY = rowy; 173 stopX = rowStart + j; 174 } 175 176 if(!style.isNull) 177 cell.style = style.get; 178 } 179 180 if(autoGrowHeight && cursor < ch.length) 181 { 182 this.height = (rowy + 2); 183 height++; 184 } 185 } 186 } 187 188 void refresh() 189 { 190 if(!this._onRefresh) 191 return; 192 193 foreach(i; 0..this._height) 194 { 195 if(!this.isRowDirty(i)) 196 continue; 197 198 const rowStart = this._width * i; 199 const rowEnd = this._width * (i + 1); 200 const row = this._cells[rowStart..rowEnd]; 201 this._onRefresh(i, row); 202 } 203 204 this._dirtyRowFlags[] = 0; 205 } 206 207 @property @safe @nogc nothrow pure 208 void onRefresh(OnRefreshFunc func) 209 { 210 this._onRefresh = func; 211 } 212 213 @property @safe @nogc nothrow 214 size_t width() const 215 { 216 return this._width; 217 } 218 219 @property @safe @nogc nothrow 220 size_t height() const 221 { 222 return this.height; 223 } 224 225 @property @safe pure 226 void height(size_t h) 227 { 228 enforce(h > 0, "Height must be greater than 0."); 229 this._cells.length = h * this._width; 230 this._dirtyRowFlags.length = (h / 64) + 1; 231 this._dirtyRowFlags[] = ulong.max; 232 this._height = h; 233 } 234 235 @safe pure 236 private void autoGrow(size_t y) 237 { 238 if(y >= this._height) 239 this.height = y + 1; 240 } 241 242 @safe @nogc nothrow pure 243 private bool isRowDirty(size_t row) 244 { 245 const byte_ = row / 64; 246 const bit = row % 64; 247 const mask = 1UL << bit; 248 return (this._dirtyRowFlags[byte_] & mask) != 0; 249 } 250 251 @safe @nogc nothrow pure 252 private void setRowDirty(size_t row) 253 { 254 const byte_ = row / 64; 255 const bit = row % 64; 256 const mask = 1UL << bit; 257 this._dirtyRowFlags[byte_] |= mask; 258 } 259 } 260 261 @safe pure 262 private void enforceSubRect( 263 size_t px, size_t py, size_t pw, size_t ph, 264 size_t cx, size_t cy, size_t cw, size_t ch 265 ) 266 { 267 enforce(cx >= px, "X is too low."); 268 enforce(cy >= py, "Y is too low."); 269 enforce(cx < pw, "X is too high."); 270 enforce(cy < ph, "Y is too high."); 271 enforce(cx + cw <= pw, "Width is too high."); 272 enforce(cy + ch <= ph, "Height is too high."); 273 enforce(cw > 0, "Width cannot be 0."); 274 enforce(ch > 0, "Height cannot be 0."); 275 }