1 // Copyright 2022 Garrett D'Amore
2 //
3 // Distributed under the Boost Software License, Version 1.0.
4 // (See accompanying file LICENSE or https://www.boost.org/LICENSE_1_0.txt)
5 
6 module dcell.cell;
7 
8 import std.algorithm;
9 import std.traits;
10 import std.utf;
11 
12 public import dcell.coord;
13 public import dcell.style;
14 
15 /** 
16  * Cell represents the contents of a single character cell on screen,
17  * or in some cases two adjacent cells.  Terminals are expected to have a uniform 
18  * display width for each cell, and to have a fixed number of cell columsn and rows.
19  * (We assume fixed pitch fonts.)  The occasion when a double wide character is present
20  * occurs for certain East Asian characters that require twice as much horizontal space
21  * to display as others.  (This can also occur with some emoji.)
22  */
23 struct Cell
24 {
25     string text; /// character content - one character followed by any combinging characters
26     Style style; /// styling for the cell
27     int width; /// display width in cells
28 
29     this(C)(C c, Style st = Style(), int w = 1) if (isSomeChar!C)
30     {
31         text = toUTF8([c]);
32         style = st;
33         width = w;
34     }
35 
36     this(S)(S s, Style st = Style(), int w = 1) if (isSomeString!S)
37     {
38         text = toUTF8(s);
39         style = st;
40         width = w;
41     }
42 }
43 
44 /**
45  * CellBuffer is a logical grid of cells containing content to display on screen.
46  * It uses double buffering which can be used to reduce redrawing content on screen,
47  * which can have a very noticeable impact on performance and responsiveness.
48  *
49  * It behaves something like a two-dimensional array, but offers some conveniences.
50  * Values returned from the indexing are constant, but new values can be assigned.
51  */
52 class CellBuffer
53 {
54     private Coord size_;
55     private Cell[] cells; // current content - linear for performance
56     private Cell[] prev; // previous content - linear for performance
57 
58     private size_t index(Coord pos) nothrow pure const
59     {
60         return index(pos.x, pos.y);
61     }
62 
63     private size_t index(size_t x, size_t y) nothrow pure const
64     {
65         assert(size_.x > 0);
66         return (y * size_.x + x);
67     }
68 
69     bool isLegal(Coord pos) nothrow pure const
70     {
71         return ((pos.x >= 0) && (pos.y >= 0) && (pos.x < size_.x) && (pos.y < size_.y));
72     }
73 
74     this(const size_t cols, const size_t rows)
75     {
76         assert((cols >= 0) && (rows >= 0) && (cols < int.max) && (rows < int.max));
77         cells = new Cell[cols * rows];
78         prev = new Cell[cols * rows];
79         size_.x = cast(int) cols;
80         size_.y = cast(int) rows;
81 
82         foreach (i; 0 .. cells.length)
83         {
84             cells[i].width = 1;
85             cells[i].text = " ";
86         }
87     }
88 
89     this(Coord size)
90     {
91         this(size.x, size.y);
92     }
93 
94     /**
95      * Is a cell dirty?  Dirty means that the cell has some content
96      * or style change that has not been written to the terminal yet.
97      * Writes of identical content to what was last displayed do not
98      * cause a cell to become dirty -- only *different* content does.
99      *
100      * Params:
101      *   pos = coordinates of cell to check
102      */
103     bool dirty(Coord pos) pure const
104     {
105         if (isLegal(pos))
106         {
107             auto ix = index(pos);
108             if (prev[ix].text == "")
109             {
110                 return true;
111             }
112             return cells[ix] != prev[ix];
113         }
114         return false;
115     }
116 
117     /**
118      * Mark a cell as either dirty or clean.
119      *
120      * Params:
121      *   pos = coordinate of sell to update
122      *   b = mark all dirty if true, or clean if false
123      */
124     void setDirty(Coord pos, bool b) pure
125     {
126         if (isLegal(pos))
127         {
128             auto ix = index(pos);
129             if (b)
130             {
131                 prev[ix].text = "";
132             }
133             else
134             {
135                 prev[ix] = cells[ix];
136             }
137         }
138     }
139 
140     /**
141      * Mark all cells as either dirty or clean.
142      *
143      * Params:
144      *   b = mark all dirty if true, or clean if false
145      */
146     void setAllDirty(bool b) pure
147     {
148         // structured this way for efficiency
149         if (b)
150         {
151             foreach (i; 0 .. prev.length)
152             {
153                 prev[i].text = "";
154             }
155         }
156         else
157         {
158             foreach (i; 0 .. prev.length)
159             {
160                 prev[i] = cells[i];
161             }
162         }
163     }
164 
165     ref Cell opIndex(Coord pos)
166     {
167         return this[pos.x, pos.y];
168     }
169 
170     ref Cell opIndex(size_t x, size_t y)
171     {
172         return cells[index(x, y)];
173     }
174 
175     Cell get(Coord pos) nothrow pure
176     {
177         if (isLegal(pos))
178         {
179             return cells[index(pos)];
180         }
181         return Cell();
182     }
183 
184     /**
185      * Set content for the cell.  
186      *
187      * Params:
188      *   c = content to store for the cell.
189      *   pos = coordinate of the cell
190      */
191     void opIndexAssign(Cell c, size_t x, size_t y) pure
192     {
193         if ((x < size_.x) && (y < size_.y))
194         {
195             if (c.text == "" || c.text[0] < ' ')
196             {
197                 c.text = " ";
198             }
199             // TODO: East Asian Width
200             c.width = 1;
201             cells[index(x, y)] = c;
202         }
203     }
204 
205     void opIndexAssign(Cell c, Coord pos) pure
206     {
207         this[pos.x, pos.y] = c;
208     }
209 
210     /**
211      * Set content for the cell, preserving existing styling.
212      *
213      * Params:
214      *   s = text (character) to display.  Note that only a single
215      *       character (including combining marks) is written.
216      *   pos = coordinate to update.
217      */
218     void opIndexAssign(string s, Coord pos) pure
219     {
220         if (s == "" || s[0] < ' ')
221         {
222             s = " ";
223         }
224         // TODO: East Asian Width
225         if (isLegal(pos))
226         {
227             auto ix = index(pos);
228             cells[ix].text = s;
229             cells[ix].width = 1;
230         }
231     }
232 
233     void opIndexAssign(Style style, Coord pos) pure
234     {
235         if (isLegal(pos))
236         {
237             cells[index(pos)].style = style;
238         }
239     }
240 
241     void opIndexAssign(string s, size_t x, size_t y) pure
242     {
243         if (s == "" || s[0] < ' ')
244         {
245             s = " ";
246         }
247 
248         cells[index(x, y)].text = s;
249     }
250 
251     void opIndexAssign(Style v, size_t x, size_t y) pure
252     {
253         cells[index(x, y)].style = v;
254     }
255 
256     int opDollar(size_t dim)()
257     {
258         if (dim == 0)
259         {
260             return size_.x;
261         }
262         else
263         {
264             return size_.y;
265         }
266     }
267 
268     void fill(Cell c) pure
269     {
270         if (c.width < 0 || c.width > 2)
271         {
272             // TODO: East Asian Widths.
273             c.width = 1;
274         }
275         if (c.text == "" || c.text[0] < ' ')
276         {
277             c.text = " ";
278         }
279         foreach (i; 0 .. cells.length)
280         {
281             cells[i] = c;
282         }
283     }
284 
285     /**
286      * Fill the entire contents, but leave any text styles undisturbed.
287      */
288     void fill(string s) pure
289     {
290         foreach (i; 0 .. cells.length)
291         {
292             cells[i].text = s;
293             cells[i].width = 1; // TODO: East Asian Width
294         }
295     }
296 
297     /**
298      * Fill the entires contents, including the given style.
299      */
300     void fill(string s, Style style) pure
301     {
302         Cell c = Cell(s, style);
303         fill(c);
304     }
305 
306     /**
307      * Resize the cell buffer.  Existing contents will be preserved,
308      * provided that they still fit.  Contents that no longer fit will
309      * be clipped (lost).  Newly added cells will be initialized to empty
310      * content.  The entire set of contents are marked dirty, because
311      * presumably everything needs to be redrawn when this happens.
312      */
313     void resize(Coord size)
314     {
315         if (size_ == size)
316         {
317             return;
318         }
319         auto newCells = new Cell[size.x * size.y];
320         foreach (i; 0 .. newCells.length)
321         {
322             // prefill with whitespace
323             newCells[i].text = " ";
324             newCells[i].width = 1;
325         }
326         // maximum dimensions to copy (minimum of dimensions)
327         int lx = min(size.x, size_.x);
328         int ly = min(size.y, size_.y);
329 
330         foreach (y; 0 .. ly)
331         {
332             foreach (x; 0 .. lx)
333             {
334                 newCells[y * size.x + x] = cells[y * size_.x + x];
335             }
336         }
337         size_ = size;
338         cells = newCells;
339         prev = new Cell[size.x * size.y];
340     }
341 
342     void resize(int cols, int rows)
343     {
344         resize(Coord(cols, rows));
345     }
346 
347     Coord size() const pure nothrow
348     {
349         return size_;
350     }
351 
352     unittest
353     {
354         auto cb = new CellBuffer(80, 24);
355         assert(cb.cells.length == 24 * 80);
356         assert(cb.prev.length == 24 * 80);
357         assert(cb.size_ == Coord(80, 24));
358 
359         assert(Cell('A').text == "A");
360 
361         cb[Coord(2, 5)] = "b";
362         assert(cb[2, 5].text == "b");
363         Cell c = cb[2, 5];
364         Style st;
365         assert(c.width == 1);
366         assert(c.text == "b");
367         assert(c.style == st);
368         assert(cb.cells[5 * cb.size_.x + 2].text == "b");
369 
370         st.bg = Color.white;
371         st.fg = Color.blue;
372         st.attr = Attr.reverse;
373         cb[3, 5] = "z";
374         cb[3, 5] = st;
375 
376         c = cb[Coord(3, 5)];
377         assert(c.style.bg == Color.white);
378         assert(c.style.fg == Color.blue);
379         assert(c.style.attr == Attr.reverse);
380 
381         cb[0, 0] = Cell("", st, 0);
382         c = cb[0, 0];
383         assert(c.text == " "); // space replaces null string
384         assert(c.width == 1);
385         assert(c.style == st);
386 
387         cb[1, 0] = Cell("\x1b", st, 0);
388         c = cb[1, 0];
389         assert(c.text == " "); // space replaces control sequence
390         assert(c.width == 1);
391         assert(c.style == st);
392 
393         cb[1, 1] = "\x1b";
394         c = cb[1, 1];
395         assert(c.text == " "); // space replaces control sequence
396         assert(c.width == 1);
397 
398         c.text = "@";
399         cb[2, 0] = c;
400         c = cb[2, 0];
401         assert(c.text == "@");
402         assert(cb.dirty(Coord(2, 0)));
403 
404         st.attr = Attr.reverse;
405         st.bg = Color.none;
406         st.fg = Color.maroon;
407         cb.fill("%", st);
408         assert(cb[1, 1].text == "%");
409         assert(cb[1, 1].style == st);
410         cb.fill("s");
411 
412         cb.setAllDirty(false);
413 
414         cb[1, 1] = "U";
415         cb[1, 1] = st;
416         assert(cb[1, 1].style == st);
417         assert(cb[1, 1].text == "U");
418         assert(cb.dirty(Coord(1, 1)));
419         assert(!cb.dirty(Coord(2, 1)));
420 
421         assert(cb.prev[0] == cb.cells[0]);
422 
423         cb.setDirty(Coord(2, 1), true);
424         assert(cb.dirty(Coord(2, 1)));
425         assert(!cb.dirty(Coord(3, 1)));
426 
427         cb.setDirty(Coord(3, 1), false);
428         assert(!cb.dirty(Coord(3, 1)));
429         cb.setAllDirty(true);
430         assert(cb.dirty(Coord(3, 1)));
431 
432         c.width = -1;
433         c.text = "A";
434         cb.fill(c);
435         assert(cb[0, 0].width == 1);
436         assert(cb[0, 0].text == "A");
437         assert(cb[1, 23].text == "A");
438         assert(cb[79, 23].text == "A");
439         cb.resize(132, 50);
440         assert(cb.size() == Coord(132, 50));
441         assert(cb[79, 23].text == "A");
442         assert(cb[80, 23].text == " ");
443         assert(cb[79, 24].text == " ");
444         cb.resize(132, 50); // this should be a no-op
445         assert(cb.size() == Coord(132, 50));
446         assert(cb[79, 23].text == "A");
447         assert(cb[80, 23].text == " ");
448         assert(cb[79, 24].text == " ");
449 
450         c.text = "";
451         cb.fill(c);
452         assert(cb[79, 23].text == " ");
453 
454         // opDollar
455         assert(cb.size() == Coord(132, 50));
456         cb[0, 0].text = "A";
457         cb[$ - 1, 0].text = "B";
458         cb[0, $ - 1].text = "C";
459         cb[$ - 1, $ - 1].text = "D";
460         assert(cb[0, 0].text == "A");
461         assert(cb[131, 0].text == "B");
462         assert(cb[0, 49].text == "C");
463         assert(cb[131, 49].text == "D");
464     }
465 }