import jexer.bits.Cell;
/**
- * GlyphMaker creates glyphs as bitmaps from a font.
+ * GlyphMakerFont creates glyphs as bitmaps from a font.
*/
-public class GlyphMaker {
+class GlyphMakerFont {
// ------------------------------------------------------------------------
// Constants --------------------------------------------------------------
// ------------------------------------------------------------------------
- /**
- * The mono font resource filename (terminus).
- */
- public static final String MONO = "terminus-ttf-4.39/TerminusTTF-Bold-4.39.ttf";
-
- /**
- * The CJK font resource filename.
- */
- public static final String CJK = "NotoSansMonoCJKhk-Regular.otf";
-
// ------------------------------------------------------------------------
// Variables --------------------------------------------------------------
// ------------------------------------------------------------------------
*/
private static boolean DEBUG = false;
- /**
- * The instance that has the mono (default) font.
- */
- private static GlyphMaker INSTANCE_MONO;
-
- /**
- * The instance that has the CJK font.
- */
- private static GlyphMaker INSTANCE_CJK;
-
/**
* If true, we were successful at getting the font dimensions.
*/
*/
private Font font = null;
- /**
- * The currently selected font size in points.
- */
- private int fontSize = 16;
-
/**
* Width of a character cell in pixels.
*/
// Constructors -----------------------------------------------------------
// ------------------------------------------------------------------------
- /**
- * Private constructor used by the static instance methods.
- *
- * @param font the font to use
- */
- private GlyphMaker(final Font font) {
- this.font = font;
- fontSize = font.getSize();
- }
-
/**
* Public constructor.
*
- * @param fontName the name of the font to use
+ * @param filename the resource filename of the font to use
* @param fontSize the size of font to use
*/
- public GlyphMaker(final String fontName, final int fontSize) {
- font = new Font(fontName, Font.PLAIN, fontSize);
+ public GlyphMakerFont(final String filename, final int fontSize) {
+ Font fontRoot = null;
+ try {
+ ClassLoader loader = Thread.currentThread().getContextClassLoader();
+ InputStream in = loader.getResourceAsStream(filename);
+ fontRoot = Font.createFont(Font.TRUETYPE_FONT, in);
+ font = fontRoot.deriveFont(Font.PLAIN, fontSize);
+ } catch (java.awt.FontFormatException e) {
+ e.printStackTrace();
+ font = new Font(Font.MONOSPACED, Font.PLAIN, fontSize);
+ } catch (java.io.IOException e) {
+ e.printStackTrace();
+ font = new Font(Font.MONOSPACED, Font.PLAIN, fontSize);
+ }
}
// ------------------------------------------------------------------------
- // GlyphMaker -------------------------------------------------------------
+ // GlyphMakerFont ---------------------------------------------------------
// ------------------------------------------------------------------------
- /**
- * Obtain the GlyphMaker instance that uses the default monospace font.
- *
- * @return the instance
- */
- public static GlyphMaker getDefault() {
-
- synchronized (GlyphMaker.class) {
- if (INSTANCE_MONO != null) {
- return INSTANCE_MONO;
- }
-
- int fallbackFontSize = 16;
- Font monoRoot = null;
- try {
- ClassLoader loader = Thread.currentThread().getContextClassLoader();
- InputStream in = loader.getResourceAsStream(MONO);
- monoRoot = Font.createFont(Font.TRUETYPE_FONT, in);
- } catch (java.awt.FontFormatException e) {
- e.printStackTrace();
- monoRoot = new Font(Font.MONOSPACED, Font.PLAIN,
- fallbackFontSize);
- } catch (java.io.IOException e) {
- e.printStackTrace();
- monoRoot = new Font(Font.MONOSPACED, Font.PLAIN,
- fallbackFontSize);
- }
- INSTANCE_MONO = new GlyphMaker(monoRoot);
- return INSTANCE_MONO;
- }
- }
-
- /**
- * Obtain the GlyphMaker instance that uses the CJK font.
- *
- * @return the instance
- */
- public static GlyphMaker getCJK() {
-
- synchronized (GlyphMaker.class) {
- if (INSTANCE_CJK != null) {
- return INSTANCE_CJK;
- }
-
- int fallbackFontSize = 16;
- Font cjkRoot = null;
- try {
- ClassLoader loader = Thread.currentThread().getContextClassLoader();
- InputStream in = loader.getResourceAsStream(CJK);
- cjkRoot = Font.createFont(Font.TRUETYPE_FONT, in);
- } catch (java.awt.FontFormatException e) {
- e.printStackTrace();
- cjkRoot = new Font(Font.MONOSPACED, Font.PLAIN,
- fallbackFontSize);
- } catch (java.io.IOException e) {
- e.printStackTrace();
- cjkRoot = new Font(Font.MONOSPACED, Font.PLAIN,
- fallbackFontSize);
- }
- INSTANCE_CJK = new GlyphMaker(cjkRoot);
- return INSTANCE_CJK;
- }
- }
-
- /**
- * Obtain the GlyphMaker instance that uses the correct font for this
- * character.
- *
- * @param ch the character
- * @return the instance
- */
- public static GlyphMaker getInstance(final int ch) {
- if (((ch >= 0x4e00) && (ch <= 0x9fff))
- || ((ch >= 0x3400) && (ch <= 0x4dbf))
- || ((ch >= 0x20000) && (ch <= 0x2ebef))
- ) {
- return getCJK();
- }
- return getDefault();
- }
-
- /**
- * Get a derived font at a specific size.
- *
- * @param fontSize the size to use
- * @return a new instance at that font size
- */
- public GlyphMaker size(final int fontSize) {
- GlyphMaker maker = new GlyphMaker(font.deriveFont(Font.PLAIN,
- fontSize));
- return maker;
- }
-
- /**
- * Get a glyph image, using the font's idea of cell width and height.
- *
- * @param cell the character to draw
- * @return the glyph as an image
- */
- public BufferedImage getImage(final Cell cell) {
- return getImage(cell, textWidth, textHeight, true);
- }
-
/**
* Get a glyph image.
*
glyphCacheBlink = new HashMap<Cell, BufferedImage>();
glyphCache = new HashMap<Cell, BufferedImage>();
- BufferedImage image = new BufferedImage(fontSize * 2, fontSize * 2,
- BufferedImage.TYPE_INT_ARGB);
+ BufferedImage image = new BufferedImage(font.getSize() * 2,
+ font.getSize() * 2, BufferedImage.TYPE_INT_ARGB);
Graphics2D gr = image.createGraphics();
gr.setFont(font);
FontMetrics fm = gr.getFontMetrics();
}
}
+
+/**
+ * GlyphMaker presents unified interface to all of its supported fonts to
+ * clients.
+ */
+public class GlyphMaker {
+
+ // ------------------------------------------------------------------------
+ // Constants --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * The mono font resource filename (terminus).
+ */
+ private static final String MONO = "terminus-ttf-4.39/TerminusTTF-Bold-4.39.ttf";
+
+ /**
+ * The CJKhk font resource filename.
+ */
+ // private static final String CJKhk = "NotoSansMonoCJKhk-Regular.otf";
+
+ /**
+ * The CJKkr font resource filename.
+ */
+ // private static final String CJKkr = "NotoSansMonoCJKkr-Regular.otf";
+
+ /**
+ * The CJKtc font resource filename.
+ */
+ private static final String CJKtc = "NotoSansMonoCJKtc-Regular.otf";
+
+ // ------------------------------------------------------------------------
+ // Variables --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * If true, enable debug messages.
+ */
+ private static boolean DEBUG = false;
+
+ /**
+ * Cache of font bundles by size.
+ */
+ private static HashMap<Integer, GlyphMaker> makers = new HashMap<Integer, GlyphMaker>();
+
+ /**
+ * The instance that has the mono (default) font.
+ */
+ private GlyphMakerFont makerMono;
+
+ /**
+ * The instance that has the CJKhk font.
+ */
+ // private GlyphMakerFont makerCJKhk;
+
+ /**
+ * The instance that has the CJKkr font.
+ */
+ // private GlyphMakerFont makerCJKkr;
+
+ /**
+ * The instance that has the CJKtc font.
+ */
+ private GlyphMakerFont makerCJKtc;
+
+ // ------------------------------------------------------------------------
+ // Constructors -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Create an instance with references to the necessary fonts.
+ *
+ * @param fontSize the size of these fonts in pixels
+ */
+ private GlyphMaker(final int fontSize) {
+ makerMono = new GlyphMakerFont(MONO, fontSize);
+ // makerCJKhk = new GlyphMakerFont(CJKhk, fontSize);
+ // makerCJKkr = new GlyphMakerFont(CJKkr, fontSize);
+ makerCJKtc = new GlyphMakerFont(CJKtc, fontSize);
+ }
+
+ // ------------------------------------------------------------------------
+ // GlyphMaker -------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Obtain the GlyphMaker instance for a particular font size.
+ *
+ * @param fontSize the size of these fonts in pixels
+ * @return the instance
+ */
+ public static GlyphMaker getInstance(final int fontSize) {
+ synchronized (GlyphMaker.class) {
+ GlyphMaker maker = makers.get(fontSize);
+ if (maker == null) {
+ maker = new GlyphMaker(fontSize);
+ makers.put(fontSize, maker);
+ }
+ return maker;
+ }
+ }
+
+ /**
+ * Get a glyph image.
+ *
+ * @param cell the character to draw
+ * @param cellWidth the width of the text cell to draw into
+ * @param cellHeight the height of the text cell to draw into
+ * @return the glyph as an image
+ */
+ public BufferedImage getImage(final Cell cell, final int cellWidth,
+ final int cellHeight) {
+
+ return getImage(cell, cellWidth, cellHeight, true);
+ }
+
+ /**
+ * Get a glyph image.
+ *
+ * @param cell the character to draw
+ * @param cellWidth the width of the text cell to draw into
+ * @param cellHeight the height of the text cell to draw into
+ * @param blinkVisible if true, the cell is visible if it is blinking
+ * @return the glyph as an image
+ */
+ public BufferedImage getImage(final Cell cell, final int cellWidth,
+ final int cellHeight, final boolean blinkVisible) {
+
+ char ch = cell.getChar();
+ /*
+ if ((ch >= 0x4e00) && (ch <= 0x9fff)) {
+ return makerCJKhk.getImage(cell, cellWidth, cellHeight, blinkVisible);
+ }
+ if ((ch >= 0x4e00) && (ch <= 0x9fff)) {
+ return makerCJKkr.getImage(cell, cellWidth, cellHeight, blinkVisible);
+ }
+ */
+ if ((ch >= 0x2e80) && (ch <= 0x9fff)) {
+ return makerCJKtc.getImage(cell, cellWidth, cellHeight, blinkVisible);
+ }
+
+ // When all else fails, use the default.
+ return makerMono.getImage(cell, cellWidth, cellHeight, blinkVisible);
+ }
+
+}
// Constants --------------------------------------------------------------
// ------------------------------------------------------------------------
+ /**
+ * How this cell needs to be displayed if it is part of a larger glyph.
+ */
+ public enum Width {
+ /**
+ * This cell is an entire glyph on its own.
+ */
+ SINGLE,
+
+ /**
+ * This cell is the left half of a wide glyph.
+ */
+ LEFT,
+
+ /**
+ * This cell is the right half of a wide glyph.
+ */
+ RIGHT,
+ }
+
/**
* The special "this cell is unset" (null) value. This is the Unicode
* "not a character" value.
*/
private char ch;
+ /**
+ * The display width of this cell.
+ */
+ private Width width = Width.SINGLE;
+
/**
* The image at this cell.
*/
// Cell -------------------------------------------------------------------
// ------------------------------------------------------------------------
-
/**
* Set the image data for this cell.
*
public void setImage(final BufferedImage image) {
this.image = image;
imageHashCode = image.hashCode();
+ width = Width.SINGLE;
}
/**
this.ch = ch;
}
+ /**
+ * Getter for cell width.
+ *
+ * @return Width.SINGLE, Width.LEFT, or Width.RIGHT
+ */
+ public Width getWidth() {
+ return width;
+ }
+
+ /**
+ * Setter for cell width.
+ *
+ * @param ch new cell width, one of Width.SINGLE, Width.LEFT, or
+ * Width.RIGHT
+ */
+ public void setWidth(final Width width) {
+ this.width = width;
+ }
+
/**
* Reset this cell to a blank.
*/
public void reset() {
super.reset();
ch = ' ';
+ width = Width.SINGLE;
image = null;
imageHashCode = 0;
invertedImage = null;
public void unset() {
super.reset();
ch = UNSET_VALUE;
+ width = Width.SINGLE;
image = null;
imageHashCode = 0;
invertedImage = null;
&& !isProtect()
&& !isRGB()
&& !isImage()
+ && (width == Width.SINGLE)
&& (ch == ' ')
) {
return true;
}
// Normal case: character and attributes must match.
- if (ch == that.ch) {
+ if ((ch == that.ch) && (width == that.width)) {
return super.equals(rhs);
}
return false;
int hash = A;
hash = (B * hash) + super.hashCode();
hash = (B * hash) + (int)ch;
+ hash = (B * hash) + width.hashCode();
if (image != null) {
/*
hash = (B * hash) + image.hashCode();
this.image = null;
this.imageHashCode = 0;
this.backgroundHashCode = 0;
+ this.width = Width.SINGLE;
super.setTo(thatAttr);
if (rhs instanceof Cell) {
Cell that = (Cell) rhs;
this.ch = that.ch;
+ this.width = that.width;
this.image = that.image;
this.invertedImage = that.invertedImage;
this.background = that.background;
import java.util.List;
import jexer.TKeypress;
-import jexer.event.TMouseEvent;
+import jexer.backend.GlyphMaker;
import jexer.bits.Color;
import jexer.bits.Cell;
import jexer.bits.CellAttributes;
+import jexer.bits.StringUtils;
+import jexer.event.TMouseEvent;
import jexer.io.ReadTimeoutException;
import jexer.io.TimeoutInputStream;
import static jexer.TKeypress.*;
*/
private int textHeight = 20;
+ /**
+ * 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.
+ */
+ GlyphMaker glyphMaker = null;
+
/**
* DECSC/DECRC save/restore a subset of the total state. This class
* encapsulates those specific flags/modes.
private void printCharacter(final char ch) {
int rightMargin = this.rightMargin;
+ if (StringUtils.width(ch) == 2) {
+ // This is a full-width character. Save two spaces, and then
+ // draw the character as two image halves.
+ int x0 = currentState.cursorX;
+ int y0 = currentState.cursorY;
+ printCharacter(' ');
+ printCharacter(' ');
+ if ((currentState.cursorX == x0 + 2)
+ && (currentState.cursorY == y0)
+ ) {
+ // We can draw both halves of the character.
+ drawHalves(x0, y0, x0 + 1, y0, ch);
+ } else if ((currentState.cursorX == x0 + 1)
+ && (currentState.cursorY == y0)
+ ) {
+ // VT100 line wrap behavior: we should be at the right
+ // margin. We can draw both halves of the character.
+ drawHalves(x0, y0, x0 + 1, y0, ch);
+ } else {
+ // The character splits across the line. Draw the entire
+ // character on the new line, giving one more space for it.
+ x0 = currentState.cursorX - 1;
+ y0 = currentState.cursorY;
+ printCharacter(' ');
+ drawHalves(x0, y0, x0 + 1, y0, ch);
+ }
+ return;
+ }
+
// Check if we have double-width, and if so chop at 40/66 instead of
// 80/132
if (display.get(currentState.cursorY).isDoubleWidth()) {
CellAttributes newCellAttributes = (CellAttributes) newCell;
newCellAttributes.setTo(currentState.attr);
DisplayLine line = display.get(currentState.cursorY);
- // Insert mode special case
- if (insertMode == true) {
- line.insert(currentState.cursorX, newCell);
- } else {
- // Replace an existing character
- line.replace(currentState.cursorX, newCell);
- }
- // Increment horizontal
- if (wrapLineFlag == false) {
- currentState.cursorX++;
- if (currentState.cursorX > rightMargin) {
- currentState.cursorX--;
+ if (StringUtils.width(ch) == 1) {
+ // Insert mode special case
+ if (insertMode == true) {
+ line.insert(currentState.cursorX, newCell);
+ } else {
+ // Replace an existing character
+ line.replace(currentState.cursorX, newCell);
+ }
+
+ // Increment horizontal
+ if (wrapLineFlag == false) {
+ currentState.cursorX++;
+ if (currentState.cursorX > rightMargin) {
+ currentState.cursorX--;
+ }
}
}
}
}
+ /**
+ * Draw the left and right cells of a two-cell-wide (full-width) glyph.
+ *
+ * @param leftX the x position to draw the left half to
+ * @param leftY the y position to draw the left half to
+ * @param rightX the x position to draw the right half to
+ * @param rightY the y position to draw the right half to
+ * @param ch the character to draw
+ */
+ private void drawHalves(final int leftX, final int leftY,
+ final int rightX, final int rightY, final char ch) {
+
+ // System.err.println("drawHalves(): " + Integer.toHexString(ch));
+
+ if (lastTextHeight != textHeight) {
+ glyphMaker = GlyphMaker.getInstance(textHeight);
+ lastTextHeight = textHeight;
+ }
+
+ Cell cell = new Cell(ch);
+ cell.setAttr(currentState.attr);
+ BufferedImage image = glyphMaker.getImage(cell, textWidth * 2,
+ textHeight);
+ BufferedImage leftImage = image.getSubimage(0, 0, textWidth,
+ textHeight);
+ BufferedImage rightImage = image.getSubimage(textWidth, 0, textWidth,
+ textHeight);
+
+ Cell left = new Cell();
+ left.setTo(cell);
+ left.setImage(leftImage);
+ left.setWidth(Cell.Width.LEFT);
+ display.get(leftY).replace(leftX, left);
+
+ Cell right = new Cell();
+ right.setTo(cell);
+ right.setImage(rightImage);
+ right.setWidth(Cell.Width.RIGHT);
+ display.get(rightY).replace(rightX, right);
+ }
+
}