From: Kevin Lamonte Date: Tue, 6 Aug 2019 02:35:43 +0000 (-0500) Subject: Refactor glyph code out X-Git-Url: https://git.nikiroo.be/?a=commitdiff_plain;h=0d86ab8480cabbe32fc87588304ddc795a4df14f;p=fanfix-jexer.git Refactor glyph code out --- diff --git a/src/jexer/TTerminalWindow.java b/src/jexer/TTerminalWindow.java index 945536d..c745635 100644 --- a/src/jexer/TTerminalWindow.java +++ b/src/jexer/TTerminalWindow.java @@ -44,6 +44,7 @@ import java.util.Map; import java.util.ResourceBundle; import jexer.backend.ECMA48Terminal; +import jexer.backend.GlyphMaker; import jexer.backend.MultiScreen; import jexer.backend.SwingTerminal; import jexer.bits.Cell; @@ -96,26 +97,9 @@ public class TTerminalWindow extends TScrollableWindow private boolean closeOnExit = false; /** - * System-dependent Y adjustment for text in the character cell - * (double-height). + * Double-height font. */ - private int doubleTextAdjustY = 0; - - /** - * System-dependent X adjustment for text in the character cell - * (double-height). - */ - private int doubleTextAdjustX = 0; - - /** - * Descent of a character cell in pixels (double-height). - */ - private int doubleMaxDescent = 0; - - /** - * Double-width font. - */ - private Font doubleFont = null; + private GlyphMaker doubleFont; /** * Last text width value. @@ -127,23 +111,18 @@ public class TTerminalWindow extends TScrollableWindow */ private int lastTextHeight = -1; - /** - * A cache of previously-rendered double-width glyphs. - */ - private Map glyphCache; - - /** - * A cache of previously-rendered double-width glyphs for blinking text, - * when it is not visible. - */ - private Map glyphCacheBlink; - /** * The blink state, used only by ECMA48 backend and when double-width * chars must be drawn. */ private boolean blinkState = true; + /** + * Timer flag, used only by ECMA48 backend and when double-width chars + * must be drawn. + */ + private boolean haveTimer = false; + // ------------------------------------------------------------------------ // Constructors ----------------------------------------------------------- // ------------------------------------------------------------------------ @@ -976,53 +955,24 @@ public class TTerminalWindow extends TScrollableWindow } if ((textWidth != lastTextWidth) || (textHeight != lastTextHeight)) { - // Screen size has changed, reset all fonts. - setupFonts(textHeight); + // Screen size has changed, reset the font. + setupFont(textHeight); lastTextWidth = textWidth; lastTextHeight = textHeight; } assert (doubleFont != null); - BufferedImage image = null; - if (cell.isBlink() && !cursorBlinkVisible) { - image = glyphCacheBlink.get(cell); + BufferedImage image; + if (line.getDoubleHeight() == 1) { + // Double-height top half: don't draw the underline. + Cell newCell = new Cell(); + newCell.setTo(cell); + newCell.setUnderline(false); + image = doubleFont.getImage(newCell, textWidth * 2, textHeight * 2, + cursorBlinkVisible); } else { - image = glyphCache.get(cell); - } - if (image == null) { - // Generate glyph and draw it to an image. - image = new BufferedImage(textWidth * 2, textHeight * 2, - BufferedImage.TYPE_INT_ARGB); - Graphics2D gr2 = image.createGraphics(); - gr2.setFont(doubleFont); - - // Draw the background rectangle, then the foreground character. - gr2.setColor(SwingTerminal.attrToBackgroundColor(cell)); - gr2.fillRect(0, 0, image.getWidth(), image.getHeight()); - if (!cell.isBlink() - || (cell.isBlink() && cursorBlinkVisible) - ) { - gr2.setColor(SwingTerminal.attrToForegroundColor(cell)); - char [] chars = new char[1]; - chars[0] = cell.getChar(); - gr2.drawChars(chars, 0, 1, doubleTextAdjustX, - (textHeight * 2) - doubleMaxDescent + doubleTextAdjustY); - - if (cell.isUnderline() && (line.getDoubleHeight() != 1)) { - gr2.fillRect(0, textHeight - 2, textWidth, 2); - } - } - gr2.dispose(); - - // Now save this generated image, using a new key that will not - // be mutated by invertCell(). - Cell key = new Cell(); - key.setTo(cell); - if (cell.isBlink() && !cursorBlinkVisible) { - glyphCacheBlink.put(key, image); - } else { - glyphCache.put(key, image); - } + image = doubleFont.getImage(cell, textWidth * 2, textHeight * 2, + cursorBlinkVisible); } // Now that we have the double-wide glyph drawn, copy the right @@ -1034,6 +984,11 @@ public class TTerminalWindow extends TScrollableWindow right.setChar(' '); BufferedImage leftImage = null; BufferedImage rightImage = null; + /* + System.err.println("image " + image + " textWidth " + textWidth + + " textHeight " + textHeight); + */ + switch (line.getDoubleHeight()) { case 1: // Top half double height @@ -1071,56 +1026,31 @@ public class TTerminalWindow extends TScrollableWindow } /** - * Set up the single and double-width fonts. + * Set up the double-width font. * * @param fontSize the size of font to request for the single-width font. * The double-width font will be 2x this value. */ - private void setupFonts(final int fontSize) { - try { - ClassLoader loader = Thread.currentThread().getContextClassLoader(); - InputStream in = loader.getResourceAsStream(SwingTerminal.FONTFILE); - Font terminusRoot = Font.createFont(Font.TRUETYPE_FONT, in); - Font terminusDouble = terminusRoot.deriveFont(Font.PLAIN, - fontSize * 2); - doubleFont = terminusDouble; - } catch (java.awt.FontFormatException e) { - new TExceptionDialog(getApplication(), e); - doubleFont = new Font(Font.MONOSPACED, Font.PLAIN, fontSize * 2); - } catch (java.io.IOException e) { - new TExceptionDialog(getApplication(), e); - doubleFont = new Font(Font.MONOSPACED, Font.PLAIN, fontSize * 2); - } - - // Get font descent. - BufferedImage image = new BufferedImage(fontSize * 10, fontSize * 10, - BufferedImage.TYPE_INT_ARGB); - Graphics2D gr = image.createGraphics(); - gr.setFont(doubleFont); - FontMetrics fm = gr.getFontMetrics(); - doubleMaxDescent = fm.getMaxDescent(); - - gr.dispose(); - - // (Re)create the glyph caches. - glyphCache = new HashMap(); - glyphCacheBlink = new HashMap(); + private void setupFont(final int fontSize) { + doubleFont = GlyphMaker.getDefault().size(fontSize * 2); // Special case: the ECMA48 backend needs to have a timer to drive // its blink state. if (getScreen() instanceof jexer.backend.ECMA48Terminal) { - // Blink every 500 millis. - long millis = 500; - getApplication().addTimer(millis, true, - new TAction() { - public void DO() { - blinkState = !blinkState; - getApplication().doRepaint(); + if (!haveTimer) { + // Blink every 500 millis. + long millis = 500; + getApplication().addTimer(millis, true, + new TAction() { + public void DO() { + blinkState = !blinkState; + getApplication().doRepaint(); + } } - } - ); + ); + haveTimer = true; + } } - } } diff --git a/src/jexer/backend/GlyphMaker.java b/src/jexer/backend/GlyphMaker.java new file mode 100644 index 0000000..0094145 --- /dev/null +++ b/src/jexer/backend/GlyphMaker.java @@ -0,0 +1,400 @@ +/* + * Jexer - Java Text User Interface + * + * The MIT License (MIT) + * + * 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"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + * @author Kevin Lamonte [kevin.lamonte@gmail.com] + * @version 1 + */ +package jexer.backend; + +import java.awt.Font; +import java.awt.FontMetrics; +import java.awt.Graphics2D; +import java.awt.geom.Rectangle2D; +import java.awt.image.BufferedImage; +import java.io.InputStream; +import java.io.IOException; +import java.util.HashMap; + +import jexer.bits.Cell; + +/** + * GlyphMaker creates glyphs as bitmaps from a font. + */ +public class GlyphMaker { + + // ------------------------------------------------------------------------ + // 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 -------------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * If true, enable debug messages. + */ + 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 boolean gotFontDimensions = false; + + /** + * The currently selected font. + */ + private Font font = null; + + /** + * The currently selected font size in points. + */ + private int fontSize = 16; + + /** + * Width of a character cell in pixels. + */ + private int textWidth = 1; + + /** + * Height of a character cell in pixels. + */ + private int textHeight = 1; + + /** + * Width of a character cell in pixels, as reported by font. + */ + private int fontTextWidth = 1; + + /** + * Height of a character cell in pixels, as reported by font. + */ + private int fontTextHeight = 1; + + /** + * Descent of a character cell in pixels. + */ + private int maxDescent = 0; + + /** + * System-dependent Y adjustment for text in the character cell. + */ + private int textAdjustY = 0; + + /** + * System-dependent X adjustment for text in the character cell. + */ + private int textAdjustX = 0; + + /** + * System-dependent height adjustment for text in the character cell. + */ + private int textAdjustHeight = 0; + + /** + * System-dependent width adjustment for text in the character cell. + */ + private int textAdjustWidth = 0; + + /** + * A cache of previously-rendered glyphs for blinking text, when it is + * not visible. + */ + private HashMap glyphCacheBlink; + + /** + * A cache of previously-rendered glyphs for non-blinking, or + * blinking-and-visible, text. + */ + private HashMap glyphCache; + + // ------------------------------------------------------------------------ + // 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 fontSize the size of font to use + */ + public GlyphMaker(final String fontName, final int fontSize) { + font = new Font(fontName, Font.PLAIN, fontSize); + } + + // ------------------------------------------------------------------------ + // GlyphMaker ------------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * 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. + * + * @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) { + + if (gotFontDimensions == false) { + // Lazy-load the text width/height and adjustments. + getFontDimensions(); + } + + BufferedImage image = null; + if (cell.isBlink() && !blinkVisible) { + image = glyphCacheBlink.get(cell); + } else { + image = glyphCache.get(cell); + } + if (image != null) { + return image; + } + + // Generate glyph and draw it. + image = new BufferedImage(cellWidth, cellHeight, + BufferedImage.TYPE_INT_ARGB); + Graphics2D gr2 = image.createGraphics(); + gr2.setFont(font); + + Cell cellColor = new Cell(); + cellColor.setTo(cell); + + // Check for reverse + if (cell.isReverse()) { + cellColor.setForeColor(cell.getBackColor()); + cellColor.setBackColor(cell.getForeColor()); + } + + // Draw the background rectangle, then the foreground character. + gr2.setColor(SwingTerminal.attrToBackgroundColor(cellColor)); + gr2.fillRect(0, 0, cellWidth, cellHeight); + + // Handle blink and underline + if (!cell.isBlink() + || (cell.isBlink() && blinkVisible) + ) { + gr2.setColor(SwingTerminal.attrToForegroundColor(cellColor)); + char [] chars = new char[1]; + chars[0] = cell.getChar(); + gr2.drawChars(chars, 0, 1, textAdjustX, + cellHeight - maxDescent + textAdjustY); + + if (cell.isUnderline()) { + gr2.fillRect(0, cellHeight - 2, cellWidth, 2); + } + } + gr2.dispose(); + + // We need a new key that will not be mutated by invertCell(). + Cell key = new Cell(); + key.setTo(cell); + if (cell.isBlink() && !blinkVisible) { + glyphCacheBlink.put(key, image); + } else { + glyphCache.put(key, image); + } + + return image; + } + + /** + * Figure out my font dimensions. + */ + private void getFontDimensions() { + glyphCacheBlink = new HashMap(); + glyphCache = new HashMap(); + + BufferedImage image = new BufferedImage(fontSize * 2, fontSize * 2, + BufferedImage.TYPE_INT_ARGB); + Graphics2D gr = image.createGraphics(); + gr.setFont(font); + FontMetrics fm = gr.getFontMetrics(); + maxDescent = fm.getMaxDescent(); + Rectangle2D bounds = fm.getMaxCharBounds(gr); + int leading = fm.getLeading(); + fontTextWidth = (int)Math.round(bounds.getWidth()); + // fontTextHeight = (int)Math.round(bounds.getHeight()) - maxDescent; + + // This produces the same number, but works better for ugly + // monospace. + fontTextHeight = fm.getMaxAscent() + maxDescent - leading; + gr.dispose(); + + textHeight = fontTextHeight + textAdjustHeight; + textWidth = fontTextWidth + textAdjustWidth; + + gotFontDimensions = true; + } + +}