- Decouple the `CursorController`, `TextSelectionCursorController`(previously `SelectionModifierCursorController`) and `TextSelectionHandleView` (previously `HandleView`) from `TerminalView` by moving them to their own class files. - Fixes #1501 which caused the `java.lang.IllegalStateException: The specified child already has a parent. You must call removeView() on the child's parent first.` exception to be thrown when long pressing the down key while simultaneously long pressing the terminal view for text selection.
242 lines
12 KiB
Java
242 lines
12 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}.
|
|
* <p/>
|
|
* Saves font metrics, so needs to be recreated each time the typeface or font size changes.
|
|
*/
|
|
public 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;
|
|
boolean lastRunInsideSelection = 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 = (cursorX == column || (codePointWcWidth == 2 && cursorX == column + 1));
|
|
final boolean insideSelection = column >= selx1 && column <= selx2;
|
|
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 || insideSelection != lastRunInsideSelection || 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 || lastRunInsideSelection);
|
|
}
|
|
measuredWidthForRun = 0.f;
|
|
lastRunStyle = style;
|
|
lastRunInsideCursor = insideCursor;
|
|
lastRunInsideSelection = insideSelection;
|
|
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 || lastRunInsideSelection);
|
|
}
|
|
}
|
|
|
|
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();
|
|
}
|
|
|
|
public float getFontWidth() {
|
|
return mFontWidth;
|
|
}
|
|
|
|
public int getFontLineSpacing() {
|
|
return mFontLineSpacing;
|
|
}
|
|
}
|