mirror of
https://github.com/termux/termux-app
synced 2024-06-18 07:07:08 +00:00
Merge 329d5cbb42
into 2f40df91e5
This commit is contained in:
commit
7d50facaef
|
@ -0,0 +1,187 @@
|
|||
package com.termux.terminal;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.graphics.Rect;
|
||||
|
||||
import android.os.SystemClock;
|
||||
|
||||
/**
|
||||
* A circular buffer of {@link TerminalRow}:s which keeps notes about what is visible on a logical screen and the scroll
|
||||
* history.
|
||||
* <p>
|
||||
* See {@link #externalToInternalRow(int)} for how to map from logical screen rows to array indices.
|
||||
*/
|
||||
public class TerminalBitmap {
|
||||
public Bitmap bitmap;
|
||||
public int cellWidth;
|
||||
public int cellHeight;
|
||||
public int scrollLines;
|
||||
public int[] cursorDelta;
|
||||
private static final String LOG_TAG = "TerminalBitmap";
|
||||
|
||||
|
||||
public TerminalBitmap(int num, WorkingTerminalBitmap sixel, int Y, int X, int cellW, int cellH, TerminalBuffer screen) {
|
||||
Bitmap bm = sixel.bitmap;
|
||||
bm = resizeBitmapConstraints(bm, sixel.width, sixel.height, cellW, cellH, screen.mColumns - X);
|
||||
addBitmap(num, bm, Y, X, cellW, cellH, screen);
|
||||
}
|
||||
|
||||
public TerminalBitmap(int num, byte[] image, int Y, int X, int cellW, int cellH, int width, int height, boolean aspect, TerminalBuffer screen) {
|
||||
Bitmap bm = null;
|
||||
int imageHeight;
|
||||
int imageWidth;
|
||||
int newWidth = width;
|
||||
int newHeight = height;
|
||||
if (height > 0 || width > 0) {
|
||||
BitmapFactory.Options options = new BitmapFactory.Options();
|
||||
options.inJustDecodeBounds = true;
|
||||
try {
|
||||
BitmapFactory.decodeByteArray(image, 0, image.length, options);
|
||||
} catch (Exception e) {
|
||||
Logger.logWarn(null, LOG_TAG, "Cannot decode image");
|
||||
}
|
||||
imageHeight = options.outHeight;
|
||||
imageWidth = options.outWidth;
|
||||
if (aspect) {
|
||||
double wFactor = 9999.0;
|
||||
double hFactor = 9999.0;
|
||||
if (width > 0) {
|
||||
wFactor = (double)width / imageWidth;
|
||||
}
|
||||
if (height > 0) {
|
||||
hFactor = (double)height / imageHeight;
|
||||
}
|
||||
double factor = Math.min(wFactor, hFactor);
|
||||
newWidth = (int)(factor * imageWidth);
|
||||
newHeight = (int)(factor * imageHeight);
|
||||
} else {
|
||||
if (height <= 0) {
|
||||
newHeight = imageHeight;
|
||||
}
|
||||
if (width <= 0) {
|
||||
newWidth = imageWidth;
|
||||
}
|
||||
}
|
||||
int scaleFactor = 1;
|
||||
while (imageHeight >= 2 * newHeight * scaleFactor && imageWidth >= 2 * newWidth * scaleFactor) {
|
||||
scaleFactor = scaleFactor * 2;
|
||||
}
|
||||
BitmapFactory.Options scaleOptions = new BitmapFactory.Options();
|
||||
scaleOptions.inSampleSize = scaleFactor;
|
||||
try {
|
||||
bm = BitmapFactory.decodeByteArray(image, 0, image.length, scaleOptions);
|
||||
} catch (Exception e) {
|
||||
Logger.logWarn(null, LOG_TAG, "Out of memory, cannot decode image");
|
||||
bitmap = null;
|
||||
return;
|
||||
}
|
||||
if (bm == null) {
|
||||
Logger.logWarn(null, LOG_TAG, "Could not decode image");
|
||||
bitmap = null;
|
||||
return;
|
||||
}
|
||||
int maxWidth = (screen.mColumns - X) * cellW;
|
||||
if (newWidth > maxWidth) {
|
||||
int cropWidth = bm.getWidth() * maxWidth / newWidth;
|
||||
try {
|
||||
bm = Bitmap.createBitmap(bm, 0, 0, cropWidth, bm.getHeight());
|
||||
newWidth = maxWidth;
|
||||
} catch(OutOfMemoryError e) {
|
||||
// This is just a memory optimization. If it fails,
|
||||
// continue (and probably fail later).
|
||||
}
|
||||
}
|
||||
try {
|
||||
bm = Bitmap.createScaledBitmap(bm, newWidth, newHeight, true);
|
||||
} catch(OutOfMemoryError e) {
|
||||
Logger.logWarn(null, LOG_TAG, "Out of memory, cannot rescale image");
|
||||
bm = null;
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
bm = BitmapFactory.decodeByteArray(image, 0, image.length);
|
||||
} catch (Exception e) {
|
||||
Logger.logWarn(null, LOG_TAG, "Out of memory, cannot decode image");
|
||||
}
|
||||
}
|
||||
|
||||
if (bm == null) {
|
||||
Logger.logWarn(null, LOG_TAG, "Cannot decode image");
|
||||
bitmap = null;
|
||||
return;
|
||||
}
|
||||
|
||||
bm = resizeBitmapConstraints(bm, bm.getWidth(), bm.getHeight(), cellW, cellH, screen.mColumns - X);
|
||||
addBitmap(num, bm, Y, X, cellW, cellH, screen);
|
||||
cursorDelta = new int[] {scrollLines, (bitmap.getWidth() + cellW - 1) / cellW};
|
||||
}
|
||||
|
||||
private void addBitmap(int num, Bitmap bm, int Y, int X, int cellW, int cellH, TerminalBuffer screen) {
|
||||
if (bm == null) {
|
||||
bitmap = null;
|
||||
return;
|
||||
}
|
||||
int width = bm.getWidth();
|
||||
int height = bm.getHeight();
|
||||
cellWidth = cellW;
|
||||
cellHeight = cellH;
|
||||
int w = Math.min(screen.mColumns - X, (width + cellW - 1) / cellW);
|
||||
int h = (height + cellH - 1) / cellH;
|
||||
int s = 0;
|
||||
for (int i=0; i<h; i++) {
|
||||
if (Y+i-s == screen.mScreenRows) {
|
||||
screen.scrollDownOneLine(0, screen.mScreenRows, TextStyle.NORMAL);
|
||||
s++;
|
||||
}
|
||||
for (int j=0; j<w ; j++) {
|
||||
screen.setChar(X+j, Y+i-s, '+', TextStyle.encodeBitmap(num, j, i));
|
||||
}
|
||||
}
|
||||
if (w * cellW < width) {
|
||||
try {
|
||||
bm = Bitmap.createBitmap(bm, 0, 0, w * cellW, height);
|
||||
} catch(OutOfMemoryError e) {
|
||||
// Image cannot be cropped to only visible part due to out of memory.
|
||||
// This causes memory waste.
|
||||
}
|
||||
}
|
||||
bitmap = bm;
|
||||
scrollLines = h - s;
|
||||
}
|
||||
|
||||
static public Bitmap resizeBitmap(Bitmap bm, int w, int h) {
|
||||
int[] pixels = new int[bm.getAllocationByteCount()];
|
||||
bm.getPixels(pixels, 0, bm.getWidth(), 0, 0, bm.getWidth(), bm.getHeight());
|
||||
Bitmap newbm;
|
||||
try {
|
||||
newbm = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
|
||||
} catch(OutOfMemoryError e) {
|
||||
// Only a minor display glitch in this case
|
||||
return bm;
|
||||
}
|
||||
int newWidth = Math.min(bm.getWidth(), w);
|
||||
int newHeight = Math.min(bm.getHeight(), h);
|
||||
newbm.setPixels(pixels, 0, bm.getWidth(), 0, 0, newWidth, newHeight);
|
||||
return newbm;
|
||||
}
|
||||
|
||||
static public Bitmap resizeBitmapConstraints(Bitmap bm, int w, int h, int cellW, int cellH, int Columns) {
|
||||
// Width and height must be multiples of the cell width and height
|
||||
// Bitmap should not extend beyonf screen width
|
||||
if (w > cellW * Columns || (w % cellW) != 0 || (h % cellH) != 0) {
|
||||
int newW = Math.min(cellW * Columns, ((w - 1) / cellW) * cellW + cellW);
|
||||
int newH = ((h - 1) / cellH) * cellH + cellH;
|
||||
try {
|
||||
bm = resizeBitmap(bm, newW, newH);
|
||||
} catch(OutOfMemoryError e) {
|
||||
// Only a minor display glitch in this case
|
||||
}
|
||||
}
|
||||
return bm;
|
||||
}
|
||||
}
|
|
@ -1,6 +1,15 @@
|
|||
package com.termux.terminal;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.HashMap;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.graphics.Rect;
|
||||
|
||||
import android.os.SystemClock;
|
||||
|
||||
/**
|
||||
* A circular buffer of {@link TerminalRow}:s which keeps notes about what is visible on a logical screen and the scroll
|
||||
|
@ -20,6 +29,11 @@ public final class TerminalBuffer {
|
|||
/** The index in the circular buffer where the visible screen starts. */
|
||||
private int mScreenFirstRow = 0;
|
||||
|
||||
public HashMap<Integer,TerminalBitmap> bitmaps;
|
||||
public WorkingTerminalBitmap workingBitmap;
|
||||
private boolean hasBitmaps;
|
||||
private long bitmapLastGC;
|
||||
|
||||
/**
|
||||
* Create a transcript screen.
|
||||
*
|
||||
|
@ -35,6 +49,9 @@ public final class TerminalBuffer {
|
|||
mLines = new TerminalRow[totalRows];
|
||||
|
||||
blockSet(0, 0, columns, screenRows, ' ', TextStyle.NORMAL);
|
||||
hasBitmaps = false;
|
||||
bitmaps = new HashMap<Integer,TerminalBitmap>();
|
||||
bitmapLastGC = SystemClock.uptimeMillis();
|
||||
}
|
||||
|
||||
public String getTranscriptText() {
|
||||
|
@ -401,6 +418,28 @@ public final class TerminalBuffer {
|
|||
if (mLines[blankRow] == null) {
|
||||
mLines[blankRow] = new TerminalRow(mColumns, style);
|
||||
} else {
|
||||
// find if a bitmap is completely scrolled out
|
||||
Set<Integer> used = new HashSet<Integer>();
|
||||
if(mLines[blankRow].mHasBitmap) {
|
||||
for (int column = 0; column < mColumns; column++) {
|
||||
final long st = mLines[blankRow].getStyle(column);
|
||||
if (TextStyle.isBitmap(st)) {
|
||||
used.add((int)(st >> 16) & 0xffff);
|
||||
}
|
||||
}
|
||||
TerminalRow nextLine = mLines[(blankRow + 1) % mTotalRows];
|
||||
if(nextLine.mHasBitmap) {
|
||||
for (int column = 0; column < mColumns; column++) {
|
||||
final long st = nextLine.getStyle(column);
|
||||
if (TextStyle.isBitmap(st)) {
|
||||
used.remove((int)(st >> 16) & 0xffff);
|
||||
}
|
||||
}
|
||||
}
|
||||
for(Integer bm: used) {
|
||||
bitmaps.remove(bm);
|
||||
}
|
||||
}
|
||||
mLines[blankRow].clear(style);
|
||||
}
|
||||
}
|
||||
|
@ -492,6 +531,92 @@ public final class TerminalBuffer {
|
|||
Arrays.fill(mLines, mScreenFirstRow - mActiveTranscriptRows, mScreenFirstRow, null);
|
||||
}
|
||||
mActiveTranscriptRows = 0;
|
||||
bitmaps.clear();
|
||||
hasBitmaps = false;
|
||||
}
|
||||
|
||||
public Bitmap getSixelBitmap(int codePoint, long style) {
|
||||
return bitmaps.get(TextStyle.bitmapNum(style)).bitmap;
|
||||
}
|
||||
|
||||
public Rect getSixelRect(int codePoint, long style ) {
|
||||
TerminalBitmap bm = bitmaps.get(TextStyle.bitmapNum(style));
|
||||
int x = TextStyle.bitmapX(style);
|
||||
int y = TextStyle.bitmapY(style);
|
||||
Rect r = new Rect(x * bm.cellWidth, y * bm.cellHeight, (x+1) * bm.cellWidth, (y+1) * bm.cellHeight);
|
||||
return r;
|
||||
}
|
||||
|
||||
public void sixelStart(int width, int height) {
|
||||
workingBitmap = new WorkingTerminalBitmap(width, height);
|
||||
}
|
||||
|
||||
public void sixelChar(int c, int rep) {
|
||||
workingBitmap.sixelChar(c, rep);
|
||||
}
|
||||
|
||||
public void sixelSetColor(int col) {
|
||||
workingBitmap.sixelSetColor(col);
|
||||
}
|
||||
|
||||
public void sixelSetColor(int col, int r, int g, int b) {
|
||||
workingBitmap.sixelSetColor(col, r, g, b);
|
||||
}
|
||||
|
||||
private int findFreeBitmap() {
|
||||
int i = 0;
|
||||
while (bitmaps.containsKey(i)) {
|
||||
i++;
|
||||
}
|
||||
return i;
|
||||
}
|
||||
|
||||
public int sixelEnd(int Y, int X, int cellW, int cellH) {
|
||||
int num = findFreeBitmap();
|
||||
bitmaps.put(num, new TerminalBitmap(num, workingBitmap, Y, X, cellW, cellH, this));
|
||||
workingBitmap = null;
|
||||
if (bitmaps.get(num).bitmap == null) {
|
||||
bitmaps.remove(num);
|
||||
return 0;
|
||||
}
|
||||
hasBitmaps = true;
|
||||
bitmapGC(30000);
|
||||
return bitmaps.get(num).scrollLines;
|
||||
}
|
||||
|
||||
public int[] addImage(byte[] image, int Y, int X, int cellW, int cellH, int width, int height, boolean aspect) {
|
||||
int num = findFreeBitmap();
|
||||
bitmaps.put(num, new TerminalBitmap(num, image, Y, X, cellW, cellH, width, height, aspect, this));
|
||||
if (bitmaps.get(num).bitmap == null) {
|
||||
bitmaps.remove(num);
|
||||
return new int[] {0,0};
|
||||
}
|
||||
hasBitmaps = true;
|
||||
bitmapGC(30000);
|
||||
return bitmaps.get(num).cursorDelta;
|
||||
}
|
||||
|
||||
public void bitmapGC(int timeDelta) {
|
||||
if (!hasBitmaps || bitmapLastGC + timeDelta > SystemClock.uptimeMillis()) {
|
||||
return;
|
||||
}
|
||||
Set<Integer> used = new HashSet<Integer>();
|
||||
for (int line = 0; line < mLines.length; line++) {
|
||||
if(mLines[line] != null && mLines[line].mHasBitmap) {
|
||||
for (int column = 0; column < mColumns; column++) {
|
||||
final long st = mLines[line].getStyle(column);
|
||||
if (TextStyle.isBitmap(st)) {
|
||||
used.add((int)(st >> 16) & 0xffff);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Set<Integer> keys = new HashSet<Integer>(bitmaps.keySet());
|
||||
for (Integer bn: keys) {
|
||||
if (!used.contains(bn)) {
|
||||
bitmaps.remove(bn);
|
||||
}
|
||||
}
|
||||
bitmapLastGC = SystemClock.uptimeMillis();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ package com.termux.terminal;
|
|||
import android.util.Base64;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
|
@ -79,6 +80,9 @@ public final class TerminalEmulator {
|
|||
private static final int ESC_CSI_SINGLE_QUOTE = 18;
|
||||
/** Escape processing: CSI ! */
|
||||
private static final int ESC_CSI_EXCLAMATION = 19;
|
||||
/** Escape processing: APC */
|
||||
private static final int ESC_APC = 20;
|
||||
private static final int ESC_APC_ESC = 21;
|
||||
|
||||
/** The number of parameter arguments. This name comes from the ANSI standard for terminal escape codes. */
|
||||
private static final int MAX_ESCAPE_PARAMETERS = 16;
|
||||
|
@ -188,6 +192,10 @@ public final class TerminalEmulator {
|
|||
|
||||
/** The current state of the escape sequence state machine. One of the ESC_* constants. */
|
||||
private int mEscapeState;
|
||||
private boolean ESC_P_escape = false;
|
||||
private boolean ESC_P_sixel = false;
|
||||
private ArrayList<Byte> ESC_OSC_data;
|
||||
private int ESC_OSC_colon = 0;
|
||||
|
||||
private final SavedScreenState mSavedStateMain = new SavedScreenState();
|
||||
private final SavedScreenState mSavedStateAlt = new SavedScreenState();
|
||||
|
@ -263,6 +271,13 @@ public final class TerminalEmulator {
|
|||
|
||||
private static final String LOG_TAG = "TerminalEmulator";
|
||||
|
||||
private int cellW = 12, cellH = 12;
|
||||
|
||||
public void setCellSize(int w, int h) {
|
||||
cellW = w;
|
||||
cellH = h;
|
||||
}
|
||||
|
||||
private boolean isDecsetInternalBitSet(int bit) {
|
||||
return (mCurrentDecSetFlags & bit) != 0;
|
||||
}
|
||||
|
@ -553,14 +568,19 @@ public final class TerminalEmulator {
|
|||
}
|
||||
|
||||
public void processCodePoint(int b) {
|
||||
mScreen.bitmapGC(300000);
|
||||
switch (b) {
|
||||
case 0: // Null character (NUL, ^@). Do nothing.
|
||||
break;
|
||||
case 7: // Bell (BEL, ^G, \a). If in an OSC sequence, BEL may terminate a string; otherwise signal bell.
|
||||
if (mEscapeState == ESC_OSC)
|
||||
doOsc(b);
|
||||
else
|
||||
else {
|
||||
if (mEscapeState == ESC_APC) {
|
||||
doApc(b);
|
||||
}
|
||||
mSession.onBell();
|
||||
}
|
||||
break;
|
||||
case 8: // Backspace (BS, ^H).
|
||||
if (mLeftMargin == mCursorCol) {
|
||||
|
@ -587,10 +607,16 @@ public final class TerminalEmulator {
|
|||
case 10: // Line feed (LF, \n).
|
||||
case 11: // Vertical tab (VT, \v).
|
||||
case 12: // Form feed (FF, \f).
|
||||
doLinefeed();
|
||||
if((mEscapeState != ESC_P || !ESC_P_sixel) && ESC_OSC_colon <= 0) {
|
||||
// Ignore CR/LF inside sixels or iterm2 data
|
||||
doLinefeed();
|
||||
}
|
||||
break;
|
||||
case 13: // Carriage return (CR, \r).
|
||||
setCursorCol(mLeftMargin);
|
||||
if((mEscapeState != ESC_P || !ESC_P_sixel) && ESC_OSC_colon <= 0) {
|
||||
// Ignore CR/LF inside sixels or iterm2 data
|
||||
setCursorCol(mLeftMargin);
|
||||
}
|
||||
break;
|
||||
case 14: // Shift Out (Ctrl-N, SO) → Switch to Alternate Character Set. This invokes the G1 character set.
|
||||
mUseLineDrawingUsesG0 = false;
|
||||
|
@ -610,9 +636,14 @@ public final class TerminalEmulator {
|
|||
// Starts an escape sequence unless we're parsing a string
|
||||
if (mEscapeState == ESC_P) {
|
||||
// XXX: Ignore escape when reading device control sequence, since it may be part of string terminator.
|
||||
ESC_P_escape = true;
|
||||
return;
|
||||
} else if (mEscapeState != ESC_OSC) {
|
||||
startEscapeSequence();
|
||||
if (mEscapeState != ESC_APC) {
|
||||
startEscapeSequence();
|
||||
} else {
|
||||
doApc(b);
|
||||
}
|
||||
} else {
|
||||
doOsc(b);
|
||||
}
|
||||
|
@ -809,6 +840,12 @@ public final class TerminalEmulator {
|
|||
break;
|
||||
case ESC_PERCENT:
|
||||
break;
|
||||
case ESC_APC:
|
||||
doApc(b);
|
||||
break;
|
||||
case ESC_APC_ESC:
|
||||
doApcEsc(b);
|
||||
break;
|
||||
case ESC_OSC:
|
||||
doOsc(b);
|
||||
break;
|
||||
|
@ -888,8 +925,17 @@ public final class TerminalEmulator {
|
|||
|
||||
/** When in {@link #ESC_P} ("device control") sequence. */
|
||||
private void doDeviceControl(int b) {
|
||||
switch (b) {
|
||||
case (byte) '\\': // End of ESC \ string Terminator
|
||||
boolean firstSixel = false;
|
||||
if (!ESC_P_sixel && (b=='$' || b=='-' || b=='#')) {
|
||||
//Check if sixel sequence that needs breaking
|
||||
String dcs = mOSCOrDeviceControlArgs.toString();
|
||||
if (dcs.matches("[0-9;]*q.*")) {
|
||||
firstSixel = true;
|
||||
}
|
||||
}
|
||||
if (firstSixel || (ESC_P_escape && b == '\\') || (ESC_P_sixel && (b=='$' || b=='-' || b=='#')))
|
||||
// ESC \ terminates OSC
|
||||
// Sixel sequences may be very long. '$' and '!' are natural for breaking the sequence.
|
||||
{
|
||||
String dcs = mOSCOrDeviceControlArgs.toString();
|
||||
// DCS $ q P t ST. Request Status String (DECRQSS)
|
||||
|
@ -990,14 +1036,102 @@ public final class TerminalEmulator {
|
|||
Logger.logError(mClient, LOG_TAG, "Invalid device termcap/terminfo name of odd length: " + part);
|
||||
}
|
||||
}
|
||||
} else if (ESC_P_sixel || dcs.matches("[0-9;]*q.*")) {
|
||||
int pos = 0;
|
||||
if (!ESC_P_sixel) {
|
||||
ESC_P_sixel = true;
|
||||
mScreen.sixelStart(100, 100);
|
||||
while (dcs.codePointAt(pos) != 'q') {
|
||||
pos++;
|
||||
}
|
||||
pos++;
|
||||
}
|
||||
if (b=='$' || b=='-') {
|
||||
// Add to string
|
||||
dcs = dcs + (char)b;
|
||||
}
|
||||
int rep = 1;
|
||||
while (pos < dcs.length()) {
|
||||
if (dcs.codePointAt(pos) == '"') {
|
||||
pos++;
|
||||
int args[]={0,0,0,0};
|
||||
int arg = 0;
|
||||
while (pos < dcs.length() && ((dcs.codePointAt(pos) >= '0' && dcs.codePointAt(pos) <= '9') || dcs.codePointAt(pos) == ';')) {
|
||||
if (dcs.codePointAt(pos) >= '0' && dcs.codePointAt(pos) <= '9') {
|
||||
args[arg] = args[arg] * 10 + dcs.codePointAt(pos) - '0';
|
||||
} else {
|
||||
arg++;
|
||||
if (arg > 3) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
pos++;
|
||||
}
|
||||
if (pos == dcs.length()) {
|
||||
break;
|
||||
}
|
||||
} else if (dcs.codePointAt(pos) == '#') {
|
||||
int col = 0;
|
||||
pos++;
|
||||
while (pos < dcs.length() && dcs.codePointAt(pos) >= '0' && dcs.codePointAt(pos) <= '9') {
|
||||
col = col * 10 + dcs.codePointAt(pos++) - '0';
|
||||
}
|
||||
if (pos == dcs.length() || dcs.codePointAt(pos) != ';') {
|
||||
mScreen.sixelSetColor(col);
|
||||
} else {
|
||||
pos++;
|
||||
int args[]={0,0,0,0};
|
||||
int arg = 0;
|
||||
while (pos < dcs.length() && ((dcs.codePointAt(pos) >= '0' && dcs.codePointAt(pos) <= '9') || dcs.codePointAt(pos) == ';')) {
|
||||
if (dcs.codePointAt(pos) >= '0' && dcs.codePointAt(pos) <= '9') {
|
||||
args[arg] = args[arg] * 10 + dcs.codePointAt(pos) - '0';
|
||||
} else {
|
||||
arg++;
|
||||
if (arg > 3) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
pos++;
|
||||
}
|
||||
if (args[0] == 2) {
|
||||
mScreen.sixelSetColor(col, args[1], args[2], args[3]);
|
||||
}
|
||||
}
|
||||
} else if (dcs.codePointAt(pos) == '!') {
|
||||
rep = 0;
|
||||
pos++;
|
||||
while (pos < dcs.length() && dcs.codePointAt(pos) >= '0' && dcs.codePointAt(pos) <= '9') {
|
||||
rep = rep * 10 + dcs.codePointAt(pos++) - '0';
|
||||
}
|
||||
} else if (dcs.codePointAt(pos) == '$' || dcs.codePointAt(pos) == '-' || (dcs.codePointAt(pos) >= '?' && dcs.codePointAt(pos) <= '~')) {
|
||||
mScreen.sixelChar(dcs.codePointAt(pos++), rep);
|
||||
rep = 1;
|
||||
} else {
|
||||
pos++;
|
||||
}
|
||||
}
|
||||
if (b == '\\') {
|
||||
ESC_P_sixel = false;
|
||||
int n = mScreen.sixelEnd(mCursorRow, mCursorCol, cellW, cellH);
|
||||
for(;n>0;n--) {
|
||||
doLinefeed();
|
||||
}
|
||||
} else {
|
||||
mOSCOrDeviceControlArgs.setLength(0);
|
||||
if (b=='#') {
|
||||
mOSCOrDeviceControlArgs.appendCodePoint(b);
|
||||
}
|
||||
// Do not finish sequence
|
||||
continueSequence(mEscapeState);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
if (LOG_ESCAPE_SEQUENCES)
|
||||
Logger.logError(mClient, LOG_TAG, "Unrecognized device control string: " + dcs);
|
||||
}
|
||||
finishSequence();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
} else {
|
||||
ESC_P_escape = false;
|
||||
if (mOSCOrDeviceControlArgs.length() > MAX_OSC_STRING_LENGTH) {
|
||||
// Too long.
|
||||
mOSCOrDeviceControlArgs.setLength(0);
|
||||
|
@ -1006,7 +1140,7 @@ public final class TerminalEmulator {
|
|||
mOSCOrDeviceControlArgs.appendCodePoint(b);
|
||||
continueSequence(mEscapeState);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private int nextTabStop(int numTabs) {
|
||||
|
@ -1389,6 +1523,7 @@ public final class TerminalEmulator {
|
|||
break;
|
||||
case 'P': // Device control string
|
||||
mOSCOrDeviceControlArgs.setLength(0);
|
||||
ESC_P_escape = false;
|
||||
continueSequence(ESC_P);
|
||||
break;
|
||||
case '[':
|
||||
|
@ -1402,10 +1537,15 @@ public final class TerminalEmulator {
|
|||
case ']': // OSC
|
||||
mOSCOrDeviceControlArgs.setLength(0);
|
||||
continueSequence(ESC_OSC);
|
||||
ESC_OSC_colon = -1;
|
||||
break;
|
||||
case '>': // DECKPNM
|
||||
setDecsetinternalBit(DECSET_BIT_APPLICATION_KEYPAD, false);
|
||||
break;
|
||||
case '_': // APC
|
||||
mOSCOrDeviceControlArgs.setLength(0);
|
||||
continueSequence(ESC_APC);
|
||||
break;
|
||||
default:
|
||||
unknownSequence(b);
|
||||
break;
|
||||
|
@ -1628,7 +1768,7 @@ public final class TerminalEmulator {
|
|||
// The important part that may still be used by some (tmux stores this value but does not currently use it)
|
||||
// is the first response parameter identifying the terminal service class, where we send 64 for "vt420".
|
||||
// This is followed by a list of attributes which is probably unused by applications. Send like xterm.
|
||||
if (getArg0(0) == 0) mSession.write("\033[?64;1;2;6;9;15;18;21;22c");
|
||||
if (getArg0(0) == 0) mSession.write("\033[?64;1;2;4;6;9;15;18;21;22c");
|
||||
break;
|
||||
case 'd': // ESC [ Pn d - Vert Position Absolute
|
||||
setCursorRow(Math.min(Math.max(1, getArg0(1)), mRows) - 1);
|
||||
|
@ -1715,8 +1855,10 @@ public final class TerminalEmulator {
|
|||
mSession.write("\033[3;0;0t");
|
||||
break;
|
||||
case 14: // Report xterm window in pixels. Result is CSI 4 ; height ; width t
|
||||
// We just report characters time 12 here.
|
||||
mSession.write(String.format(Locale.US, "\033[4;%d;%dt", mRows * 12, mColumns * 12));
|
||||
mSession.write(String.format(Locale.US, "\033[4;%d;%dt", mRows * cellH, mColumns * cellW));
|
||||
break;
|
||||
case 16: // Report xterm window in pixels. Result is CSI 4 ; height ; width t
|
||||
mSession.write(String.format(Locale.US, "\033[6;%d;%dt", cellH, cellW));
|
||||
break;
|
||||
case 18: // Report the size of the text area in characters. Result is CSI 8 ; height ; width t
|
||||
mSession.write(String.format(Locale.US, "\033[8;%d;%dt", mRows, mColumns));
|
||||
|
@ -1868,6 +2010,33 @@ public final class TerminalEmulator {
|
|||
}
|
||||
}
|
||||
|
||||
private void doApc(int b) {
|
||||
switch (b) {
|
||||
case 7: // Bell.
|
||||
break;
|
||||
case 27: // Escape.
|
||||
continueSequence(ESC_APC_ESC);
|
||||
break;
|
||||
default:
|
||||
collectOSCArgs(b);
|
||||
continueSequence(ESC_OSC);
|
||||
}
|
||||
}
|
||||
|
||||
private void doApcEsc(int b) {
|
||||
switch (b) {
|
||||
case '\\':
|
||||
break;
|
||||
default:
|
||||
// The ESC character was not followed by a \, so insert the ESC and
|
||||
// the current character in arg buffer.
|
||||
collectOSCArgs(27);
|
||||
collectOSCArgs(b);
|
||||
continueSequence(ESC_APC);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void doOsc(int b) {
|
||||
switch (b) {
|
||||
case 7: // Bell.
|
||||
|
@ -1878,6 +2047,22 @@ public final class TerminalEmulator {
|
|||
break;
|
||||
default:
|
||||
collectOSCArgs(b);
|
||||
if (ESC_OSC_colon == -1 && b == ':') {
|
||||
// Collect base64 data for OSC 1337
|
||||
ESC_OSC_colon = mOSCOrDeviceControlArgs.length();
|
||||
ESC_OSC_data = new ArrayList<Byte>(65536);
|
||||
} else if (ESC_OSC_colon >= 0 && mOSCOrDeviceControlArgs.length() - ESC_OSC_colon == 4) {
|
||||
try {
|
||||
byte[] decoded = Base64.decode(mOSCOrDeviceControlArgs.substring(ESC_OSC_colon), 0);
|
||||
for (int i = 0 ; i < decoded.length; i++) {
|
||||
ESC_OSC_data.add(decoded[i]);
|
||||
}
|
||||
} catch(Exception e) {
|
||||
// Ignore non-Base64 data.
|
||||
}
|
||||
mOSCOrDeviceControlArgs.setLength(ESC_OSC_colon);
|
||||
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -1900,6 +2085,8 @@ public final class TerminalEmulator {
|
|||
/** An Operating System Controls (OSC) Set Text Parameters. May come here from BEL or ST. */
|
||||
private void doOscSetTextParameters(String bellOrStringTerminator) {
|
||||
int value = -1;
|
||||
int osc_colon = ESC_OSC_colon;
|
||||
ESC_OSC_colon = -1;
|
||||
String textParameter = "";
|
||||
// Extract initial $value from initial "$value;..." string.
|
||||
for (int mOSCArgTokenizerIndex = 0; mOSCArgTokenizerIndex < mOSCOrDeviceControlArgs.length(); mOSCArgTokenizerIndex++) {
|
||||
|
@ -2035,6 +2222,105 @@ public final class TerminalEmulator {
|
|||
break;
|
||||
case 119: // Reset highlight color.
|
||||
break;
|
||||
case 1337: // iTerm extemsions
|
||||
if (textParameter.startsWith("File=")) {
|
||||
int pos = 5;
|
||||
boolean inline = false;
|
||||
boolean aspect = true;
|
||||
int width = -1;
|
||||
int height = -1;
|
||||
while (pos < textParameter.length()) {
|
||||
int eqpos = textParameter.indexOf('=', pos);
|
||||
if (eqpos == -1) {
|
||||
break;
|
||||
}
|
||||
int semicolonpos = textParameter.indexOf(';', eqpos);
|
||||
if (semicolonpos == -1) {
|
||||
semicolonpos = textParameter.length() - 1;
|
||||
}
|
||||
String k = textParameter.substring(pos, eqpos);
|
||||
String v = textParameter.substring(eqpos + 1, semicolonpos);
|
||||
pos = semicolonpos + 1;
|
||||
if (k.equalsIgnoreCase("inline")) {
|
||||
inline = v.equals("1");
|
||||
}
|
||||
if (k.equalsIgnoreCase("preserveAspectRatio")) {
|
||||
aspect = ! v.equals("0");
|
||||
}
|
||||
if (k.equalsIgnoreCase("width")) {
|
||||
double factor = cellW;
|
||||
int div = 1;
|
||||
int e = v.length();
|
||||
if (v.endsWith("px")) {
|
||||
factor = 1;
|
||||
e -= 2;
|
||||
} else if (v.endsWith("%")) {
|
||||
factor = 0.01 * cellW * mColumns;
|
||||
e -= 1;
|
||||
}
|
||||
try {
|
||||
width = (int)(factor * Integer.parseInt(v.substring(0,e)));
|
||||
} catch(Exception ex) {
|
||||
}
|
||||
}
|
||||
if (k.equalsIgnoreCase("height")) {
|
||||
double factor = cellH;
|
||||
int div = 1;
|
||||
int e = v.length();
|
||||
if (v.endsWith("px")) {
|
||||
factor = 1;
|
||||
e -= 2;
|
||||
} else if (v.endsWith("%")) {
|
||||
factor = 0.01 * cellH * mRows;
|
||||
e -= 1;
|
||||
}
|
||||
try {
|
||||
height = (int)(factor * Integer.parseInt(v.substring(0,e)));
|
||||
} catch(Exception ex) {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!inline) {
|
||||
finishSequence();
|
||||
return;
|
||||
}
|
||||
if (osc_colon >= 0 && mOSCOrDeviceControlArgs.length() > osc_colon) {
|
||||
while (mOSCOrDeviceControlArgs.length() - osc_colon < 4) {
|
||||
mOSCOrDeviceControlArgs.append('=');
|
||||
}
|
||||
try {
|
||||
byte[] decoded = Base64.decode(mOSCOrDeviceControlArgs.substring(osc_colon), 0);
|
||||
for (int i = 0 ; i < decoded.length; i++) {
|
||||
ESC_OSC_data.add(decoded[i]);
|
||||
}
|
||||
} catch(Exception e) {
|
||||
// Ignore non-Base64 data.
|
||||
}
|
||||
mOSCOrDeviceControlArgs.setLength(osc_colon);
|
||||
}
|
||||
if (osc_colon >= 0) {
|
||||
byte[] result = new byte[ESC_OSC_data.size()];
|
||||
for(int i = 0; i < ESC_OSC_data.size(); i++) {
|
||||
result[i] = ESC_OSC_data.get(i).byteValue();
|
||||
}
|
||||
int[] res = mScreen.addImage(result, mCursorRow, mCursorCol, cellW, cellH, width, height, aspect);
|
||||
int col = res[1] + mCursorCol;
|
||||
if (col < mColumns -1) {
|
||||
res[0] -= 1;
|
||||
} else {
|
||||
col = 0;
|
||||
}
|
||||
for(;res[0] > 0; res[0]--) {
|
||||
doLinefeed();
|
||||
}
|
||||
mCursorCol = col;
|
||||
ESC_OSC_data.clear();
|
||||
} else {
|
||||
}
|
||||
} else if (textParameter.startsWith("ReportCellSize")) {
|
||||
mSession.write(String.format(Locale.US, "\0331337;ReportCellSize=%d;%d\007", cellH, cellW));
|
||||
}
|
||||
break;
|
||||
default:
|
||||
unknownParameter(value);
|
||||
break;
|
||||
|
@ -2455,6 +2741,10 @@ public final class TerminalEmulator {
|
|||
|
||||
mColors.reset();
|
||||
mSession.onColorsChanged();
|
||||
|
||||
ESC_P_escape = false;
|
||||
ESC_P_sixel = false;
|
||||
ESC_OSC_colon = -1;
|
||||
}
|
||||
|
||||
public String getSelectedText(int x1, int y1, int x2, int y2) {
|
||||
|
|
|
@ -49,6 +49,8 @@ public final class TerminalRow {
|
|||
final long[] mStyle;
|
||||
/** If this row might contain chars with width != 1, used for deactivating fast path */
|
||||
boolean mHasNonOneWidthOrSurrogateChars;
|
||||
/** If this row has a bitmap. Used for performace only */
|
||||
public boolean mHasBitmap;
|
||||
|
||||
/** Construct a blank row (containing only whitespace, ' ') with a specified style. */
|
||||
public TerminalRow(int columns, long style) {
|
||||
|
@ -146,6 +148,7 @@ public final class TerminalRow {
|
|||
Arrays.fill(mStyle, style);
|
||||
mSpaceUsed = (short) mColumns;
|
||||
mHasNonOneWidthOrSurrogateChars = false;
|
||||
mHasBitmap = false;
|
||||
}
|
||||
|
||||
// https://github.com/steven676/Android-Terminal-Emulator/commit/9a47042620bec87617f0b4f5d50568535668fe26
|
||||
|
@ -155,6 +158,10 @@ public final class TerminalRow {
|
|||
|
||||
mStyle[columnToSet] = style;
|
||||
|
||||
if (!mHasBitmap && TextStyle.isBitmap(style)) {
|
||||
mHasBitmap = true;
|
||||
}
|
||||
|
||||
final int newCodePointDisplayWidth = WcWidth.width(codePoint);
|
||||
|
||||
// Fast path when we don't have any chars with width != 1
|
||||
|
|
|
@ -35,6 +35,8 @@ public final class TextStyle {
|
|||
private final static int CHARACTER_ATTRIBUTE_TRUECOLOR_FOREGROUND = 1 << 9;
|
||||
/** If true (24-bit) color is used for the cell for foreground. */
|
||||
private final static int CHARACTER_ATTRIBUTE_TRUECOLOR_BACKGROUND= 1 << 10;
|
||||
/** If true, character represents a bitmap slice, not text. */
|
||||
public final static int BITMAP = 1 << 15;
|
||||
|
||||
public final static int COLOR_INDEX_FOREGROUND = 256;
|
||||
public final static int COLOR_INDEX_BACKGROUND = 257;
|
||||
|
@ -87,4 +89,24 @@ public final class TextStyle {
|
|||
return (int) (style & 0b11111111111);
|
||||
}
|
||||
|
||||
public static long encodeBitmap(int num, int X, int Y) {
|
||||
return ((long)num << 16) | ((long)Y << 32) | ((long)X << 48) | BITMAP;
|
||||
}
|
||||
|
||||
public static boolean isBitmap(long style) {
|
||||
return (style & 0x8000) != 0;
|
||||
}
|
||||
|
||||
public static int bitmapNum(long style) {
|
||||
return (int)(style & 0xffff0000) >> 16;
|
||||
}
|
||||
|
||||
public static int bitmapX(long style) {
|
||||
return (int)((style >> 48) & 0xfff);
|
||||
}
|
||||
|
||||
public static int bitmapY(long style) {
|
||||
return (int)((style >> 32) & 0xfff);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,110 @@
|
|||
package com.termux.terminal;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
|
||||
/**
|
||||
* A circular buffer of {@link TerminalRow}:s which keeps notes about what is visible on a logical screen and the scroll
|
||||
* history.
|
||||
* <p>
|
||||
* See {@link #externalToInternalRow(int)} for how to map from logical screen rows to array indices.
|
||||
*/
|
||||
public final class WorkingTerminalBitmap {
|
||||
final private int sixelInitialColorMap[] = {0xFF000000, 0xFF3333CC, 0xFFCC2323, 0xFF33CC33, 0xFFCC33CC, 0xFF33CCCC, 0xFFCCCC33, 0xFF777777,
|
||||
0xFF444444, 0xFF565699, 0xFF994444, 0xFF569956, 0xFF995699, 0xFF569999, 0xFF999956, 0xFFCCCCCC};
|
||||
private int[] colorMap;
|
||||
private int curX;
|
||||
private int curY;
|
||||
private int color;
|
||||
public int width;
|
||||
public int height;
|
||||
public Bitmap bitmap;
|
||||
private static final String LOG_TAG = "WorkingTerminalBitmap";
|
||||
|
||||
public WorkingTerminalBitmap(int w, int h) {
|
||||
try {
|
||||
bitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
|
||||
} catch (OutOfMemoryError e) {
|
||||
Logger.logWarn(null, LOG_TAG, "Out of memory - sixel ignored");
|
||||
bitmap = null;
|
||||
}
|
||||
bitmap.eraseColor(0);
|
||||
width = 0;
|
||||
height = 0;
|
||||
curX = 0;
|
||||
curY = 0;
|
||||
colorMap = new int[256];
|
||||
for (int i=0; i<16; i++) {
|
||||
colorMap[i] = sixelInitialColorMap[i];
|
||||
}
|
||||
color = colorMap[0];
|
||||
}
|
||||
|
||||
public void sixelChar(int c, int rep) {
|
||||
if (bitmap == null) {
|
||||
return;
|
||||
}
|
||||
if (c == '$') {
|
||||
curX = 0;
|
||||
return;
|
||||
}
|
||||
if (c == '-') {
|
||||
curX = 0;
|
||||
curY += 6;
|
||||
return;
|
||||
}
|
||||
if (bitmap.getWidth() < curX + rep) {
|
||||
try {
|
||||
bitmap = TerminalBitmap.resizeBitmap(bitmap, curX + rep + 100, bitmap.getHeight());
|
||||
} catch(OutOfMemoryError e) {
|
||||
Logger.logWarn(null, LOG_TAG, "Out of memory - sixel truncated");
|
||||
}
|
||||
}
|
||||
if (bitmap.getHeight() < curY + 6) {
|
||||
// Very unlikely to resize both at the same time
|
||||
try {
|
||||
bitmap = TerminalBitmap.resizeBitmap(bitmap, bitmap.getWidth(), curY + 100);
|
||||
} catch(OutOfMemoryError e) {
|
||||
Logger.logWarn(null, LOG_TAG, "Out of memory - sixel truncated");
|
||||
}
|
||||
}
|
||||
if (curX + rep > bitmap.getWidth()) {
|
||||
rep = bitmap.getWidth() - curX;
|
||||
}
|
||||
if ( curY + 6 > bitmap.getHeight()) {
|
||||
return;
|
||||
}
|
||||
if (rep > 0 && c >= '?' && c <= '~') {
|
||||
int b = c - '?';
|
||||
if (curY + 6 > height) {
|
||||
height = curY + 6;
|
||||
}
|
||||
while (rep-- > 0) {
|
||||
for (int i = 0 ; i < 6 ; i++) {
|
||||
if ((b & (1<<i)) != 0) {
|
||||
bitmap.setPixel(curX, curY + i, color);
|
||||
}
|
||||
}
|
||||
curX += 1;
|
||||
if (curX > width) {
|
||||
width = curX;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void sixelSetColor(int col) {
|
||||
if (col >= 0 && col < 256) {
|
||||
color = colorMap[col];
|
||||
}
|
||||
}
|
||||
|
||||
public void sixelSetColor(int col, int r, int g, int b) {
|
||||
if (col >= 0 && col < 256) {
|
||||
int red = Math.min(255, r*255/100);
|
||||
int green = Math.min(255, g*255/100);
|
||||
int blue = Math.min(255, b*255/100);
|
||||
color = 0xff000000 + (red << 16) + (green << 8) + blue;
|
||||
colorMap[col] = color;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,8 +1,11 @@
|
|||
package com.termux.view;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.RectF;
|
||||
import android.graphics.Typeface;
|
||||
|
||||
import com.termux.terminal.TerminalBuffer;
|
||||
|
@ -65,6 +68,7 @@ public final class TerminalRenderer {
|
|||
final TerminalBuffer screen = mEmulator.getScreen();
|
||||
final int[] palette = mEmulator.mColors.mCurrentColors;
|
||||
final int cursorShape = mEmulator.getCursorStyle();
|
||||
mEmulator.setCellSize((int)mFontWidth, (int)mFontLineSpacing);
|
||||
|
||||
if (reverseVideo)
|
||||
canvas.drawColor(palette[TextStyle.COLOR_INDEX_FOREGROUND], PorterDuff.Mode.SRC);
|
||||
|
@ -98,10 +102,28 @@ public final class TerminalRenderer {
|
|||
final boolean charIsHighsurrogate = Character.isHighSurrogate(charAtIndex);
|
||||
final int charsForCodePoint = charIsHighsurrogate ? 2 : 1;
|
||||
final int codePoint = charIsHighsurrogate ? Character.toCodePoint(charAtIndex, line[currentCharIndex + 1]) : charAtIndex;
|
||||
final long style = lineObject.getStyle(column);
|
||||
if (TextStyle.isBitmap(style)) {
|
||||
Bitmap bm = mEmulator.getScreen().getSixelBitmap(codePoint, style);
|
||||
if (bm != null) {
|
||||
float left = column * mFontWidth;
|
||||
float top = heightOffset - mFontLineSpacing;
|
||||
RectF r = new RectF(left, top, left + mFontWidth, top + mFontLineSpacing);
|
||||
canvas.drawBitmap(mEmulator.getScreen().getSixelBitmap(codePoint, style), mEmulator.getScreen().getSixelRect(codePoint, style), r, null);
|
||||
}
|
||||
column += 1;
|
||||
measuredWidthForRun = 0.f;
|
||||
lastRunStyle = 0;
|
||||
lastRunInsideCursor = false;
|
||||
lastRunStartColumn = column + 1;
|
||||
lastRunStartIndex = currentCharIndex;
|
||||
lastRunFontWidthMismatch = false;
|
||||
currentCharIndex += charsForCodePoint;
|
||||
continue;
|
||||
}
|
||||
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
|
||||
|
@ -112,7 +134,7 @@ public final class TerminalRenderer {
|
|||
final boolean fontWidthMismatch = Math.abs(measuredCodePointWidth / mFontWidth - codePointWcWidth) > 0.01;
|
||||
|
||||
if (style != lastRunStyle || insideCursor != lastRunInsideCursor || insideSelection != lastRunInsideSelection || fontWidthMismatch || lastRunFontWidthMismatch) {
|
||||
if (column == 0) {
|
||||
if (column == 0 || column == lastRunStartColumn) {
|
||||
// Skip first column as there is nothing to draw, just record the current style.
|
||||
} else {
|
||||
final int columnWidthSinceLastRun = column - lastRunStartColumn;
|
||||
|
|
Loading…
Reference in New Issue
Block a user