diff --git a/twinkle-chart/src/main/java/org/codejive/twinkle/widgets/graphs/bar/FracBarRenderer.java b/twinkle-chart/src/main/java/org/codejive/twinkle/widgets/graphs/bar/FracBarRenderer.java index fadabef..d9c52a9 100644 --- a/twinkle-chart/src/main/java/org/codejive/twinkle/widgets/graphs/bar/FracBarRenderer.java +++ b/twinkle-chart/src/main/java/org/codejive/twinkle/widgets/graphs/bar/FracBarRenderer.java @@ -121,24 +121,24 @@ public void render(Canvas canvas, Number value) { int y = reversed || horizontal ? 0 : size.height() - 1; // Place full blocks first for (int i = 0; i < fullChunks; i++) { - canvas.setCharAt(x, y, Style.UNSTYLED, blocks[0]); + canvas.putCharAt(x, y, Style.UNSTYLED, blocks[0]); x += direction.dx; y += direction.dy; } // Append remainder partial block if any if (remainder > 0) { - canvas.setCharAt(x, y, Style.UNSTYLED, blocks[remainder]); + canvas.putCharAt(x, y, Style.UNSTYLED, blocks[remainder]); x += direction.dx; y += direction.dy; } else if (overflow) { // Or an overflow block - canvas.setCharAt(x, y, Style.UNSTYLED, BLOCK_OVERFLOW); + canvas.putCharAt(x, y, Style.UNSTYLED, BLOCK_OVERFLOW); x += direction.dx; y += direction.dy; } // Fill the rest with spaces int sizeLeft = maxSize - fullChunks - (overflow || remainder > 0 ? 1 : 0); for (int i = 0; i < sizeLeft; i++) { - canvas.setCharAt(x, y, Style.UNSTYLED, ' '); + canvas.putCharAt(x, y, Style.UNSTYLED, ' '); x += direction.dx; y += direction.dy; } diff --git a/twinkle-chart/src/main/java/org/codejive/twinkle/widgets/graphs/plot/Plot.java b/twinkle-chart/src/main/java/org/codejive/twinkle/widgets/graphs/plot/Plot.java index 7a4bfc2..23a8612 100644 --- a/twinkle-chart/src/main/java/org/codejive/twinkle/widgets/graphs/plot/Plot.java +++ b/twinkle-chart/src/main/java/org/codejive/twinkle/widgets/graphs/plot/Plot.java @@ -98,7 +98,7 @@ public Plot plot(int x, int y, Style style) { char newDot = selectDot(rx, ry); char existingDot = canvas.charAt(cx, cy); char combinedDot = combineDots(existingDot, newDot); - canvas.setCharAt(cx, cy, style, combinedDot); + canvas.putCharAt(cx, cy, style, combinedDot); return this; } @@ -110,14 +110,14 @@ public Plot unplot(int x, int y) { char removeDot = selectDot(rx, ry); char existingDot = canvas.charAt(cx, cy); char combinedDot = uncombineDots(existingDot, removeDot); - canvas.setCharAt(cx, cy, currentStyle, combinedDot); + canvas.putCharAt(cx, cy, currentStyle, combinedDot); return this; } public Plot clear() { for (int y = 0; y < size().height(); y++) { for (int x = 0; x < size().width(); x++) { - canvas.setCharAt(x, y, currentStyle, ' '); + canvas.putCharAt(x, y, currentStyle, ' '); } } return this; diff --git a/twinkle-core/src/main/java/org/codejive/twinkle/core/decor/SimpleBorderRenderer.java b/twinkle-core/src/main/java/org/codejive/twinkle/core/decor/SimpleBorderRenderer.java index a872b61..ee921cd 100644 --- a/twinkle-core/src/main/java/org/codejive/twinkle/core/decor/SimpleBorderRenderer.java +++ b/twinkle-core/src/main/java/org/codejive/twinkle/core/decor/SimpleBorderRenderer.java @@ -94,15 +94,15 @@ public SimpleBorderRenderer style(Style style) { @Override public void render(Canvas canvas) { - canvas.setCharAt(0, 0, style, corner(cornerStyle.topLeftChar, leftLineStyle, topLineStyle)); + canvas.putCharAt(0, 0, style, corner(cornerStyle.topLeftChar, leftLineStyle, topLineStyle)); canvas.drawHLineAt(1, 0, canvas.size().width() - 1, style, topLineStyle.horizontalChar); - canvas.setCharAt( + canvas.putCharAt( canvas.size().width() - 1, 0, style, corner(cornerStyle.topRightChar, rightLineStyle, topLineStyle)); canvas.drawVLineAt(0, 1, canvas.size().height() - 1, style, leftLineStyle.verticalChar); - canvas.setCharAt( + canvas.putCharAt( 0, canvas.size().height() - 1, style, @@ -113,7 +113,7 @@ public void render(Canvas canvas) { canvas.size().width() - 1, style, bottomLineStyle.horizontalChar); - canvas.setCharAt( + canvas.putCharAt( canvas.size().width() - 1, canvas.size().height() - 1, style, diff --git a/twinkle-core/src/main/java/org/codejive/twinkle/core/text/Buffer.java b/twinkle-core/src/main/java/org/codejive/twinkle/core/text/Buffer.java index 90b644e..97e5314 100644 --- a/twinkle-core/src/main/java/org/codejive/twinkle/core/text/Buffer.java +++ b/twinkle-core/src/main/java/org/codejive/twinkle/core/text/Buffer.java @@ -1,7 +1,5 @@ package org.codejive.twinkle.core.text; -import static org.codejive.twinkle.core.text.LineBuffer.REPLACEMENT_CHAR; - import java.io.IOException; import org.codejive.twinkle.ansi.Ansi; import org.codejive.twinkle.ansi.Style; @@ -13,11 +11,13 @@ public interface Buffer extends Canvas, Printable { + char REPLACEMENT_CHAR = '\uFFFD'; + @NonNull Buffer resize(@NonNull Size newSize); @Override default @NonNull View view(int left, int top, int width, int height) { - return view(new Rect(left, top, width, height)); + return view(Rect.of(left, top, width, height)); } @Override @@ -31,34 +31,62 @@ public interface Buffer extends Canvas, Printable { return new BufferImpl(size); } - static @NonNull Buffer of(@NonNull LineBuffer buffer) { - Rect rect = Rect.of(buffer.length(), 1); - LineBuffer[] lines = new LineBuffer[] {buffer}; - return new BufferImpl(rect, lines); - } + Buffer EMPTY = + new BufferImpl(Size.of(0, 1)) { + @Override + public @NonNull Buffer resize(@NonNull Size newSize) { + if (!newSize.equals(Size.of(0, 1))) { + throw new UnsupportedOperationException("Cannot resize EMPTY"); + } + return this; + } + }; interface View extends Buffer { - View moveTo(int x, int y); + @NonNull View moveTo(int x, int y); + + @NonNull View moveBy(int dx, int dy); + } +} + +abstract class BufferBase implements Buffer { + public abstract Rect rect(); + + public abstract int putStringAt(@NonNull Rect rect, int x, int y, @NonNull StyledIterator iter); + + public abstract void copyTo(@NonNull Rect rect, @NonNull Canvas canvas, int x, int y); + + public abstract String toString(@NonNull Rect rect); + + public abstract @NonNull Appendable toAnsi( + @NonNull Rect rect, @NonNull Appendable appendable, @NonNull Style currentStyle) + throws IOException; - View moveBy(int dx, int dy); + protected Rect limitedRect(@NonNull Rect rect) { + return this.rect().limited(rect.appliedTo(this.rect())); } } -class BufferImpl implements Buffer { - protected @NonNull Rect rect; - protected @NonNull LineBuffer[] lines; +class BufferImpl extends BufferBase { + private @NonNull Rect rect; + private int[] cpBuffer; + private String[] graphemeBuffer; + private long[] styleBuffer; public BufferImpl(@NonNull Size size) { - this.rect = Rect.of(size); - this.lines = new LineBuffer[size.height()]; - for (int i = 0; i < size.height(); i++) { - lines[i] = createBuffer(size.width()); - } + this.rect = Rect.of(0, 0, size); + int totalSize = size.width() * size.height(); + this.cpBuffer = new int[totalSize]; + this.graphemeBuffer = new String[totalSize]; + this.styleBuffer = new long[totalSize]; } - protected BufferImpl(@NonNull Rect rect, @NonNull LineBuffer[] lines) { - this.rect = rect; - this.lines = lines; + private BufferImpl( + @NonNull Size size, int[] cpBuffer, String[] graphemeBuffer, long[] styleBuffer) { + this.rect = Rect.of(0, 0, size); + this.cpBuffer = cpBuffer; + this.graphemeBuffer = graphemeBuffer; + this.styleBuffer = styleBuffer; } @Override @@ -66,64 +94,109 @@ protected BufferImpl(@NonNull Rect rect, @NonNull LineBuffer[] lines) { return rect.size(); } - protected @NonNull Rect rect() { + @Override + public @NonNull Rect rect() { return rect; } @Override public char charAt(int x, int y) { - if (outside(x, y, 0)) { + if (outside(x, y, 1)) { return REPLACEMENT_CHAR; } - return line(y).charAt(applyXOffset(x)); + int index = index(x, y); + if (graphemeBuffer[index] != null || Character.charCount(cpBuffer[index]) == 2) { + return REPLACEMENT_CHAR; + } + return (char) cpBuffer[index]; } @Override public int codepointAt(int x, int y) { - if (outside(x, y, 0)) { + if (outside(x, y, 1)) { return REPLACEMENT_CHAR; } - return line(y).codepointAt(applyXOffset(x)); + return cpBuffer[index(x, y)]; } @Override public @NonNull String graphemeAt(int x, int y) { - if (outside(x, y, 0)) { + if (outside(x, y, 1)) { return String.valueOf(REPLACEMENT_CHAR); } - return line(y).graphemeAt(applyXOffset(x)); + return graphemeAt_(index(x, y)); + } + + private @NonNull String graphemeAt_(int index) { + if (graphemeBuffer[index] != null) { + return graphemeBuffer[index]; + } + return new String(Character.toChars(cpBuffer[index])); + } + + public void graphemeAt(@NonNull Appendable appendable, int x, int y) throws IOException { + if (outside(x, y, 1)) { + appendable.append(REPLACEMENT_CHAR); + return; + } + graphemeAt_(appendable, index(x, y)); + } + + public void graphemeAt_(@NonNull Appendable appendable, int index) throws IOException { + if (graphemeBuffer[index] != null) { + appendable.append(graphemeBuffer[index]); + } else { + int cp = cpBuffer[index]; + if (cp == '\0') { + cp = ' '; + } + if (Character.isBmpCodePoint(cp)) { + appendable.append((char) cp); + } else if (Character.isValidCodePoint(cp)) { + appendable.append(Character.highSurrogate(cp)); + appendable.append(Character.lowSurrogate(cp)); + } else { + appendable.append(REPLACEMENT_CHAR); + } + } } @Override public @NonNull Style styleAt(int x, int y) { - if (outside(x, y, 0)) { + if (outside(x, y, 1)) { return Style.UNSTYLED; } - return line(y).styleAt(applyXOffset(x)); + return Style.of(styleBuffer[index(x, y)]); } @Override - public void setCharAt(int x, int y, @NonNull Style style, char c) { - if (outside(x, y, 0)) { + public void putCharAt(int x, int y, @NonNull Style style, char c) { + if (outside(x, y, 1)) { return; } - line(y).setCharAt(applyXOffset(x), style, c); + if (Character.isSurrogate(c)) { + c = REPLACEMENT_CHAR; + } + setCharAt_(index(x, y), style.state(), c, null); } @Override - public void setCharAt(int x, int y, @NonNull Style style, int cp) { - if (outside(x, y, 0)) { + public void putCharAt(int x, int y, @NonNull Style style, int cp) { + if (outside(x, y, 1)) { return; } - line(y).setCharAt(applyXOffset(x), style, cp); + setCharAt_(index(x, y), style.state(), cp, null); } @Override - public void setCharAt(int x, int y, @NonNull Style style, @NonNull CharSequence grapheme) { - if (outside(x, y, 0)) { + public void putCharAt(int x, int y, @NonNull Style style, @NonNull CharSequence grapheme) { + if (outside(x, y, 1)) { return; } - line(y).setCharAt(applyXOffset(x), style, grapheme); + if (grapheme.length() == 0) { + return; + } + setCharAt_(index(x, y), style.state(), -1, grapheme.toString()); } @Override @@ -131,99 +204,167 @@ public int putStringAt(int x, int y, @NonNull Style style, @NonNull CharSequence if (outside(x, y, str.length())) { return str.length(); } - return line(y).putStringAt(applyXOffset(x), style, str); + return putStringAt(x, y, StyledIterator.of(str, style)); } @Override public int putStringAt(int x, int y, @NonNull StyledIterator iter) { - return line(y).putStringAt(applyXOffset(x), iter); + return putStringAt(rect, x, y, iter); + } + + @Override + public int putStringAt(@NonNull Rect rect, int x, int y, @NonNull StyledIterator iter) { + Rect limitedRect = limitedRect(rect); + int startIndex = index(x, y); + int maxLength = limitedRect.right() - x + 1; + int cnt = 0; + + while (iter.hasNext()) { + int cp = iter.next(); + if (cp == '\n') { + break; + } + if (iter.width() == 0) { + continue; + } + Style style = iter.style(); + if (iter.width() == 2 && (cnt + 1) >= maxLength) { + setCharAt_(startIndex + cnt, style.state(), REPLACEMENT_CHAR, null); + break; + } + if (cnt < maxLength) { + if (iter.isComplex()) { + setCharAt_(startIndex + cnt, style.state(), -1, iter.sequence()); + } else { + setCharAt_(startIndex + cnt, style.state(), cp, null); + } + } + cnt++; + if (iter.width() == 2 && cnt < maxLength) { + setSkipAt(startIndex + cnt); + cnt++; + } + } + return cnt; } @Override public void drawHLineAt(int x, int y, int x2, @NonNull Style style, char c) { for (int i = x; i < x2; i++) { - setCharAt(i, y, style, c); + putCharAt(i, y, style, c); } } @Override public void drawVLineAt(int x, int y, int y2, @NonNull Style style, char c) { for (int i = y; i < y2; i++) { - setCharAt(x, i, style, c); + putCharAt(x, i, style, c); } } @Override - public void copyTo(Canvas canvas, int x, int y) { - for (int i = 0; i < lines.length; i++) { - for (int j = 0; j < lines[i].length(); j++) { - canvas.setCharAt(x + j, y + i, styleAt(j, i), charAt(j, i)); - } - } + public void copyTo(@NonNull Canvas canvas, int x, int y) { + copyTo(rect, canvas, x, y); } @Override - public @NonNull Buffer resize(@NonNull Size newSize) { - if (newSize.equals(size())) { - return this; - } - LineBuffer[] newLines = new LineBuffer[newSize.height()]; - for (int i = 0; i < newSize.height(); i++) { - if (i < lines.length) { - newLines[i] = lines[i].resize(newSize.width()); - } else { - newLines[i] = createBuffer(newSize.width()); + public void copyTo(@NonNull Rect rect, @NonNull Canvas canvas, int x, int y) { + Rect limitedRect = limitedRect(rect); + for (int j = limitedRect.top(); j <= limitedRect.bottom(); j++) { + for (int i = limitedRect.left(); i <= limitedRect.right(); i++) { + int index = index(i, j); + if (graphemeBuffer[index] == null) { + canvas.putCharAt(x + i, y + j, Style.of(styleBuffer[index]), cpBuffer[index]); + } else { + canvas.putCharAt( + x + i, y + j, Style.of(styleBuffer[index]), graphemeBuffer[index]); + } } } - lines = newLines; - Rect r = rect(); - rect = Rect.of(r.left(), r.top(), newSize); - return this; } - private @NonNull LineBuffer createBuffer(int width) { - return LineBuffer.of(width); + private boolean shouldSkipAt(int index) { + return cpBuffer[index] == -1 && graphemeBuffer[index] == null && styleBuffer[index] == -1; } - @Override - public Buffer.@NonNull View view(@NonNull Rect viewRect) { - return new BufferViewImpl(this, viewRect, lines); + private void setSkipAt(int index) { + setCharAt_(index, -1, -1, null); } - private LineBuffer line(int y) { - y = applyYOffset(y); - return lines[y]; + private void setCharAt_(int index, long styleState, int cp, String grapheme) { + cpBuffer[index] = cp; + graphemeBuffer[index] = grapheme; + styleBuffer[index] = styleState; } - private int applyXOffset(int x) { - return x + rect().left(); + @Override + public @NonNull Buffer resize(@NonNull Size newSize) { + if (newSize.equals(size())) { + return this; + } + + int newTotalSize = newSize.width() * newSize.height(); + int[] newCpBuffer = new int[newTotalSize]; + String[] newGraphemeBuffer = new String[newTotalSize]; + long[] newStyleBuffer = new long[newTotalSize]; + + Size oldSize = size(); + int copyHeight = Math.min(newSize.height(), oldSize.height()); + int copyWidth = Math.min(newSize.width(), oldSize.width()); + + for (int y = 0; y < copyHeight; y++) { + int oldIndex = y * oldSize.width(); + int newIndex = y * newSize.width(); + System.arraycopy(cpBuffer, oldIndex, newCpBuffer, newIndex, copyWidth); + System.arraycopy(graphemeBuffer, oldIndex, newGraphemeBuffer, newIndex, copyWidth); + System.arraycopy(styleBuffer, oldIndex, newStyleBuffer, newIndex, copyWidth); + } + + rect = Rect.of(0, 0, newSize); + cpBuffer = newCpBuffer; + graphemeBuffer = newGraphemeBuffer; + styleBuffer = newStyleBuffer; + + return this; } - private int applyYOffset(int y) { - return y + rect().top(); + @Override + public Buffer.@NonNull View view(@NonNull Rect viewRect) { + return new BufferViewImpl(this, viewRect); } private boolean outside(int x, int y, int length) { - int xAdjusted = applyXOffset(x); - Rect r = rect(); - return (xAdjusted + length) < r.left() || xAdjusted > r.right() || invalidYOffset(y); + Size sz = size(); + return x + length <= 0 || x >= sz.width() || y < 0 || y >= sz.height(); } - private boolean invalidYOffset(int y) { - int yAdjusted = applyYOffset(y); - Rect r = rect(); - return yAdjusted < r.top() || yAdjusted > r.bottom() || y < 0 || y >= lines.length; + private int index(int x, int y) { + return y * rect.width() + x; } @Override public String toString() { - // Assuming only single-width characters for capacity estimation - // plus one extra for newline + return toString(rect); + } + + public String toString(@NonNull Rect rect) { int initialCapacity = (size().width() + 1) * size().height(); StringBuilder sb = new StringBuilder(initialCapacity); - for (int y = 0; y < size().height(); y++) { - sb.append(line(y).toString()); - if (y < size().height() - 1) { + Rect limitedRect = limitedRect(rect); + for (int y = limitedRect.top(); y <= limitedRect.bottom(); y++) { + for (int x = limitedRect.left(); x <= limitedRect.right(); x++) { + int index = index(x, y); + if (shouldSkipAt(index)) { + continue; + } + String g = graphemeAt_(index); + if (g.isEmpty() || g.charAt(0) == '\0') { + sb.append(' '); + } else { + sb.append(g); + } + } + if (y < limitedRect.bottom()) { sb.append('\n'); } } @@ -232,8 +373,6 @@ public String toString() { @Override public @NonNull String toAnsiString(Style currentStyle) { - // Assuming only single-width characters for capacity estimation - // plus 20 extra for escape codes and newline int initialCapacity = (size().width() + 1) * size().height(); StringBuilder sb = new StringBuilder(initialCapacity); try { @@ -246,14 +385,31 @@ public String toString() { @Override public @NonNull Appendable toAnsi(Appendable appendable, Style currentStyle) throws IOException { + return toAnsi(rect, appendable, currentStyle); + } + + public @NonNull Appendable toAnsi( + @NonNull Rect rect, @NonNull Appendable appendable, @NonNull Style currentStyle) + throws IOException { if (currentStyle == Style.UNKNOWN) { currentStyle = Style.DEFAULT; appendable.append(Ansi.STYLE_RESET); } - for (int y = 0; y < size().height(); y++) { - line(y).toAnsi(appendable, currentStyle); - currentStyle = line(y).styleAt(size().width() - 1); - if (y < size().height() - 1) { + Rect limitedRect = limitedRect(rect); + for (int y = limitedRect.top(); y <= limitedRect.bottom(); y++) { + for (int x = limitedRect.left(); x <= limitedRect.right(); x++) { + int index = index(x, y); + if (shouldSkipAt(index)) { + continue; + } + if (styleBuffer[index] != currentStyle.state()) { + Style style = Style.of(styleBuffer[index]); + style.toAnsi(appendable, currentStyle); + currentStyle = Style.of(styleBuffer[index]); + } + graphemeAt_(appendable, index); + } + if (y < limitedRect.bottom()) { appendable.append('\n'); } } @@ -261,27 +417,23 @@ public String toString() { } } -class BufferViewImpl extends BufferImpl implements Buffer.View { - protected final @NonNull BufferImpl parentPanel; +class BufferViewImpl extends BufferBase implements Buffer.View { + private final @NonNull BufferBase parent; + private @NonNull Rect rect; - protected BufferViewImpl( - @NonNull BufferImpl parentPanel, @NonNull Rect rect, @NonNull LineBuffer[] lines) { - super(rect, lines); - this.parentPanel = parentPanel; + protected BufferViewImpl(@NonNull BufferBase parent, @NonNull Rect rect) { + this.parent = parent; + this.rect = rect; } @Override - protected @NonNull Rect rect() { - Rect pr = parentPanel.rect(); - return Rect.of( - this.rect.left() + pr.left(), - this.rect.top() + pr.top(), - Math.min( - this.rect.size().width(), - Math.max(0, pr.size().width() - this.rect.left())), - Math.min( - this.rect.size().height(), - Math.max(0, pr.size().height() - this.rect.top()))); + public @NonNull Size size() { + return rect.size(); + } + + @Override + public @NonNull Rect rect() { + return rect; } @Override @@ -289,22 +441,166 @@ protected BufferViewImpl( if (newSize.equals(size())) { return this; } - Rect r = rect(); - rect = Rect.of(r.left(), r.top(), newSize); + rect = Rect.of(rect.left(), rect.top(), newSize); return this; } @Override - public View moveTo(int x, int y) { - Rect r = rect(); - rect = Rect.of(x, y, r.size()); + public char charAt(int x, int y) { + if (outside(x, y, 1)) { + return REPLACEMENT_CHAR; + } + return parent.charAt(adjustX(x), adjustY(y)); + } + + @Override + public int codepointAt(int x, int y) { + if (outside(x, y, 1)) { + return REPLACEMENT_CHAR; + } + return parent.codepointAt(adjustX(x), adjustY(y)); + } + + @Override + public @NonNull String graphemeAt(int x, int y) { + if (outside(x, y, 1)) { + return String.valueOf(REPLACEMENT_CHAR); + } + return parent.graphemeAt(adjustX(x), adjustY(y)); + } + + @Override + public @NonNull Style styleAt(int x, int y) { + if (outside(x, y, 1)) { + return Style.UNSTYLED; + } + return parent.styleAt(adjustX(x), adjustY(y)); + } + + @Override + public void putCharAt(int x, int y, @NonNull Style style, char c) { + if (outside(x, y, 1)) { + return; + } + parent.putCharAt(adjustX(x), adjustY(y), style, c); + } + + @Override + public void putCharAt(int x, int y, @NonNull Style style, int cp) { + if (outside(x, y, 1)) { + return; + } + parent.putCharAt(adjustX(x), adjustY(y), style, cp); + } + + @Override + public void putCharAt(int x, int y, @NonNull Style style, @NonNull CharSequence grapheme) { + if (outside(x, y, 1)) { + return; + } + parent.putCharAt(adjustX(x), adjustY(y), style, grapheme); + } + + @Override + public int putStringAt(int x, int y, @NonNull Style style, @NonNull CharSequence str) { + if (outside(x, y, str.length())) { + return str.length(); + } + return putStringAt(x, y, StyledIterator.of(str, style)); + } + + @Override + public int putStringAt(int x, int y, @NonNull StyledIterator iter) { + if (outside(x, y, 1)) { + return 0; + } + return parent.putStringAt(rect, adjustX(x), adjustY(y), iter); + } + + @Override + public int putStringAt(@NonNull Rect rect, int x, int y, @NonNull StyledIterator iter) { + if (outside(x, y, 1)) { + return 0; + } + return parent.putStringAt(limitedRect(rect), adjustX(x), adjustY(y), iter); + } + + @Override + public void drawHLineAt(int x, int y, int x2, @NonNull Style style, char c) { + int ax = adjustX(x); + int ay = adjustY(y); + int ax2 = adjustX(x2); + parent.drawHLineAt(ax, ay, ax2, style, c); + } + + @Override + public void drawVLineAt(int x, int y, int y2, @NonNull Style style, char c) { + int ax = adjustX(x); + int ay = adjustY(y); + int ay2 = adjustY(y2); + parent.drawVLineAt(ax, ay, ay2, style, c); + } + + @Override + public void copyTo(@NonNull Canvas canvas, int x, int y) { + parent.copyTo(rect, canvas, adjustX(x), adjustY(y)); + } + + @Override + public void copyTo(@NonNull Rect rect, @NonNull Canvas canvas, int x, int y) { + parent.copyTo(limitedRect(rect), canvas, adjustX(x), adjustY(y)); + } + + @Override + public @NonNull View view(@NonNull Rect rect) { + return new BufferViewImpl(this, rect); + } + + @Override + public @NonNull View moveTo(int x, int y) { + rect = Rect.of(x, y, rect.size()); return this; } @Override - public View moveBy(int dx, int dy) { - Rect r = rect(); - rect = Rect.of(r.left() + dx, r.top() + dy, r.size()); + public @NonNull View moveBy(int dx, int dy) { + rect = Rect.of(rect.left() + dx, rect.top() + dy, rect.size()); return this; } + + @Override + public String toString() { + return parent.toString(rect); + } + + @Override + public String toString(@NonNull Rect rect) { + return parent.toString(limitedRect(rect)); + } + + @Override + public @NonNull Appendable toAnsi(Appendable appendable, Style currentStyle) + throws IOException { + return parent.toAnsi(rect, appendable, currentStyle); + } + + @Override + public @NonNull Appendable toAnsi( + @NonNull Rect rect, @NonNull Appendable appendable, @NonNull Style currentStyle) + throws IOException { + return parent.toAnsi(limitedRect(rect), appendable, currentStyle); + } + + private boolean outside(int x, int y, int length) { + Size sz = size(); + return x + length <= 0 || x >= sz.width() || y < 0 || y >= sz.height(); + } + + private int adjustX(int x) { + return rect.left() + x; + } + + private int adjustY(int y) { + return rect.top() + y; + } } diff --git a/twinkle-core/src/main/java/org/codejive/twinkle/core/text/Canvas.java b/twinkle-core/src/main/java/org/codejive/twinkle/core/text/Canvas.java index 72a42bc..64c383c 100644 --- a/twinkle-core/src/main/java/org/codejive/twinkle/core/text/Canvas.java +++ b/twinkle-core/src/main/java/org/codejive/twinkle/core/text/Canvas.java @@ -15,11 +15,11 @@ public interface Canvas extends Sized { @NonNull Style styleAt(int x, int y); - void setCharAt(int x, int y, @NonNull Style style, char c); + void putCharAt(int x, int y, @NonNull Style style, char c); - void setCharAt(int x, int y, @NonNull Style style, int cp); + void putCharAt(int x, int y, @NonNull Style style, int cp); - void setCharAt(int x, int y, @NonNull Style style, @NonNull CharSequence grapheme); + void putCharAt(int x, int y, @NonNull Style style, @NonNull CharSequence grapheme); int putStringAt(int x, int y, @NonNull Style style, @NonNull CharSequence str); diff --git a/twinkle-core/src/main/java/org/codejive/twinkle/core/text/LineBuffer.java b/twinkle-core/src/main/java/org/codejive/twinkle/core/text/LineBuffer.java deleted file mode 100644 index d6f3256..0000000 --- a/twinkle-core/src/main/java/org/codejive/twinkle/core/text/LineBuffer.java +++ /dev/null @@ -1,367 +0,0 @@ -package org.codejive.twinkle.core.text; - -import java.io.IOException; -import org.codejive.twinkle.ansi.Ansi; -import org.codejive.twinkle.ansi.Style; -import org.codejive.twinkle.util.Printable; -import org.codejive.twinkle.util.StyledIterator; -import org.jspecify.annotations.NonNull; - -public interface LineBuffer extends Printable { - - char REPLACEMENT_CHAR = '\uFFFD'; - - int length(); - - char charAt(int index); - - int codepointAt(int i); - - @NonNull String graphemeAt(int i); - - @NonNull Style styleAt(int i); - - void setCharAt(int index, @NonNull Style style, char c); - - void setCharAt(int index, @NonNull Style style, int cp); - - void setCharAt(int index, @NonNull Style style, @NonNull CharSequence grapheme); - - int putStringAt(int index, @NonNull Style style, @NonNull CharSequence str); - - int putStringAt(int index, @NonNull StyledIterator iter); - - @NonNull LineBuffer subSequence(int start, int end); - - @NonNull LineBuffer resize(int newSize); - - LineBuffer EMPTY = - new LineBufferImpl(0) { - @Override - public @NonNull LineBufferImpl resize(int newSize) { - if (newSize != 0) { - throw new UnsupportedOperationException("Cannot resize EMPTY"); - } - return this; - } - }; - - static @NonNull LineBuffer of(int width) { - return new LineBufferImpl(width); - } -} - -class LineBufferImpl implements LineBuffer { - protected int[] cpBuffer; - protected String[] graphemeBuffer; - protected long[] styleBuffer; - - public LineBufferImpl(int size) { - cpBuffer = new int[size]; - graphemeBuffer = new String[size]; - styleBuffer = new long[size]; - } - - protected LineBufferImpl(int[] cpBuffer, String[] graphemeBuffer, long[] styleBuffer) { - if (cpBuffer.length != styleBuffer.length || cpBuffer.length != graphemeBuffer.length) { - throw new IllegalArgumentException( - "Codepoint, grapheme and style buffers must have the same length"); - } - this.cpBuffer = cpBuffer; - this.graphemeBuffer = graphemeBuffer; - this.styleBuffer = styleBuffer; - } - - @Override - public int length() { - return cpBuffer.length; - } - - @Override - public char charAt(int index) { - if (invalidIndex(index)) { - return REPLACEMENT_CHAR; - } - if (graphemeBuffer[index] != null || Character.charCount(cpBuffer[index]) == 2) { - // TODO log warning about extended Unicode characters not being supported - return REPLACEMENT_CHAR; - } - return (char) cpBuffer[index]; - } - - @Override - public int codepointAt(int index) { - if (invalidIndex(index)) { - return REPLACEMENT_CHAR; - } - return cpBuffer[index]; - } - - @Override - public @NonNull String graphemeAt(int index) { - if (invalidIndex(index)) { - return String.valueOf(REPLACEMENT_CHAR); - } - if (graphemeBuffer[index] != null) { - return graphemeBuffer[index]; - } - return new String(Character.toChars(cpBuffer[index])); - } - - @Override - public @NonNull Style styleAt(int index) { - if (invalidIndex(index)) { - return Style.UNSTYLED; - } - return Style.of(styleBuffer[index]); - } - - @Override - public void setCharAt(int index, @NonNull Style style, char ch) { - if (invalidIndex(index)) { - return; - } - if (Character.isSurrogate(ch)) { - // TODO log warning about surrogate characters not being supported - ch = REPLACEMENT_CHAR; - } - setCharAt_(index, style, ch); - } - - private void setCharAt_(int index, @NonNull Style style, char ch) { - if (Character.isSurrogate(ch)) { - // TODO log warning about surrogate characters not being supported - ch = REPLACEMENT_CHAR; - } - setCharAt_(index, style.state(), ch, null); - } - - @Override - public void setCharAt(int index, @NonNull Style style, int cp) { - if (invalidIndex(index)) { - return; - } - setCharAt_(index, style, cp); - } - - private void setCharAt_(int index, @NonNull Style style, int cp) { - setCharAt_(index, style.state(), cp, null); - } - - @Override - public void setCharAt(int index, @NonNull Style style, @NonNull CharSequence grapheme) { - if (invalidIndex(index)) { - return; - } - setCharAt_(index, style, grapheme); - } - - private void setCharAt_(int index, @NonNull Style style, @NonNull CharSequence grapheme) { - if (grapheme.length() == 0) { - return; - } - setCharAt_(index, style.state(), -1, grapheme.toString()); - } - - private boolean shouldSkipAt(int index) { - return cpBuffer[index] == -1 && graphemeBuffer[index] == null && styleBuffer[index] == -1; - } - - private void setSkipAt(int index) { - setCharAt_(index, -1, -1, null); - } - - private void setCharAt_(int index, long styleState, int cp, String grapheme) { - cpBuffer[index] = cp; - graphemeBuffer[index] = grapheme; - styleBuffer[index] = styleState; - } - - @Override - public int putStringAt(int index, @NonNull Style style, @NonNull CharSequence str) { - return putStringAt(index, StyledIterator.of(str, style)); - } - - @Override - public int putStringAt(int index, @NonNull StyledIterator iter) { - int minIndex = 0; - int maxIndex = cpBuffer.length; - int startIndex = Math.max(index, minIndex); - int len = maxIndex - startIndex; - int cnt = 0; - while (iter.hasNext()) { - int cp = iter.next(); - if (cp == '\n') { - // We only deal with single lines here, so stop at newline - break; - } - if (iter.width() == 0) { - // Skip any zero-width characters - continue; - } - Style style = iter.style(); - if (iter.width() == 2 && (cnt + 1) >= len) { - // Not enough space for a wide character - setCharAt_(startIndex + cnt, style, REPLACEMENT_CHAR); - break; - } - if (cnt < len) { - if (iter.isComplex()) { - setCharAt_(startIndex + cnt, style, iter.sequence()); - } else { - setCharAt_(startIndex + cnt, style, cp); - } - } - cnt++; - if (iter.width() == 2 && cnt < len) { - // We're dealing with a wide character, so we need to mark the next cell as skipped - setSkipAt(startIndex + cnt); - cnt++; - } - } - return cnt; - } - - @Override - public @NonNull LineBufferImpl subSequence(int start, int end) { - if (start < 0 || end > length() || start > end) { - throw new IndexOutOfBoundsException( - "Invalid subsequence range: " + start + " to " + end); - } - int subLength = end - start; - int[] subCpBuffer = new int[subLength]; - String[] subGraphemeBuffer = new String[subLength]; - long[] subStyleBuffer = new long[subLength]; - System.arraycopy(cpBuffer, start, subCpBuffer, 0, subLength); - System.arraycopy(graphemeBuffer, start, subGraphemeBuffer, 0, subLength); - System.arraycopy(styleBuffer, start, subStyleBuffer, 0, subLength); - return new LineBufferImpl(subCpBuffer, subGraphemeBuffer, subStyleBuffer); - } - - @Override - public @NonNull LineBufferImpl resize(int newSize) { - if (newSize == cpBuffer.length) { - return this; - } - int[] newCpBuffer = new int[newSize]; - String[] newGraphemeBuffer = new String[newSize]; - long[] newStyleBuffer = new long[newSize]; - int copyLength = Math.min(newSize, length()); - System.arraycopy(cpBuffer, 0, newCpBuffer, 0, copyLength); - System.arraycopy(graphemeBuffer, 0, newGraphemeBuffer, 0, copyLength); - System.arraycopy(styleBuffer, 0, newStyleBuffer, 0, copyLength); - cpBuffer = newCpBuffer; - graphemeBuffer = newGraphemeBuffer; - styleBuffer = newStyleBuffer; - return this; - } - - private static int codepointCount(@NonNull CharSequence str) { - int count = 0; - for (int i = 0; i < str.length(); ) { - int cp = codepointAt(str, i); - count++; - i += Character.charCount(cp); - } - return count; - } - - private static int codepointAt(@NonNull CharSequence str, int index) { - if (index < 0 || index >= str.length()) { - return REPLACEMENT_CHAR; - } - char ch = str.charAt(index); - if (Character.isHighSurrogate(ch) && (index + 1) < str.length()) { - char low = str.charAt(index + 1); - if (Character.isLowSurrogate(low)) { - return Character.toCodePoint(ch, low); - } - } else if (Character.isLowSurrogate(ch) && index > 0) { - char high = str.charAt(index - 1); - if (Character.isHighSurrogate(high)) { - return Character.toCodePoint(high, ch); - } - } - return ch; - } - - private boolean invalidIndex(int index) { - return index < 0 || index >= cpBuffer.length; - } - - private boolean outside(int index, int length) { - return (index + length) <= 0 || index >= cpBuffer.length; - } - - @Override - public @NonNull String toString() { - // Assuming only single-width characters for capacity estimation - int initialCapacity = length(); - StringBuilder sb = new StringBuilder(initialCapacity); - for (int i = 0; i < length(); i++) { - if (shouldSkipAt(i)) { - continue; - } - if (graphemeBuffer[i] != null) { - sb.append(graphemeBuffer[i]); - } else { - int cp = cpBuffer[i]; - if (cp == '\0') { - cp = ' '; - } - sb.appendCodePoint(cp); - } - } - return sb.toString(); - } - - @Override - public @NonNull String toAnsiString(Style currentStyle) { - // Assuming only single-width characters for capacity estimation - // plus 20 extra for escape codes - int initialCapacity = length() + 20; - StringBuilder sb = new StringBuilder(initialCapacity); - try { - return toAnsi(sb, currentStyle).toString(); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - @Override - public @NonNull Appendable toAnsi(Appendable appendable, Style currentStyle) - throws IOException { - if (currentStyle == Style.UNKNOWN) { - currentStyle = Style.DEFAULT; - appendable.append(Ansi.STYLE_RESET); - } - for (int i = 0; i < length(); i++) { - if (shouldSkipAt(i)) { - continue; - } - if (styleBuffer[i] != currentStyle.state()) { - Style style = Style.of(styleBuffer[i]); - style.toAnsi(appendable, currentStyle); - currentStyle = Style.of(styleBuffer[i]); - } - if (graphemeBuffer[i] != null) { - appendable.append(graphemeBuffer[i]); - } else { - int cp = cpBuffer[i]; - if (cp == '\0') { - cp = ' '; - } - if (Character.isBmpCodePoint(cp)) { - appendable.append((char) cp); - } else if (Character.isValidCodePoint(cp)) { - appendable.append(Character.lowSurrogate(cp)); - appendable.append(Character.highSurrogate(cp)); - } else { - throw new IllegalArgumentException( - String.format("Not a valid Unicode code point: 0x%X", cp)); - } - } - } - return appendable; - } -} diff --git a/twinkle-core/src/main/java/org/codejive/twinkle/core/util/Rect.java b/twinkle-core/src/main/java/org/codejive/twinkle/core/util/Rect.java index cb9d037..4747cae 100644 --- a/twinkle-core/src/main/java/org/codejive/twinkle/core/util/Rect.java +++ b/twinkle-core/src/main/java/org/codejive/twinkle/core/util/Rect.java @@ -82,6 +82,10 @@ public Rect limited(@NonNull Rect availableRect) { return new Rect(l, t, r - l + 1, b - t + 1); } + public Rect appliedTo(@NonNull Rect rect) { + return new Rect(rect.left() + left, rect.top() + top, width(), height()); + } + @Override public String toString() { return "Rect{" diff --git a/twinkle-core/src/test/java/org/codejive/twinkle/core/text/LineBufferTimings.java b/twinkle-core/src/test/java/org/codejive/twinkle/core/text/BufferTimings.java similarity index 81% rename from twinkle-core/src/test/java/org/codejive/twinkle/core/text/LineBufferTimings.java rename to twinkle-core/src/test/java/org/codejive/twinkle/core/text/BufferTimings.java index b4b7b28..06a1a91 100644 --- a/twinkle-core/src/test/java/org/codejive/twinkle/core/text/LineBufferTimings.java +++ b/twinkle-core/src/test/java/org/codejive/twinkle/core/text/BufferTimings.java @@ -2,7 +2,7 @@ import org.codejive.twinkle.ansi.Style; -public class LineBufferTimings { +public class BufferTimings { private static int iterations = 1_000_000; public static void main(String[] args) { @@ -41,34 +41,34 @@ public static void main(String[] args) { }); System.out.println("Timing simple strings:"); - timeSimpleString(LineBuffer.of(1000)); + timeSimpleString(Buffer.of(1000, 1)); System.out.println("Timing strings with surrogates:"); - timeStringWithSurrogates(LineBuffer.of(1000)); + timeStringWithSurrogates(Buffer.of(1000, 1)); } - private static void timeSimpleString(LineBuffer buffer) { + private static void timeSimpleString(Buffer buffer) { titer( buffer.getClass().getSimpleName(), () -> { for (int i = 0; i < 500; i += 10) { - buffer.putStringAt(i, Style.UNSTYLED, "0123456789"); + buffer.putStringAt(i, 0, Style.UNSTYLED, "0123456789"); } for (int i = 500; i < 1000; i += 10) { - buffer.putStringAt(i, Style.UNSTYLED, "0123456789"); + buffer.putStringAt(i, 0, Style.UNSTYLED, "0123456789"); } }); } - private static void timeStringWithSurrogates(LineBuffer buffer) { + private static void timeStringWithSurrogates(Buffer buffer) { titer( buffer.getClass().getSimpleName(), () -> { for (int i = 0; i < 500; i += 10) { - buffer.putStringAt(i, Style.UNSTYLED, "0123456789"); + buffer.putStringAt(i, 0, Style.UNSTYLED, "0123456789"); } for (int i = 500; i < 1000; i += 10) { - buffer.putStringAt(i, Style.UNSTYLED, "01234\uD83D\uDE8056789"); + buffer.putStringAt(i, 0, Style.UNSTYLED, "01234\uD83D\uDE8056789"); } }); } diff --git a/twinkle-core/src/test/java/org/codejive/twinkle/core/text/TestBuffer.java b/twinkle-core/src/test/java/org/codejive/twinkle/core/text/TestBuffer.java index c02b671..39f2fa7 100644 --- a/twinkle-core/src/test/java/org/codejive/twinkle/core/text/TestBuffer.java +++ b/twinkle-core/src/test/java/org/codejive/twinkle/core/text/TestBuffer.java @@ -2,6 +2,7 @@ import static org.assertj.core.api.Assertions.assertThat; +import org.codejive.twinkle.ansi.Ansi; import org.codejive.twinkle.ansi.Color; import org.codejive.twinkle.ansi.Style; import org.codejive.twinkle.core.util.Size; @@ -10,15 +11,88 @@ public class TestBuffer { @Test - public void testPanelCreation() { + public void testBufferCreation() { Buffer buffer = Buffer.of(10, 5); Size size = buffer.size(); assertThat(size.width()).isEqualTo(10); assertThat(size.height()).isEqualTo(5); } + public void testStyledBufferPutGetChar() { + Buffer buffer = Buffer.of(10, 1); + for (int i = 0; i < buffer.size().width(); i++) { + buffer.putCharAt(i, 0, Style.ITALIC, (char) ('a' + i)); + } + for (int i = 0; i < buffer.size().width(); i++) { + assertThat(buffer.charAt(i, 0)).isEqualTo((char) ('a' + i)); + assertThat(buffer.styleAt(i, 0)).isEqualTo(Style.DEFAULT.italic()); + } + } + + @Test + public void testStyledBufferPutCharToString() { + Buffer buffer = Buffer.of(10, 1); + for (int i = 0; i < buffer.size().width(); i++) { + buffer.putCharAt(i, 0, Style.ITALIC, (char) ('a' + i)); + } + assertThat(buffer.toString()).isEqualTo("abcdefghij"); + } + + @Test + public void testStyledBufferPutCharToAnsiString() { + Buffer buffer = Buffer.of(10, 1); + for (int i = 0; i < buffer.size().width(); i++) { + Style style = i < 5 ? Style.ITALIC : Style.UNDERLINED; + buffer.putCharAt(i, 0, style, (char) ('a' + i)); + } + assertThat(buffer.toAnsiString()) + .isEqualTo( + Ansi.STYLE_RESET + + Ansi.style(Ansi.ITALICIZED) + + "abcde" + + Ansi.style(Ansi.NOTITALICIZED, Ansi.UNDERLINED) + + "fghij"); + } + + @Test + public void testStyledBufferPutCharToAnsiStringWithCurrentStyle() { + Buffer buffer = Buffer.of(10, 1); + for (int i = 0; i < buffer.size().width(); i++) { + Style style = i < 5 ? Style.ITALIC : Style.UNDERLINED; + buffer.putCharAt(i, 0, style, (char) ('a' + i)); + } + assertThat(buffer.toAnsiString(Style.DEFAULT.italic())) + .isEqualTo("abcde" + Ansi.style(Ansi.NOTITALICIZED, Ansi.UNDERLINED) + "fghij"); + } + + @Test + public void testStyledBufferPutCharToAnsiStringWithUnderAndOverflow() { + Buffer buffer = Buffer.of(10, 1); + for (int i = 0; i < buffer.size().width() + 10; i++) { + Style style = i < 10 ? Style.ITALIC : Style.UNDERLINED; + buffer.putCharAt(i - 5, 0, style, (char) ('a' + i)); + } + assertThat(buffer.toAnsiString()) + .isEqualTo( + Ansi.STYLE_RESET + + Ansi.style(Ansi.ITALICIZED) + + "fghij" + + Ansi.style(Ansi.NOTITALICIZED, Ansi.UNDERLINED) + + "klmno"); + } + + @Test + public void testStyledBufferPutStringGetChar() { + Buffer buffer = Buffer.of(10, 1); + buffer.putStringAt(0, 0, Style.DEFAULT.italic(), "abcdefghij"); + for (int i = 0; i < buffer.size().width(); i++) { + assertThat(buffer.charAt(i, 0)).isEqualTo((char) ('a' + i)); + assertThat(buffer.styleAt(i, 0)).isEqualTo(Style.DEFAULT.italic()); + } + } + @Test - public void testPanelDefaultInnerContent() { + public void testBufferDefaultInnerContent() { Buffer buffer = Buffer.of(10, 5); Size size = buffer.size(); for (int y = 0; y < size.height(); y++) { @@ -30,7 +104,7 @@ public void testPanelDefaultInnerContent() { } @Test - public void testPanelDefaultOuterContent() { + public void testBufferDefaultOuterContent() { Buffer buffer = Buffer.of(10, 5); Size size = buffer.size(); for (int y = -5; y < size.height() + 5; y++) { @@ -38,15 +112,15 @@ public void testPanelDefaultOuterContent() { if (x >= 0 && x < size.width() && y >= 0 && y < size.height()) { continue; // Skip inner content } - assertThat(buffer.charAt(x, y)).isEqualTo(LineBuffer.REPLACEMENT_CHAR); + assertThat(buffer.charAt(x, y)).isEqualTo(Buffer.REPLACEMENT_CHAR); assertThat(buffer.styleAt(x, y)).isEqualTo(Style.UNSTYLED); } } } @Test - public void testPanelNewContents() { - Buffer buffer = createPanel(); + public void testBufferNewContents() { + Buffer buffer = createBuffer(); Size size = buffer.size(); for (int y = 0; y < size.height(); y++) { for (int x = 0; x < size.width(); x++) { @@ -57,9 +131,9 @@ public void testPanelNewContents() { } @Test - public void testPanelView() { - Buffer buffer = createPanel(); - Canvas view = buffer.view(1, 1, 3, 3); + public void testBufferView() { + Buffer buffer = createBuffer(); + Buffer view = buffer.view(1, 1, 3, 3); Size size = view.size(); for (int y = 0; y < size.height(); y++) { for (int x = 0; x < size.width(); x++) { @@ -72,24 +146,33 @@ public void testPanelView() { } @Test - public void testPanelViewOutside() { - Buffer buffer = createPanel(); - Canvas view = buffer.view(1, 1, 3, 3); + public void testBufferViewPutString() { + Buffer buffer = createBuffer(); + Buffer view = buffer.view(1, 1, 3, 3); + view.putStringAt(1, 1, Style.DEFAULT.fgColor(Color.BasicColor.RED), "XYZ"); + assertThat(buffer.toString()) + .isEqualTo("ABCDE\n" + "FGHIJ\n" + "KLXYO\n" + "PQRST\n" + "UVWXY"); + } + + @Test + public void testBufferViewOutside() { + Buffer buffer = createBuffer(); + Buffer view = buffer.view(1, 1, 3, 3); Size size = view.size(); for (int y = -2; y < size.height() + 2; y++) { for (int x = -2; x < size.width() + 2; x++) { if (x >= 0 && x < size.width() && y >= 0 && y < size.height()) { continue; // Skip inner content } - assertThat(view.charAt(x, y)).isEqualTo(LineBuffer.REPLACEMENT_CHAR); + assertThat(view.charAt(x, y)).isEqualTo(Buffer.REPLACEMENT_CHAR); assertThat(view.styleAt(x, y)).isEqualTo(Style.UNSTYLED); } } } @Test - public void testPanelNestedView() { - Buffer buffer = createPanel(); + public void testBufferNestedView() { + Buffer buffer = createBuffer(); Buffer view1 = buffer.view(1, 1, 3, 3); Buffer view2 = view1.view(1, 1, 2, 2); Size size = view2.size(); @@ -101,11 +184,34 @@ public void testPanelNestedView() { .isEqualTo(Style.DEFAULT.fgColor(Color.indexed(x + 2))); } } + assertThat(view2.toString()).isEqualTo("MN\nRS"); + assertThat(view2.toAnsiString()) + .isEqualTo( + Ansi.CSI + + "0m" + + Ansi.CSI + + "38;5;2mM" + + Ansi.CSI + + "38;5;3mN\n" + + Ansi.CSI + + "38;5;2mR" + + Ansi.CSI + + "38;5;3mS"); + } + + @Test + public void testBufferNestedViewPutString() { + Buffer buffer = createBuffer(); + Buffer view1 = buffer.view(1, 1, 3, 3); + Buffer view2 = view1.view(1, 1, 2, 2); + view2.putStringAt(0, 1, Style.DEFAULT.fgColor(Color.BasicColor.RED), "XYZ"); + assertThat(buffer.toString()) + .isEqualTo("ABCDE\n" + "FGHIJ\n" + "KLMNO\n" + "PQXYT\n" + "UVWXY"); } @Test - public void testPanelNestedViewOutside() { - Buffer buffer = createPanel(); + public void testBufferNestedViewOutside() { + Buffer buffer = createBuffer(); Buffer view1 = buffer.view(1, 1, 3, 3); Buffer view2 = view1.view(1, 1, 2, 2); Size size = view2.size(); @@ -114,15 +220,30 @@ public void testPanelNestedViewOutside() { if (x >= 0 && x < size.width() && y >= 0 && y < size.height()) { continue; // Skip inner content } - assertThat(view2.charAt(x, y)).isEqualTo(LineBuffer.REPLACEMENT_CHAR); + assertThat(view2.charAt(x, y)).isEqualTo(Buffer.REPLACEMENT_CHAR); + assertThat(view2.styleAt(x, y)).isEqualTo(Style.UNSTYLED); + } + } + } + + @Test + public void testBufferNestedViewOutsideInside() { + Buffer buffer = createBuffer(); + Buffer view1 = buffer.view(10, 10, 3, 3); + Buffer view2 = view1.view(-10, -10, 2, 2); + + Size size = view2.size(); + for (int y = 0; y < size.height(); y++) { + for (int x = 0; x < size.width(); x++) { + assertThat(view2.charAt(x, y)).isEqualTo(Buffer.REPLACEMENT_CHAR); assertThat(view2.styleAt(x, y)).isEqualTo(Style.UNSTYLED); } } } @Test - public void testPanelNestedViewMoved() { - Buffer buffer = createPanel(); + public void testBufferNestedViewMoved() { + Buffer buffer = createBuffer(); Buffer.View view1 = buffer.view(1, 1, 3, 3); Buffer view2 = view1.view(1, 1, 2, 2); @@ -140,8 +261,8 @@ public void testPanelNestedViewMoved() { } @Test - public void testPanelNestedViewMovedFullyOutside() { - Buffer buffer = createPanel(); + public void testBufferNestedViewMovedFullyOutside() { + Buffer buffer = createBuffer(); Buffer.View view1 = buffer.view(1, 1, 3, 3); Buffer view2 = view1.view(1, 1, 2, 2); @@ -150,15 +271,15 @@ public void testPanelNestedViewMovedFullyOutside() { Size size = view2.size(); for (int y = 0; y < size.height(); y++) { for (int x = 0; x < size.width(); x++) { - assertThat(view2.charAt(x, y)).isEqualTo(LineBuffer.REPLACEMENT_CHAR); + assertThat(view2.charAt(x, y)).isEqualTo(Buffer.REPLACEMENT_CHAR); assertThat(view2.styleAt(x, y)).isEqualTo(Style.UNSTYLED); } } } @Test - public void testPanelNestedViewMovedPartiallyOutside() { - Buffer buffer = createPanel(); + public void testBufferNestedViewMovedPartiallyOutside() { + Buffer buffer = createBuffer(); Buffer.View view1 = buffer.view(1, 1, 3, 3); Buffer view2 = view1.view(1, 1, 2, 2); @@ -172,19 +293,19 @@ public void testPanelNestedViewMovedPartiallyOutside() { assertThat(view2.styleAt(x, y)) .isEqualTo(Style.DEFAULT.fgColor(Color.indexed(x + 4))); } else { - assertThat(view2.charAt(x, y)).isEqualTo(LineBuffer.REPLACEMENT_CHAR); + assertThat(view2.charAt(x, y)).isEqualTo(Buffer.REPLACEMENT_CHAR); assertThat(view2.styleAt(x, y)).isEqualTo(Style.UNSTYLED); } } } } - private Buffer createPanel() { + private Buffer createBuffer() { Buffer buffer = Buffer.of(5, 5); Size size = buffer.size(); for (int y = 0; y < size.height(); y++) { for (int x = 0; x < size.width(); x++) { - buffer.setCharAt( + buffer.putCharAt( x, y, Style.ofFgColor(Color.indexed(x)), diff --git a/twinkle-core/src/test/java/org/codejive/twinkle/core/text/TestLineBuffer.java b/twinkle-core/src/test/java/org/codejive/twinkle/core/text/TestLineBuffer.java deleted file mode 100644 index e59232b..0000000 --- a/twinkle-core/src/test/java/org/codejive/twinkle/core/text/TestLineBuffer.java +++ /dev/null @@ -1,89 +0,0 @@ -package org.codejive.twinkle.core.text; - -import static org.assertj.core.api.Assertions.assertThat; - -import org.codejive.twinkle.ansi.Ansi; -import org.codejive.twinkle.ansi.Style; -import org.junit.jupiter.api.Test; - -public class TestLineBuffer { - @Test - public void testStyledBufferCreation() { - LineBuffer buffer = LineBuffer.of(10); - assertThat(buffer.length()).isEqualTo(10); - } - - @Test - public void testStyledBufferPutGetChar() { - LineBuffer buffer = LineBuffer.of(10); - for (int i = 0; i < buffer.length(); i++) { - buffer.setCharAt(i, Style.ITALIC, (char) ('a' + i)); - } - for (int i = 0; i < buffer.length(); i++) { - assertThat(buffer.charAt(i)).isEqualTo((char) ('a' + i)); - assertThat(buffer.styleAt(i)).isEqualTo(Style.DEFAULT.italic()); - } - } - - @Test - public void testStyledBufferPutCharToString() { - LineBuffer buffer = LineBuffer.of(10); - for (int i = 0; i < buffer.length(); i++) { - buffer.setCharAt(i, Style.ITALIC, (char) ('a' + i)); - } - assertThat(buffer.toString()).isEqualTo("abcdefghij"); - } - - @Test - public void testStyledBufferPutCharToAnsiString() { - LineBuffer buffer = LineBuffer.of(10); - for (int i = 0; i < buffer.length(); i++) { - Style style = i < 5 ? Style.ITALIC : Style.UNDERLINED; - buffer.setCharAt(i, style, (char) ('a' + i)); - } - assertThat(buffer.toAnsiString()) - .isEqualTo( - Ansi.STYLE_RESET - + Ansi.style(Ansi.ITALICIZED) - + "abcde" - + Ansi.style(Ansi.NOTITALICIZED, Ansi.UNDERLINED) - + "fghij"); - } - - @Test - public void testStyledBufferPutCharToAnsiStringWithCurrentStyle() { - LineBuffer buffer = LineBuffer.of(10); - for (int i = 0; i < buffer.length(); i++) { - Style style = i < 5 ? Style.ITALIC : Style.UNDERLINED; - buffer.setCharAt(i, style, (char) ('a' + i)); - } - assertThat(buffer.toAnsiString(Style.DEFAULT.italic())) - .isEqualTo("abcde" + Ansi.style(Ansi.NOTITALICIZED, Ansi.UNDERLINED) + "fghij"); - } - - @Test - public void testStyledBufferPutCharToAnsiStringWithUnderAndOverflow() { - LineBuffer buffer = LineBuffer.of(10); - for (int i = 0; i < buffer.length() + 10; i++) { - Style style = i < 10 ? Style.ITALIC : Style.UNDERLINED; - buffer.setCharAt(i - 5, style, (char) ('a' + i)); - } - assertThat(buffer.toAnsiString()) - .isEqualTo( - Ansi.STYLE_RESET - + Ansi.style(Ansi.ITALICIZED) - + "fghij" - + Ansi.style(Ansi.NOTITALICIZED, Ansi.UNDERLINED) - + "klmno"); - } - - @Test - public void testStyledBufferPutStringGetChar() { - LineBuffer buffer = LineBuffer.of(10); - buffer.putStringAt(0, Style.DEFAULT.italic(), "abcdefghij"); - for (int i = 0; i < buffer.length(); i++) { - assertThat(buffer.charAt(i)).isEqualTo((char) ('a' + i)); - assertThat(buffer.styleAt(i)).isEqualTo(Style.DEFAULT.italic()); - } - } -}