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 }