#18 move to event-driven main loop
[nikiroo-utils.git] / src / jexer / backend / 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 * Get dirty flag.
182 *
183 * @return if true, the logical screen is not in sync with the physical
184 * screen
185 */
186 public final boolean isDirty() {
187 for (int x = 0; x < width; x++) {
188 for (int y = 0; y < height; y++) {
189 if (!logical[x][y].equals(physical[x][y])) {
190 return true;
191 }
192 if (logical[x][y].isBlink()) {
193 // Blinking screens are always dirty. There is
194 // opportunity for a Netscape blink tag joke here...
195 return true;
196 }
197 }
198 }
199
200 return false;
201 }
202
203 /**
204 * Set if the user explicitly wants to redraw everything starting with a
205 * ECMATerminal.clearAll().
206 */
207 protected boolean reallyCleared;
208
209 /**
210 * If true, the cursor is visible and should be placed onscreen at
211 * (cursorX, cursorY) during a call to flushPhysical().
212 */
213 protected boolean cursorVisible;
214
215 /**
216 * Cursor X position if visible.
217 */
218 protected int cursorX;
219
220 /**
221 * Cursor Y position if visible.
222 */
223 protected int cursorY;
224
225 /**
226 * Get the attributes at one location.
227 *
228 * @param x column coordinate. 0 is the left-most column.
229 * @param y row coordinate. 0 is the top-most row.
230 * @return attributes at (x, y)
231 */
232 public final CellAttributes getAttrXY(final int x, final int y) {
233 CellAttributes attr = new CellAttributes();
234 if ((x >= 0) && (x < width) && (y >= 0) && (y < height)) {
235 attr.setTo(logical[x][y]);
236 }
237 return attr;
238 }
239
240 /**
241 * Get the cell at one location.
242 *
243 * @param x column coordinate. 0 is the left-most column.
244 * @param y row coordinate. 0 is the top-most row.
245 * @return the character + attributes
246 */
247 public Cell getCharXY(final int x, final int y) {
248 Cell cell = new Cell();
249 if ((x >= 0) && (x < width) && (y >= 0) && (y < height)) {
250 cell.setTo(logical[x][y]);
251 }
252 return cell;
253 }
254
255 /**
256 * Set the attributes at one location.
257 *
258 * @param x column coordinate. 0 is the left-most column.
259 * @param y row coordinate. 0 is the top-most row.
260 * @param attr attributes to use (bold, foreColor, backColor)
261 */
262 public final void putAttrXY(final int x, final int y,
263 final CellAttributes attr) {
264
265 putAttrXY(x, y, attr, true);
266 }
267
268 /**
269 * Set the attributes at one location.
270 *
271 * @param x column coordinate. 0 is the left-most column.
272 * @param y row coordinate. 0 is the top-most row.
273 * @param attr attributes to use (bold, foreColor, backColor)
274 * @param clip if true, honor clipping/offset
275 */
276 public final void putAttrXY(final int x, final int y,
277 final CellAttributes attr, final boolean clip) {
278
279 int X = x;
280 int Y = y;
281
282 if (clip) {
283 if ((x < clipLeft)
284 || (x >= clipRight)
285 || (y < clipTop)
286 || (y >= clipBottom)
287 ) {
288 return;
289 }
290 X += offsetX;
291 Y += offsetY;
292 }
293
294 if ((X >= 0) && (X < width) && (Y >= 0) && (Y < height)) {
295 logical[X][Y].setTo(attr);
296 }
297 }
298
299 /**
300 * Fill the entire screen with one character with attributes.
301 *
302 * @param ch character to draw
303 * @param attr attributes to use (bold, foreColor, backColor)
304 */
305 public final void putAll(final char ch, final CellAttributes attr) {
306
307 for (int x = 0; x < width; x++) {
308 for (int y = 0; y < height; y++) {
309 putCharXY(x, y, ch, attr);
310 }
311 }
312 }
313
314 /**
315 * Render one character with attributes.
316 *
317 * @param x column coordinate. 0 is the left-most column.
318 * @param y row coordinate. 0 is the top-most row.
319 * @param ch character + attributes to draw
320 */
321 public final void putCharXY(final int x, final int y, final Cell ch) {
322 putCharXY(x, y, ch.getChar(), ch);
323 }
324
325 /**
326 * Render one character with attributes.
327 *
328 * @param x column coordinate. 0 is the left-most column.
329 * @param y row coordinate. 0 is the top-most row.
330 * @param ch character to draw
331 * @param attr attributes to use (bold, foreColor, backColor)
332 */
333 public final void putCharXY(final int x, final int y, final char ch,
334 final CellAttributes attr) {
335
336 if ((x < clipLeft)
337 || (x >= clipRight)
338 || (y < clipTop)
339 || (y >= clipBottom)
340 ) {
341 return;
342 }
343
344 int X = x + offsetX;
345 int Y = y + offsetY;
346
347 // System.err.printf("putCharXY: %d, %d, %c\n", X, Y, ch);
348
349 if ((X >= 0) && (X < width) && (Y >= 0) && (Y < height)) {
350
351 // Do not put control characters on the display
352 assert (ch >= 0x20);
353 assert (ch != 0x7F);
354
355 logical[X][Y].setTo(attr);
356 logical[X][Y].setChar(ch);
357 }
358 }
359
360 /**
361 * Render one character without changing the underlying attributes.
362 *
363 * @param x column coordinate. 0 is the left-most column.
364 * @param y row coordinate. 0 is the top-most row.
365 * @param ch character to draw
366 */
367 public final void putCharXY(final int x, final int y, final char ch) {
368
369 if ((x < clipLeft)
370 || (x >= clipRight)
371 || (y < clipTop)
372 || (y >= clipBottom)
373 ) {
374 return;
375 }
376
377 int X = x + offsetX;
378 int Y = y + offsetY;
379
380 // System.err.printf("putCharXY: %d, %d, %c\n", X, Y, ch);
381
382 if ((X >= 0) && (X < width) && (Y >= 0) && (Y < height)) {
383 logical[X][Y].setChar(ch);
384 }
385 }
386
387 /**
388 * Render a string. Does not wrap if the string exceeds the line.
389 *
390 * @param x column coordinate. 0 is the left-most column.
391 * @param y row coordinate. 0 is the top-most row.
392 * @param str string to draw
393 * @param attr attributes to use (bold, foreColor, backColor)
394 */
395 public final void putStringXY(final int x, final int y, final String str,
396 final CellAttributes attr) {
397
398 int i = x;
399 for (int j = 0; j < str.length(); j++) {
400 char ch = str.charAt(j);
401 putCharXY(i, y, ch, attr);
402 i++;
403 if (i == width) {
404 break;
405 }
406 }
407 }
408
409 /**
410 * Render a string without changing the underlying attribute. Does not
411 * wrap if the string exceeds the line.
412 *
413 * @param x column coordinate. 0 is the left-most column.
414 * @param y row coordinate. 0 is the top-most row.
415 * @param str string to draw
416 */
417 public final void putStringXY(final int x, final int y, final String str) {
418
419 int i = x;
420 for (int j = 0; j < str.length(); j++) {
421 char ch = str.charAt(j);
422 putCharXY(i, y, ch);
423 i++;
424 if (i == width) {
425 break;
426 }
427 }
428 }
429
430 /**
431 * Draw a vertical line from (x, y) to (x, y + n).
432 *
433 * @param x column coordinate. 0 is the left-most column.
434 * @param y row coordinate. 0 is the top-most row.
435 * @param n number of characters to draw
436 * @param ch character to draw
437 * @param attr attributes to use (bold, foreColor, backColor)
438 */
439 public final void vLineXY(final int x, final int y, final int n,
440 final char ch, final CellAttributes attr) {
441
442 for (int i = y; i < y + n; i++) {
443 putCharXY(x, i, ch, attr);
444 }
445 }
446
447 /**
448 * Draw a horizontal line from (x, y) to (x + n, y).
449 *
450 * @param x column coordinate. 0 is the left-most column.
451 * @param y row coordinate. 0 is the top-most row.
452 * @param n number of characters to draw
453 * @param ch character to draw
454 * @param attr attributes to use (bold, foreColor, backColor)
455 */
456 public final void hLineXY(final int x, final int y, final int n,
457 final char ch, final CellAttributes attr) {
458
459 for (int i = x; i < x + n; i++) {
460 putCharXY(i, y, ch, attr);
461 }
462 }
463
464 /**
465 * Reallocate screen buffers.
466 *
467 * @param width new width
468 * @param height new height
469 */
470 private synchronized void reallocate(final int width, final int height) {
471 if (logical != null) {
472 for (int row = 0; row < this.height; row++) {
473 for (int col = 0; col < this.width; col++) {
474 logical[col][row] = null;
475 }
476 }
477 logical = null;
478 }
479 logical = new Cell[width][height];
480 if (physical != null) {
481 for (int row = 0; row < this.height; row++) {
482 for (int col = 0; col < this.width; col++) {
483 physical[col][row] = null;
484 }
485 }
486 physical = null;
487 }
488 physical = new Cell[width][height];
489
490 for (int row = 0; row < height; row++) {
491 for (int col = 0; col < width; col++) {
492 physical[col][row] = new Cell();
493 logical[col][row] = new Cell();
494 }
495 }
496
497 this.width = width;
498 this.height = height;
499
500 clipLeft = 0;
501 clipTop = 0;
502 clipRight = width;
503 clipBottom = height;
504
505 reallyCleared = true;
506 }
507
508 /**
509 * Change the width. Everything on-screen will be destroyed and must be
510 * redrawn.
511 *
512 * @param width new screen width
513 */
514 public final synchronized void setWidth(final int width) {
515 reallocate(width, this.height);
516 }
517
518 /**
519 * Change the height. Everything on-screen will be destroyed and must be
520 * redrawn.
521 *
522 * @param height new screen height
523 */
524 public final synchronized void setHeight(final int height) {
525 reallocate(this.width, height);
526 }
527
528 /**
529 * Change the width and height. Everything on-screen will be destroyed
530 * and must be redrawn.
531 *
532 * @param width new screen width
533 * @param height new screen height
534 */
535 public final void setDimensions(final int width, final int height) {
536 reallocate(width, height);
537 }
538
539 /**
540 * Get the height.
541 *
542 * @return current screen height
543 */
544 public final synchronized int getHeight() {
545 return this.height;
546 }
547
548 /**
549 * Get the width.
550 *
551 * @return current screen width
552 */
553 public final synchronized int getWidth() {
554 return this.width;
555 }
556
557 /**
558 * Public constructor. Sets everything to not-bold, white-on-black.
559 */
560 protected LogicalScreen() {
561 offsetX = 0;
562 offsetY = 0;
563 width = 80;
564 height = 24;
565 logical = null;
566 physical = null;
567 reallocate(width, height);
568 }
569
570 /**
571 * Reset screen to not-bold, white-on-black. Also flushes the offset and
572 * clip variables.
573 */
574 public final synchronized void reset() {
575 for (int row = 0; row < height; row++) {
576 for (int col = 0; col < width; col++) {
577 logical[col][row].reset();
578 }
579 }
580 resetClipping();
581 }
582
583 /**
584 * Flush the offset and clip variables.
585 */
586 public final void resetClipping() {
587 offsetX = 0;
588 offsetY = 0;
589 clipLeft = 0;
590 clipTop = 0;
591 clipRight = width;
592 clipBottom = height;
593 }
594
595 /**
596 * Clear the logical screen.
597 */
598 public final void clear() {
599 reset();
600 }
601
602 /**
603 * Clear the physical screen.
604 */
605 public final void clearPhysical() {
606 for (int row = 0; row < height; row++) {
607 for (int col = 0; col < width; col++) {
608 physical[col][row].reset();
609 }
610 }
611 }
612
613 /**
614 * Draw a box with a border and empty background.
615 *
616 * @param left left column of box. 0 is the left-most row.
617 * @param top top row of the box. 0 is the top-most row.
618 * @param right right column of box
619 * @param bottom bottom row of the box
620 * @param border attributes to use for the border
621 * @param background attributes to use for the background
622 */
623 public final void drawBox(final int left, final int top,
624 final int right, final int bottom,
625 final CellAttributes border, final CellAttributes background) {
626
627 drawBox(left, top, right, bottom, border, background, 1, false);
628 }
629
630 /**
631 * Draw a box with a border and empty background.
632 *
633 * @param left left column of box. 0 is the left-most row.
634 * @param top top row of the box. 0 is the top-most row.
635 * @param right right column of box
636 * @param bottom bottom row of the box
637 * @param border attributes to use for the border
638 * @param background attributes to use for the background
639 * @param borderType if 1, draw a single-line border; if 2, draw a
640 * double-line border; if 3, draw double-line top/bottom edges and
641 * single-line left/right edges (like Qmodem)
642 * @param shadow if true, draw a "shadow" on the box
643 */
644 public final void drawBox(final int left, final int top,
645 final int right, final int bottom,
646 final CellAttributes border, final CellAttributes background,
647 final int borderType, final boolean shadow) {
648
649 int boxWidth = right - left;
650 int boxHeight = bottom - top;
651
652 char cTopLeft;
653 char cTopRight;
654 char cBottomLeft;
655 char cBottomRight;
656 char cHSide;
657 char cVSide;
658
659 switch (borderType) {
660 case 1:
661 cTopLeft = GraphicsChars.ULCORNER;
662 cTopRight = GraphicsChars.URCORNER;
663 cBottomLeft = GraphicsChars.LLCORNER;
664 cBottomRight = GraphicsChars.LRCORNER;
665 cHSide = GraphicsChars.SINGLE_BAR;
666 cVSide = GraphicsChars.WINDOW_SIDE;
667 break;
668
669 case 2:
670 cTopLeft = GraphicsChars.WINDOW_LEFT_TOP_DOUBLE;
671 cTopRight = GraphicsChars.WINDOW_RIGHT_TOP_DOUBLE;
672 cBottomLeft = GraphicsChars.WINDOW_LEFT_BOTTOM_DOUBLE;
673 cBottomRight = GraphicsChars.WINDOW_RIGHT_BOTTOM_DOUBLE;
674 cHSide = GraphicsChars.DOUBLE_BAR;
675 cVSide = GraphicsChars.WINDOW_SIDE_DOUBLE;
676 break;
677
678 case 3:
679 cTopLeft = GraphicsChars.WINDOW_LEFT_TOP;
680 cTopRight = GraphicsChars.WINDOW_RIGHT_TOP;
681 cBottomLeft = GraphicsChars.WINDOW_LEFT_BOTTOM;
682 cBottomRight = GraphicsChars.WINDOW_RIGHT_BOTTOM;
683 cHSide = GraphicsChars.WINDOW_TOP;
684 cVSide = GraphicsChars.WINDOW_SIDE;
685 break;
686 default:
687 throw new IllegalArgumentException("Invalid border type: "
688 + borderType);
689 }
690
691 // Place the corner characters
692 putCharXY(left, top, cTopLeft, border);
693 putCharXY(left + boxWidth - 1, top, cTopRight, border);
694 putCharXY(left, top + boxHeight - 1, cBottomLeft, border);
695 putCharXY(left + boxWidth - 1, top + boxHeight - 1, cBottomRight,
696 border);
697
698 // Draw the box lines
699 hLineXY(left + 1, top, boxWidth - 2, cHSide, border);
700 vLineXY(left, top + 1, boxHeight - 2, cVSide, border);
701 hLineXY(left + 1, top + boxHeight - 1, boxWidth - 2, cHSide, border);
702 vLineXY(left + boxWidth - 1, top + 1, boxHeight - 2, cVSide, border);
703
704 // Fill in the interior background
705 for (int i = 1; i < boxHeight - 1; i++) {
706 hLineXY(1 + left, i + top, boxWidth - 2, ' ', background);
707 }
708
709 if (shadow) {
710 // Draw a shadow
711 drawBoxShadow(left, top, right, bottom);
712 }
713 }
714
715 /**
716 * Draw a box shadow.
717 *
718 * @param left left column of box. 0 is the left-most row.
719 * @param top top row of the box. 0 is the top-most row.
720 * @param right right column of box
721 * @param bottom bottom row of the box
722 */
723 public final void drawBoxShadow(final int left, final int top,
724 final int right, final int bottom) {
725
726 int boxTop = top;
727 int boxLeft = left;
728 int boxWidth = right - left;
729 int boxHeight = bottom - top;
730 CellAttributes shadowAttr = new CellAttributes();
731
732 // Shadows do not honor clipping but they DO honor offset.
733 int oldClipRight = clipRight;
734 int oldClipBottom = clipBottom;
735 /*
736 clipRight = boxWidth + 2;
737 clipBottom = boxHeight + 1;
738 */
739 clipRight = width;
740 clipBottom = height;
741
742 for (int i = 0; i < boxHeight; i++) {
743 putAttrXY(boxLeft + boxWidth, boxTop + 1 + i, shadowAttr);
744 putAttrXY(boxLeft + boxWidth + 1, boxTop + 1 + i, shadowAttr);
745 }
746 for (int i = 0; i < boxWidth; i++) {
747 putAttrXY(boxLeft + 2 + i, boxTop + boxHeight, shadowAttr);
748 }
749 clipRight = oldClipRight;
750 clipBottom = oldClipBottom;
751 }
752
753 /**
754 * Default implementation does nothing.
755 */
756 public void flushPhysical() {}
757
758 /**
759 * Put the cursor at (x,y).
760 *
761 * @param visible if true, the cursor should be visible
762 * @param x column coordinate to put the cursor on
763 * @param y row coordinate to put the cursor on
764 */
765 public void putCursor(final boolean visible, final int x, final int y) {
766 if ((cursorY >= 0)
767 && (cursorX >= 0)
768 && (cursorY <= height - 1)
769 && (cursorX <= width - 1)
770 ) {
771 // Make the current cursor position dirty
772 if (physical[cursorX][cursorY].getChar() == 'Q') {
773 physical[cursorX][cursorY].setChar('X');
774 } else {
775 physical[cursorX][cursorY].setChar('Q');
776 }
777 }
778
779 cursorVisible = visible;
780 cursorX = x;
781 cursorY = y;
782 }
783
784 /**
785 * Hide the cursor.
786 */
787 public final void hideCursor() {
788 cursorVisible = false;
789 }
790
791 /**
792 * Get the cursor visibility.
793 *
794 * @return true if the cursor is visible
795 */
796 public boolean isCursorVisible() {
797 return cursorVisible;
798 }
799
800 /**
801 * Get the cursor X position.
802 *
803 * @return the cursor x column position
804 */
805 public int getCursorX() {
806 return cursorX;
807 }
808
809 /**
810 * Get the cursor Y position.
811 *
812 * @return the cursor y row position
813 */
814 public int getCursorY() {
815 return cursorY;
816 }
817
818 /**
819 * Set the window title. Default implementation does nothing.
820 *
821 * @param title the new title
822 */
823 public void setTitle(final String title) {}
824
825 }