package com.termux.view; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.PorterDuff; import android.graphics.Typeface; import com.termux.terminal.TerminalBuffer; import com.termux.terminal.TerminalEmulator; import com.termux.terminal.TerminalRow; import com.termux.terminal.TextStyle; import com.termux.terminal.WcWidth; /** * Renderer of a {@link TerminalEmulator} into a {@link Canvas}. * <p/> * Saves font metrics, so needs to be recreated each time the typeface or font size changes. */ final class TerminalRenderer { final int mTextSize; final Typeface mTypeface; private final Paint mTextPaint = new Paint(); /** The width of a single mono spaced character obtained by {@link Paint#measureText(String)} on a single 'X'. */ final float mFontWidth; /** The {@link Paint#getFontSpacing()}. See http://www.fampennings.nl/maarten/android/08numgrid/font.png */ final int mFontLineSpacing; /** The {@link Paint#ascent()}. See http://www.fampennings.nl/maarten/android/08numgrid/font.png */ private final int mFontAscent; /** The {@link #mFontLineSpacing} + {@link #mFontAscent}. */ final int mFontLineSpacingAndAscent; private final float[] asciiMeasures = new float[127]; public TerminalRenderer(int textSize, Typeface typeface) { mTextSize = textSize; mTypeface = typeface; mTextPaint.setTypeface(typeface); mTextPaint.setAntiAlias(true); mTextPaint.setTextSize(textSize); mFontLineSpacing = (int) Math.ceil(mTextPaint.getFontSpacing()); mFontAscent = (int) Math.ceil(mTextPaint.ascent()); mFontLineSpacingAndAscent = mFontLineSpacing + mFontAscent; mFontWidth = mTextPaint.measureText("X"); StringBuilder sb = new StringBuilder(" "); for (int i = 0; i < asciiMeasures.length; i++) { sb.setCharAt(0, (char) i); asciiMeasures[i] = mTextPaint.measureText(sb, 0, 1); } } /** Render the terminal to a canvas with at a specified row scroll, and an optional rectangular selection. */ public final void render(TerminalEmulator mEmulator, Canvas canvas, int topRow, int selectionY1, int selectionY2, int selectionX1, int selectionX2) { final boolean reverseVideo = mEmulator.isReverseVideo(); final int endRow = topRow + mEmulator.mRows; final int columns = mEmulator.mColumns; final int cursorCol = mEmulator.getCursorCol(); final int cursorRow = mEmulator.getCursorRow(); final boolean cursorVisible = mEmulator.isShowingCursor(); final TerminalBuffer screen = mEmulator.getScreen(); final int[] palette = mEmulator.mColors.mCurrentColors; final int cursorShape = mEmulator.getCursorStyle(); if (reverseVideo) canvas.drawColor(palette[TextStyle.COLOR_INDEX_FOREGROUND], PorterDuff.Mode.SRC); float heightOffset = mFontLineSpacingAndAscent; for (int row = topRow; row < endRow; row++) { heightOffset += mFontLineSpacing; final int cursorX = (row == cursorRow && cursorVisible) ? cursorCol : -1; int selx1 = -1, selx2 = -1; if (row >= selectionY1 && row <= selectionY2) { if (row == selectionY1) selx1 = selectionX1; selx2 = (row == selectionY2) ? selectionX2 : mEmulator.mColumns; } TerminalRow lineObject = screen.allocateFullLineIfNecessary(screen.externalToInternalRow(row)); final char[] line = lineObject.mText; final int charsUsedInLine = lineObject.getSpaceUsed(); long lastRunStyle = 0; boolean lastRunInsideCursor = false; int lastRunStartColumn = -1; int lastRunStartIndex = 0; boolean lastRunFontWidthMismatch = false; int currentCharIndex = 0; float measuredWidthForRun = 0.f; for (int column = 0; column < columns; ) { final char charAtIndex = line[currentCharIndex]; final boolean charIsHighsurrogate = Character.isHighSurrogate(charAtIndex); final int charsForCodePoint = charIsHighsurrogate ? 2 : 1; final int codePoint = charIsHighsurrogate ? Character.toCodePoint(charAtIndex, line[currentCharIndex + 1]) : charAtIndex; final int codePointWcWidth = WcWidth.width(codePoint); final boolean insideCursor = (column >= selx1 && column <= selx2) || (cursorX == column || (codePointWcWidth == 2 && cursorX == column + 1)); final long style = lineObject.getStyle(column); // Check if the measured text width for this code point is not the same as that expected by wcwidth(). // This could happen for some fonts which are not truly monospace, or for more exotic characters such as // smileys which android font renders as wide. // If this is detected, we draw this code point scaled to match what wcwidth() expects. final float measuredCodePointWidth = (codePoint < asciiMeasures.length) ? asciiMeasures[codePoint] : mTextPaint.measureText(line, currentCharIndex, charsForCodePoint); final boolean fontWidthMismatch = Math.abs(measuredCodePointWidth / mFontWidth - codePointWcWidth) > 0.01; if (style != lastRunStyle || insideCursor != lastRunInsideCursor || fontWidthMismatch || lastRunFontWidthMismatch) { if (column == 0) { // Skip first column as there is nothing to draw, just record the current style. } else { final int columnWidthSinceLastRun = column - lastRunStartColumn; final int charsSinceLastRun = currentCharIndex - lastRunStartIndex; int cursorColor = lastRunInsideCursor ? mEmulator.mColors.mCurrentColors[TextStyle.COLOR_INDEX_CURSOR] : 0; drawTextRun(canvas, line, palette, heightOffset, lastRunStartColumn, columnWidthSinceLastRun, lastRunStartIndex, charsSinceLastRun, measuredWidthForRun, cursorColor, cursorShape, lastRunStyle, reverseVideo); } measuredWidthForRun = 0.f; lastRunStyle = style; lastRunInsideCursor = insideCursor; lastRunStartColumn = column; lastRunStartIndex = currentCharIndex; lastRunFontWidthMismatch = fontWidthMismatch; } measuredWidthForRun += measuredCodePointWidth; column += codePointWcWidth; currentCharIndex += charsForCodePoint; while (currentCharIndex < charsUsedInLine && WcWidth.width(line, currentCharIndex) <= 0) { // Eat combining chars so that they are treated as part of the last non-combining code point, // instead of e.g. being considered inside the cursor in the next run. currentCharIndex += Character.isHighSurrogate(line[currentCharIndex]) ? 2 : 1; } } final int columnWidthSinceLastRun = columns - lastRunStartColumn; final int charsSinceLastRun = currentCharIndex - lastRunStartIndex; int cursorColor = lastRunInsideCursor ? mEmulator.mColors.mCurrentColors[TextStyle.COLOR_INDEX_CURSOR] : 0; drawTextRun(canvas, line, palette, heightOffset, lastRunStartColumn, columnWidthSinceLastRun, lastRunStartIndex, charsSinceLastRun, measuredWidthForRun, cursorColor, cursorShape, lastRunStyle, reverseVideo); } } private void drawTextRun(Canvas canvas, char[] text, int[] palette, float y, int startColumn, int runWidthColumns, int startCharIndex, int runWidthChars, float mes, int cursor, int cursorStyle, long textStyle, boolean reverseVideo) { int foreColor = TextStyle.decodeForeColor(textStyle); final int effect = TextStyle.decodeEffect(textStyle); int backColor = TextStyle.decodeBackColor(textStyle); final boolean bold = (effect & (TextStyle.CHARACTER_ATTRIBUTE_BOLD | TextStyle.CHARACTER_ATTRIBUTE_BLINK)) != 0; final boolean underline = (effect & TextStyle.CHARACTER_ATTRIBUTE_UNDERLINE) != 0; final boolean italic = (effect & TextStyle.CHARACTER_ATTRIBUTE_ITALIC) != 0; final boolean strikeThrough = (effect & TextStyle.CHARACTER_ATTRIBUTE_STRIKETHROUGH) != 0; final boolean dim = (effect & TextStyle.CHARACTER_ATTRIBUTE_DIM) != 0; if ((foreColor & 0xff000000) != 0xff000000) { // Let bold have bright colors if applicable (one of the first 8): if (bold && foreColor >= 0 && foreColor < 8) foreColor += 8; foreColor = palette[foreColor]; } if ((backColor & 0xff000000) != 0xff000000) { backColor = palette[backColor]; } // Reverse video here if _one and only one_ of the reverse flags are set: final boolean reverseVideoHere = reverseVideo ^ (effect & (TextStyle.CHARACTER_ATTRIBUTE_INVERSE)) != 0; if (reverseVideoHere) { int tmp = foreColor; foreColor = backColor; backColor = tmp; } float left = startColumn * mFontWidth; float right = left + runWidthColumns * mFontWidth; mes = mes / mFontWidth; boolean savedMatrix = false; if (Math.abs(mes - runWidthColumns) > 0.01) { canvas.save(); canvas.scale(runWidthColumns / mes, 1.f); left *= mes / runWidthColumns; right *= mes / runWidthColumns; savedMatrix = true; } if (backColor != palette[TextStyle.COLOR_INDEX_BACKGROUND]) { // Only draw non-default background. mTextPaint.setColor(backColor); canvas.drawRect(left, y - mFontLineSpacingAndAscent + mFontAscent, right, y, mTextPaint); } if (cursor != 0) { mTextPaint.setColor(cursor); float cursorHeight = mFontLineSpacingAndAscent - mFontAscent; if (cursorStyle == TerminalEmulator.CURSOR_STYLE_UNDERLINE) cursorHeight /= 4.; else if (cursorStyle == TerminalEmulator.CURSOR_STYLE_BAR) right -= ((right - left) * 3) / 4.; canvas.drawRect(left, y - cursorHeight, right, y, mTextPaint); } if ((effect & TextStyle.CHARACTER_ATTRIBUTE_INVISIBLE) == 0) { if (dim) { int red = (0xFF & (foreColor >> 16)); int green = (0xFF & (foreColor >> 8)); int blue = (0xFF & foreColor); // Dim color handling used by libvte which in turn took it from xterm // (https://bug735245.bugzilla-attachments.gnome.org/attachment.cgi?id=284267): red = red * 2 / 3; green = green * 2 / 3; blue = blue * 2 / 3; foreColor = 0xFF000000 + (red << 16) + (green << 8) + blue; } mTextPaint.setFakeBoldText(bold); mTextPaint.setUnderlineText(underline); mTextPaint.setTextSkewX(italic ? -0.35f : 0.f); mTextPaint.setStrikeThruText(strikeThrough); mTextPaint.setColor(foreColor); // The text alignment is the default Paint.Align.LEFT. canvas.drawText(text, startCharIndex, runWidthChars, left, y - mFontLineSpacingAndAscent, mTextPaint); } if (savedMatrix) canvas.restore(); } }