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 }