mirror of
https://github.com/fankes/termux-app.git
synced 2025-09-06 02:35:19 +08:00
234 lines
10 KiB
Java
234 lines
10 KiB
Java
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}.
|
|
*
|
|
* 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;
|
|
|
|
int fillColor = palette[reverseVideo ? TextStyle.COLOR_INDEX_FOREGROUND : TextStyle.COLOR_INDEX_BACKGROUND];
|
|
canvas.drawColor(fillColor, 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();
|
|
|
|
int 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 int 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;
|
|
drawTextRun(canvas, line, palette, heightOffset, lastRunStartColumn, columnWidthSinceLastRun, lastRunStartIndex, charsSinceLastRun,
|
|
measuredWidthForRun, lastRunInsideCursor, 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;
|
|
drawTextRun(canvas, line, palette, heightOffset, lastRunStartColumn, columnWidthSinceLastRun, lastRunStartIndex, charsSinceLastRun,
|
|
measuredWidthForRun, lastRunInsideCursor, lastRunStyle, reverseVideo);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param canvas
|
|
* the canvas to render on
|
|
* @param palette
|
|
* the color palette to look up colors from textStyle
|
|
* @param y
|
|
* height offset into the canvas where to render the line: line * {@link #mFontLineSpacing}
|
|
* @param startColumn
|
|
* the run offset in columns
|
|
* @param runWidthColumns
|
|
* the run width in columns - this is computed from wcwidth() and may not be what the font measures to
|
|
* @param text
|
|
* the java char array to render text from
|
|
* @param startCharIndex
|
|
* index into the text array where to start
|
|
* @param runWidthChars
|
|
* number of java characters from the text array to render
|
|
* @param cursor
|
|
* true if rendering a cursor or selection
|
|
* @param textStyle
|
|
* the background, foreground and effect encoded using {@link TextStyle}
|
|
* @param reverseVideo
|
|
* if the screen is rendered with the global reverse video flag set
|
|
*/
|
|
private void drawTextRun(Canvas canvas, char[] text, int[] palette, float y, int startColumn, int runWidthColumns, int startCharIndex, int runWidthChars,
|
|
float mes, boolean cursor, int textStyle, boolean reverseVideo) {
|
|
int foreColor = TextStyle.decodeForeColor(textStyle);
|
|
int backColor = TextStyle.decodeBackColor(textStyle);
|
|
final int effect = TextStyle.decodeEffect(textStyle);
|
|
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;
|
|
}
|
|
|
|
// Reverse video here if _one and only one_ of the reverse flags are set:
|
|
boolean reverseVideoHere = reverseVideo ^ (effect & (TextStyle.CHARACTER_ATTRIBUTE_INVERSE)) != 0;
|
|
// Switch if _one and only one_ of reverse video and cursor is set:
|
|
if (reverseVideoHere ^ cursor) {
|
|
int tmp = foreColor;
|
|
foreColor = backColor;
|
|
backColor = tmp;
|
|
}
|
|
|
|
if (backColor != TextStyle.COLOR_INDEX_BACKGROUND) {
|
|
// Only draw non-default background.
|
|
mTextPaint.setColor(palette[backColor]);
|
|
canvas.drawRect(left, y - mFontLineSpacingAndAscent + mFontAscent, right, y, mTextPaint);
|
|
}
|
|
|
|
if ((effect & TextStyle.CHARACTER_ATTRIBUTE_INVISIBLE) == 0) {
|
|
// Treat blink as bold:
|
|
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;
|
|
|
|
int foreColorARGB = palette[foreColor];
|
|
if (dim) {
|
|
int red = (0xFF & (foreColorARGB >> 16));
|
|
int green = (0xFF & (foreColorARGB >> 8));
|
|
int blue = (0xFF & foreColorARGB);
|
|
// 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;
|
|
foreColorARGB = 0xFF000000 + (red << 16) + (green << 8) + blue;
|
|
}
|
|
|
|
mTextPaint.setFakeBoldText(bold);
|
|
mTextPaint.setUnderlineText(underline);
|
|
mTextPaint.setTextSkewX(italic ? -0.35f : 0.f);
|
|
mTextPaint.setStrikeThruText(strikeThrough);
|
|
mTextPaint.setColor(foreColorARGB);
|
|
|
|
// The text alignment is the default Paint.Align.LEFT.
|
|
canvas.drawText(text, startCharIndex, runWidthChars, left, y - mFontLineSpacingAndAscent, mTextPaint);
|
|
}
|
|
|
|
if (savedMatrix) canvas.restore();
|
|
}
|
|
}
|