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 }