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