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 java
.awt
.image
.BufferedImage
;
33 import jexer
.backend
.GlyphMaker
;
34 import jexer
.bits
.Cell
;
35 import jexer
.bits
.CellAttributes
;
36 import jexer
.bits
.GraphicsChars
;
37 import jexer
.bits
.StringUtils
;
40 * A logical screen composed of a 2D array of Cells.
42 public class LogicalScreen
implements Screen
{
44 // ------------------------------------------------------------------------
45 // Variables --------------------------------------------------------------
46 // ------------------------------------------------------------------------
49 * Width of the visible window.
54 * Height of the visible window.
59 * Drawing offset for x.
64 * Drawing offset for y.
69 * Ignore anything drawn right of clipRight.
71 private int clipRight
;
74 * Ignore anything drawn below clipBottom.
76 private int clipBottom
;
79 * Ignore anything drawn left of clipLeft.
84 * Ignore anything drawn above clipTop.
89 * The physical screen last sent out on flush().
91 protected Cell
[][] physical
;
94 * The logical screen being rendered to.
96 protected Cell
[][] logical
;
99 * Set if the user explicitly wants to redraw everything starting with a
100 * ECMATerminal.clearAll().
102 protected boolean reallyCleared
;
105 * If true, the cursor is visible and should be placed onscreen at
106 * (cursorX, cursorY) during a call to flushPhysical().
108 protected boolean cursorVisible
;
111 * Cursor X position if visible.
113 protected int cursorX
;
116 * Cursor Y position if visible.
118 protected int cursorY
;
121 * The last used height of a character cell in pixels, only used for
124 private int lastTextHeight
= -1;
127 * The glyph drawer for full-width chars.
129 private GlyphMaker glyphMaker
= null;
131 // ------------------------------------------------------------------------
132 // Constructors -----------------------------------------------------------
133 // ------------------------------------------------------------------------
136 * Public constructor. Sets everything to not-bold, white-on-black.
138 protected LogicalScreen() {
145 reallocate(width
, height
);
148 // ------------------------------------------------------------------------
149 // Screen -----------------------------------------------------------------
150 // ------------------------------------------------------------------------
153 * Get the width of a character cell in pixels.
155 * @return the width in pixels of a character cell
157 public int getTextWidth() {
158 // Default width is 16 pixels.
163 * Get the height of a character cell in pixels.
165 * @return the height in pixels of a character cell
167 public int getTextHeight() {
168 // Default height is 20 pixels.
173 * Set drawing offset for x.
175 * @param offsetX new drawing offset
177 public final void setOffsetX(final int offsetX
) {
178 this.offsetX
= offsetX
;
182 * Set drawing offset for y.
184 * @param offsetY new drawing offset
186 public final void setOffsetY(final int offsetY
) {
187 this.offsetY
= offsetY
;
191 * Get right drawing clipping boundary.
193 * @return drawing boundary
195 public final int getClipRight() {
200 * Set right drawing clipping boundary.
202 * @param clipRight new boundary
204 public final void setClipRight(final int clipRight
) {
205 this.clipRight
= clipRight
;
209 * Get bottom drawing clipping boundary.
211 * @return drawing boundary
213 public final int getClipBottom() {
218 * Set bottom drawing clipping boundary.
220 * @param clipBottom new boundary
222 public final void setClipBottom(final int clipBottom
) {
223 this.clipBottom
= clipBottom
;
227 * Get left drawing clipping boundary.
229 * @return drawing boundary
231 public final int getClipLeft() {
236 * Set left drawing clipping boundary.
238 * @param clipLeft new boundary
240 public final void setClipLeft(final int clipLeft
) {
241 this.clipLeft
= clipLeft
;
245 * Get top drawing clipping boundary.
247 * @return drawing boundary
249 public final int getClipTop() {
254 * Set top drawing clipping boundary.
256 * @param clipTop new boundary
258 public final void setClipTop(final int clipTop
) {
259 this.clipTop
= clipTop
;
265 * @return if true, the logical screen is not in sync with the physical
268 public final boolean isDirty() {
269 for (int x
= 0; x
< width
; x
++) {
270 for (int y
= 0; y
< height
; y
++) {
271 if (!logical
[x
][y
].equals(physical
[x
][y
])) {
274 if (logical
[x
][y
].isBlink()) {
275 // Blinking screens are always dirty. There is
276 // opportunity for a Netscape blink tag joke here...
286 * Get the attributes 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 attributes at (x, y)
292 public final CellAttributes
getAttrXY(final int x
, final int y
) {
293 CellAttributes attr
= new CellAttributes();
294 if ((x
>= 0) && (x
< width
) && (y
>= 0) && (y
< height
)) {
295 attr
.setTo(logical
[x
][y
]);
301 * Get the cell 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 * @return the character + attributes
307 public Cell
getCharXY(final int x
, final int y
) {
308 Cell cell
= new Cell();
309 if ((x
>= 0) && (x
< width
) && (y
>= 0) && (y
< height
)) {
310 cell
.setTo(logical
[x
][y
]);
316 * Set the attributes at one location.
318 * @param x column coordinate. 0 is the left-most column.
319 * @param y row coordinate. 0 is the top-most row.
320 * @param attr attributes to use (bold, foreColor, backColor)
322 public final void putAttrXY(final int x
, final int y
,
323 final CellAttributes attr
) {
325 putAttrXY(x
, y
, attr
, true);
329 * Set the attributes at one location.
331 * @param x column coordinate. 0 is the left-most column.
332 * @param y row coordinate. 0 is the top-most row.
333 * @param attr attributes to use (bold, foreColor, backColor)
334 * @param clip if true, honor clipping/offset
336 public final void putAttrXY(final int x
, final int y
,
337 final CellAttributes attr
, final boolean clip
) {
354 if ((X
>= 0) && (X
< width
) && (Y
>= 0) && (Y
< height
)) {
355 logical
[X
][Y
].setTo(attr
);
357 // If this happens to be the cursor position, make the position
359 if ((cursorX
== X
) && (cursorY
== Y
)) {
360 physical
[cursorX
][cursorY
].unset();
361 unsetImageRow(cursorY
);
367 * Fill the entire screen with one character with attributes.
369 * @param ch character to draw
370 * @param attr attributes to use (bold, foreColor, backColor)
372 public final void putAll(final char ch
, final CellAttributes attr
) {
374 for (int x
= 0; x
< width
; x
++) {
375 for (int y
= 0; y
< height
; y
++) {
376 putCharXY(x
, y
, ch
, attr
);
382 * Render one character with attributes.
384 * @param x column coordinate. 0 is the left-most column.
385 * @param y row coordinate. 0 is the top-most row.
386 * @param ch character + attributes to draw
388 public final void putCharXY(final int x
, final int y
, final Cell ch
) {
397 if ((StringUtils
.width(ch
.getChar()) == 2) && (!ch
.isImage())) {
398 putFullwidthCharXY(x
, y
, ch
);
405 // System.err.printf("putCharXY: %d, %d, %c\n", X, Y, ch);
407 if ((X
>= 0) && (X
< width
) && (Y
>= 0) && (Y
< height
)) {
409 // Do not put control characters on the display
411 assert (ch
.getChar() >= 0x20);
412 assert (ch
.getChar() != 0x7F);
414 logical
[X
][Y
].setTo(ch
);
416 // If this happens to be the cursor position, make the position
418 if ((cursorX
== X
) && (cursorY
== Y
)) {
419 physical
[cursorX
][cursorY
].unset();
420 unsetImageRow(cursorY
);
426 * Render one character with attributes.
428 * @param x column coordinate. 0 is the left-most column.
429 * @param y row coordinate. 0 is the top-most row.
430 * @param ch character to draw
431 * @param attr attributes to use (bold, foreColor, backColor)
433 public final void putCharXY(final int x
, final int y
, final char ch
,
434 final CellAttributes attr
) {
444 if (StringUtils
.width(ch
) == 2) {
445 putFullwidthCharXY(x
, y
, ch
, attr
);
452 // System.err.printf("putCharXY: %d, %d, %c\n", X, Y, ch);
454 if ((X
>= 0) && (X
< width
) && (Y
>= 0) && (Y
< height
)) {
456 // Do not put control characters on the display
460 logical
[X
][Y
].setTo(attr
);
461 logical
[X
][Y
].setChar(ch
);
463 // If this happens to be the cursor position, make the position
465 if ((cursorX
== X
) && (cursorY
== Y
)) {
466 physical
[cursorX
][cursorY
].unset();
467 unsetImageRow(cursorY
);
473 * Render one character without changing the underlying attributes.
475 * @param x column coordinate. 0 is the left-most column.
476 * @param y row coordinate. 0 is the top-most row.
477 * @param ch character to draw
479 public final void putCharXY(final int x
, final int y
, final char ch
) {
489 if (StringUtils
.width(ch
) == 2) {
490 putFullwidthCharXY(x
, y
, ch
);
497 // System.err.printf("putCharXY: %d, %d, %c\n", X, Y, ch);
499 if ((X
>= 0) && (X
< width
) && (Y
>= 0) && (Y
< height
)) {
500 logical
[X
][Y
].setChar(ch
);
502 // If this happens to be the cursor position, make the position
504 if ((cursorX
== X
) && (cursorY
== Y
)) {
505 physical
[cursorX
][cursorY
].unset();
506 unsetImageRow(cursorY
);
512 * Render a string. Does not wrap if the string exceeds the line.
514 * @param x column coordinate. 0 is the left-most column.
515 * @param y row coordinate. 0 is the top-most row.
516 * @param str string to draw
517 * @param attr attributes to use (bold, foreColor, backColor)
519 public final void putStringXY(final int x
, final int y
, final String str
,
520 final CellAttributes attr
) {
523 for (int j
= 0; j
< str
.length(); j
++) {
524 char ch
= str
.charAt(j
);
525 putCharXY(i
, y
, ch
, attr
);
526 i
+= StringUtils
.width(ch
);
534 * Render a string without changing the underlying attribute. Does not
535 * wrap if the string exceeds the line.
537 * @param x column coordinate. 0 is the left-most column.
538 * @param y row coordinate. 0 is the top-most row.
539 * @param str string to draw
541 public final void putStringXY(final int x
, final int y
, final String str
) {
544 for (int j
= 0; j
< str
.length(); j
++) {
545 char ch
= str
.charAt(j
);
547 i
+= StringUtils
.width(ch
);
555 * Draw a vertical line from (x, y) to (x, y + n).
557 * @param x column coordinate. 0 is the left-most column.
558 * @param y row coordinate. 0 is the top-most row.
559 * @param n number of characters to draw
560 * @param ch character to draw
561 * @param attr attributes to use (bold, foreColor, backColor)
563 public final void vLineXY(final int x
, final int y
, final int n
,
564 final char ch
, final CellAttributes attr
) {
566 for (int i
= y
; i
< y
+ n
; i
++) {
567 putCharXY(x
, i
, ch
, attr
);
572 * Draw a horizontal line from (x, y) to (x + n, y).
574 * @param x column coordinate. 0 is the left-most column.
575 * @param y row coordinate. 0 is the top-most row.
576 * @param n number of characters to draw
577 * @param ch character to draw
578 * @param attr attributes to use (bold, foreColor, backColor)
580 public final void hLineXY(final int x
, final int y
, final int n
,
581 final char ch
, final CellAttributes attr
) {
583 for (int i
= x
; i
< x
+ n
; i
++) {
584 putCharXY(i
, y
, ch
, attr
);
589 * Change the width. Everything on-screen will be destroyed and must be
592 * @param width new screen width
594 public final synchronized void setWidth(final int width
) {
595 reallocate(width
, this.height
);
599 * Change the height. Everything on-screen will be destroyed and must be
602 * @param height new screen height
604 public final synchronized void setHeight(final int height
) {
605 reallocate(this.width
, height
);
609 * Change the width and height. Everything on-screen will be destroyed
610 * and must be redrawn.
612 * @param width new screen width
613 * @param height new screen height
615 public final void setDimensions(final int width
, final int height
) {
616 reallocate(width
, height
);
621 * Resize the physical screen to match the logical screen dimensions.
623 public void resizeToScreen() {
624 // Subclasses are expected to override this.
630 * @return current screen height
632 public final synchronized int getHeight() {
639 * @return current screen width
641 public final synchronized int getWidth() {
646 * Reset screen to not-bold, white-on-black. Also flushes the offset and
649 public final synchronized void reset() {
650 for (int row
= 0; row
< height
; row
++) {
651 for (int col
= 0; col
< width
; col
++) {
652 logical
[col
][row
].reset();
659 * Flush the offset and clip variables.
661 public final void resetClipping() {
671 * Clear the logical screen.
673 public final void clear() {
678 * Draw a box with a border and empty background.
680 * @param left left column of box. 0 is the left-most column.
681 * @param top top row of the box. 0 is the top-most row.
682 * @param right right column of box
683 * @param bottom bottom row of the box
684 * @param border attributes to use for the border
685 * @param background attributes to use for the background
687 public final void drawBox(final int left
, final int top
,
688 final int right
, final int bottom
,
689 final CellAttributes border
, final CellAttributes background
) {
691 drawBox(left
, top
, right
, bottom
, border
, background
, 1, false);
695 * Draw a box with a border and empty background.
697 * @param left left column of box. 0 is the left-most column.
698 * @param top top row of the box. 0 is the top-most row.
699 * @param right right column of box
700 * @param bottom bottom row of the box
701 * @param border attributes to use for the border
702 * @param background attributes to use for the background
703 * @param borderType if 1, draw a single-line border; if 2, draw a
704 * double-line border; if 3, draw double-line top/bottom edges and
705 * single-line left/right edges (like Qmodem)
706 * @param shadow if true, draw a "shadow" on the box
708 public final void drawBox(final int left
, final int top
,
709 final int right
, final int bottom
,
710 final CellAttributes border
, final CellAttributes background
,
711 final int borderType
, final boolean shadow
) {
713 int boxWidth
= right
- left
;
714 int boxHeight
= bottom
- top
;
723 switch (borderType
) {
725 cTopLeft
= GraphicsChars
.ULCORNER
;
726 cTopRight
= GraphicsChars
.URCORNER
;
727 cBottomLeft
= GraphicsChars
.LLCORNER
;
728 cBottomRight
= GraphicsChars
.LRCORNER
;
729 cHSide
= GraphicsChars
.SINGLE_BAR
;
730 cVSide
= GraphicsChars
.WINDOW_SIDE
;
734 cTopLeft
= GraphicsChars
.WINDOW_LEFT_TOP_DOUBLE
;
735 cTopRight
= GraphicsChars
.WINDOW_RIGHT_TOP_DOUBLE
;
736 cBottomLeft
= GraphicsChars
.WINDOW_LEFT_BOTTOM_DOUBLE
;
737 cBottomRight
= GraphicsChars
.WINDOW_RIGHT_BOTTOM_DOUBLE
;
738 cHSide
= GraphicsChars
.DOUBLE_BAR
;
739 cVSide
= GraphicsChars
.WINDOW_SIDE_DOUBLE
;
743 cTopLeft
= GraphicsChars
.WINDOW_LEFT_TOP
;
744 cTopRight
= GraphicsChars
.WINDOW_RIGHT_TOP
;
745 cBottomLeft
= GraphicsChars
.WINDOW_LEFT_BOTTOM
;
746 cBottomRight
= GraphicsChars
.WINDOW_RIGHT_BOTTOM
;
747 cHSide
= GraphicsChars
.WINDOW_TOP
;
748 cVSide
= GraphicsChars
.WINDOW_SIDE
;
751 throw new IllegalArgumentException("Invalid border type: "
755 // Place the corner characters
756 putCharXY(left
, top
, cTopLeft
, border
);
757 putCharXY(left
+ boxWidth
- 1, top
, cTopRight
, border
);
758 putCharXY(left
, top
+ boxHeight
- 1, cBottomLeft
, border
);
759 putCharXY(left
+ boxWidth
- 1, top
+ boxHeight
- 1, cBottomRight
,
762 // Draw the box lines
763 hLineXY(left
+ 1, top
, boxWidth
- 2, cHSide
, border
);
764 vLineXY(left
, top
+ 1, boxHeight
- 2, cVSide
, border
);
765 hLineXY(left
+ 1, top
+ boxHeight
- 1, boxWidth
- 2, cHSide
, border
);
766 vLineXY(left
+ boxWidth
- 1, top
+ 1, boxHeight
- 2, cVSide
, border
);
768 // Fill in the interior background
769 for (int i
= 1; i
< boxHeight
- 1; i
++) {
770 hLineXY(1 + left
, i
+ top
, boxWidth
- 2, ' ', background
);
775 drawBoxShadow(left
, top
, right
, bottom
);
782 * @param left left column of box. 0 is the left-most column.
783 * @param top top row of the box. 0 is the top-most row.
784 * @param right right column of box
785 * @param bottom bottom row of the box
787 public final void drawBoxShadow(final int left
, final int top
,
788 final int right
, final int bottom
) {
792 int boxWidth
= right
- left
;
793 int boxHeight
= bottom
- top
;
794 CellAttributes shadowAttr
= new CellAttributes();
796 // Shadows do not honor clipping but they DO honor offset.
797 int oldClipRight
= clipRight
;
798 int oldClipBottom
= clipBottom
;
799 // When offsetX or offsetY go negative, we need to increase the clip
801 clipRight
= width
- offsetX
;
802 clipBottom
= height
- offsetY
;
804 for (int i
= 0; i
< boxHeight
; i
++) {
805 putAttrXY(boxLeft
+ boxWidth
, boxTop
+ 1 + i
, shadowAttr
);
806 putAttrXY(boxLeft
+ boxWidth
+ 1, boxTop
+ 1 + i
, shadowAttr
);
808 for (int i
= 0; i
< boxWidth
; i
++) {
809 putAttrXY(boxLeft
+ 2 + i
, boxTop
+ boxHeight
, shadowAttr
);
811 clipRight
= oldClipRight
;
812 clipBottom
= oldClipBottom
;
816 * Default implementation does nothing.
818 public void flushPhysical() {}
821 * Put the cursor at (x,y).
823 * @param visible if true, the cursor should be visible
824 * @param x column coordinate to put the cursor on
825 * @param y row coordinate to put the cursor on
827 public void putCursor(final boolean visible
, final int x
, final int y
) {
830 && (cursorY
<= height
- 1)
831 && (cursorX
<= width
- 1)
833 // Make the current cursor position dirty
834 physical
[cursorX
][cursorY
].unset();
835 unsetImageRow(cursorY
);
838 cursorVisible
= visible
;
846 public final void hideCursor() {
847 cursorVisible
= false;
851 * Get the cursor visibility.
853 * @return true if the cursor is visible
855 public boolean isCursorVisible() {
856 return cursorVisible
;
860 * Get the cursor X position.
862 * @return the cursor x column position
864 public int getCursorX() {
869 * Get the cursor Y position.
871 * @return the cursor y row position
873 public int getCursorY() {
878 * Set the window title. Default implementation does nothing.
880 * @param title the new title
882 public void setTitle(final String title
) {}
884 // ------------------------------------------------------------------------
885 // LogicalScreen ----------------------------------------------------------
886 // ------------------------------------------------------------------------
889 * Reallocate screen buffers.
891 * @param width new width
892 * @param height new height
894 private synchronized void reallocate(final int width
, final int height
) {
895 if (logical
!= null) {
896 for (int row
= 0; row
< this.height
; row
++) {
897 for (int col
= 0; col
< this.width
; col
++) {
898 logical
[col
][row
] = null;
903 logical
= new Cell
[width
][height
];
904 if (physical
!= null) {
905 for (int row
= 0; row
< this.height
; row
++) {
906 for (int col
= 0; col
< this.width
; col
++) {
907 physical
[col
][row
] = null;
912 physical
= new Cell
[width
][height
];
914 for (int row
= 0; row
< height
; row
++) {
915 for (int col
= 0; col
< width
; col
++) {
916 physical
[col
][row
] = new Cell();
917 logical
[col
][row
] = new Cell();
922 this.height
= height
;
929 reallyCleared
= true;
933 * Clear the physical screen.
935 public final void clearPhysical() {
936 for (int row
= 0; row
< height
; row
++) {
937 for (int col
= 0; col
< width
; col
++) {
938 physical
[col
][row
].unset();
944 * Unset every image cell on one row of the physical screen, forcing
945 * images on that row to be redrawn.
947 * @param y row coordinate. 0 is the top-most row.
949 public final void unsetImageRow(final int y
) {
950 if ((y
< 0) || (y
>= height
)) {
953 for (int x
= 0; x
< width
; x
++) {
954 if (logical
[x
][y
].isImage()) {
955 physical
[x
][y
].unset();
961 * Render one fullwidth cell.
963 * @param x column coordinate. 0 is the left-most column.
964 * @param y row coordinate. 0 is the top-most row.
965 * @param cell the cell to draw
967 public final void putFullwidthCharXY(final int x
, final int y
,
970 if (lastTextHeight
!= getTextHeight()) {
971 glyphMaker
= GlyphMaker
.getInstance(getTextHeight());
972 lastTextHeight
= getTextHeight();
974 BufferedImage image
= glyphMaker
.getImage(cell
, getTextWidth() * 2,
976 BufferedImage leftImage
= image
.getSubimage(0, 0, getTextWidth(),
978 BufferedImage rightImage
= image
.getSubimage(getTextWidth(), 0,
979 getTextWidth(), getTextHeight());
981 Cell left
= new Cell();
983 left
.setImage(leftImage
);
984 left
.setWidth(Cell
.Width
.LEFT
);
985 // Blank out the char itself, so that shadows do not leave artifacts.
987 putCharXY(x
, y
, left
);
989 Cell right
= new Cell();
991 right
.setImage(rightImage
);
992 right
.setWidth(Cell
.Width
.RIGHT
);
993 // Blank out the char itself, so that shadows do not leave artifacts.
995 putCharXY(x
+ 1, y
, right
);
999 * Render one fullwidth character with attributes.
1001 * @param x column coordinate. 0 is the left-most column.
1002 * @param y row coordinate. 0 is the top-most row.
1003 * @param ch character to draw
1004 * @param attr attributes to use (bold, foreColor, backColor)
1006 public final void putFullwidthCharXY(final int x
, final int y
,
1007 final char ch
, final CellAttributes attr
) {
1009 Cell cell
= new Cell(ch
);
1011 putFullwidthCharXY(x
, y
, cell
);
1015 * Render one fullwidth character with attributes.
1017 * @param x column coordinate. 0 is the left-most column.
1018 * @param y row coordinate. 0 is the top-most row.
1019 * @param ch character to draw
1021 public final void putFullwidthCharXY(final int x
, final int y
,
1024 Cell cell
= new Cell(ch
);
1025 cell
.setAttr(getAttrXY(x
, y
));
1026 putFullwidthCharXY(x
, y
, cell
);