2 * Jexer - Java Text User Interface
4 * The MIT License (MIT)
6 * Copyright (C) 2019 Kevin Lamonte
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:
15 * The above copyright notice and this permission notice shall be included in
16 * all copies or substantial portions of the Software.
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.
26 * @author Kevin Lamonte [kevin.lamonte@gmail.com]
29 package jexer
.backend
;
31 import jexer
.bits
.Cell
;
32 import jexer
.bits
.CellAttributes
;
33 import jexer
.bits
.GraphicsChars
;
36 * A logical screen composed of a 2D array of Cells.
38 public class LogicalScreen
implements Screen
{
40 // ------------------------------------------------------------------------
41 // Variables --------------------------------------------------------------
42 // ------------------------------------------------------------------------
45 * Width of the visible window.
50 * Height of the visible window.
55 * Drawing offset for x.
60 * Drawing offset for y.
65 * Ignore anything drawn right of clipRight.
67 private int clipRight
;
70 * Ignore anything drawn below clipBottom.
72 private int clipBottom
;
75 * Ignore anything drawn left of clipLeft.
80 * Ignore anything drawn above clipTop.
85 * The physical screen last sent out on flush().
87 protected Cell
[][] physical
;
90 * The logical screen being rendered to.
92 protected Cell
[][] logical
;
95 * Set if the user explicitly wants to redraw everything starting with a
96 * ECMATerminal.clearAll().
98 protected boolean reallyCleared
;
101 * If true, the cursor is visible and should be placed onscreen at
102 * (cursorX, cursorY) during a call to flushPhysical().
104 protected boolean cursorVisible
;
107 * Cursor X position if visible.
109 protected int cursorX
;
112 * Cursor Y position if visible.
114 protected int cursorY
;
116 // ------------------------------------------------------------------------
117 // Constructors -----------------------------------------------------------
118 // ------------------------------------------------------------------------
121 * Public constructor. Sets everything to not-bold, white-on-black.
123 protected LogicalScreen() {
130 reallocate(width
, height
);
133 // ------------------------------------------------------------------------
134 // Screen -----------------------------------------------------------------
135 // ------------------------------------------------------------------------
138 * Get the width of a character cell in pixels.
140 * @return the width in pixels of a character cell
142 public int getTextWidth() {
143 // Default width is 16 pixels.
148 * Get the height of a character cell in pixels.
150 * @return the height in pixels of a character cell
152 public int getTextHeight() {
153 // Default height is 20 pixels.
158 * Set drawing offset for x.
160 * @param offsetX new drawing offset
162 public final void setOffsetX(final int offsetX
) {
163 this.offsetX
= offsetX
;
167 * Set drawing offset for y.
169 * @param offsetY new drawing offset
171 public final void setOffsetY(final int offsetY
) {
172 this.offsetY
= offsetY
;
176 * Get right drawing clipping boundary.
178 * @return drawing boundary
180 public final int getClipRight() {
185 * Set right drawing clipping boundary.
187 * @param clipRight new boundary
189 public final void setClipRight(final int clipRight
) {
190 this.clipRight
= clipRight
;
194 * Get bottom drawing clipping boundary.
196 * @return drawing boundary
198 public final int getClipBottom() {
203 * Set bottom drawing clipping boundary.
205 * @param clipBottom new boundary
207 public final void setClipBottom(final int clipBottom
) {
208 this.clipBottom
= clipBottom
;
212 * Get left drawing clipping boundary.
214 * @return drawing boundary
216 public final int getClipLeft() {
221 * Set left drawing clipping boundary.
223 * @param clipLeft new boundary
225 public final void setClipLeft(final int clipLeft
) {
226 this.clipLeft
= clipLeft
;
230 * Get top drawing clipping boundary.
232 * @return drawing boundary
234 public final int getClipTop() {
239 * Set top drawing clipping boundary.
241 * @param clipTop new boundary
243 public final void setClipTop(final int clipTop
) {
244 this.clipTop
= clipTop
;
250 * @return if true, the logical screen is not in sync with the physical
253 public final boolean isDirty() {
254 for (int x
= 0; x
< width
; x
++) {
255 for (int y
= 0; y
< height
; y
++) {
256 if (!logical
[x
][y
].equals(physical
[x
][y
])) {
259 if (logical
[x
][y
].isBlink()) {
260 // Blinking screens are always dirty. There is
261 // opportunity for a Netscape blink tag joke here...
271 * Get the attributes at one location.
273 * @param x column coordinate. 0 is the left-most column.
274 * @param y row coordinate. 0 is the top-most row.
275 * @return attributes at (x, y)
277 public final CellAttributes
getAttrXY(final int x
, final int y
) {
278 CellAttributes attr
= new CellAttributes();
279 if ((x
>= 0) && (x
< width
) && (y
>= 0) && (y
< height
)) {
280 attr
.setTo(logical
[x
][y
]);
286 * Get the cell at one location.
288 * @param x column coordinate. 0 is the left-most column.
289 * @param y row coordinate. 0 is the top-most row.
290 * @return the character + attributes
292 public Cell
getCharXY(final int x
, final int y
) {
293 Cell cell
= new Cell();
294 if ((x
>= 0) && (x
< width
) && (y
>= 0) && (y
< height
)) {
295 cell
.setTo(logical
[x
][y
]);
301 * Set the attributes at one location.
303 * @param x column coordinate. 0 is the left-most column.
304 * @param y row coordinate. 0 is the top-most row.
305 * @param attr attributes to use (bold, foreColor, backColor)
307 public final void putAttrXY(final int x
, final int y
,
308 final CellAttributes attr
) {
310 putAttrXY(x
, y
, attr
, true);
314 * Set the attributes at one location.
316 * @param x column coordinate. 0 is the left-most column.
317 * @param y row coordinate. 0 is the top-most row.
318 * @param attr attributes to use (bold, foreColor, backColor)
319 * @param clip if true, honor clipping/offset
321 public final void putAttrXY(final int x
, final int y
,
322 final CellAttributes attr
, final boolean clip
) {
339 if ((X
>= 0) && (X
< width
) && (Y
>= 0) && (Y
< height
)) {
340 logical
[X
][Y
].setTo(attr
);
342 // If this happens to be the cursor position, make the position
344 if ((cursorX
== X
) && (cursorY
== Y
)) {
345 physical
[cursorX
][cursorY
].unset();
346 unsetImageRow(cursorY
);
352 * Fill the entire screen with one character with attributes.
354 * @param ch character to draw
355 * @param attr attributes to use (bold, foreColor, backColor)
357 public final void putAll(final char ch
, final CellAttributes attr
) {
359 for (int x
= 0; x
< width
; x
++) {
360 for (int y
= 0; y
< height
; y
++) {
361 putCharXY(x
, y
, ch
, attr
);
367 * Render one character with attributes.
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 + attributes to draw
373 public final void putCharXY(final int x
, final int y
, final Cell ch
) {
385 // System.err.printf("putCharXY: %d, %d, %c\n", X, Y, ch);
387 if ((X
>= 0) && (X
< width
) && (Y
>= 0) && (Y
< height
)) {
389 // Do not put control characters on the display
391 assert (ch
.getChar() >= 0x20);
392 assert (ch
.getChar() != 0x7F);
394 logical
[X
][Y
].setTo(ch
);
396 // If this happens to be the cursor position, make the position
398 if ((cursorX
== X
) && (cursorY
== Y
)) {
399 physical
[cursorX
][cursorY
].unset();
400 unsetImageRow(cursorY
);
406 * Render one character with attributes.
408 * @param x column coordinate. 0 is the left-most column.
409 * @param y row coordinate. 0 is the top-most row.
410 * @param ch character to draw
411 * @param attr attributes to use (bold, foreColor, backColor)
413 public final void putCharXY(final int x
, final int y
, final char ch
,
414 final CellAttributes attr
) {
427 // System.err.printf("putCharXY: %d, %d, %c\n", X, Y, ch);
429 if ((X
>= 0) && (X
< width
) && (Y
>= 0) && (Y
< height
)) {
431 // Do not put control characters on the display
435 logical
[X
][Y
].setTo(attr
);
436 logical
[X
][Y
].setChar(ch
);
438 // If this happens to be the cursor position, make the position
440 if ((cursorX
== X
) && (cursorY
== Y
)) {
441 physical
[cursorX
][cursorY
].unset();
442 unsetImageRow(cursorY
);
448 * Render one character without changing the underlying attributes.
450 * @param x column coordinate. 0 is the left-most column.
451 * @param y row coordinate. 0 is the top-most row.
452 * @param ch character to draw
454 public final void putCharXY(final int x
, final int y
, final char ch
) {
467 // System.err.printf("putCharXY: %d, %d, %c\n", X, Y, ch);
469 if ((X
>= 0) && (X
< width
) && (Y
>= 0) && (Y
< height
)) {
470 logical
[X
][Y
].setChar(ch
);
472 // If this happens to be the cursor position, make the position
474 if ((cursorX
== X
) && (cursorY
== Y
)) {
475 physical
[cursorX
][cursorY
].unset();
476 unsetImageRow(cursorY
);
482 * Render a string. Does not wrap if the string exceeds the line.
484 * @param x column coordinate. 0 is the left-most column.
485 * @param y row coordinate. 0 is the top-most row.
486 * @param str string to draw
487 * @param attr attributes to use (bold, foreColor, backColor)
489 public final void putStringXY(final int x
, final int y
, final String str
,
490 final CellAttributes attr
) {
493 for (int j
= 0; j
< str
.length(); j
++) {
494 char ch
= str
.charAt(j
);
495 putCharXY(i
, y
, ch
, attr
);
504 * Render a string without changing the underlying attribute. Does not
505 * wrap if the string exceeds the line.
507 * @param x column coordinate. 0 is the left-most column.
508 * @param y row coordinate. 0 is the top-most row.
509 * @param str string to draw
511 public final void putStringXY(final int x
, final int y
, final String str
) {
514 for (int j
= 0; j
< str
.length(); j
++) {
515 char ch
= str
.charAt(j
);
525 * Draw a vertical line from (x, y) to (x, y + n).
527 * @param x column coordinate. 0 is the left-most column.
528 * @param y row coordinate. 0 is the top-most row.
529 * @param n number of characters to draw
530 * @param ch character to draw
531 * @param attr attributes to use (bold, foreColor, backColor)
533 public final void vLineXY(final int x
, final int y
, final int n
,
534 final char ch
, final CellAttributes attr
) {
536 for (int i
= y
; i
< y
+ n
; i
++) {
537 putCharXY(x
, i
, ch
, attr
);
542 * Draw a horizontal line from (x, y) to (x + n, y).
544 * @param x column coordinate. 0 is the left-most column.
545 * @param y row coordinate. 0 is the top-most row.
546 * @param n number of characters to draw
547 * @param ch character to draw
548 * @param attr attributes to use (bold, foreColor, backColor)
550 public final void hLineXY(final int x
, final int y
, final int n
,
551 final char ch
, final CellAttributes attr
) {
553 for (int i
= x
; i
< x
+ n
; i
++) {
554 putCharXY(i
, y
, ch
, attr
);
559 * Change the width. Everything on-screen will be destroyed and must be
562 * @param width new screen width
564 public final synchronized void setWidth(final int width
) {
565 reallocate(width
, this.height
);
569 * Change the height. Everything on-screen will be destroyed and must be
572 * @param height new screen height
574 public final synchronized void setHeight(final int height
) {
575 reallocate(this.width
, height
);
579 * Change the width and height. Everything on-screen will be destroyed
580 * and must be redrawn.
582 * @param width new screen width
583 * @param height new screen height
585 public final void setDimensions(final int width
, final int height
) {
586 reallocate(width
, height
);
591 * Resize the physical screen to match the logical screen dimensions.
593 public void resizeToScreen() {
594 // Subclasses are expected to override this.
600 * @return current screen height
602 public final synchronized int getHeight() {
609 * @return current screen width
611 public final synchronized int getWidth() {
616 * Reset screen to not-bold, white-on-black. Also flushes the offset and
619 public final synchronized void reset() {
620 for (int row
= 0; row
< height
; row
++) {
621 for (int col
= 0; col
< width
; col
++) {
622 logical
[col
][row
].reset();
629 * Flush the offset and clip variables.
631 public final void resetClipping() {
641 * Clear the logical screen.
643 public final void clear() {
648 * Draw a box with a border and empty background.
650 * @param left left column of box. 0 is the left-most column.
651 * @param top top row of the box. 0 is the top-most row.
652 * @param right right column of box
653 * @param bottom bottom row of the box
654 * @param border attributes to use for the border
655 * @param background attributes to use for the background
657 public final void drawBox(final int left
, final int top
,
658 final int right
, final int bottom
,
659 final CellAttributes border
, final CellAttributes background
) {
661 drawBox(left
, top
, right
, bottom
, border
, background
, 1, false);
665 * Draw a box with a border and empty background.
667 * @param left left column of box. 0 is the left-most column.
668 * @param top top row of the box. 0 is the top-most row.
669 * @param right right column of box
670 * @param bottom bottom row of the box
671 * @param border attributes to use for the border
672 * @param background attributes to use for the background
673 * @param borderType if 1, draw a single-line border; if 2, draw a
674 * double-line border; if 3, draw double-line top/bottom edges and
675 * single-line left/right edges (like Qmodem)
676 * @param shadow if true, draw a "shadow" on the box
678 public final void drawBox(final int left
, final int top
,
679 final int right
, final int bottom
,
680 final CellAttributes border
, final CellAttributes background
,
681 final int borderType
, final boolean shadow
) {
683 int boxWidth
= right
- left
;
684 int boxHeight
= bottom
- top
;
693 switch (borderType
) {
695 cTopLeft
= GraphicsChars
.ULCORNER
;
696 cTopRight
= GraphicsChars
.URCORNER
;
697 cBottomLeft
= GraphicsChars
.LLCORNER
;
698 cBottomRight
= GraphicsChars
.LRCORNER
;
699 cHSide
= GraphicsChars
.SINGLE_BAR
;
700 cVSide
= GraphicsChars
.WINDOW_SIDE
;
704 cTopLeft
= GraphicsChars
.WINDOW_LEFT_TOP_DOUBLE
;
705 cTopRight
= GraphicsChars
.WINDOW_RIGHT_TOP_DOUBLE
;
706 cBottomLeft
= GraphicsChars
.WINDOW_LEFT_BOTTOM_DOUBLE
;
707 cBottomRight
= GraphicsChars
.WINDOW_RIGHT_BOTTOM_DOUBLE
;
708 cHSide
= GraphicsChars
.DOUBLE_BAR
;
709 cVSide
= GraphicsChars
.WINDOW_SIDE_DOUBLE
;
713 cTopLeft
= GraphicsChars
.WINDOW_LEFT_TOP
;
714 cTopRight
= GraphicsChars
.WINDOW_RIGHT_TOP
;
715 cBottomLeft
= GraphicsChars
.WINDOW_LEFT_BOTTOM
;
716 cBottomRight
= GraphicsChars
.WINDOW_RIGHT_BOTTOM
;
717 cHSide
= GraphicsChars
.WINDOW_TOP
;
718 cVSide
= GraphicsChars
.WINDOW_SIDE
;
721 throw new IllegalArgumentException("Invalid border type: "
725 // Place the corner characters
726 putCharXY(left
, top
, cTopLeft
, border
);
727 putCharXY(left
+ boxWidth
- 1, top
, cTopRight
, border
);
728 putCharXY(left
, top
+ boxHeight
- 1, cBottomLeft
, border
);
729 putCharXY(left
+ boxWidth
- 1, top
+ boxHeight
- 1, cBottomRight
,
732 // Draw the box lines
733 hLineXY(left
+ 1, top
, boxWidth
- 2, cHSide
, border
);
734 vLineXY(left
, top
+ 1, boxHeight
- 2, cVSide
, border
);
735 hLineXY(left
+ 1, top
+ boxHeight
- 1, boxWidth
- 2, cHSide
, border
);
736 vLineXY(left
+ boxWidth
- 1, top
+ 1, boxHeight
- 2, cVSide
, border
);
738 // Fill in the interior background
739 for (int i
= 1; i
< boxHeight
- 1; i
++) {
740 hLineXY(1 + left
, i
+ top
, boxWidth
- 2, ' ', background
);
745 drawBoxShadow(left
, top
, right
, bottom
);
752 * @param left left column of box. 0 is the left-most column.
753 * @param top top row of the box. 0 is the top-most row.
754 * @param right right column of box
755 * @param bottom bottom row of the box
757 public final void drawBoxShadow(final int left
, final int top
,
758 final int right
, final int bottom
) {
762 int boxWidth
= right
- left
;
763 int boxHeight
= bottom
- top
;
764 CellAttributes shadowAttr
= new CellAttributes();
766 // Shadows do not honor clipping but they DO honor offset.
767 int oldClipRight
= clipRight
;
768 int oldClipBottom
= clipBottom
;
769 // When offsetX or offsetY go negative, we need to increase the clip
771 clipRight
= width
- offsetX
;
772 clipBottom
= height
- offsetY
;
774 for (int i
= 0; i
< boxHeight
; i
++) {
775 putAttrXY(boxLeft
+ boxWidth
, boxTop
+ 1 + i
, shadowAttr
);
776 putAttrXY(boxLeft
+ boxWidth
+ 1, boxTop
+ 1 + i
, shadowAttr
);
778 for (int i
= 0; i
< boxWidth
; i
++) {
779 putAttrXY(boxLeft
+ 2 + i
, boxTop
+ boxHeight
, shadowAttr
);
781 clipRight
= oldClipRight
;
782 clipBottom
= oldClipBottom
;
786 * Default implementation does nothing.
788 public void flushPhysical() {}
791 * Put the cursor at (x,y).
793 * @param visible if true, the cursor should be visible
794 * @param x column coordinate to put the cursor on
795 * @param y row coordinate to put the cursor on
797 public void putCursor(final boolean visible
, final int x
, final int y
) {
800 && (cursorY
<= height
- 1)
801 && (cursorX
<= width
- 1)
803 // Make the current cursor position dirty
804 physical
[cursorX
][cursorY
].unset();
805 unsetImageRow(cursorY
);
808 cursorVisible
= visible
;
816 public final void hideCursor() {
817 cursorVisible
= false;
821 * Get the cursor visibility.
823 * @return true if the cursor is visible
825 public boolean isCursorVisible() {
826 return cursorVisible
;
830 * Get the cursor X position.
832 * @return the cursor x column position
834 public int getCursorX() {
839 * Get the cursor Y position.
841 * @return the cursor y row position
843 public int getCursorY() {
848 * Set the window title. Default implementation does nothing.
850 * @param title the new title
852 public void setTitle(final String title
) {}
854 // ------------------------------------------------------------------------
855 // LogicalScreen ----------------------------------------------------------
856 // ------------------------------------------------------------------------
859 * Reallocate screen buffers.
861 * @param width new width
862 * @param height new height
864 private synchronized void reallocate(final int width
, final int height
) {
865 if (logical
!= null) {
866 for (int row
= 0; row
< this.height
; row
++) {
867 for (int col
= 0; col
< this.width
; col
++) {
868 logical
[col
][row
] = null;
873 logical
= new Cell
[width
][height
];
874 if (physical
!= null) {
875 for (int row
= 0; row
< this.height
; row
++) {
876 for (int col
= 0; col
< this.width
; col
++) {
877 physical
[col
][row
] = null;
882 physical
= new Cell
[width
][height
];
884 for (int row
= 0; row
< height
; row
++) {
885 for (int col
= 0; col
< width
; col
++) {
886 physical
[col
][row
] = new Cell();
887 logical
[col
][row
] = new Cell();
892 this.height
= height
;
899 reallyCleared
= true;
903 * Clear the physical screen.
905 public final void clearPhysical() {
906 for (int row
= 0; row
< height
; row
++) {
907 for (int col
= 0; col
< width
; col
++) {
908 physical
[col
][row
].unset();
914 * Unset every image cell on one row of the physical screen, forcing
915 * images on that row to be redrawn.
917 * @param y row coordinate. 0 is the top-most row.
919 public final void unsetImageRow(final int y
) {
920 if ((y
< 0) || (y
>= height
)) {
923 for (int x
= 0; x
< width
; x
++) {
924 if (logical
[x
][y
].isImage()) {
925 physical
[x
][y
].unset();