2 * Jexer - Java Text User Interface
4 * The MIT License (MIT)
6 * Copyright (C) 2017 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
{
41 * Width of the visible window.
46 * Height of the visible window.
51 * Drawing offset for x.
56 * Set drawing offset for x.
58 * @param offsetX new drawing offset
60 public final void setOffsetX(final int offsetX
) {
61 this.offsetX
= offsetX
;
65 * Drawing offset for y.
70 * Set drawing offset for y.
72 * @param offsetY new drawing offset
74 public final void setOffsetY(final int offsetY
) {
75 this.offsetY
= offsetY
;
79 * Ignore anything drawn right of clipRight.
81 private int clipRight
;
84 * Get right drawing clipping boundary.
86 * @return drawing boundary
88 public final int getClipRight() {
93 * Set right drawing clipping boundary.
95 * @param clipRight new boundary
97 public final void setClipRight(final int clipRight
) {
98 this.clipRight
= clipRight
;
102 * Ignore anything drawn below clipBottom.
104 private int clipBottom
;
107 * Get bottom drawing clipping boundary.
109 * @return drawing boundary
111 public final int getClipBottom() {
116 * Set bottom drawing clipping boundary.
118 * @param clipBottom new boundary
120 public final void setClipBottom(final int clipBottom
) {
121 this.clipBottom
= clipBottom
;
125 * Ignore anything drawn left of clipLeft.
127 private int clipLeft
;
130 * Get left drawing clipping boundary.
132 * @return drawing boundary
134 public final int getClipLeft() {
139 * Set left drawing clipping boundary.
141 * @param clipLeft new boundary
143 public final void setClipLeft(final int clipLeft
) {
144 this.clipLeft
= clipLeft
;
148 * Ignore anything drawn above clipTop.
153 * Get top drawing clipping boundary.
155 * @return drawing boundary
157 public final int getClipTop() {
162 * Set top drawing clipping boundary.
164 * @param clipTop new boundary
166 public final void setClipTop(final int clipTop
) {
167 this.clipTop
= clipTop
;
171 * The physical screen last sent out on flush().
173 protected Cell
[][] physical
;
176 * The logical screen being rendered to.
178 protected Cell
[][] logical
;
183 * @return if true, the logical screen is not in sync with the physical
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
])) {
192 if (logical
[x
][y
].isBlink()) {
193 // Blinking screens are always dirty. There is
194 // opportunity for a Netscape blink tag joke here...
204 * Set if the user explicitly wants to redraw everything starting with a
205 * ECMATerminal.clearAll().
207 protected boolean reallyCleared
;
210 * If true, the cursor is visible and should be placed onscreen at
211 * (cursorX, cursorY) during a call to flushPhysical().
213 protected boolean cursorVisible
;
216 * Cursor X position if visible.
218 protected int cursorX
;
221 * Cursor Y position if visible.
223 protected int cursorY
;
226 * Get the attributes at one location.
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)
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
]);
241 * Get the cell at one location.
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
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
]);
256 * Set the attributes at one location.
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)
262 public final void putAttrXY(final int x
, final int y
,
263 final CellAttributes attr
) {
265 putAttrXY(x
, y
, attr
, true);
269 * Set the attributes at one location.
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
276 public final void putAttrXY(final int x
, final int y
,
277 final CellAttributes attr
, final boolean clip
) {
294 if ((X
>= 0) && (X
< width
) && (Y
>= 0) && (Y
< height
)) {
295 logical
[X
][Y
].setTo(attr
);
297 // If this happens to be the cursor position, make the position
299 if ((cursorX
== X
) && (cursorY
== Y
)) {
300 if (physical
[cursorX
][cursorY
].getChar() == 'Q') {
301 physical
[cursorX
][cursorY
].setChar('X');
303 physical
[cursorX
][cursorY
].setChar('Q');
310 * Fill the entire screen with one character with attributes.
312 * @param ch character to draw
313 * @param attr attributes to use (bold, foreColor, backColor)
315 public final void putAll(final char ch
, final CellAttributes attr
) {
317 for (int x
= 0; x
< width
; x
++) {
318 for (int y
= 0; y
< height
; y
++) {
319 putCharXY(x
, y
, ch
, attr
);
325 * Render one character with attributes.
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 + attributes to draw
331 public final void putCharXY(final int x
, final int y
, final Cell ch
) {
332 putCharXY(x
, y
, ch
.getChar(), ch
);
336 * Render one character with attributes.
338 * @param x column coordinate. 0 is the left-most column.
339 * @param y row coordinate. 0 is the top-most row.
340 * @param ch character to draw
341 * @param attr attributes to use (bold, foreColor, backColor)
343 public final void putCharXY(final int x
, final int y
, final char ch
,
344 final CellAttributes attr
) {
357 // System.err.printf("putCharXY: %d, %d, %c\n", X, Y, ch);
359 if ((X
>= 0) && (X
< width
) && (Y
>= 0) && (Y
< height
)) {
361 // Do not put control characters on the display
365 logical
[X
][Y
].setTo(attr
);
366 logical
[X
][Y
].setChar(ch
);
368 // If this happens to be the cursor position, make the position
370 if ((cursorX
== X
) && (cursorY
== Y
)) {
371 if (physical
[cursorX
][cursorY
].getChar() == 'Q') {
372 physical
[cursorX
][cursorY
].setChar('X');
374 physical
[cursorX
][cursorY
].setChar('Q');
381 * Render one character without changing the underlying attributes.
383 * @param x column coordinate. 0 is the left-most column.
384 * @param y row coordinate. 0 is the top-most row.
385 * @param ch character to draw
387 public final void putCharXY(final int x
, final int y
, final char ch
) {
400 // System.err.printf("putCharXY: %d, %d, %c\n", X, Y, ch);
402 if ((X
>= 0) && (X
< width
) && (Y
>= 0) && (Y
< height
)) {
403 logical
[X
][Y
].setChar(ch
);
405 // If this happens to be the cursor position, make the position
407 if ((cursorX
== X
) && (cursorY
== Y
)) {
408 if (physical
[cursorX
][cursorY
].getChar() == 'Q') {
409 physical
[cursorX
][cursorY
].setChar('X');
411 physical
[cursorX
][cursorY
].setChar('Q');
418 * Render a string. Does not wrap if the string exceeds the line.
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 * @param attr attributes to use (bold, foreColor, backColor)
425 public final void putStringXY(final int x
, final int y
, final String str
,
426 final CellAttributes attr
) {
429 for (int j
= 0; j
< str
.length(); j
++) {
430 char ch
= str
.charAt(j
);
431 putCharXY(i
, y
, ch
, attr
);
440 * Render a string without changing the underlying attribute. Does not
441 * wrap if the string exceeds the line.
443 * @param x column coordinate. 0 is the left-most column.
444 * @param y row coordinate. 0 is the top-most row.
445 * @param str string to draw
447 public final void putStringXY(final int x
, final int y
, final String str
) {
450 for (int j
= 0; j
< str
.length(); j
++) {
451 char ch
= str
.charAt(j
);
461 * Draw a vertical line from (x, y) to (x, y + n).
463 * @param x column coordinate. 0 is the left-most column.
464 * @param y row coordinate. 0 is the top-most row.
465 * @param n number of characters to draw
466 * @param ch character to draw
467 * @param attr attributes to use (bold, foreColor, backColor)
469 public final void vLineXY(final int x
, final int y
, final int n
,
470 final char ch
, final CellAttributes attr
) {
472 for (int i
= y
; i
< y
+ n
; i
++) {
473 putCharXY(x
, i
, ch
, attr
);
478 * Draw a horizontal line from (x, y) to (x + n, y).
480 * @param x column coordinate. 0 is the left-most column.
481 * @param y row coordinate. 0 is the top-most row.
482 * @param n number of characters to draw
483 * @param ch character to draw
484 * @param attr attributes to use (bold, foreColor, backColor)
486 public final void hLineXY(final int x
, final int y
, final int n
,
487 final char ch
, final CellAttributes attr
) {
489 for (int i
= x
; i
< x
+ n
; i
++) {
490 putCharXY(i
, y
, ch
, attr
);
495 * Reallocate screen buffers.
497 * @param width new width
498 * @param height new height
500 private synchronized void reallocate(final int width
, final int height
) {
501 if (logical
!= null) {
502 for (int row
= 0; row
< this.height
; row
++) {
503 for (int col
= 0; col
< this.width
; col
++) {
504 logical
[col
][row
] = null;
509 logical
= new Cell
[width
][height
];
510 if (physical
!= null) {
511 for (int row
= 0; row
< this.height
; row
++) {
512 for (int col
= 0; col
< this.width
; col
++) {
513 physical
[col
][row
] = null;
518 physical
= new Cell
[width
][height
];
520 for (int row
= 0; row
< height
; row
++) {
521 for (int col
= 0; col
< width
; col
++) {
522 physical
[col
][row
] = new Cell();
523 logical
[col
][row
] = new Cell();
528 this.height
= height
;
535 reallyCleared
= true;
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
);
572 * @return current screen height
574 public final synchronized int getHeight() {
581 * @return current screen width
583 public final synchronized int getWidth() {
588 * Public constructor. Sets everything to not-bold, white-on-black.
590 protected LogicalScreen() {
597 reallocate(width
, height
);
601 * Reset screen to not-bold, white-on-black. Also flushes the offset and
604 public final synchronized void reset() {
605 for (int row
= 0; row
< height
; row
++) {
606 for (int col
= 0; col
< width
; col
++) {
607 logical
[col
][row
].reset();
614 * Flush the offset and clip variables.
616 public final void resetClipping() {
626 * Clear the logical screen.
628 public final void clear() {
633 * Clear the physical screen.
635 public final void clearPhysical() {
636 for (int row
= 0; row
< height
; row
++) {
637 for (int col
= 0; col
< width
; col
++) {
638 physical
[col
][row
].reset();
644 * Draw a box with a border and empty background.
646 * @param left left column of box. 0 is the left-most row.
647 * @param top top row of the box. 0 is the top-most row.
648 * @param right right column of box
649 * @param bottom bottom row of the box
650 * @param border attributes to use for the border
651 * @param background attributes to use for the background
653 public final void drawBox(final int left
, final int top
,
654 final int right
, final int bottom
,
655 final CellAttributes border
, final CellAttributes background
) {
657 drawBox(left
, top
, right
, bottom
, border
, background
, 1, false);
661 * Draw a box with a border and empty background.
663 * @param left left column of box. 0 is the left-most row.
664 * @param top top row of the box. 0 is the top-most row.
665 * @param right right column of box
666 * @param bottom bottom row of the box
667 * @param border attributes to use for the border
668 * @param background attributes to use for the background
669 * @param borderType if 1, draw a single-line border; if 2, draw a
670 * double-line border; if 3, draw double-line top/bottom edges and
671 * single-line left/right edges (like Qmodem)
672 * @param shadow if true, draw a "shadow" on the box
674 public final void drawBox(final int left
, final int top
,
675 final int right
, final int bottom
,
676 final CellAttributes border
, final CellAttributes background
,
677 final int borderType
, final boolean shadow
) {
679 int boxWidth
= right
- left
;
680 int boxHeight
= bottom
- top
;
689 switch (borderType
) {
691 cTopLeft
= GraphicsChars
.ULCORNER
;
692 cTopRight
= GraphicsChars
.URCORNER
;
693 cBottomLeft
= GraphicsChars
.LLCORNER
;
694 cBottomRight
= GraphicsChars
.LRCORNER
;
695 cHSide
= GraphicsChars
.SINGLE_BAR
;
696 cVSide
= GraphicsChars
.WINDOW_SIDE
;
700 cTopLeft
= GraphicsChars
.WINDOW_LEFT_TOP_DOUBLE
;
701 cTopRight
= GraphicsChars
.WINDOW_RIGHT_TOP_DOUBLE
;
702 cBottomLeft
= GraphicsChars
.WINDOW_LEFT_BOTTOM_DOUBLE
;
703 cBottomRight
= GraphicsChars
.WINDOW_RIGHT_BOTTOM_DOUBLE
;
704 cHSide
= GraphicsChars
.DOUBLE_BAR
;
705 cVSide
= GraphicsChars
.WINDOW_SIDE_DOUBLE
;
709 cTopLeft
= GraphicsChars
.WINDOW_LEFT_TOP
;
710 cTopRight
= GraphicsChars
.WINDOW_RIGHT_TOP
;
711 cBottomLeft
= GraphicsChars
.WINDOW_LEFT_BOTTOM
;
712 cBottomRight
= GraphicsChars
.WINDOW_RIGHT_BOTTOM
;
713 cHSide
= GraphicsChars
.WINDOW_TOP
;
714 cVSide
= GraphicsChars
.WINDOW_SIDE
;
717 throw new IllegalArgumentException("Invalid border type: "
721 // Place the corner characters
722 putCharXY(left
, top
, cTopLeft
, border
);
723 putCharXY(left
+ boxWidth
- 1, top
, cTopRight
, border
);
724 putCharXY(left
, top
+ boxHeight
- 1, cBottomLeft
, border
);
725 putCharXY(left
+ boxWidth
- 1, top
+ boxHeight
- 1, cBottomRight
,
728 // Draw the box lines
729 hLineXY(left
+ 1, top
, boxWidth
- 2, cHSide
, border
);
730 vLineXY(left
, top
+ 1, boxHeight
- 2, cVSide
, border
);
731 hLineXY(left
+ 1, top
+ boxHeight
- 1, boxWidth
- 2, cHSide
, border
);
732 vLineXY(left
+ boxWidth
- 1, top
+ 1, boxHeight
- 2, cVSide
, border
);
734 // Fill in the interior background
735 for (int i
= 1; i
< boxHeight
- 1; i
++) {
736 hLineXY(1 + left
, i
+ top
, boxWidth
- 2, ' ', background
);
741 drawBoxShadow(left
, top
, right
, bottom
);
748 * @param left left column of box. 0 is the left-most row.
749 * @param top top row of the box. 0 is the top-most row.
750 * @param right right column of box
751 * @param bottom bottom row of the box
753 public final void drawBoxShadow(final int left
, final int top
,
754 final int right
, final int bottom
) {
758 int boxWidth
= right
- left
;
759 int boxHeight
= bottom
- top
;
760 CellAttributes shadowAttr
= new CellAttributes();
762 // Shadows do not honor clipping but they DO honor offset.
763 int oldClipRight
= clipRight
;
764 int oldClipBottom
= clipBottom
;
766 clipRight = boxWidth + 2;
767 clipBottom = boxHeight + 1;
772 for (int i
= 0; i
< boxHeight
; i
++) {
773 putAttrXY(boxLeft
+ boxWidth
, boxTop
+ 1 + i
, shadowAttr
);
774 putAttrXY(boxLeft
+ boxWidth
+ 1, boxTop
+ 1 + i
, shadowAttr
);
776 for (int i
= 0; i
< boxWidth
; i
++) {
777 putAttrXY(boxLeft
+ 2 + i
, boxTop
+ boxHeight
, shadowAttr
);
779 clipRight
= oldClipRight
;
780 clipBottom
= oldClipBottom
;
784 * Default implementation does nothing.
786 public void flushPhysical() {}
789 * Put the cursor at (x,y).
791 * @param visible if true, the cursor should be visible
792 * @param x column coordinate to put the cursor on
793 * @param y row coordinate to put the cursor on
795 public void putCursor(final boolean visible
, final int x
, final int y
) {
798 && (cursorY
<= height
- 1)
799 && (cursorX
<= width
- 1)
801 // Make the current cursor position dirty
802 if (physical
[cursorX
][cursorY
].getChar() == 'Q') {
803 physical
[cursorX
][cursorY
].setChar('X');
805 physical
[cursorX
][cursorY
].setChar('Q');
809 cursorVisible
= visible
;
817 public final void hideCursor() {
818 cursorVisible
= false;
822 * Get the cursor visibility.
824 * @return true if the cursor is visible
826 public boolean isCursorVisible() {
827 return cursorVisible
;
831 * Get the cursor X position.
833 * @return the cursor x column position
835 public int getCursorX() {
840 * Get the cursor Y position.
842 * @return the cursor y row position
844 public int getCursorY() {
849 * Set the window title. Default implementation does nothing.
851 * @param title the new title
853 public void setTitle(final String title
) {}