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 * Set drawing offset for x.
140 * @param offsetX new drawing offset
142 public final void setOffsetX(final int offsetX
) {
143 this.offsetX
= offsetX
;
147 * Set drawing offset for y.
149 * @param offsetY new drawing offset
151 public final void setOffsetY(final int offsetY
) {
152 this.offsetY
= offsetY
;
156 * Get right drawing clipping boundary.
158 * @return drawing boundary
160 public final int getClipRight() {
165 * Set right drawing clipping boundary.
167 * @param clipRight new boundary
169 public final void setClipRight(final int clipRight
) {
170 this.clipRight
= clipRight
;
174 * Get bottom drawing clipping boundary.
176 * @return drawing boundary
178 public final int getClipBottom() {
183 * Set bottom drawing clipping boundary.
185 * @param clipBottom new boundary
187 public final void setClipBottom(final int clipBottom
) {
188 this.clipBottom
= clipBottom
;
192 * Get left drawing clipping boundary.
194 * @return drawing boundary
196 public final int getClipLeft() {
201 * Set left drawing clipping boundary.
203 * @param clipLeft new boundary
205 public final void setClipLeft(final int clipLeft
) {
206 this.clipLeft
= clipLeft
;
210 * Get top drawing clipping boundary.
212 * @return drawing boundary
214 public final int getClipTop() {
219 * Set top drawing clipping boundary.
221 * @param clipTop new boundary
223 public final void setClipTop(final int clipTop
) {
224 this.clipTop
= clipTop
;
230 * @return if true, the logical screen is not in sync with the physical
233 public final boolean isDirty() {
234 for (int x
= 0; x
< width
; x
++) {
235 for (int y
= 0; y
< height
; y
++) {
236 if (!logical
[x
][y
].equals(physical
[x
][y
])) {
239 if (logical
[x
][y
].isBlink()) {
240 // Blinking screens are always dirty. There is
241 // opportunity for a Netscape blink tag joke here...
251 * Get the attributes at one location.
253 * @param x column coordinate. 0 is the left-most column.
254 * @param y row coordinate. 0 is the top-most row.
255 * @return attributes at (x, y)
257 public final CellAttributes
getAttrXY(final int x
, final int y
) {
258 CellAttributes attr
= new CellAttributes();
259 if ((x
>= 0) && (x
< width
) && (y
>= 0) && (y
< height
)) {
260 attr
.setTo(logical
[x
][y
]);
266 * Get the cell at one location.
268 * @param x column coordinate. 0 is the left-most column.
269 * @param y row coordinate. 0 is the top-most row.
270 * @return the character + attributes
272 public Cell
getCharXY(final int x
, final int y
) {
273 Cell cell
= new Cell();
274 if ((x
>= 0) && (x
< width
) && (y
>= 0) && (y
< height
)) {
275 cell
.setTo(logical
[x
][y
]);
281 * Set the attributes at one location.
283 * @param x column coordinate. 0 is the left-most column.
284 * @param y row coordinate. 0 is the top-most row.
285 * @param attr attributes to use (bold, foreColor, backColor)
287 public final void putAttrXY(final int x
, final int y
,
288 final CellAttributes attr
) {
290 putAttrXY(x
, y
, attr
, true);
294 * Set the attributes at one location.
296 * @param x column coordinate. 0 is the left-most column.
297 * @param y row coordinate. 0 is the top-most row.
298 * @param attr attributes to use (bold, foreColor, backColor)
299 * @param clip if true, honor clipping/offset
301 public final void putAttrXY(final int x
, final int y
,
302 final CellAttributes attr
, final boolean clip
) {
319 if ((X
>= 0) && (X
< width
) && (Y
>= 0) && (Y
< height
)) {
320 logical
[X
][Y
].setTo(attr
);
322 // If this happens to be the cursor position, make the position
324 if ((cursorX
== X
) && (cursorY
== Y
)) {
325 physical
[cursorX
][cursorY
].unset();
326 unsetImageRow(cursorY
);
332 * Fill the entire screen with one character with attributes.
334 * @param ch character to draw
335 * @param attr attributes to use (bold, foreColor, backColor)
337 public final void putAll(final char ch
, final CellAttributes attr
) {
339 for (int x
= 0; x
< width
; x
++) {
340 for (int y
= 0; y
< height
; y
++) {
341 putCharXY(x
, y
, ch
, attr
);
347 * Render one character with attributes.
349 * @param x column coordinate. 0 is the left-most column.
350 * @param y row coordinate. 0 is the top-most row.
351 * @param ch character + attributes to draw
353 public final void putCharXY(final int x
, final int y
, final Cell ch
) {
365 // System.err.printf("putCharXY: %d, %d, %c\n", X, Y, ch);
367 if ((X
>= 0) && (X
< width
) && (Y
>= 0) && (Y
< height
)) {
369 // Do not put control characters on the display
371 assert (ch
.getChar() >= 0x20);
372 assert (ch
.getChar() != 0x7F);
374 logical
[X
][Y
].setTo(ch
);
376 // If this happens to be the cursor position, make the position
378 if ((cursorX
== X
) && (cursorY
== Y
)) {
379 physical
[cursorX
][cursorY
].unset();
380 unsetImageRow(cursorY
);
386 * Render one character with attributes.
388 * @param x column coordinate. 0 is the left-most column.
389 * @param y row coordinate. 0 is the top-most row.
390 * @param ch character to draw
391 * @param attr attributes to use (bold, foreColor, backColor)
393 public final void putCharXY(final int x
, final int y
, final char ch
,
394 final CellAttributes attr
) {
407 // System.err.printf("putCharXY: %d, %d, %c\n", X, Y, ch);
409 if ((X
>= 0) && (X
< width
) && (Y
>= 0) && (Y
< height
)) {
411 // Do not put control characters on the display
415 logical
[X
][Y
].setTo(attr
);
416 logical
[X
][Y
].setChar(ch
);
418 // If this happens to be the cursor position, make the position
420 if ((cursorX
== X
) && (cursorY
== Y
)) {
421 physical
[cursorX
][cursorY
].unset();
422 unsetImageRow(cursorY
);
428 * Render one character without changing the underlying attributes.
430 * @param x column coordinate. 0 is the left-most column.
431 * @param y row coordinate. 0 is the top-most row.
432 * @param ch character to draw
434 public final void putCharXY(final int x
, final int y
, final char ch
) {
447 // System.err.printf("putCharXY: %d, %d, %c\n", X, Y, ch);
449 if ((X
>= 0) && (X
< width
) && (Y
>= 0) && (Y
< height
)) {
450 logical
[X
][Y
].setChar(ch
);
452 // If this happens to be the cursor position, make the position
454 if ((cursorX
== X
) && (cursorY
== Y
)) {
455 physical
[cursorX
][cursorY
].unset();
456 unsetImageRow(cursorY
);
462 * Render a string. Does not wrap if the string exceeds the line.
464 * @param x column coordinate. 0 is the left-most column.
465 * @param y row coordinate. 0 is the top-most row.
466 * @param str string to draw
467 * @param attr attributes to use (bold, foreColor, backColor)
469 public final void putStringXY(final int x
, final int y
, final String str
,
470 final CellAttributes attr
) {
473 for (int j
= 0; j
< str
.length(); j
++) {
474 char ch
= str
.charAt(j
);
475 putCharXY(i
, y
, ch
, attr
);
484 * Render a string without changing the underlying attribute. Does not
485 * wrap if the string exceeds the line.
487 * @param x column coordinate. 0 is the left-most column.
488 * @param y row coordinate. 0 is the top-most row.
489 * @param str string to draw
491 public final void putStringXY(final int x
, final int y
, final String str
) {
494 for (int j
= 0; j
< str
.length(); j
++) {
495 char ch
= str
.charAt(j
);
505 * Draw a vertical line from (x, y) to (x, y + n).
507 * @param x column coordinate. 0 is the left-most column.
508 * @param y row coordinate. 0 is the top-most row.
509 * @param n number of characters to draw
510 * @param ch character to draw
511 * @param attr attributes to use (bold, foreColor, backColor)
513 public final void vLineXY(final int x
, final int y
, final int n
,
514 final char ch
, final CellAttributes attr
) {
516 for (int i
= y
; i
< y
+ n
; i
++) {
517 putCharXY(x
, i
, ch
, attr
);
522 * Draw a horizontal line from (x, y) to (x + n, y).
524 * @param x column coordinate. 0 is the left-most column.
525 * @param y row coordinate. 0 is the top-most row.
526 * @param n number of characters to draw
527 * @param ch character to draw
528 * @param attr attributes to use (bold, foreColor, backColor)
530 public final void hLineXY(final int x
, final int y
, final int n
,
531 final char ch
, final CellAttributes attr
) {
533 for (int i
= x
; i
< x
+ n
; i
++) {
534 putCharXY(i
, y
, ch
, attr
);
539 * Change the width. Everything on-screen will be destroyed and must be
542 * @param width new screen width
544 public final synchronized void setWidth(final int width
) {
545 reallocate(width
, this.height
);
549 * Change the height. Everything on-screen will be destroyed and must be
552 * @param height new screen height
554 public final synchronized void setHeight(final int height
) {
555 reallocate(this.width
, height
);
559 * Change the width and height. Everything on-screen will be destroyed
560 * and must be redrawn.
562 * @param width new screen width
563 * @param height new screen height
565 public final void setDimensions(final int width
, final int height
) {
566 reallocate(width
, height
);
571 * Resize the physical screen to match the logical screen dimensions.
573 public void resizeToScreen() {
574 // Subclasses are expected to override this.
580 * @return current screen height
582 public final synchronized int getHeight() {
589 * @return current screen width
591 public final synchronized int getWidth() {
596 * Reset screen to not-bold, white-on-black. Also flushes the offset and
599 public final synchronized void reset() {
600 for (int row
= 0; row
< height
; row
++) {
601 for (int col
= 0; col
< width
; col
++) {
602 logical
[col
][row
].reset();
609 * Flush the offset and clip variables.
611 public final void resetClipping() {
621 * Clear the logical screen.
623 public final void clear() {
628 * Draw a box with a border and empty background.
630 * @param left left column of box. 0 is the left-most column.
631 * @param top top row of the box. 0 is the top-most row.
632 * @param right right column of box
633 * @param bottom bottom row of the box
634 * @param border attributes to use for the border
635 * @param background attributes to use for the background
637 public final void drawBox(final int left
, final int top
,
638 final int right
, final int bottom
,
639 final CellAttributes border
, final CellAttributes background
) {
641 drawBox(left
, top
, right
, bottom
, border
, background
, 1, false);
645 * Draw a box with a border and empty background.
647 * @param left left column of box. 0 is the left-most column.
648 * @param top top row of the box. 0 is the top-most row.
649 * @param right right column of box
650 * @param bottom bottom row of the box
651 * @param border attributes to use for the border
652 * @param background attributes to use for the background
653 * @param borderType if 1, draw a single-line border; if 2, draw a
654 * double-line border; if 3, draw double-line top/bottom edges and
655 * single-line left/right edges (like Qmodem)
656 * @param shadow if true, draw a "shadow" on the box
658 public final void drawBox(final int left
, final int top
,
659 final int right
, final int bottom
,
660 final CellAttributes border
, final CellAttributes background
,
661 final int borderType
, final boolean shadow
) {
663 int boxWidth
= right
- left
;
664 int boxHeight
= bottom
- top
;
673 switch (borderType
) {
675 cTopLeft
= GraphicsChars
.ULCORNER
;
676 cTopRight
= GraphicsChars
.URCORNER
;
677 cBottomLeft
= GraphicsChars
.LLCORNER
;
678 cBottomRight
= GraphicsChars
.LRCORNER
;
679 cHSide
= GraphicsChars
.SINGLE_BAR
;
680 cVSide
= GraphicsChars
.WINDOW_SIDE
;
684 cTopLeft
= GraphicsChars
.WINDOW_LEFT_TOP_DOUBLE
;
685 cTopRight
= GraphicsChars
.WINDOW_RIGHT_TOP_DOUBLE
;
686 cBottomLeft
= GraphicsChars
.WINDOW_LEFT_BOTTOM_DOUBLE
;
687 cBottomRight
= GraphicsChars
.WINDOW_RIGHT_BOTTOM_DOUBLE
;
688 cHSide
= GraphicsChars
.DOUBLE_BAR
;
689 cVSide
= GraphicsChars
.WINDOW_SIDE_DOUBLE
;
693 cTopLeft
= GraphicsChars
.WINDOW_LEFT_TOP
;
694 cTopRight
= GraphicsChars
.WINDOW_RIGHT_TOP
;
695 cBottomLeft
= GraphicsChars
.WINDOW_LEFT_BOTTOM
;
696 cBottomRight
= GraphicsChars
.WINDOW_RIGHT_BOTTOM
;
697 cHSide
= GraphicsChars
.WINDOW_TOP
;
698 cVSide
= GraphicsChars
.WINDOW_SIDE
;
701 throw new IllegalArgumentException("Invalid border type: "
705 // Place the corner characters
706 putCharXY(left
, top
, cTopLeft
, border
);
707 putCharXY(left
+ boxWidth
- 1, top
, cTopRight
, border
);
708 putCharXY(left
, top
+ boxHeight
- 1, cBottomLeft
, border
);
709 putCharXY(left
+ boxWidth
- 1, top
+ boxHeight
- 1, cBottomRight
,
712 // Draw the box lines
713 hLineXY(left
+ 1, top
, boxWidth
- 2, cHSide
, border
);
714 vLineXY(left
, top
+ 1, boxHeight
- 2, cVSide
, border
);
715 hLineXY(left
+ 1, top
+ boxHeight
- 1, boxWidth
- 2, cHSide
, border
);
716 vLineXY(left
+ boxWidth
- 1, top
+ 1, boxHeight
- 2, cVSide
, border
);
718 // Fill in the interior background
719 for (int i
= 1; i
< boxHeight
- 1; i
++) {
720 hLineXY(1 + left
, i
+ top
, boxWidth
- 2, ' ', background
);
725 drawBoxShadow(left
, top
, right
, bottom
);
732 * @param left left column of box. 0 is the left-most column.
733 * @param top top row of the box. 0 is the top-most row.
734 * @param right right column of box
735 * @param bottom bottom row of the box
737 public final void drawBoxShadow(final int left
, final int top
,
738 final int right
, final int bottom
) {
742 int boxWidth
= right
- left
;
743 int boxHeight
= bottom
- top
;
744 CellAttributes shadowAttr
= new CellAttributes();
746 // Shadows do not honor clipping but they DO honor offset.
747 int oldClipRight
= clipRight
;
748 int oldClipBottom
= clipBottom
;
749 // When offsetX or offsetY go negative, we need to increase the clip
751 clipRight
= width
- offsetX
;
752 clipBottom
= height
- offsetY
;
754 for (int i
= 0; i
< boxHeight
; i
++) {
755 putAttrXY(boxLeft
+ boxWidth
, boxTop
+ 1 + i
, shadowAttr
);
756 putAttrXY(boxLeft
+ boxWidth
+ 1, boxTop
+ 1 + i
, shadowAttr
);
758 for (int i
= 0; i
< boxWidth
; i
++) {
759 putAttrXY(boxLeft
+ 2 + i
, boxTop
+ boxHeight
, shadowAttr
);
761 clipRight
= oldClipRight
;
762 clipBottom
= oldClipBottom
;
766 * Default implementation does nothing.
768 public void flushPhysical() {}
771 * Put the cursor at (x,y).
773 * @param visible if true, the cursor should be visible
774 * @param x column coordinate to put the cursor on
775 * @param y row coordinate to put the cursor on
777 public void putCursor(final boolean visible
, final int x
, final int y
) {
780 && (cursorY
<= height
- 1)
781 && (cursorX
<= width
- 1)
783 // Make the current cursor position dirty
784 physical
[cursorX
][cursorY
].unset();
785 unsetImageRow(cursorY
);
788 cursorVisible
= visible
;
796 public final void hideCursor() {
797 cursorVisible
= false;
801 * Get the cursor visibility.
803 * @return true if the cursor is visible
805 public boolean isCursorVisible() {
806 return cursorVisible
;
810 * Get the cursor X position.
812 * @return the cursor x column position
814 public int getCursorX() {
819 * Get the cursor Y position.
821 * @return the cursor y row position
823 public int getCursorY() {
828 * Set the window title. Default implementation does nothing.
830 * @param title the new title
832 public void setTitle(final String title
) {}
834 // ------------------------------------------------------------------------
835 // LogicalScreen ----------------------------------------------------------
836 // ------------------------------------------------------------------------
839 * Reallocate screen buffers.
841 * @param width new width
842 * @param height new height
844 private synchronized void reallocate(final int width
, final int height
) {
845 if (logical
!= null) {
846 for (int row
= 0; row
< this.height
; row
++) {
847 for (int col
= 0; col
< this.width
; col
++) {
848 logical
[col
][row
] = null;
853 logical
= new Cell
[width
][height
];
854 if (physical
!= null) {
855 for (int row
= 0; row
< this.height
; row
++) {
856 for (int col
= 0; col
< this.width
; col
++) {
857 physical
[col
][row
] = null;
862 physical
= new Cell
[width
][height
];
864 for (int row
= 0; row
< height
; row
++) {
865 for (int col
= 0; col
< width
; col
++) {
866 physical
[col
][row
] = new Cell();
867 logical
[col
][row
] = new Cell();
872 this.height
= height
;
879 reallyCleared
= true;
883 * Clear the physical screen.
885 public final void clearPhysical() {
886 for (int row
= 0; row
< height
; row
++) {
887 for (int col
= 0; col
< width
; col
++) {
888 physical
[col
][row
].unset();
894 * Unset every image cell on one row of the physical screen, forcing
895 * images on that row to be redrawn.
897 * @param y row coordinate. 0 is the top-most row.
899 public final void unsetImageRow(final int y
) {
900 if ((y
< 0) || (y
>= height
)) {
903 for (int x
= 0; x
< width
; x
++) {
904 if (logical
[x
][y
].isImage()) {
905 physical
[x
][y
].unset();