bfe1c7226700df9993d0dd25010b13ebe9c7e54f
[fanfix.git] / LogicalScreen.java
1 /*
2 * Jexer - Java Text User Interface
3 *
4 * The MIT License (MIT)
5 *
6 * Copyright (C) 2017 Kevin Lamonte
7 *
8 * Permission is hereby granted, free of charge, to any person obtaining a
9 * copy of this software and associated documentation files (the "Software"),
10 * to deal in the Software without restriction, including without limitation
11 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
12 * and/or sell copies of the Software, and to permit persons to whom the
13 * Software is furnished to do so, subject to the following conditions:
14 *
15 * The above copyright notice and this permission notice shall be included in
16 * all copies or substantial portions of the Software.
17 *
18 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
21 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
23 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
24 * DEALINGS IN THE SOFTWARE.
25 *
26 * @author Kevin Lamonte [kevin.lamonte@gmail.com]
27 * @version 1
28 */
29 package jexer.backend;
30
31 import jexer.bits.Cell;
32 import jexer.bits.CellAttributes;
33 import jexer.bits.GraphicsChars;
34
35 /**
36 * A logical screen composed of a 2D array of Cells.
37 */
38 public class LogicalScreen implements Screen {
39
40 /**
41 * Width of the visible window.
42 */
43 protected int width;
44
45 /**
46 * Height of the visible window.
47 */
48 protected int height;
49
50 /**
51 * Drawing offset for x.
52 */
53 private int offsetX;
54
55 /**
56 * Set drawing offset for x.
57 *
58 * @param offsetX new drawing offset
59 */
60 public final void setOffsetX(final int offsetX) {
61 this.offsetX = offsetX;
62 }
63
64 /**
65 * Drawing offset for y.
66 */
67 private int offsetY;
68
69 /**
70 * Set drawing offset for y.
71 *
72 * @param offsetY new drawing offset
73 */
74 public final void setOffsetY(final int offsetY) {
75 this.offsetY = offsetY;
76 }
77
78 /**
79 * Ignore anything drawn right of clipRight.
80 */
81 private int clipRight;
82
83 /**
84 * Get right drawing clipping boundary.
85 *
86 * @return drawing boundary
87 */
88 public final int getClipRight() {
89 return clipRight;
90 }
91
92 /**
93 * Set right drawing clipping boundary.
94 *
95 * @param clipRight new boundary
96 */
97 public final void setClipRight(final int clipRight) {
98 this.clipRight = clipRight;
99 }
100
101 /**
102 * Ignore anything drawn below clipBottom.
103 */
104 private int clipBottom;
105
106 /**
107 * Get bottom drawing clipping boundary.
108 *
109 * @return drawing boundary
110 */
111 public final int getClipBottom() {
112 return clipBottom;
113 }
114
115 /**
116 * Set bottom drawing clipping boundary.
117 *
118 * @param clipBottom new boundary
119 */
120 public final void setClipBottom(final int clipBottom) {
121 this.clipBottom = clipBottom;
122 }
123
124 /**
125 * Ignore anything drawn left of clipLeft.
126 */
127 private int clipLeft;
128
129 /**
130 * Get left drawing clipping boundary.
131 *
132 * @return drawing boundary
133 */
134 public final int getClipLeft() {
135 return clipLeft;
136 }
137
138 /**
139 * Set left drawing clipping boundary.
140 *
141 * @param clipLeft new boundary
142 */
143 public final void setClipLeft(final int clipLeft) {
144 this.clipLeft = clipLeft;
145 }
146
147 /**
148 * Ignore anything drawn above clipTop.
149 */
150 private int clipTop;
151
152 /**
153 * Get top drawing clipping boundary.
154 *
155 * @return drawing boundary
156 */
157 public final int getClipTop() {
158 return clipTop;
159 }
160
161 /**
162 * Set top drawing clipping boundary.
163 *
164 * @param clipTop new boundary
165 */
166 public final void setClipTop(final int clipTop) {
167 this.clipTop = clipTop;
168 }
169
170 /**
171 * The physical screen last sent out on flush().
172 */
173 protected Cell [][] physical;
174
175 /**
176 * The logical screen being rendered to.
177 */
178 protected Cell [][] logical;
179
180 /**
181 * When true, logical != physical.
182 */
183 protected volatile boolean dirty;
184
185 /**
186 * Get dirty flag.
187 *
188 * @return if true, the logical screen is not in sync with the physical
189 * screen
190 */
191 public final boolean isDirty() {
192 return dirty;
193 }
194
195 /**
196 * Set if the user explicitly wants to redraw everything starting with a
197 * ECMATerminal.clearAll().
198 */
199 protected boolean reallyCleared;
200
201 /**
202 * If true, the cursor is visible and should be placed onscreen at
203 * (cursorX, cursorY) during a call to flushPhysical().
204 */
205 protected boolean cursorVisible;
206
207 /**
208 * Cursor X position if visible.
209 */
210 protected int cursorX;
211
212 /**
213 * Cursor Y position if visible.
214 */
215 protected int cursorY;
216
217 /**
218 * Get the attributes at one location.
219 *
220 * @param x column coordinate. 0 is the left-most column.
221 * @param y row coordinate. 0 is the top-most row.
222 * @return attributes at (x, y)
223 */
224 public final CellAttributes getAttrXY(final int x, final int y) {
225 CellAttributes attr = new CellAttributes();
226 if ((x >= 0) && (x < width) && (y >= 0) && (y < height)) {
227 attr.setTo(logical[x][y]);
228 }
229 return attr;
230 }
231
232 /**
233 * Get the cell at one location.
234 *
235 * @param x column coordinate. 0 is the left-most column.
236 * @param y row coordinate. 0 is the top-most row.
237 * @return the character + attributes
238 */
239 public Cell getCharXY(final int x, final int y) {
240 Cell cell = new Cell();
241 if ((x >= 0) && (x < width) && (y >= 0) && (y < height)) {
242 cell.setTo(logical[x][y]);
243 }
244 return cell;
245 }
246
247 /**
248 * Set the attributes at one location.
249 *
250 * @param x column coordinate. 0 is the left-most column.
251 * @param y row coordinate. 0 is the top-most row.
252 * @param attr attributes to use (bold, foreColor, backColor)
253 */
254 public final void putAttrXY(final int x, final int y,
255 final CellAttributes attr) {
256
257 putAttrXY(x, y, attr, true);
258 }
259
260 /**
261 * Set the attributes at one location.
262 *
263 * @param x column coordinate. 0 is the left-most column.
264 * @param y row coordinate. 0 is the top-most row.
265 * @param attr attributes to use (bold, foreColor, backColor)
266 * @param clip if true, honor clipping/offset
267 */
268 public final void putAttrXY(final int x, final int y,
269 final CellAttributes attr, final boolean clip) {
270
271 int X = x;
272 int Y = y;
273
274 if (clip) {
275 if ((x < clipLeft)
276 || (x >= clipRight)
277 || (y < clipTop)
278 || (y >= clipBottom)
279 ) {
280 return;
281 }
282 X += offsetX;
283 Y += offsetY;
284 }
285
286 if ((X >= 0) && (X < width) && (Y >= 0) && (Y < height)) {
287 dirty = true;
288 logical[X][Y].setForeColor(attr.getForeColor());
289 logical[X][Y].setBackColor(attr.getBackColor());
290 logical[X][Y].setBold(attr.isBold());
291 logical[X][Y].setBlink(attr.isBlink());
292 logical[X][Y].setReverse(attr.isReverse());
293 logical[X][Y].setUnderline(attr.isUnderline());
294 logical[X][Y].setProtect(attr.isProtect());
295 }
296 }
297
298 /**
299 * Fill the entire screen with one character with attributes.
300 *
301 * @param ch character to draw
302 * @param attr attributes to use (bold, foreColor, backColor)
303 */
304 public final void putAll(final char ch, final CellAttributes attr) {
305
306 for (int x = 0; x < width; x++) {
307 for (int y = 0; y < height; y++) {
308 putCharXY(x, y, ch, attr);
309 }
310 }
311 }
312
313 /**
314 * Render one character with attributes.
315 *
316 * @param x column coordinate. 0 is the left-most column.
317 * @param y row coordinate. 0 is the top-most row.
318 * @param ch character + attributes to draw
319 */
320 public final void putCharXY(final int x, final int y, final Cell ch) {
321 putCharXY(x, y, ch.getChar(), ch);
322 }
323
324 /**
325 * Render one character with attributes.
326 *
327 * @param x column coordinate. 0 is the left-most column.
328 * @param y row coordinate. 0 is the top-most row.
329 * @param ch character to draw
330 * @param attr attributes to use (bold, foreColor, backColor)
331 */
332 public final void putCharXY(final int x, final int y, final char ch,
333 final CellAttributes attr) {
334
335 if ((x < clipLeft)
336 || (x >= clipRight)
337 || (y < clipTop)
338 || (y >= clipBottom)
339 ) {
340 return;
341 }
342
343 int X = x + offsetX;
344 int Y = y + offsetY;
345
346 // System.err.printf("putCharXY: %d, %d, %c\n", X, Y, ch);
347
348 if ((X >= 0) && (X < width) && (Y >= 0) && (Y < height)) {
349 dirty = true;
350
351 // Do not put control characters on the display
352 assert (ch >= 0x20);
353 assert (ch != 0x7F);
354
355 logical[X][Y].setChar(ch);
356 logical[X][Y].setForeColor(attr.getForeColor());
357 logical[X][Y].setBackColor(attr.getBackColor());
358 logical[X][Y].setBold(attr.isBold());
359 logical[X][Y].setBlink(attr.isBlink());
360 logical[X][Y].setReverse(attr.isReverse());
361 logical[X][Y].setUnderline(attr.isUnderline());
362 logical[X][Y].setProtect(attr.isProtect());
363 }
364 }
365
366 /**
367 * Render one character without changing the underlying attributes.
368 *
369 * @param x column coordinate. 0 is the left-most column.
370 * @param y row coordinate. 0 is the top-most row.
371 * @param ch character to draw
372 */
373 public final void putCharXY(final int x, final int y, final char ch) {
374
375 if ((x < clipLeft)
376 || (x >= clipRight)
377 || (y < clipTop)
378 || (y >= clipBottom)
379 ) {
380 return;
381 }
382
383 int X = x + offsetX;
384 int Y = y + offsetY;
385
386 // System.err.printf("putCharXY: %d, %d, %c\n", X, Y, ch);
387
388 if ((X >= 0) && (X < width) && (Y >= 0) && (Y < height)) {
389 dirty = true;
390 logical[X][Y].setChar(ch);
391 }
392 }
393
394 /**
395 * Render a string. Does not wrap if the string exceeds the line.
396 *
397 * @param x column coordinate. 0 is the left-most column.
398 * @param y row coordinate. 0 is the top-most row.
399 * @param str string to draw
400 * @param attr attributes to use (bold, foreColor, backColor)
401 */
402 public final void putStringXY(final int x, final int y, final String str,
403 final CellAttributes attr) {
404
405 int i = x;
406 for (int j = 0; j < str.length(); j++) {
407 char ch = str.charAt(j);
408 putCharXY(i, y, ch, attr);
409 i++;
410 if (i == width) {
411 break;
412 }
413 }
414 }
415
416 /**
417 * Render a string without changing the underlying attribute. Does not
418 * wrap if the string exceeds the line.
419 *
420 * @param x column coordinate. 0 is the left-most column.
421 * @param y row coordinate. 0 is the top-most row.
422 * @param str string to draw
423 */
424 public final void putStringXY(final int x, final int y, final String str) {
425
426 int i = x;
427 for (int j = 0; j < str.length(); j++) {
428 char ch = str.charAt(j);
429 putCharXY(i, y, ch);
430 i++;
431 if (i == width) {
432 break;
433 }
434 }
435 }
436
437 /**
438 * Draw a vertical line from (x, y) to (x, y + n).
439 *
440 * @param x column coordinate. 0 is the left-most column.
441 * @param y row coordinate. 0 is the top-most row.
442 * @param n number of characters to draw
443 * @param ch character to draw
444 * @param attr attributes to use (bold, foreColor, backColor)
445 */
446 public final void vLineXY(final int x, final int y, final int n,
447 final char ch, final CellAttributes attr) {
448
449 for (int i = y; i < y + n; i++) {
450 putCharXY(x, i, ch, attr);
451 }
452 }
453
454 /**
455 * Draw a horizontal line from (x, y) to (x + n, y).
456 *
457 * @param x column coordinate. 0 is the left-most column.
458 * @param y row coordinate. 0 is the top-most row.
459 * @param n number of characters to draw
460 * @param ch character to draw
461 * @param attr attributes to use (bold, foreColor, backColor)
462 */
463 public final void hLineXY(final int x, final int y, final int n,
464 final char ch, final CellAttributes attr) {
465
466 for (int i = x; i < x + n; i++) {
467 putCharXY(i, y, ch, attr);
468 }
469 }
470
471 /**
472 * Reallocate screen buffers.
473 *
474 * @param width new width
475 * @param height new height
476 */
477 private synchronized void reallocate(final int width, final int height) {
478 if (logical != null) {
479 for (int row = 0; row < this.height; row++) {
480 for (int col = 0; col < this.width; col++) {
481 logical[col][row] = null;
482 }
483 }
484 logical = null;
485 }
486 logical = new Cell[width][height];
487 if (physical != null) {
488 for (int row = 0; row < this.height; row++) {
489 for (int col = 0; col < this.width; col++) {
490 physical[col][row] = null;
491 }
492 }
493 physical = null;
494 }
495 physical = new Cell[width][height];
496
497 for (int row = 0; row < height; row++) {
498 for (int col = 0; col < width; col++) {
499 physical[col][row] = new Cell();
500 logical[col][row] = new Cell();
501 }
502 }
503
504 this.width = width;
505 this.height = height;
506
507 clipLeft = 0;
508 clipTop = 0;
509 clipRight = width;
510 clipBottom = height;
511
512 reallyCleared = true;
513 dirty = true;
514 }
515
516 /**
517 * Change the width. Everything on-screen will be destroyed and must be
518 * redrawn.
519 *
520 * @param width new screen width
521 */
522 public final synchronized void setWidth(final int width) {
523 reallocate(width, this.height);
524 }
525
526 /**
527 * Change the height. Everything on-screen will be destroyed and must be
528 * redrawn.
529 *
530 * @param height new screen height
531 */
532 public final synchronized void setHeight(final int height) {
533 reallocate(this.width, height);
534 }
535
536 /**
537 * Change the width and height. Everything on-screen will be destroyed
538 * and must be redrawn.
539 *
540 * @param width new screen width
541 * @param height new screen height
542 */
543 public final void setDimensions(final int width, final int height) {
544 reallocate(width, height);
545 }
546
547 /**
548 * Get the height.
549 *
550 * @return current screen height
551 */
552 public final synchronized int getHeight() {
553 return this.height;
554 }
555
556 /**
557 * Get the width.
558 *
559 * @return current screen width
560 */
561 public final synchronized int getWidth() {
562 return this.width;
563 }
564
565 /**
566 * Public constructor. Sets everything to not-bold, white-on-black.
567 */
568 protected LogicalScreen() {
569 offsetX = 0;
570 offsetY = 0;
571 width = 80;
572 height = 24;
573 logical = null;
574 physical = null;
575 reallocate(width, height);
576 }
577
578 /**
579 * Reset screen to not-bold, white-on-black. Also flushes the offset and
580 * clip variables.
581 */
582 public final synchronized void reset() {
583 dirty = true;
584 for (int row = 0; row < height; row++) {
585 for (int col = 0; col < width; col++) {
586 logical[col][row].reset();
587 }
588 }
589 resetClipping();
590 }
591
592 /**
593 * Flush the offset and clip variables.
594 */
595 public final void resetClipping() {
596 offsetX = 0;
597 offsetY = 0;
598 clipLeft = 0;
599 clipTop = 0;
600 clipRight = width;
601 clipBottom = height;
602 }
603
604 /**
605 * Clear the logical screen.
606 */
607 public final void clear() {
608 reset();
609 }
610
611 /**
612 * Clear the physical screen.
613 */
614 public final void clearPhysical() {
615 dirty = true;
616 for (int row = 0; row < height; row++) {
617 for (int col = 0; col < width; col++) {
618 physical[col][row].reset();
619 }
620 }
621 }
622
623 /**
624 * Draw a box with a border and empty background.
625 *
626 * @param left left column of box. 0 is the left-most row.
627 * @param top top row of the box. 0 is the top-most row.
628 * @param right right column of box
629 * @param bottom bottom row of the box
630 * @param border attributes to use for the border
631 * @param background attributes to use for the background
632 */
633 public final void drawBox(final int left, final int top,
634 final int right, final int bottom,
635 final CellAttributes border, final CellAttributes background) {
636
637 drawBox(left, top, right, bottom, border, background, 1, false);
638 }
639
640 /**
641 * Draw a box with a border and empty background.
642 *
643 * @param left left column of box. 0 is the left-most row.
644 * @param top top row of the box. 0 is the top-most row.
645 * @param right right column of box
646 * @param bottom bottom row of the box
647 * @param border attributes to use for the border
648 * @param background attributes to use for the background
649 * @param borderType if 1, draw a single-line border; if 2, draw a
650 * double-line border; if 3, draw double-line top/bottom edges and
651 * single-line left/right edges (like Qmodem)
652 * @param shadow if true, draw a "shadow" on the box
653 */
654 public final void drawBox(final int left, final int top,
655 final int right, final int bottom,
656 final CellAttributes border, final CellAttributes background,
657 final int borderType, final boolean shadow) {
658
659 int boxWidth = right - left;
660 int boxHeight = bottom - top;
661
662 char cTopLeft;
663 char cTopRight;
664 char cBottomLeft;
665 char cBottomRight;
666 char cHSide;
667 char cVSide;
668
669 switch (borderType) {
670 case 1:
671 cTopLeft = GraphicsChars.ULCORNER;
672 cTopRight = GraphicsChars.URCORNER;
673 cBottomLeft = GraphicsChars.LLCORNER;
674 cBottomRight = GraphicsChars.LRCORNER;
675 cHSide = GraphicsChars.SINGLE_BAR;
676 cVSide = GraphicsChars.WINDOW_SIDE;
677 break;
678
679 case 2:
680 cTopLeft = GraphicsChars.WINDOW_LEFT_TOP_DOUBLE;
681 cTopRight = GraphicsChars.WINDOW_RIGHT_TOP_DOUBLE;
682 cBottomLeft = GraphicsChars.WINDOW_LEFT_BOTTOM_DOUBLE;
683 cBottomRight = GraphicsChars.WINDOW_RIGHT_BOTTOM_DOUBLE;
684 cHSide = GraphicsChars.DOUBLE_BAR;
685 cVSide = GraphicsChars.WINDOW_SIDE_DOUBLE;
686 break;
687
688 case 3:
689 cTopLeft = GraphicsChars.WINDOW_LEFT_TOP;
690 cTopRight = GraphicsChars.WINDOW_RIGHT_TOP;
691 cBottomLeft = GraphicsChars.WINDOW_LEFT_BOTTOM;
692 cBottomRight = GraphicsChars.WINDOW_RIGHT_BOTTOM;
693 cHSide = GraphicsChars.WINDOW_TOP;
694 cVSide = GraphicsChars.WINDOW_SIDE;
695 break;
696 default:
697 throw new IllegalArgumentException("Invalid border type: "
698 + borderType);
699 }
700
701 // Place the corner characters
702 putCharXY(left, top, cTopLeft, border);
703 putCharXY(left + boxWidth - 1, top, cTopRight, border);
704 putCharXY(left, top + boxHeight - 1, cBottomLeft, border);
705 putCharXY(left + boxWidth - 1, top + boxHeight - 1, cBottomRight,
706 border);
707
708 // Draw the box lines
709 hLineXY(left + 1, top, boxWidth - 2, cHSide, border);
710 vLineXY(left, top + 1, boxHeight - 2, cVSide, border);
711 hLineXY(left + 1, top + boxHeight - 1, boxWidth - 2, cHSide, border);
712 vLineXY(left + boxWidth - 1, top + 1, boxHeight - 2, cVSide, border);
713
714 // Fill in the interior background
715 for (int i = 1; i < boxHeight - 1; i++) {
716 hLineXY(1 + left, i + top, boxWidth - 2, ' ', background);
717 }
718
719 if (shadow) {
720 // Draw a shadow
721 drawBoxShadow(left, top, right, bottom);
722 }
723 }
724
725 /**
726 * Draw a box shadow.
727 *
728 * @param left left column of box. 0 is the left-most row.
729 * @param top top row of the box. 0 is the top-most row.
730 * @param right right column of box
731 * @param bottom bottom row of the box
732 */
733 public final void drawBoxShadow(final int left, final int top,
734 final int right, final int bottom) {
735
736 int boxTop = top;
737 int boxLeft = left;
738 int boxWidth = right - left;
739 int boxHeight = bottom - top;
740 CellAttributes shadowAttr = new CellAttributes();
741
742 // Shadows do not honor clipping but they DO honor offset.
743 int oldClipRight = clipRight;
744 int oldClipBottom = clipBottom;
745 /*
746 clipRight = boxWidth + 2;
747 clipBottom = boxHeight + 1;
748 */
749 clipRight = width;
750 clipBottom = height;
751
752 for (int i = 0; i < boxHeight; i++) {
753 putAttrXY(boxLeft + boxWidth, boxTop + 1 + i, shadowAttr);
754 putAttrXY(boxLeft + boxWidth + 1, boxTop + 1 + i, shadowAttr);
755 }
756 for (int i = 0; i < boxWidth; i++) {
757 putAttrXY(boxLeft + 2 + i, boxTop + boxHeight, shadowAttr);
758 }
759 clipRight = oldClipRight;
760 clipBottom = oldClipBottom;
761 }
762
763 /**
764 * Default implementation does nothing.
765 */
766 public void flushPhysical() {}
767
768 /**
769 * Put the cursor at (x,y).
770 *
771 * @param visible if true, the cursor should be visible
772 * @param x column coordinate to put the cursor on
773 * @param y row coordinate to put the cursor on
774 */
775 public void putCursor(final boolean visible, final int x, final int y) {
776
777 cursorVisible = visible;
778 cursorX = x;
779 cursorY = y;
780 }
781
782 /**
783 * Hide the cursor.
784 */
785 public final void hideCursor() {
786 cursorVisible = false;
787 }
788
789 /**
790 * Get the cursor visibility.
791 *
792 * @return true if the cursor is visible
793 */
794 public boolean isCursorVisible() {
795 return cursorVisible;
796 }
797
798 /**
799 * Get the cursor X position.
800 *
801 * @return the cursor x column position
802 */
803 public int getCursorX() {
804 return cursorX;
805 }
806
807 /**
808 * Get the cursor Y position.
809 *
810 * @return the cursor y row position
811 */
812 public int getCursorY() {
813 return cursorY;
814 }
815
816 /**
817 * Set the window title. Default implementation does nothing.
818 *
819 * @param title the new title
820 */
821 public void setTitle(final String title) {}
822
823 }