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