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 }