Merge commit '77d3a60869e7a780c6ae069e51530e1eacece5e2'
[fanfix.git] / src / jexer / backend / LogicalScreen.java
index c24703e7a23098740804f5be8f4873b27ceb1324..22b7e95f6564aad97431ca2e60a2fec09e9d2365 100644 (file)
@@ -3,7 +3,7 @@
  *
  * The MIT License (MIT)
  *
- * Copyright (C) 2017 Kevin Lamonte
+ * Copyright (C) 2019 Kevin Lamonte
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
  * copy of this software and associated documentation files (the "Software"),
  */
 package jexer.backend;
 
+import java.awt.image.BufferedImage;
+
+import jexer.backend.GlyphMaker;
 import jexer.bits.Cell;
 import jexer.bits.CellAttributes;
+import jexer.bits.Clipboard;
 import jexer.bits.GraphicsChars;
+import jexer.bits.StringUtils;
 
 /**
  * A logical screen composed of a 2D array of Cells.
@@ -113,6 +118,17 @@ public class LogicalScreen implements Screen {
      */
     protected int cursorY;
 
+    /**
+     * The last used height of a character cell in pixels, only used for
+     * full-width chars.
+     */
+    private int lastTextHeight = -1;
+
+    /**
+     * The glyph drawer for full-width chars.
+     */
+    private GlyphMaker glyphMaker = null;
+
     // ------------------------------------------------------------------------
     // Constructors -----------------------------------------------------------
     // ------------------------------------------------------------------------
@@ -134,6 +150,26 @@ public class LogicalScreen implements Screen {
     // Screen -----------------------------------------------------------------
     // ------------------------------------------------------------------------
 
+    /**
+     * Get the width of a character cell in pixels.
+     *
+     * @return the width in pixels of a character cell
+     */
+    public int getTextWidth() {
+        // Default width is 16 pixels.
+        return 16;
+    }
+
+    /**
+     * Get the height of a character cell in pixels.
+     *
+     * @return the height in pixels of a character cell
+     */
+    public int getTextHeight() {
+        // Default height is 20 pixels.
+        return 20;
+    }
+
     /**
      * Set drawing offset for x.
      *
@@ -322,11 +358,8 @@ public class LogicalScreen implements Screen {
             // If this happens to be the cursor position, make the position
             // dirty.
             if ((cursorX == X) && (cursorY == Y)) {
-                if (physical[cursorX][cursorY].getChar() == 'Q') {
-                    physical[cursorX][cursorY].setChar('X');
-                } else {
-                    physical[cursorX][cursorY].setChar('Q');
-                }
+                physical[cursorX][cursorY].unset();
+                unsetImageRow(cursorY);
             }
         }
     }
@@ -337,7 +370,7 @@ public class LogicalScreen implements Screen {
      * @param ch character to draw
      * @param attr attributes to use (bold, foreColor, backColor)
      */
-    public final void putAll(final char ch, final CellAttributes attr) {
+    public final void putAll(final int ch, final CellAttributes attr) {
 
         for (int x = 0; x < width; x++) {
             for (int y = 0; y < height; y++) {
@@ -354,7 +387,40 @@ public class LogicalScreen implements Screen {
      * @param ch character + attributes to draw
      */
     public final void putCharXY(final int x, final int y, final Cell ch) {
-        putCharXY(x, y, ch.getChar(), ch);
+        if ((x < clipLeft)
+            || (x >= clipRight)
+            || (y < clipTop)
+            || (y >= clipBottom)
+        ) {
+            return;
+        }
+
+        if ((StringUtils.width(ch.getChar()) == 2) && (!ch.isImage())) {
+            putFullwidthCharXY(x, y, ch);
+            return;
+        }
+
+        int X = x + offsetX;
+        int Y = y + offsetY;
+
+        // System.err.printf("putCharXY: %d, %d, %c\n", X, Y, ch);
+
+        if ((X >= 0) && (X < width) && (Y >= 0) && (Y < height)) {
+
+            // Do not put control characters on the display
+            if (!ch.isImage()) {
+                assert (ch.getChar() >= 0x20);
+                assert (ch.getChar() != 0x7F);
+            }
+            logical[X][Y].setTo(ch);
+
+            // If this happens to be the cursor position, make the position
+            // dirty.
+            if ((cursorX == X) && (cursorY == Y)) {
+                physical[cursorX][cursorY].unset();
+                unsetImageRow(cursorY);
+            }
+        }
     }
 
     /**
@@ -365,7 +431,7 @@ public class LogicalScreen implements Screen {
      * @param ch character to draw
      * @param attr attributes to use (bold, foreColor, backColor)
      */
-    public final void putCharXY(final int x, final int y, final char ch,
+    public final void putCharXY(final int x, final int y, final int ch,
         final CellAttributes attr) {
 
         if ((x < clipLeft)
@@ -376,6 +442,11 @@ public class LogicalScreen implements Screen {
             return;
         }
 
+        if (StringUtils.width(ch) == 2) {
+            putFullwidthCharXY(x, y, ch, attr);
+            return;
+        }
+
         int X = x + offsetX;
         int Y = y + offsetY;
 
@@ -393,11 +464,8 @@ public class LogicalScreen implements Screen {
             // If this happens to be the cursor position, make the position
             // dirty.
             if ((cursorX == X) && (cursorY == Y)) {
-                if (physical[cursorX][cursorY].getChar() == 'Q') {
-                    physical[cursorX][cursorY].setChar('X');
-                } else {
-                    physical[cursorX][cursorY].setChar('Q');
-                }
+                physical[cursorX][cursorY].unset();
+                unsetImageRow(cursorY);
             }
         }
     }
@@ -409,8 +477,7 @@ public class LogicalScreen implements Screen {
      * @param y row coordinate.  0 is the top-most row.
      * @param ch character to draw
      */
-    public final void putCharXY(final int x, final int y, final char ch) {
-
+    public final void putCharXY(final int x, final int y, final int ch) {
         if ((x < clipLeft)
             || (x >= clipRight)
             || (y < clipTop)
@@ -419,6 +486,11 @@ public class LogicalScreen implements Screen {
             return;
         }
 
+        if (StringUtils.width(ch) == 2) {
+            putFullwidthCharXY(x, y, ch);
+            return;
+        }
+
         int X = x + offsetX;
         int Y = y + offsetY;
 
@@ -430,11 +502,8 @@ public class LogicalScreen implements Screen {
             // If this happens to be the cursor position, make the position
             // dirty.
             if ((cursorX == X) && (cursorY == Y)) {
-                if (physical[cursorX][cursorY].getChar() == 'Q') {
-                    physical[cursorX][cursorY].setChar('X');
-                } else {
-                    physical[cursorX][cursorY].setChar('Q');
-                }
+                physical[cursorX][cursorY].unset();
+                unsetImageRow(cursorY);
             }
         }
     }
@@ -451,10 +520,11 @@ public class LogicalScreen implements Screen {
         final CellAttributes attr) {
 
         int i = x;
-        for (int j = 0; j < str.length(); j++) {
-            char ch = str.charAt(j);
+        for (int j = 0; j < str.length();) {
+            int ch = str.codePointAt(j);
+            j += Character.charCount(ch);
             putCharXY(i, y, ch, attr);
-            i++;
+            i += StringUtils.width(ch);
             if (i == width) {
                 break;
             }
@@ -472,10 +542,11 @@ public class LogicalScreen implements Screen {
     public final void putStringXY(final int x, final int y, final String str) {
 
         int i = x;
-        for (int j = 0; j < str.length(); j++) {
-            char ch = str.charAt(j);
+        for (int j = 0; j < str.length();) {
+            int ch = str.codePointAt(j);
+            j += Character.charCount(ch);
             putCharXY(i, y, ch);
-            i++;
+            i += StringUtils.width(ch);
             if (i == width) {
                 break;
             }
@@ -492,7 +563,7 @@ public class LogicalScreen implements Screen {
      * @param attr attributes to use (bold, foreColor, backColor)
      */
     public final void vLineXY(final int x, final int y, final int n,
-        final char ch, final CellAttributes attr) {
+        final int ch, final CellAttributes attr) {
 
         for (int i = y; i < y + n; i++) {
             putCharXY(x, i, ch, attr);
@@ -509,7 +580,7 @@ public class LogicalScreen implements Screen {
      * @param attr attributes to use (bold, foreColor, backColor)
      */
     public final void hLineXY(final int x, final int y, final int n,
-        final char ch, final CellAttributes attr) {
+        final int ch, final CellAttributes attr) {
 
         for (int i = x; i < x + n; i++) {
             putCharXY(i, y, ch, attr);
@@ -545,6 +616,14 @@ public class LogicalScreen implements Screen {
      */
     public final void setDimensions(final int width, final int height) {
         reallocate(width, height);
+        resizeToScreen();
+    }
+
+    /**
+     * Resize the physical screen to match the logical screen dimensions.
+     */
+    public void resizeToScreen() {
+        // Subclasses are expected to override this.
     }
 
     /**
@@ -600,7 +679,7 @@ public class LogicalScreen implements Screen {
     /**
      * Draw a box with a border and empty background.
      *
-     * @param left left column of box.  0 is the left-most row.
+     * @param left left column of box.  0 is the left-most column.
      * @param top top row of the box.  0 is the top-most row.
      * @param right right column of box
      * @param bottom bottom row of the box
@@ -617,7 +696,7 @@ public class LogicalScreen implements Screen {
     /**
      * Draw a box with a border and empty background.
      *
-     * @param left left column of box.  0 is the left-most row.
+     * @param left left column of box.  0 is the left-most column.
      * @param top top row of the box.  0 is the top-most row.
      * @param right right column of box
      * @param bottom bottom row of the box
@@ -702,7 +781,7 @@ public class LogicalScreen implements Screen {
     /**
      * Draw a box shadow.
      *
-     * @param left left column of box.  0 is the left-most row.
+     * @param left left column of box.  0 is the left-most column.
      * @param top top row of the box.  0 is the top-most row.
      * @param right right column of box
      * @param bottom bottom row of the box
@@ -719,19 +798,36 @@ public class LogicalScreen implements Screen {
         // Shadows do not honor clipping but they DO honor offset.
         int oldClipRight = clipRight;
         int oldClipBottom = clipBottom;
-        /*
-        clipRight = boxWidth + 2;
-        clipBottom = boxHeight + 1;
-        */
-        clipRight = width;
-        clipBottom = height;
+        // When offsetX or offsetY go negative, we need to increase the clip
+        // bounds.
+        clipRight = width - offsetX;
+        clipBottom = height - offsetY;
 
         for (int i = 0; i < boxHeight; i++) {
-            putAttrXY(boxLeft + boxWidth, boxTop + 1 + i, shadowAttr);
-            putAttrXY(boxLeft + boxWidth + 1, boxTop + 1 + i, shadowAttr);
+            Cell cell = getCharXY(offsetX + boxLeft + boxWidth,
+                offsetY + boxTop + 1 + i);
+            if (cell.getWidth() == Cell.Width.SINGLE) {
+                putAttrXY(boxLeft + boxWidth, boxTop + 1 + i, shadowAttr);
+            } else {
+                putCharXY(boxLeft + boxWidth, boxTop + 1 + i, ' ', shadowAttr);
+            }
+            cell = getCharXY(offsetX + boxLeft + boxWidth + 1,
+                offsetY + boxTop + 1 + i);
+            if (cell.getWidth() == Cell.Width.SINGLE) {
+                putAttrXY(boxLeft + boxWidth + 1, boxTop + 1 + i, shadowAttr);
+            } else {
+                putCharXY(boxLeft + boxWidth + 1, boxTop + 1 + i, ' ',
+                    shadowAttr);
+            }
         }
         for (int i = 0; i < boxWidth; i++) {
-            putAttrXY(boxLeft + 2 + i, boxTop + boxHeight, shadowAttr);
+            Cell cell = getCharXY(offsetX + boxLeft + 2 + i,
+                offsetY + boxTop + boxHeight);
+            if (cell.getWidth() == Cell.Width.SINGLE) {
+                putAttrXY(boxLeft + 2 + i, boxTop + boxHeight, shadowAttr);
+            } else {
+                putCharXY(boxLeft + 2 + i, boxTop + boxHeight, ' ', shadowAttr);
+            }
         }
         clipRight = oldClipRight;
         clipBottom = oldClipBottom;
@@ -756,11 +852,8 @@ public class LogicalScreen implements Screen {
             && (cursorX <= width - 1)
         ) {
             // Make the current cursor position dirty
-            if (physical[cursorX][cursorY].getChar() == 'Q') {
-                physical[cursorX][cursorY].setChar('X');
-            } else {
-                physical[cursorX][cursorY].setChar('Q');
-            }
+            physical[cursorX][cursorY].unset();
+            unsetImageRow(cursorY);
         }
 
         cursorVisible = visible;
@@ -863,9 +956,271 @@ public class LogicalScreen implements Screen {
     public final void clearPhysical() {
         for (int row = 0; row < height; row++) {
             for (int col = 0; col < width; col++) {
-                physical[col][row].reset();
+                physical[col][row].unset();
+            }
+        }
+    }
+
+    /**
+     * Unset every image cell on one row of the physical screen, forcing
+     * images on that row to be redrawn.
+     *
+     * @param y row coordinate.  0 is the top-most row.
+     */
+    public final void unsetImageRow(final int y) {
+        if ((y < 0) || (y >= height)) {
+            return;
+        }
+        for (int x = 0; x < width; x++) {
+            if (logical[x][y].isImage()) {
+                physical[x][y].unset();
+            }
+        }
+    }
+
+    /**
+     * Render one fullwidth cell.
+     *
+     * @param x column coordinate.  0 is the left-most column.
+     * @param y row coordinate.  0 is the top-most row.
+     * @param cell the cell to draw
+     */
+    public final void putFullwidthCharXY(final int x, final int y,
+        final Cell cell) {
+
+        int cellWidth = getTextWidth();
+        int cellHeight = getTextHeight();
+
+        if (lastTextHeight != cellHeight) {
+            glyphMaker = GlyphMaker.getInstance(cellHeight);
+            lastTextHeight = cellHeight;
+        }
+        BufferedImage image = glyphMaker.getImage(cell, cellWidth * 2,
+            cellHeight);
+        BufferedImage leftImage = image.getSubimage(0, 0, cellWidth,
+            cellHeight);
+        BufferedImage rightImage = image.getSubimage(cellWidth, 0, cellWidth,
+            cellHeight);
+
+        Cell left = new Cell(cell);
+        left.setImage(leftImage);
+        left.setWidth(Cell.Width.LEFT);
+        putCharXY(x, y, left);
+
+        Cell right = new Cell(cell);
+        right.setImage(rightImage);
+        right.setWidth(Cell.Width.RIGHT);
+        putCharXY(x + 1, y, right);
+    }
+
+    /**
+     * Render one fullwidth character with attributes.
+     *
+     * @param x column coordinate.  0 is the left-most column.
+     * @param y row coordinate.  0 is the top-most row.
+     * @param ch character to draw
+     * @param attr attributes to use (bold, foreColor, backColor)
+     */
+    public final void putFullwidthCharXY(final int x, final int y,
+        final int ch, final CellAttributes attr) {
+
+        Cell cell = new Cell(ch, attr);
+        putFullwidthCharXY(x, y, cell);
+    }
+
+    /**
+     * Render one fullwidth character with attributes.
+     *
+     * @param x column coordinate.  0 is the left-most column.
+     * @param y row coordinate.  0 is the top-most row.
+     * @param ch character to draw
+     */
+    public final void putFullwidthCharXY(final int x, final int y,
+        final int ch) {
+
+        Cell cell = new Cell(ch);
+        cell.setAttr(getAttrXY(x, y));
+        putFullwidthCharXY(x, y, cell);
+    }
+
+    /**
+     * Invert the cell color at a position, including both halves of a
+     * double-width cell.
+     *
+     * @param x column position
+     * @param y row position
+     */
+    public void invertCell(final int x, final int y) {
+        invertCell(x, y, false);
+    }
+
+    /**
+     * Invert the cell color at a position.
+     *
+     * @param x column position
+     * @param y row position
+     * @param onlyThisCell if true, only invert this cell, otherwise invert
+     * both halves of a double-width cell if necessary
+     */
+    public void invertCell(final int x, final int y,
+        final boolean onlyThisCell) {
+
+        Cell cell = getCharXY(x, y);
+        if (cell.isImage()) {
+            cell.invertImage();
+        }
+        if (cell.getForeColorRGB() < 0) {
+            cell.setForeColor(cell.getForeColor().invert());
+        } else {
+            cell.setForeColorRGB(cell.getForeColorRGB() ^ 0x00ffffff);
+        }
+        if (cell.getBackColorRGB() < 0) {
+            cell.setBackColor(cell.getBackColor().invert());
+        } else {
+            cell.setBackColorRGB(cell.getBackColorRGB() ^ 0x00ffffff);
+        }
+        putCharXY(x, y, cell);
+        if ((onlyThisCell == true) || (cell.getWidth() == Cell.Width.SINGLE)) {
+            return;
+        }
+
+        // This cell is one half of a fullwidth glyph.  Invert the other
+        // half.
+        if (cell.getWidth() == Cell.Width.LEFT) {
+            if (x < width - 1) {
+                Cell rightHalf = getCharXY(x + 1, y);
+                if (rightHalf.getWidth() == Cell.Width.RIGHT) {
+                    invertCell(x + 1, y, true);
+                    return;
+                }
+            }
+        }
+        if (cell.getWidth() == Cell.Width.RIGHT) {
+            if (x > 0) {
+                Cell leftHalf = getCharXY(x - 1, y);
+                if (leftHalf.getWidth() == Cell.Width.LEFT) {
+                    invertCell(x - 1, y, true);
+                }
+            }
+        }
+    }
+
+    /**
+     * Set a selection area on the screen.
+     *
+     * @param x0 the starting X position of the selection
+     * @param y0 the starting Y position of the selection
+     * @param x1 the ending X position of the selection
+     * @param y1 the ending Y position of the selection
+     * @param rectangle if true, this is a rectangle select
+     */
+    public void setSelection(final int x0, final int y0,
+        final int x1, final int y1, final boolean rectangle) {
+
+        int startX = x0;
+        int startY = y0;
+        int endX = x1;
+        int endY = y1;
+
+        if (((x1 < x0) && (y1 == y0))
+            || (y1 < y0)
+        ) {
+            // The user dragged from bottom-to-top and/or right-to-left.
+            // Reverse the coordinates for the inverted section.
+            startX = x1;
+            startY = y1;
+            endX = x0;
+            endY = y0;
+        }
+        if (rectangle) {
+            for (int y = startY; y <= endY; y++) {
+                for (int x = startX; x <= endX; x++) {
+                    invertCell(x, y);
+                }
+            }
+        } else {
+            if (endY > startY) {
+                for (int x = startX; x < width; x++) {
+                    invertCell(x, startY);
+                }
+                for (int y = startY + 1; y < endY; y++) {
+                    for (int x = 0; x < width; x++) {
+                        invertCell(x, y);
+                    }
+                }
+                for (int x = 0; x <= endX; x++) {
+                    invertCell(x, endY);
+                }
+            } else {
+                assert (startY == endY);
+                for (int x = startX; x <= endX; x++) {
+                    invertCell(x, startY);
+                }
+            }
+        }
+    }
+
+    /**
+     * Copy the screen selection area to the clipboard.
+     *
+     * @param clipboard the clipboard to use
+     * @param x0 the starting X position of the selection
+     * @param y0 the starting Y position of the selection
+     * @param x1 the ending X position of the selection
+     * @param y1 the ending Y position of the selection
+     * @param rectangle if true, this is a rectangle select
+     */
+    public void copySelection(final Clipboard clipboard,
+        final int x0, final int y0, final int x1, final int y1,
+        final boolean rectangle) {
+
+        StringBuilder sb = new StringBuilder();
+
+        int startX = x0;
+        int startY = y0;
+        int endX = x1;
+        int endY = y1;
+
+        if (((x1 < x0) && (y1 == y0))
+            || (y1 < y0)
+        ) {
+            // The user dragged from bottom-to-top and/or right-to-left.
+            // Reverse the coordinates for the inverted section.
+            startX = x1;
+            startY = y1;
+            endX = x0;
+            endY = y0;
+        }
+        if (rectangle) {
+            for (int y = startY; y <= endY; y++) {
+                for (int x = startX; x <= endX; x++) {
+                    sb.append(Character.toChars(getCharXY(x, y).getChar()));
+                }
+                sb.append("\n");
+            }
+        } else {
+            if (endY > startY) {
+                for (int x = startX; x < width; x++) {
+                    sb.append(Character.toChars(getCharXY(x, startY).getChar()));
+                }
+                sb.append("\n");
+                for (int y = startY + 1; y < endY; y++) {
+                    for (int x = 0; x < width; x++) {
+                        sb.append(Character.toChars(getCharXY(x, y).getChar()));
+                    }
+                    sb.append("\n");
+                }
+                for (int x = 0; x <= endX; x++) {
+                    sb.append(Character.toChars(getCharXY(x, endY).getChar()));
+                }
+            } else {
+                assert (startY == endY);
+                for (int x = startX; x <= endX; x++) {
+                    sb.append(Character.toChars(getCharXY(x, startY).getChar()));
+                }
             }
         }
+        clipboard.copyText(sb.toString());
     }
 
 }