From 64d0dfb2ba5a50c58f20d4f48e9898b9f985552c Mon Sep 17 00:00:00 2001 From: Tako Schotanus Date: Mon, 5 Jan 2026 13:06:08 +0100 Subject: [PATCH 01/11] feat: added `Text` and utilities for text iteration --- pom.xml | 7 +- .../java/org/codejive/twinkle/ansi/Style.java | 139 ++++- .../twinkle/{ansi => util}/Printable.java | 3 +- .../twinkle/util/SequenceIterator.java | 478 ++++++++++++++++++ .../codejive/twinkle/util/StyledIterator.java | 113 +++++ .../twinkle/util/TestSequenceIterator.java | 269 ++++++++++ .../twinkle/util/TestStyledIterator.java | 81 +++ .../org/codejive/twinkle/core/text/Line.java | 25 + .../twinkle/core/text/StyledBuffer.java | 5 +- .../core/text/StyledCodepointBuffer.java | 97 +++- .../org/codejive/twinkle/core/text/Text.java | 51 ++ .../codejive/twinkle/core/widget/Canvas.java | 3 + .../codejive/twinkle/core/widget/Panel.java | 2 +- .../core/widget/StyledBufferPanel.java | 6 + .../codejive/twinkle/core/text/TestLine.java | 10 + .../codejive/twinkle/core/text/TestText.java | 54 ++ 16 files changed, 1307 insertions(+), 36 deletions(-) rename twinkle-ansi/src/main/java/org/codejive/twinkle/{ansi => util}/Printable.java (97%) create mode 100644 twinkle-ansi/src/main/java/org/codejive/twinkle/util/SequenceIterator.java create mode 100644 twinkle-ansi/src/main/java/org/codejive/twinkle/util/StyledIterator.java create mode 100644 twinkle-ansi/src/test/java/org/codejive/twinkle/util/TestSequenceIterator.java create mode 100644 twinkle-ansi/src/test/java/org/codejive/twinkle/util/TestStyledIterator.java create mode 100644 twinkle-core/src/main/java/org/codejive/twinkle/core/text/Text.java create mode 100644 twinkle-core/src/test/java/org/codejive/twinkle/core/text/TestText.java diff --git a/pom.xml b/pom.xml index 563419d..504b5cd 100644 --- a/pom.xml +++ b/pom.xml @@ -73,7 +73,7 @@ ${google-java-format.version} - + @@ -91,8 +91,9 @@ 8 8 - 11 - 11 + 15 + 15 + diff --git a/twinkle-ansi/src/main/java/org/codejive/twinkle/ansi/Style.java b/twinkle-ansi/src/main/java/org/codejive/twinkle/ansi/Style.java index 80a78f8..c28c5e4 100644 --- a/twinkle-ansi/src/main/java/org/codejive/twinkle/ansi/Style.java +++ b/twinkle-ansi/src/main/java/org/codejive/twinkle/ansi/Style.java @@ -3,6 +3,7 @@ import java.io.IOException; import java.util.ArrayList; import java.util.List; +import org.codejive.twinkle.util.Printable; import org.jspecify.annotations.NonNull; public class Style implements Printable { @@ -229,7 +230,7 @@ public boolean isStrikethrough() { } public @NonNull Style fgColor(@NonNull Color color) { - long newState = (state & ~MASK_FG_COLOR) | (encodeColor(color) << SHIFT_FG_COLOR); + long newState = applyFgColor(state, color); return of(newState); } @@ -239,7 +240,7 @@ public boolean isStrikethrough() { } public @NonNull Style bgColor(@NonNull Color color) { - long newState = (state & ~MASK_BG_COLOR) | (encodeColor(color) << SHIFT_BG_COLOR); + long newState = applyBgColor(state, color); return of(newState); } @@ -315,6 +316,140 @@ private static long encodeColor(@NonNull Color color) { return result; } + public static long parse(@NonNull String ansiSequence) { + return parse(F_UNSTYLED, ansiSequence); + } + + public static long parse(long currentStyleState, @NonNull String ansiSequence) { + if (!ansiSequence.startsWith(Ansi.CSI) || !ansiSequence.endsWith("m")) { + return currentStyleState; + } + + String content = ansiSequence.substring(2, ansiSequence.length() - 1); + String[] parts = content.split("[;:]", -1); + int[] codes = new int[parts.length]; + for (int i = 0; i < parts.length; i++) { + try { + // Empty parameters are assumed to be 0 otherwise parse as integer + codes[i] = parts[i].isEmpty() ? 0 : Integer.parseInt(parts[i]); + } catch (NumberFormatException e) { + codes[i] = -1; // Invalid code, will be ignored + } + } + + long state = currentStyleState; + for (int i = 0; i < codes.length; i++) { + int code = codes[i]; + switch (code) { + case -1: + // Invalid code, ignore + break; + case 0: + state = 0; + break; + case 1: + state |= F_BOLD; + break; + case 2: + state |= F_FAINT; + break; + case 3: + state |= F_ITALIC; + break; + case 4: + state |= F_UNDERLINED; + break; + case 5: + state |= F_BLINK; + break; + case 7: + state |= F_INVERSE; + break; + case 8: + state |= F_HIDDEN; + break; + case 9: + state |= F_STRIKETHROUGH; + break; + case 22: + state &= ~(F_BOLD | F_FAINT); + break; + case 23: + state &= ~F_ITALIC; + break; + case 24: + state &= ~F_UNDERLINED; + break; + case 25: + state &= ~F_BLINK; + break; + case 27: + state &= ~F_INVERSE; + break; + case 28: + state &= ~F_HIDDEN; + break; + case 29: + state &= ~F_STRIKETHROUGH; + break; + case 39: + state &= ~MASK_FG_COLOR; + break; + case 49: + state &= ~MASK_BG_COLOR; + break; + default: + if (code >= 30 && code <= 37) { + Color c = Color.basic(code - 30, Color.BasicColor.Intensity.normal); + state = applyFgColor(state, c); + } else if (code >= 90 && code <= 97) { + Color c = Color.basic(code - 90, Color.BasicColor.Intensity.bright); + state = applyFgColor(state, c); + } else if (code >= 40 && code <= 47) { + Color c = Color.basic(code - 40, Color.BasicColor.Intensity.normal); + state = applyBgColor(state, c); + } else if (code >= 100 && code <= 107) { + Color c = Color.basic(code - 100, Color.BasicColor.Intensity.bright); + state = applyBgColor(state, c); + } else if (code == 38 || code == 48) { + boolean isFg = (code == 38); + if (i + 1 < codes.length) { + int type = codes[i + 1]; + if (type == 5 && i + 2 < codes.length) { + Color c = Color.indexed(codes[i + 2]); + if (isFg) { + state = applyFgColor(state, c); + } else { + state = applyBgColor(state, c); + } + i += 2; + } else if (type == 2 && i + 4 < codes.length) { + Color c = Color.rgb(codes[i + 2], codes[i + 3], codes[i + 4]); + if (isFg) { + state = applyFgColor(state, c); + } else { + state = applyBgColor(state, c); + } + i += 4; + } + } + } + break; + } + } + return state; + } + + private static long applyFgColor(long state, Color color) { + long encoded = encodeColor(color); + return (state & ~MASK_FG_COLOR) | (encoded << SHIFT_FG_COLOR); + } + + private static long applyBgColor(long state, Color color) { + long encoded = encodeColor(color); + return (state & ~MASK_BG_COLOR) | (encoded << SHIFT_BG_COLOR); + } + @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/twinkle-ansi/src/main/java/org/codejive/twinkle/ansi/Printable.java b/twinkle-ansi/src/main/java/org/codejive/twinkle/util/Printable.java similarity index 97% rename from twinkle-ansi/src/main/java/org/codejive/twinkle/ansi/Printable.java rename to twinkle-ansi/src/main/java/org/codejive/twinkle/util/Printable.java index dd2c3fb..b32a588 100644 --- a/twinkle-ansi/src/main/java/org/codejive/twinkle/ansi/Printable.java +++ b/twinkle-ansi/src/main/java/org/codejive/twinkle/util/Printable.java @@ -1,6 +1,7 @@ -package org.codejive.twinkle.ansi; +package org.codejive.twinkle.util; import java.io.IOException; +import org.codejive.twinkle.ansi.Style; import org.jspecify.annotations.NonNull; public interface Printable { diff --git a/twinkle-ansi/src/main/java/org/codejive/twinkle/util/SequenceIterator.java b/twinkle-ansi/src/main/java/org/codejive/twinkle/util/SequenceIterator.java new file mode 100644 index 0000000..a3c6d7c --- /dev/null +++ b/twinkle-ansi/src/main/java/org/codejive/twinkle/util/SequenceIterator.java @@ -0,0 +1,478 @@ +package org.codejive.twinkle.util; + +import java.io.IOException; +import java.io.PushbackReader; +import java.io.Reader; +import java.util.NoSuchElementException; +import org.codejive.twinkle.ansi.Ansi; + +/** + * An iterator that reads from a text source and yields Unicode grapheme clusters, ANSI escape + * sequences and simple codepoints while handling line endings as single units (even when in reality + * they might be 2 characters). + */ +public interface SequenceIterator { + /** Returns true if there is still input to read. */ + boolean hasNext(); + + /** + * Returns the next lead codepoint of the next sequence. Call {@link #sequence()} to get the + * full sequence. In case of ANSI escape sequences, this returns ESC (0x1B). In case of line + * endings, this returns NEWLINE (0x0A). + * + * @throws NoSuchElementException if there is no more input. + */ + int next(); + + /** + * Returns true if the last returned sequence from {@link #next()} is complex, i.e., consists of + * multiple codepoints or is an escape sequence. + */ + boolean isComplex(); + + /** + * Returns the visual width of the current sequence in columns. Escape sequences return 0, most + * characters return 1, and Full-width/Wide characters return 2. + */ + int width(); + + /** Returns the full sequence of the last returned codepoint from {@link #next()}. */ + String sequence(); + + /** Returns the start index of the current sequence in characters. */ + int begin(); + + /** Returns the end index of the current sequence in characters. */ + int end(); + + static SequenceIterator of(CharSequence text) { + return new CharSequenceSequenceIterator(text); + } + + static SequenceIterator of(Reader input) { + return new ReaderSequenceIterator(input); + } +} + +abstract class BaseSequenceIterator implements SequenceIterator { + protected int currentWidth = 0; + + protected boolean shouldBreak(int prev, int curr, int riCount) { + if (isL(prev) && (isL(curr) || isV(curr) || isLV(curr) || isLVT(curr))) return false; + if ((isLV(prev) || isV(prev)) && (isV(curr) || isT(curr))) return false; + if ((isLVT(prev) || isT(prev)) && isT(curr)) return false; + int type = Character.getType(curr); + if (type == Character.NON_SPACING_MARK + || type == Character.COMBINING_SPACING_MARK + || curr == 0x200D + || prev == 0x200D) return false; + if (isRegionalIndicator(prev) && isRegionalIndicator(curr)) return (riCount % 2 == 0); + return !(isPrepend(prev) || isVirama(prev)); + } + + protected static boolean isRegionalIndicator(int cp) { + return cp >= 0x1F1E6 && cp <= 0x1F1FF; + } + + protected static boolean isL(int cp) { + return (cp >= 0x1100 && cp <= 0x115F); + } + + protected static boolean isV(int cp) { + return (cp >= 0x1160 && cp <= 0x11A7); + } + + protected static boolean isT(int cp) { + return (cp >= 0x11A8 && cp <= 0x11FF); + } + + protected static boolean isLV(int cp) { + return (cp >= 0xAC00 && cp <= 0xD7A3 && (cp - 0xAC00) % 28 == 0); + } + + protected static boolean isLVT(int cp) { + return (cp >= 0xAC00 && cp <= 0xD7A3 && (cp - 0xAC00) % 28 != 0); + } + + protected static boolean isVirama(int cp) { + return (cp >= 0x094D && cp <= 0x0D4D && (cp & 0xFF) == 0x4D) || cp == 0x0D4D; + } + + protected static boolean isPrepend(int cp) { + return cp == 0x0600 + || cp == 0x0601 + || cp == 0x0602 + || cp == 0x0603 + || cp == 0x0604 + || cp == 0x0605 + || cp == 0x06DD + || cp == 0x070F; + } + + @Override + public int width() { + return currentWidth; + } + + protected int calculateWidth(int cp) { + // ANSI escapes and Control characters (except space) are 0 width + if (cp == Ansi.ESC || (cp < 0x20 && cp != '\n' && cp != '\r') || cp == 0x7F) { + return 0; + } + // Line breaks are handled as 0-width movements + if (cp == '\n' || cp == '\r') { + return 0; + } + + if (isWide(cp)) { + return 2; + } + + return 1; + } + + private boolean isWide(int cp) { + // East Asian Wide (W) and Fullwidth (F) + if ((cp >= 0x1100 && cp <= 0x115F) + || // Hangul Jamo + (cp >= 0x2E80 && cp <= 0xA4CF && cp != 0x303F) + || // CJK Radicals, Symbols, Han + (cp >= 0xAC00 && cp <= 0xD7A3) + || // Hangul Syllables + (cp >= 0xF900 && cp <= 0xFAFF) + || // CJK Compatibility Ideographs + (cp >= 0xFE10 && cp <= 0xFE19) + || // Vertical forms + (cp >= 0xFE30 && cp <= 0xFE6F) + || // CJK Compatibility Forms + (cp >= 0xFF00 && cp <= 0xFF60) + || // Fullwidth Forms + (cp >= 0xFFE0 && cp <= 0xFFE6)) { + return true; + } + + // Plane 2 and 3 (SIP/TIP) are almost entirely CJK Ideographs (Wide) + if (cp >= 0x20000 && cp <= 0x3FFFD) { + return true; + } + + // Common Emoji Presentation ranges (Simplified) + // Includes Miscellaneous Symbols and Pictographs, Emoticons, Transport, etc. + if ((cp >= 0x1F300 && cp <= 0x1F64F) + || (cp >= 0x1F680 && cp <= 0x1F6FF) + || (cp >= 0x1F900 && cp <= 0x1F9FF) + || (cp >= 0x1F200 && cp <= 0x1F2FF)) { + return true; + } + + return false; + } +} + +/** + * A high-performance implementation of SequenceIterator that operates directly on a CharSequence + * without using Streams or Readers. + */ +class CharSequenceSequenceIterator extends BaseSequenceIterator { + private final CharSequence text; + private final int length; + + private int cursor = 0; + private int sequenceStart = 0; + private int sequenceEnd = 0; + private int nextLeadCodePoint = -1; + private boolean primed = false; + + public CharSequenceSequenceIterator(CharSequence text) { + this.text = text; + length = text.length(); + } + + @Override + public boolean hasNext() { + if (!primed) { + primeNext(); + } + return cursor < length || primed && nextLeadCodePoint != -1; + } + + @Override + public int next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + primed = false; + return nextLeadCodePoint; + } + + @Override + public boolean isComplex() { + return (sequenceEnd - sequenceStart) > Character.charCount(nextLeadCodePoint); + } + + @Override + public String sequence() { + return text.subSequence(sequenceStart, sequenceEnd).toString(); + } + + @Override + public int begin() { + return sequenceStart; + } + + @Override + public int end() { + return sequenceEnd; + } + + private void primeNext() { + if (cursor >= length) { + nextLeadCodePoint = -1; + return; + } + + sequenceStart = cursor; + int cp = Character.codePointAt(text, cursor); + nextLeadCodePoint = cp; + currentWidth = calculateWidth(cp); + cursor += Character.charCount(cp); + + if (cp == Ansi.ESC) { + consumeAnsi(); + } else if (cp == '\r' || cp == '\n') { + if (cp == '\r' && cursor < length && text.charAt(cursor) == '\n') { + cursor++; // Consume the \n of \r\n + } + nextLeadCodePoint = '\n'; + } else { + int riCount = isRegionalIndicator(cp) ? 1 : 0; + int prevCp = cp; + + while (cursor < length) { + int curr = Character.codePointAt(text, cursor); + + if (curr == '\r' + || curr == '\n' + || curr == Ansi.ESC + || shouldBreak(prevCp, curr, riCount)) { + break; + } + + cursor += Character.charCount(curr); + riCount = isRegionalIndicator(curr) ? riCount + 1 : 0; + prevCp = curr; + } + } + + sequenceEnd = cursor; + primed = true; + } + + private void consumeAnsi() { + if (cursor >= length) return; + char c = text.charAt(cursor++); + + if (c == '[') { + // --- CSI (Control Sequence Introducer) --- + // Format: ESC [ (Parameters) (Intermediate bytes) (Final Byte) + // The Final Byte is always in the range 0x40 to 0x7E. + while (cursor < length) { + char n = text.charAt(cursor++); + if (n >= 0x40 && n <= 0x7E) break; + } + } else if (c == ']') { + // --- OSC (Operating System Command) --- + // Format: ESC ] (Command string) (Terminator) + // Terminator is usually BEL (0x07) or ST (ESC \) + while (cursor < length) { + char n = text.charAt(cursor++); + if (n == 0x07) break; // BEL + if (n == Ansi.ESC && cursor < length && text.charAt(cursor) == '\\') { + cursor++; // Consume '\' + break; + } + } + } + } +} + +/** + * An iterator that reads from a Reader and yields Unicode grapheme clusters, ANSI escape sequences + * and simple codepoints while handling line endings as single units (even when in reality they + * might be 2 characters). + */ +class ReaderSequenceIterator extends BaseSequenceIterator { + private final PushbackReader reader; + private final StringBuilder currentSequence = new StringBuilder(); + private int nextLeadCodePoint = -1; + private boolean primed = false; + private boolean exhausted = false; + private int position = 0; + private int sequenceStart = 0; + + private static final int NEWLINE = '\n'; + + /** Creates a SequenceIterator that reads from the given Reader. */ + ReaderSequenceIterator(Reader reader) { + this.reader = new PushbackReader(reader, 4); + } + + @Override + public boolean hasNext() { + if (!primed) { + primeNext(); + } + return !exhausted; + } + + @Override + public int next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + primed = false; + return nextLeadCodePoint; + } + + @Override + public boolean isComplex() { + return sequence().length() > Character.charCount(nextLeadCodePoint); + } + + @Override + public String sequence() { + return currentSequence.toString(); + } + + /** Returns the start index of the current sequence in characters. */ + @Override + public int begin() { + return sequenceStart; + } + + /** Returns the end index of the current sequence in characters. */ + @Override + public int end() { + return sequenceStart + currentSequence.length(); + } + + private void primeNext() { + currentSequence.setLength(0); + sequenceStart = position; + try { + int cp = readCodePoint(); + if (cp == -1) { + exhausted = true; + nextLeadCodePoint = -1; + return; + } + + nextLeadCodePoint = cp; + currentWidth = calculateWidth(cp); + currentSequence.append(Character.toChars(cp)); + + if (cp == Ansi.ESC) { + consumeAnsi(currentSequence); + } else if (cp == '\r' || cp == '\n') { + if (cp == '\r') { + int next = read(); + if (next == '\n') { + currentSequence.append('\n'); + } else if (next != -1) { + unread(next); + } + } + nextLeadCodePoint = NEWLINE; + } else { + int riCount = isRegionalIndicator(cp) ? 1 : 0; + int prevCp = cp; + while (true) { + int curr = readCodePoint(); + if (curr == -1) break; + if (curr == '\r' + || curr == '\n' + || curr == Ansi.ESC + || shouldBreak(prevCp, curr, riCount)) { + unreadCodePoint(curr); + break; + } + currentSequence.append(Character.toChars(curr)); + riCount = isRegionalIndicator(curr) ? riCount + 1 : 0; + prevCp = curr; + } + } + } catch (IOException e) { + exhausted = true; + } + primed = true; + } + + private int readCodePoint() throws IOException { + int c1 = read(); + if (c1 == -1) return -1; + if (Character.isHighSurrogate((char) c1)) { + int c2 = read(); + if (c2 != -1) return Character.toCodePoint((char) c1, (char) c2); + } + return c1; + } + + private void unreadCodePoint(int cp) throws IOException { + char[] chars = Character.toChars(cp); + for (int i = chars.length - 1; i >= 0; i--) unread(chars[i]); + } + + private void consumeAnsi(StringBuilder sb) throws IOException { + int c = read(); + if (c == -1) return; + sb.append((char) c); + + if (c == '[') { + // --- CSI (Control Sequence Introducer) --- + // Format: ESC [ (Parameters) (Intermediate bytes) (Final Byte) + // The Final Byte is always in the range 0x40 to 0x7E. + while (true) { + int n = read(); + if (n == -1) break; + sb.append((char) n); + if (n >= 0x40 && n <= 0x7E) + break; // Reached the 'Final Byte' (e.g., 'm' in ESC[31m) + } + } else if (c == ']') { + // --- OSC (Operating System Command) --- + // Format: ESC ] (Command string) (Terminator) + // Terminator is usually BEL (0x07) or ST (ESC \) + while (true) { + int n = read(); + if (n == -1) break; + + if (n == 0x07) { // Standard BEL terminator + sb.append((char) n); + break; + } + if (n == Ansi.ESC) { // Check for ST terminator (ESC \) + int n2 = read(); + if (n2 == '\\') { + sb.append((char) n).append((char) n2); + break; + } + if (n2 != -1) unread(n2); + } + sb.append((char) n); + } + } + } + + private int read() throws IOException { + int c = reader.read(); + if (c != -1) { + position++; + } + return c; + } + + private void unread(int c) throws IOException { + reader.unread(c); + position--; + } +} diff --git a/twinkle-ansi/src/main/java/org/codejive/twinkle/util/StyledIterator.java b/twinkle-ansi/src/main/java/org/codejive/twinkle/util/StyledIterator.java new file mode 100644 index 0000000..0c3e52a --- /dev/null +++ b/twinkle-ansi/src/main/java/org/codejive/twinkle/util/StyledIterator.java @@ -0,0 +1,113 @@ +package org.codejive.twinkle.util; + +import java.util.NoSuchElementException; +import org.codejive.twinkle.ansi.Ansi; +import org.codejive.twinkle.ansi.Style; + +/** + * An iterator that wraps a SequenceIterator and tracks the current style state based on ANSI escape + * sequences encountered in the input. + */ +public class StyledIterator implements SequenceIterator { + private final SequenceIterator delegate; + private long currentStyleState; + private int nextCodePoint = -1; + private boolean primed = false; + private boolean exhausted = false; + + /** + * Creates a StyledIterator that wraps the given SequenceIterator and starts with an unstyled + * initial state. + */ + public StyledIterator(SequenceIterator delegate) { + this(delegate, Style.F_UNSTYLED); + } + + /** + * Creates a StyledIterator that wraps the given SequenceIterator and starts with the given + * initial style state. + */ + public StyledIterator(SequenceIterator delegate, long currentStyleState) { + this.delegate = delegate; + this.currentStyleState = currentStyleState; + } + + /** Returns true if there is still input to read. */ + @Override + public boolean hasNext() { + if (!primed) { + primeNext(); + } + return !exhausted; + } + + /** + * Returns the next lead codepoint of the next sequence, skipping over any ANSI escape sequences + * and updating the current style state. Call {@link #sequence()} to get the full sequence and + * {@link #style()} to get the current style. In case of line endings, this returns NEWLINE + * (0x0A). + * + * @throws NoSuchElementException if there is no more input. + */ + @Override + public int next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + primed = false; + return nextCodePoint; + } + + @Override + public boolean isComplex() { + return delegate.isComplex(); + } + + @Override + public int width() { + return delegate.width(); + } + + @Override + public int begin() { + return delegate.begin(); + } + + @Override + public int end() { + return delegate.end(); + } + + @Override + public String sequence() { + return delegate.sequence(); + } + + /** Returns the current style based on the ANSI escape sequences encountered so far. */ + public Style style() { + return Style.of(currentStyleState); + } + + /** Returns the current style state based on the ANSI escape sequences encountered so far. */ + public long styleState() { + return currentStyleState; + } + + private void primeNext() { + while (delegate.hasNext()) { + int cp = delegate.next(); + if (cp == Ansi.ESC) { + String ansiSequence = delegate.sequence(); + if (ansiSequence.startsWith(Ansi.CSI) && ansiSequence.endsWith("m")) { + currentStyleState = Style.parse(currentStyleState, ansiSequence); + } + } else { + nextCodePoint = cp; + primed = true; + return; + } + } + exhausted = true; + primed = true; + } +} diff --git a/twinkle-ansi/src/test/java/org/codejive/twinkle/util/TestSequenceIterator.java b/twinkle-ansi/src/test/java/org/codejive/twinkle/util/TestSequenceIterator.java new file mode 100644 index 0000000..73e9aea --- /dev/null +++ b/twinkle-ansi/src/test/java/org/codejive/twinkle/util/TestSequenceIterator.java @@ -0,0 +1,269 @@ +package org.codejive.twinkle.util; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +public class TestSequenceIterator { + + @Test + public void testNormalText() { + String input = "abc 123"; + SequenceIterator it = SequenceIterator.of(input); + + assertThat(it.hasNext()).isTrue(); + assertThat(it.next()).isEqualTo('a'); + assertThat(it.sequence()).isEqualTo("a"); + assertThat(it.width()).isEqualTo(1); + assertThat(it.isComplex()).isFalse(); + + assertThat(it.hasNext()).isTrue(); + assertThat(it.next()).isEqualTo('b'); + assertThat(it.sequence()).isEqualTo("b"); + assertThat(it.width()).isEqualTo(1); + assertThat(it.isComplex()).isFalse(); + + assertThat(it.hasNext()).isTrue(); + assertThat(it.next()).isEqualTo('c'); + assertThat(it.sequence()).isEqualTo("c"); + assertThat(it.width()).isEqualTo(1); + assertThat(it.isComplex()).isFalse(); + + assertThat(it.hasNext()).isTrue(); + assertThat(it.next()).isEqualTo(' '); + assertThat(it.sequence()).isEqualTo(" "); + assertThat(it.width()).isEqualTo(1); + assertThat(it.isComplex()).isFalse(); + + assertThat(it.hasNext()).isTrue(); + assertThat(it.next()).isEqualTo('1'); + assertThat(it.sequence()).isEqualTo("1"); + assertThat(it.width()).isEqualTo(1); + assertThat(it.isComplex()).isFalse(); + + assertThat(it.hasNext()).isTrue(); + assertThat(it.next()).isEqualTo('2'); + assertThat(it.sequence()).isEqualTo("2"); + assertThat(it.width()).isEqualTo(1); + assertThat(it.isComplex()).isFalse(); + + assertThat(it.hasNext()).isTrue(); + assertThat(it.next()).isEqualTo('3'); + assertThat(it.sequence()).isEqualTo("3"); + assertThat(it.width()).isEqualTo(1); + assertThat(it.isComplex()).isFalse(); + + assertThat(it.hasNext()).isFalse(); + } + + @Test + public void testSurrogatePairs() { + // G clef character: U+1D11E (surrogate pair \uD834\uDD1E) + String input = "\uD834\uDD1E"; + SequenceIterator it = SequenceIterator.of(input); + + assertThat(it.hasNext()).isTrue(); + assertThat(it.next()).isEqualTo(0x1D11E); + assertThat(it.sequence()).isEqualTo("\uD834\uDD1E"); + assertThat(it.width()).isEqualTo(1); + assertThat(it.isComplex()).isFalse(); + + assertThat(it.hasNext()).isFalse(); + } + + @Test + public void testComplexGraphemes() { + // Family emoji: Man + ZWJ + Woman + ZWJ + Girl + ZWJ + Boy + // U+1F468 U+200D U+1F469 U+200D U+1F467 U+200D U+1F466 + String familyEmoji = "\uD83D\uDC68\u200D\uD83D\uDC69\u200D\uD83D\uDC67\u200D\uD83D\uDC66"; + // Letter 'a' with acute accent: a + U+0301 + String aAcute = "a\u0301"; + + String input = familyEmoji + aAcute; + SequenceIterator it = SequenceIterator.of(input); + + // Test Family Emoji + assertThat(it.hasNext()).isTrue(); + // next() returns the first code point of the sequence + assertThat(it.next()).isEqualTo(0x1F468); + assertThat(it.sequence()).isEqualTo(familyEmoji); + assertThat(it.width()).isEqualTo(2); + assertThat(it.isComplex()).isTrue(); + + // Test Combining Character + assertThat(it.hasNext()).isTrue(); + assertThat(it.next()).isEqualTo('a'); + assertThat(it.sequence()).isEqualTo(aAcute); + assertThat(it.width()).isEqualTo(1); + assertThat(it.isComplex()).isTrue(); + + assertThat(it.hasNext()).isFalse(); + } + + @Test + public void testAnsiEscapeSequences() { + // CSI: Red color + String csi = "\u001B[31m"; + // OSC: Window title with BEL terminator + String oscBel = "\u001B]0;Title\u0007"; + // OSC: Hyperlink with ST (ESC \) terminator + String oscSt = "\u001B]8;;http://example.com\u001B\\"; + + String input = csi + "A" + oscBel + "B" + oscSt; + SequenceIterator it = SequenceIterator.of(input); + + // CSI + assertThat(it.hasNext()).isTrue(); + assertThat(it.next()).isEqualTo(0x1B); + assertThat(it.sequence()).isEqualTo(csi); + assertThat(it.width()).isEqualTo(0); + assertThat(it.isComplex()).isTrue(); + + // Normal char 'A' + assertThat(it.hasNext()).isTrue(); + assertThat(it.next()).isEqualTo('A'); + assertThat(it.sequence()).isEqualTo("A"); + assertThat(it.width()).isEqualTo(1); + assertThat(it.isComplex()).isFalse(); + + // OSC BEL + assertThat(it.hasNext()).isTrue(); + assertThat(it.next()).isEqualTo(0x1B); + assertThat(it.sequence()).isEqualTo(oscBel); + assertThat(it.width()).isEqualTo(0); + assertThat(it.isComplex()).isTrue(); + + // Normal char 'B' + assertThat(it.hasNext()).isTrue(); + assertThat(it.next()).isEqualTo('B'); + assertThat(it.sequence()).isEqualTo("B"); + assertThat(it.width()).isEqualTo(1); + assertThat(it.isComplex()).isFalse(); + + // OSC ST + assertThat(it.hasNext()).isTrue(); + assertThat(it.next()).isEqualTo(0x1B); + assertThat(it.sequence()).isEqualTo(oscSt); + assertThat(it.width()).isEqualTo(0); + assertThat(it.isComplex()).isTrue(); + + assertThat(it.hasNext()).isFalse(); + } + + @Test + public void testNewlines() { + // \n, \r\n, \r should all be treated as newlines + String input = "a\nb\r\nc\rd"; + SequenceIterator it = SequenceIterator.of(input); + + // a + it.next(); + assertThat(it.sequence()).isEqualTo("a"); + assertThat(it.width()).isEqualTo(1); + assertThat(it.isComplex()).isFalse(); + + // \n + assertThat(it.next()).isEqualTo('\n'); + assertThat(it.sequence()).isEqualTo("\n"); + assertThat(it.width()).isEqualTo(0); + assertThat(it.isComplex()).isFalse(); + + // b + it.next(); + assertThat(it.sequence()).isEqualTo("b"); + assertThat(it.width()).isEqualTo(1); + assertThat(it.isComplex()).isFalse(); + + // \r\n -> returns \n code point, sequence is \r\n + assertThat(it.next()).isEqualTo('\n'); + assertThat(it.sequence()).isEqualTo("\r\n"); + assertThat(it.width()).isEqualTo(0); + assertThat(it.isComplex()).isTrue(); + + // c + it.next(); + assertThat(it.sequence()).isEqualTo("c"); + assertThat(it.width()).isEqualTo(1); + assertThat(it.isComplex()).isFalse(); + + // \r -> returns \n code point, sequence is \r + assertThat(it.next()).isEqualTo('\n'); + assertThat(it.sequence()).isEqualTo("\r"); + assertThat(it.width()).isEqualTo(0); + assertThat(it.isComplex()).isFalse(); + + // d + it.next(); + assertThat(it.sequence()).isEqualTo("d"); + assertThat(it.width()).isEqualTo(1); + assertThat(it.isComplex()).isFalse(); + + assertThat(it.hasNext()).isFalse(); + } + + @Test + public void testCombined() { + // "Hi " + FamilyEmoji + " " + RedColor + "Bold" + Reset + String familyEmoji = "\uD83D\uDC68\u200D\uD83D\uDC69\u200D\uD83D\uDC67\u200D\uD83D\uDC66"; + String red = "\u001B[31m"; + String reset = "\u001B[0m"; + + String input = "Hi " + familyEmoji + " " + red + "Bold" + reset; + SequenceIterator it = SequenceIterator.of(input); + + // H + assertThat(it.next()).isEqualTo('H'); + assertThat(it.width()).isEqualTo(1); + assertThat(it.isComplex()).isFalse(); + // i + assertThat(it.next()).isEqualTo('i'); + assertThat(it.width()).isEqualTo(1); + assertThat(it.isComplex()).isFalse(); + // space + assertThat(it.next()).isEqualTo(' '); + assertThat(it.width()).isEqualTo(1); + assertThat(it.isComplex()).isFalse(); + + // Family Emoji + assertThat(it.next()).isEqualTo(0x1F468); + assertThat(it.sequence()).isEqualTo(familyEmoji); + assertThat(it.width()).isEqualTo(2); + assertThat(it.isComplex()).isTrue(); + + // space + assertThat(it.next()).isEqualTo(' '); + assertThat(it.width()).isEqualTo(1); + assertThat(it.isComplex()).isFalse(); + + // Red ANSI + assertThat(it.next()).isEqualTo(0x1B); + assertThat(it.sequence()).isEqualTo(red); + assertThat(it.width()).isEqualTo(0); + assertThat(it.isComplex()).isTrue(); + + // B + assertThat(it.next()).isEqualTo('B'); + assertThat(it.width()).isEqualTo(1); + assertThat(it.isComplex()).isFalse(); + // o + assertThat(it.next()).isEqualTo('o'); + assertThat(it.width()).isEqualTo(1); + assertThat(it.isComplex()).isFalse(); + // l + assertThat(it.next()).isEqualTo('l'); + assertThat(it.width()).isEqualTo(1); + assertThat(it.isComplex()).isFalse(); + // d + assertThat(it.next()).isEqualTo('d'); + assertThat(it.width()).isEqualTo(1); + assertThat(it.isComplex()).isFalse(); + + // Reset ANSI + assertThat(it.next()).isEqualTo(0x1B); + assertThat(it.sequence()).isEqualTo(reset); + assertThat(it.width()).isEqualTo(0); + assertThat(it.isComplex()).isTrue(); + + assertThat(it.hasNext()).isFalse(); + } +} diff --git a/twinkle-ansi/src/test/java/org/codejive/twinkle/util/TestStyledIterator.java b/twinkle-ansi/src/test/java/org/codejive/twinkle/util/TestStyledIterator.java new file mode 100644 index 0000000..49b4a02 --- /dev/null +++ b/twinkle-ansi/src/test/java/org/codejive/twinkle/util/TestStyledIterator.java @@ -0,0 +1,81 @@ +package org.codejive.twinkle.util; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.codejive.twinkle.ansi.Style; +import org.junit.jupiter.api.Test; + +public class TestStyledIterator { + + @Test + public void testPlainSequence() { + SequenceIterator seqIter = SequenceIterator.of("abc"); + StyledIterator it = new StyledIterator(seqIter); + + assertThat(it.hasNext()).isTrue(); + assertThat(it.next()).isEqualTo('a'); + assertThat(it.sequence()).isEqualTo("a"); + assertThat(it.styleState()).isEqualTo(Style.F_UNSTYLED); + + assertThat(it.hasNext()).isTrue(); + assertThat(it.next()).isEqualTo('b'); + assertThat(it.sequence()).isEqualTo("b"); + + assertThat(it.hasNext()).isTrue(); + assertThat(it.next()).isEqualTo('c'); + assertThat(it.sequence()).isEqualTo("c"); + + assertThat(it.hasNext()).isFalse(); + } + + @Test + public void testStyledSequence() { + String red = "\u001B[31m"; + SequenceIterator seqIter = SequenceIterator.of(red + "a"); + StyledIterator it = new StyledIterator(seqIter); + + assertThat(it.hasNext()).isTrue(); + // The iterator should consume the ESC sequence internally and advance to 'a' + assertThat(it.next()).isEqualTo('a'); + assertThat(it.sequence()).isEqualTo("a"); + + // Verify style state changed + assertThat(it.styleState()).isNotEqualTo(Style.F_UNSTYLED); + + assertThat(it.hasNext()).isFalse(); + } + + @Test + public void testSkipNonStyleAnsi() { + String up = "\u001B[1A"; // Cursor Up + SequenceIterator seqIter = SequenceIterator.of(up + "a"); + StyledIterator it = new StyledIterator(seqIter); + + assertThat(it.hasNext()).isTrue(); + assertThat(it.next()).isEqualTo('a'); + // Style should NOT change for non-SGR sequences + assertThat(it.styleState()).isEqualTo(Style.F_UNSTYLED); + + assertThat(it.hasNext()).isFalse(); + } + + @Test + public void testMixedSequences() { + String red = "\u001B[31m"; + String reset = "\u001B[0m"; + SequenceIterator seqIter = SequenceIterator.of("a" + red + "b" + reset + "c"); + StyledIterator it = new StyledIterator(seqIter); + + // 'a' + assertThat(it.next()).isEqualTo('a'); + assertThat(it.styleState()).isEqualTo(Style.F_UNSTYLED); + + // 'b' (RED consumed) + assertThat(it.next()).isEqualTo('b'); + assertThat(it.styleState()).isNotEqualTo(Style.F_UNSTYLED); + + // 'c' (RESET consumed) + assertThat(it.next()).isEqualTo('c'); + assertThat(it.styleState()).isEqualTo(Style.F_UNSTYLED); + } +} diff --git a/twinkle-core/src/main/java/org/codejive/twinkle/core/text/Line.java b/twinkle-core/src/main/java/org/codejive/twinkle/core/text/Line.java index 8e79625..35beb4d 100644 --- a/twinkle-core/src/main/java/org/codejive/twinkle/core/text/Line.java +++ b/twinkle-core/src/main/java/org/codejive/twinkle/core/text/Line.java @@ -6,6 +6,7 @@ import org.codejive.twinkle.ansi.Style; import org.codejive.twinkle.core.widget.Canvas; import org.codejive.twinkle.core.widget.Renderable; +import org.codejive.twinkle.util.StyledIterator; public class Line implements Renderable { private final List spans; @@ -31,6 +32,30 @@ protected Line(Span... spans) { Collections.addAll(this.spans, spans); } + public static Line of(StyledIterator iter) { + List spans = new ArrayList<>(); + StringBuilder sb = new StringBuilder(); + long currentStyleState = -1; + while (iter.hasNext()) { + long cp = iter.next(); + if (cp == '\n') { + break; + } + if (iter.styleState() != currentStyleState) { + if (sb.length() > 0) { + spans.add(Span.of(sb.toString(), currentStyleState)); + sb.setLength(0); + } + currentStyleState = iter.styleState(); + } + sb.appendCodePoint((int) cp); + } + if (sb.length() > 0) { + spans.add(Span.of(sb.toString(), currentStyleState)); + } + return new Line(spans.toArray(new Span[0])); + } + @Override public void render(Canvas canvas) { int x = 0; diff --git a/twinkle-core/src/main/java/org/codejive/twinkle/core/text/StyledBuffer.java b/twinkle-core/src/main/java/org/codejive/twinkle/core/text/StyledBuffer.java index 6be21ad..4461428 100644 --- a/twinkle-core/src/main/java/org/codejive/twinkle/core/text/StyledBuffer.java +++ b/twinkle-core/src/main/java/org/codejive/twinkle/core/text/StyledBuffer.java @@ -1,7 +1,8 @@ package org.codejive.twinkle.core.text; -import org.codejive.twinkle.ansi.Printable; 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 StyledBuffer extends StyledCharSequence, Printable { @@ -34,6 +35,8 @@ default int putStringAt(int index, @NonNull Style style, @NonNull CharSequence s int putStringAt(int index, @NonNull StyledCharSequence str); + int putStringAt(int index, @NonNull StyledIterator iter); + @NonNull StyledBuffer resize(int newSize); StyledBuffer EMPTY = diff --git a/twinkle-core/src/main/java/org/codejive/twinkle/core/text/StyledCodepointBuffer.java b/twinkle-core/src/main/java/org/codejive/twinkle/core/text/StyledCodepointBuffer.java index 425a378..f6cd2fa 100644 --- a/twinkle-core/src/main/java/org/codejive/twinkle/core/text/StyledCodepointBuffer.java +++ b/twinkle-core/src/main/java/org/codejive/twinkle/core/text/StyledCodepointBuffer.java @@ -3,23 +3,27 @@ import java.io.IOException; import org.codejive.twinkle.ansi.Ansi; import org.codejive.twinkle.ansi.Style; +import org.codejive.twinkle.util.StyledIterator; import org.jspecify.annotations.NonNull; public class StyledCodepointBuffer implements StyledBuffer { protected int[] cpBuffer; + protected String[] graphemeBuffer; protected long[] styleBuffer; public StyledCodepointBuffer(int size) { cpBuffer = new int[size]; + graphemeBuffer = new String[size]; styleBuffer = new long[size]; } - protected StyledCodepointBuffer(int[] cpBuffer, long[] styleBuffer) { - if (cpBuffer.length != styleBuffer.length) { + protected StyledCodepointBuffer(int[] cpBuffer, String[] graphemeBuffer, long[] styleBuffer) { + if (cpBuffer.length != styleBuffer.length || cpBuffer.length != graphemeBuffer.length) { throw new IllegalArgumentException( - "Codepoint buffer and style buffer must have the same length"); + "Codepoint, grapheme and style buffers must have the same length"); } this.cpBuffer = cpBuffer; + this.graphemeBuffer = graphemeBuffer; this.styleBuffer = styleBuffer; } @@ -33,7 +37,7 @@ public char charAt(int index) { if (invalidIndex(index)) { return REPLACEMENT_CHAR; } - if (Character.charCount(cpBuffer[index]) == 2) { + if (graphemeBuffer[index] != null || Character.charCount(cpBuffer[index]) == 2) { // TODO log warning about extended Unicode characters not being supported return REPLACEMENT_CHAR; } @@ -53,6 +57,9 @@ public int codepointAt(int index) { if (invalidIndex(index)) { return String.valueOf(REPLACEMENT_CHAR); } + if (graphemeBuffer[index] != null) { + return graphemeBuffer[index]; + } return new String(Character.toChars(cpBuffer[index])); } @@ -90,6 +97,7 @@ private void setCharAt_(int index, long styleState, char ch) { ch = REPLACEMENT_CHAR; } cpBuffer[index] = ch; + graphemeBuffer[index] = null; styleBuffer[index] = styleState; } @@ -103,6 +111,7 @@ public void setCharAt(int index, long styleState, int cp) { private void setCharAt_(int index, long styleState, int cp) { cpBuffer[index] = cp; + graphemeBuffer[index] = null; styleBuffer[index] = styleState; } @@ -118,14 +127,8 @@ private void setCharAt_(int index, long styleState, @NonNull CharSequence graphe if (grapheme.length() == 0) { return; } - int cp; - if (codepointCount(grapheme) > 1) { - // TODO log warning about extended Unicode graphemes not being supported - cp = REPLACEMENT_CHAR; - } else { - cp = codepointAt(grapheme, 0); - } - cpBuffer[index] = cp; + cpBuffer[index] = -1; + graphemeBuffer[index] = grapheme.toString(); styleBuffer[index] = styleState; } @@ -169,6 +172,31 @@ public int putStringAt(int index, @NonNull StyledCharSequence str) { return str.length(); } + @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()) { + long style = iter.next(); + int cp = iter.next(); + if (cp == '\n') { + break; + } + if (cnt < len) { + if (iter.isComplex()) { + setCharAt_(startIndex + cnt, style, iter.sequence()); + } else { + setCharAt_(startIndex + cnt, style, cp); + } + } + cnt++; + } + return cnt; + } + @Override public @NonNull StyledCharSequence subSequence(int start, int end) { if (start < 0 || end > length() || start > end) { @@ -177,10 +205,12 @@ public int putStringAt(int index, @NonNull StyledCharSequence str) { } 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 StyledCodepointBuffer(subCpBuffer, subStyleBuffer); + return new StyledCodepointBuffer(subCpBuffer, subGraphemeBuffer, subStyleBuffer); } @Override @@ -189,11 +219,14 @@ public int putStringAt(int index, @NonNull StyledCharSequence str) { 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; } @@ -241,11 +274,15 @@ private boolean outside(int index, int length) { int initialCapacity = length(); StringBuilder sb = new StringBuilder(initialCapacity); for (int i = 0; i < length(); i++) { - int cp = cpBuffer[i]; - if (cp == '\0') { - cp = ' '; + if (graphemeBuffer[i] != null) { + sb.append(graphemeBuffer[i]); + } else { + int cp = cpBuffer[i]; + if (cp == '\0') { + cp = ' '; + } + sb.appendCodePoint(cp); } - sb.appendCodePoint(cp); } return sb.toString(); } @@ -304,18 +341,22 @@ private boolean outside(int index, int length) { style.toAnsi(appendable, lastStyleState); lastStyleState = styleBuffer[i]; } - 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)); + if (graphemeBuffer[i] != null) { + appendable.append(graphemeBuffer[i]); } else { - throw new IllegalArgumentException( - String.format("Not a valid Unicode code point: 0x%X", cp)); + 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/text/Text.java b/twinkle-core/src/main/java/org/codejive/twinkle/core/text/Text.java new file mode 100644 index 0000000..236a4ec --- /dev/null +++ b/twinkle-core/src/main/java/org/codejive/twinkle/core/text/Text.java @@ -0,0 +1,51 @@ +package org.codejive.twinkle.core.text; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import org.codejive.twinkle.ansi.Style; +import org.codejive.twinkle.core.widget.Canvas; +import org.codejive.twinkle.core.widget.Renderable; +import org.codejive.twinkle.util.StyledIterator; + +public class Text implements Renderable { + private final List lines; + + public static Text of(String text) { + return new Text(new Line((Span.of(text)))); + } + + public static Text of(String text, Style style) { + return of(text, style.state()); + } + + public static Text of(String text, long styleState) { + return new Text(new Line((Span.of(text, styleState)))); + } + + public static Text of(Line... lines) { + return new Text(lines); + } + + public static Text of(StyledIterator iter) { + List lines = new ArrayList<>(); + while (iter.hasNext()) { + lines.add(Line.of(iter)); + } + return new Text(lines.toArray(new Line[0])); + } + + protected Text(Line... lines) { + this.lines = new ArrayList<>(lines.length); + Collections.addAll(this.lines, lines); + } + + @Override + public void render(Canvas canvas) { + int y = 0; + for (Line line : lines) { + line.render(canvas.view(0, y, canvas.size().width(), 1)); + y++; + } + } +} diff --git a/twinkle-core/src/main/java/org/codejive/twinkle/core/widget/Canvas.java b/twinkle-core/src/main/java/org/codejive/twinkle/core/widget/Canvas.java index df54dda..cb12835 100644 --- a/twinkle-core/src/main/java/org/codejive/twinkle/core/widget/Canvas.java +++ b/twinkle-core/src/main/java/org/codejive/twinkle/core/widget/Canvas.java @@ -2,6 +2,7 @@ import org.codejive.twinkle.ansi.Style; import org.codejive.twinkle.core.text.StyledCharSequence; +import org.codejive.twinkle.util.StyledIterator; import org.jspecify.annotations.NonNull; public interface Canvas extends Sized { @@ -41,6 +42,8 @@ default int putStringAt(int x, int y, @NonNull Style style, @NonNull CharSequenc int putStringAt(int x, int y, @NonNull StyledCharSequence str); + int putStringAt(int x, int y, @NonNull StyledIterator iter); + default void drawHLineAt(int x, int y, int x2, Style style, char c) { drawHLineAt(x, y, x2, style.state(), c); } diff --git a/twinkle-core/src/main/java/org/codejive/twinkle/core/widget/Panel.java b/twinkle-core/src/main/java/org/codejive/twinkle/core/widget/Panel.java index d8f4799..21c519b 100644 --- a/twinkle-core/src/main/java/org/codejive/twinkle/core/widget/Panel.java +++ b/twinkle-core/src/main/java/org/codejive/twinkle/core/widget/Panel.java @@ -1,7 +1,7 @@ package org.codejive.twinkle.core.widget; -import org.codejive.twinkle.ansi.Printable; import org.codejive.twinkle.core.text.StyledBuffer; +import org.codejive.twinkle.util.Printable; import org.jspecify.annotations.NonNull; public interface Panel extends Canvas, Printable { diff --git a/twinkle-core/src/main/java/org/codejive/twinkle/core/widget/StyledBufferPanel.java b/twinkle-core/src/main/java/org/codejive/twinkle/core/widget/StyledBufferPanel.java index 6ee82f5..ff6ae38 100644 --- a/twinkle-core/src/main/java/org/codejive/twinkle/core/widget/StyledBufferPanel.java +++ b/twinkle-core/src/main/java/org/codejive/twinkle/core/widget/StyledBufferPanel.java @@ -7,6 +7,7 @@ import org.codejive.twinkle.ansi.Style; import org.codejive.twinkle.core.text.StyledBuffer; import org.codejive.twinkle.core.text.StyledCharSequence; +import org.codejive.twinkle.util.StyledIterator; import org.jspecify.annotations.NonNull; public class StyledBufferPanel implements Panel { @@ -115,6 +116,11 @@ public int putStringAt(int x, int y, @NonNull StyledCharSequence str) { return line(y).putStringAt(applyXOffset(x), str); } + @Override + public int putStringAt(int x, int y, @NonNull StyledIterator iter) { + return line(y).putStringAt(applyXOffset(x), iter); + } + @Override public void drawHLineAt(int x, int y, int x2, long styleState, char c) { for (int i = x; i < x2; i++) { diff --git a/twinkle-core/src/test/java/org/codejive/twinkle/core/text/TestLine.java b/twinkle-core/src/test/java/org/codejive/twinkle/core/text/TestLine.java index 25ce872..0c86f44 100644 --- a/twinkle-core/src/test/java/org/codejive/twinkle/core/text/TestLine.java +++ b/twinkle-core/src/test/java/org/codejive/twinkle/core/text/TestLine.java @@ -5,6 +5,8 @@ import org.codejive.twinkle.ansi.Ansi; import org.codejive.twinkle.ansi.Style; import org.codejive.twinkle.core.widget.Panel; +import org.codejive.twinkle.util.SequenceIterator; +import org.codejive.twinkle.util.StyledIterator; import org.junit.jupiter.api.Test; public class TestLine { @@ -35,4 +37,12 @@ public void testRenderMultipleSpans() { + Ansi.style(Ansi.NORMAL) + "C"); } + + @Test + public void testOfStyledIterator() { + StyledIterator iter = new StyledIterator(SequenceIterator.of("Line 1")); + Panel p = Panel.of(6, 1); + Line.of(iter).render(p); + assertThat(p.toString()).isEqualTo("Line 1"); + } } diff --git a/twinkle-core/src/test/java/org/codejive/twinkle/core/text/TestText.java b/twinkle-core/src/test/java/org/codejive/twinkle/core/text/TestText.java new file mode 100644 index 0000000..e173877 --- /dev/null +++ b/twinkle-core/src/test/java/org/codejive/twinkle/core/text/TestText.java @@ -0,0 +1,54 @@ +package org.codejive.twinkle.core.text; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.codejive.twinkle.ansi.Style; +import org.codejive.twinkle.core.widget.Panel; +import org.codejive.twinkle.util.SequenceIterator; +import org.codejive.twinkle.util.StyledIterator; +import org.junit.jupiter.api.Test; + +public class TestText { + + @Test + public void testOfSimpleString() { + Panel pnl = Panel.of(11, 1); + Text.of("Hello World").render(pnl); + assertThat(pnl.toString()).isEqualTo("Hello World"); + } + + @Test + public void testOfStyledString() { + Style style = Style.BOLD; + Panel pnl = Panel.of(11, 1); + Text.of("Hello World", style).render(pnl); + assertThat(pnl.toAnsiString(Style.F_UNSTYLED)) + .isEqualTo(style.toAnsiString() + "Hello World"); + } + + @Test + public void testOfStyleState() { + Style style = Style.BOLD; + Panel pnl = Panel.of(11, 1); + Text.of("Hello World", style.state()).render(pnl); + assertThat(pnl.toAnsiString(Style.F_UNSTYLED)) + .isEqualTo(style.toAnsiString() + "Hello World"); + } + + @Test + public void testOfLines() { + Line line1 = Line.of("Line 1"); + Line line2 = Line.of("Line 2"); + Panel pnl = Panel.of(6, 2); + Text.of(line1, line2).render(pnl); + assertThat(pnl.toString()).isEqualTo("Line 1\nLine 2"); + } + + @Test + public void testOfStyledIterator() { + StyledIterator iter = new StyledIterator(SequenceIterator.of("Line 1\nLine 2")); + Panel pnl = Panel.of(6, 2); + Text.of(iter).render(pnl); + assertThat(pnl.toString()).isEqualTo("Line 1\nLine 2"); + } +} From 3ff09a504ff1009c3669247b08d056060a8d0846 Mon Sep 17 00:00:00 2001 From: Tako Schotanus Date: Mon, 5 Jan 2026 14:40:38 +0100 Subject: [PATCH 02/11] refactor: deleted `StyledStringBuilder` --- .../twinkle/core/text/StyledCharSequence.java | 12 -- .../core/text/StyledStringBuilder.java | 170 ------------------ .../twinkle/core/text/TestStyledBuffer.java | 16 -- 3 files changed, 198 deletions(-) delete mode 100644 twinkle-core/src/main/java/org/codejive/twinkle/core/text/StyledStringBuilder.java diff --git a/twinkle-core/src/main/java/org/codejive/twinkle/core/text/StyledCharSequence.java b/twinkle-core/src/main/java/org/codejive/twinkle/core/text/StyledCharSequence.java index 844294c..0c56ee8 100644 --- a/twinkle-core/src/main/java/org/codejive/twinkle/core/text/StyledCharSequence.java +++ b/twinkle-core/src/main/java/org/codejive/twinkle/core/text/StyledCharSequence.java @@ -28,16 +28,4 @@ public interface StyledCharSequence { // @Override @NonNull StyledCharSequence subSequence(int start, int end); - - static @NonNull StyledCharSequence fromString(@NonNull Style style, @NonNull String str) { - StyledStringBuilder builder = new StyledStringBuilder(str.length()); - builder.append(style, str); - return builder; - } - - static @NonNull StyledCharSequence fromString(long styleState, @NonNull String str) { - StyledStringBuilder builder = new StyledStringBuilder(str.length()); - builder.append(styleState, str); - return builder; - } } diff --git a/twinkle-core/src/main/java/org/codejive/twinkle/core/text/StyledStringBuilder.java b/twinkle-core/src/main/java/org/codejive/twinkle/core/text/StyledStringBuilder.java deleted file mode 100644 index de47983..0000000 --- a/twinkle-core/src/main/java/org/codejive/twinkle/core/text/StyledStringBuilder.java +++ /dev/null @@ -1,170 +0,0 @@ -package org.codejive.twinkle.core.text; - -import org.codejive.twinkle.ansi.Style; -import org.jspecify.annotations.NonNull; - -public class StyledStringBuilder implements StyledCharSequence { - private StyledBuffer buffer; - private int length; - - private static final int INITIAL_CAPACITY = 16; - - public static StyledStringBuilder create() { - return new StyledStringBuilder(INITIAL_CAPACITY); - } - - public static StyledStringBuilder create(int initialCapacity) { - return new StyledStringBuilder(initialCapacity); - } - - public static StyledStringBuilder of(Style style, CharSequence str) { - return of(style.state(), str); - } - - public static StyledStringBuilder of(long styleState, CharSequence str) { - StyledStringBuilder builder = new StyledStringBuilder(str.length() + INITIAL_CAPACITY); - builder.append(styleState, str); - return builder; - } - - public static StyledStringBuilder of(StyledCharSequence str) { - StyledStringBuilder builder = new StyledStringBuilder(str.length() + INITIAL_CAPACITY); - builder.append(str); - return builder; - } - - public StyledStringBuilder(int initialCapacity) { - this.buffer = StyledBuffer.of(initialCapacity); - this.length = 0; - } - - public StyledStringBuilder append(StyledCharSequence str) { - ensureCapacity(str.length()); - length += buffer.putStringAt(length, str); - return this; - } - - public StyledStringBuilder append(Style style, CharSequence str) { - ensureCapacity(str.length()); - length += buffer.putStringAt(length, style, str); - return this; - } - - public StyledStringBuilder append(long styleState, CharSequence str) { - ensureCapacity(str.length()); - length += buffer.putStringAt(length, styleState, str); - return this; - } - - public StyledStringBuilder append(Style style, Object obj) { - String str = String.valueOf(obj); - return append(style, str); - } - - public StyledStringBuilder append(long styleState, Object obj) { - String str = String.valueOf(obj); - return append(styleState, str); - } - - public StyledStringBuilder append(Style style, char ch) { - String str = String.valueOf(ch); - return append(style, str); - } - - public StyledStringBuilder append(long styleState, char ch) { - String str = String.valueOf(ch); - return append(styleState, str); - } - - public StyledStringBuilder append(Style style, long number) { - String str = String.valueOf(number); - return append(style, str); - } - - public StyledStringBuilder append(long styleState, long number) { - String str = String.valueOf(number); - return append(styleState, str); - } - - public StyledStringBuilder append(Style style, int number) { - String str = String.valueOf(number); - return append(style, str); - } - - public StyledStringBuilder append(long styleState, int number) { - String str = String.valueOf(number); - return append(styleState, str); - } - - public StyledStringBuilder append(Style style, double number) { - String str = String.valueOf(number); - return append(style, str); - } - - public StyledStringBuilder append(long styleState, double number) { - String str = String.valueOf(number); - return append(styleState, str); - } - - public StyledStringBuilder append(Style style, float number) { - String str = String.valueOf(number); - return append(style, str); - } - - public StyledStringBuilder append(long styleState, float number) { - String str = String.valueOf(number); - return append(styleState, str); - } - - public StyledStringBuilder append(Style style, boolean bool) { - String str = String.valueOf(bool); - return append(style, str); - } - - public StyledStringBuilder append(long styleState, boolean bool) { - String str = String.valueOf(bool); - return append(styleState, str); - } - - @Override - public int length() { - return length; - } - - @Override - public char charAt(int index) { - return buffer.charAt(index); - } - - @Override - public int codepointAt(int index) { - return buffer.codepointAt(index); - } - - @Override - public @NonNull String graphemeAt(int index) { - return buffer.graphemeAt(index); - } - - @Override - public long styleStateAt(int index) { - return buffer.styleStateAt(index); - } - - @Override - public @NonNull Style styleAt(int index) { - return buffer.styleAt(index); - } - - @Override - public @NonNull StyledCharSequence subSequence(int start, int end) { - return buffer.subSequence(start, end); - } - - private void ensureCapacity(int extraLength) { - int requiredLength = length + extraLength; - if (requiredLength > buffer.length()) { - buffer = buffer.resize(requiredLength + 2 * INITIAL_CAPACITY); - } - } -} diff --git a/twinkle-core/src/test/java/org/codejive/twinkle/core/text/TestStyledBuffer.java b/twinkle-core/src/test/java/org/codejive/twinkle/core/text/TestStyledBuffer.java index 49ac054..0f5043a 100644 --- a/twinkle-core/src/test/java/org/codejive/twinkle/core/text/TestStyledBuffer.java +++ b/twinkle-core/src/test/java/org/codejive/twinkle/core/text/TestStyledBuffer.java @@ -86,20 +86,4 @@ public void testStyledBufferPutStringGetChar() { assertThat(buffer.styleStateAt(i)).isEqualTo(Style.ITALIC.state()); } } - - @Test - public void testStyledBufferPutStyledString() { - StyledBuffer buffer = StyledBuffer.of(10); - buffer.putStringAt(0, StyledStringBuilder.of(Style.ITALIC, "abcdefghij")); - assertThat(buffer.toAnsiString()) - .isEqualTo(Ansi.STYLE_RESET + Ansi.style(Ansi.ITALICIZED) + "abcdefghij"); - } - - @Test - public void testStyledBufferPutStyledStringWithUnderAndOverflow() { - StyledBuffer buffer = StyledBuffer.of(10); - buffer.putStringAt(-5, StyledStringBuilder.of(Style.ITALIC, "xxxxxabcdefghijxxxxx")); - assertThat(buffer.toAnsiString()) - .isEqualTo(Ansi.STYLE_RESET + Ansi.style(Ansi.ITALICIZED) + "abcdefghij"); - } } From f9cf8c6ada9b9572eb34a817711dae30d087cc64 Mon Sep 17 00:00:00 2001 From: Tako Schotanus Date: Mon, 5 Jan 2026 15:44:11 +0100 Subject: [PATCH 03/11] refactor: renamed, deleted and simplified Renamed `StyledBuffer` to `LineBuffer` and made implementations part of the file. Renamed `Panel` to `Buffer` and made implementations and `PanelView` part of the file. Deleted `StyledCharSequence` and folded its API into `LineBuffer`. --- .../twinkle/widgets/graphs/plot/Plot.java | 4 +- .../src/test/java/examples/BarDemo.java | 40 ++--- .../test/java/examples/MathPlotColorDemo.java | 8 +- .../src/test/java/examples/MathPlotDemo.java | 8 +- .../test/java/examples/MathPlotFourDemo.java | 16 +- ...edCodepointBuffer.java => LineBuffer.java} | 93 ++++++++--- .../twinkle/core/text/StyledBuffer.java | 56 ------- .../twinkle/core/text/StyledCharSequence.java | 31 ---- .../{StyledBufferPanel.java => Buffer.java} | 151 ++++++++++-------- .../codejive/twinkle/core/widget/Canvas.java | 3 - .../codejive/twinkle/core/widget/Panel.java | 32 ---- .../twinkle/core/widget/PanelView.java | 7 - .../twinkle/core/widget/Rectangular.java | 7 - ...ferTimings.java => LineBufferTimings.java} | 10 +- .../codejive/twinkle/core/text/TestLine.java | 8 +- ...tStyledBuffer.java => TestLineBuffer.java} | 16 +- .../codejive/twinkle/core/text/TestSpan.java | 4 +- .../codejive/twinkle/core/text/TestText.java | 32 ++-- .../{TestPanel.java => TestBuffer.java} | 94 +++++------ 19 files changed, 276 insertions(+), 344 deletions(-) rename twinkle-core/src/main/java/org/codejive/twinkle/core/text/{StyledCodepointBuffer.java => LineBuffer.java} (84%) delete mode 100644 twinkle-core/src/main/java/org/codejive/twinkle/core/text/StyledBuffer.java delete mode 100644 twinkle-core/src/main/java/org/codejive/twinkle/core/text/StyledCharSequence.java rename twinkle-core/src/main/java/org/codejive/twinkle/core/widget/{StyledBufferPanel.java => Buffer.java} (69%) delete mode 100644 twinkle-core/src/main/java/org/codejive/twinkle/core/widget/Panel.java delete mode 100644 twinkle-core/src/main/java/org/codejive/twinkle/core/widget/PanelView.java delete mode 100644 twinkle-core/src/main/java/org/codejive/twinkle/core/widget/Rectangular.java rename twinkle-core/src/test/java/org/codejive/twinkle/core/text/{StyledBufferTimings.java => LineBufferTimings.java} (91%) rename twinkle-core/src/test/java/org/codejive/twinkle/core/text/{TestStyledBuffer.java => TestLineBuffer.java} (88%) rename twinkle-core/src/test/java/org/codejive/twinkle/core/widget/{TestPanel.java => TestBuffer.java} (65%) 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 aa1b034..804065a 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 @@ -1,8 +1,8 @@ package org.codejive.twinkle.widgets.graphs.plot; import org.codejive.twinkle.ansi.Style; +import org.codejive.twinkle.core.widget.Buffer; import org.codejive.twinkle.core.widget.Canvas; -import org.codejive.twinkle.core.widget.Panel; import org.codejive.twinkle.core.widget.Size; import org.codejive.twinkle.core.widget.Widget; import org.jspecify.annotations.NonNull; @@ -49,7 +49,7 @@ public class Plot implements Widget { private static final int dotIndex[] = {1, 2, 4, 7, 6, 13, 14, 8, 9, 11}; public static Plot of(Size size) { - return new Plot(Panel.of(size)); + return new Plot(Buffer.of(size)); } public static Plot of(Canvas canvas) { diff --git a/twinkle-chart/src/test/java/examples/BarDemo.java b/twinkle-chart/src/test/java/examples/BarDemo.java index 19f29ac..d3e1fde 100644 --- a/twinkle-chart/src/test/java/examples/BarDemo.java +++ b/twinkle-chart/src/test/java/examples/BarDemo.java @@ -1,7 +1,7 @@ package examples; +import org.codejive.twinkle.core.widget.Buffer; import org.codejive.twinkle.core.widget.Canvas; -import org.codejive.twinkle.core.widget.Panel; import org.codejive.twinkle.widgets.graphs.bar.Bar; import org.codejive.twinkle.widgets.graphs.bar.BarConfig; import org.codejive.twinkle.widgets.graphs.bar.FracBarConfig; @@ -19,45 +19,45 @@ public static void main(String[] args) { } private static void printSimpleBar() { - Panel pnl = Panel.of(20, 1); + Buffer buf = Buffer.of(20, 1); Bar b = Bar.bar().setValue(42); - b.render(pnl); - System.out.println(pnl); + b.render(buf); + System.out.println(buf); } private static void printHorizontalBars() { - Panel pnl = Panel.of(20, 4); + Buffer buf = Buffer.of(20, 4); FracBarConfig cfg = FracBarConfig.create(); - renderHorizontal(pnl, cfg); - System.out.println(pnl); + renderHorizontal(buf, cfg); + System.out.println(buf); cfg.direction(BarConfig.Direction.R2L); - renderHorizontal(pnl, cfg); - System.out.println(pnl); + renderHorizontal(buf, cfg); + System.out.println(buf); } - private static void renderHorizontal(Panel pnl, FracBarConfig cfg) { - for (int i = 0; i < pnl.size().height(); i++) { - Canvas v = pnl.view(0, i, 20, 1); + private static void renderHorizontal(Buffer buf, FracBarConfig cfg) { + for (int i = 0; i < buf.size().height(); i++) { + Canvas v = buf.view(0, i, 20, 1); Bar b = new Bar(cfg).setValue(30 + i * 27); b.render(v); } } private static void printVerticalBars() { - Panel pnl = Panel.of(16, 8); + Buffer buf = Buffer.of(16, 8); FracBarConfig cfg = FracBarConfig.create().direction(BarConfig.Direction.B2T); - renderVertical(pnl, cfg); - System.out.println(pnl); + renderVertical(buf, cfg); + System.out.println(buf); cfg.direction(BarConfig.Direction.T2B); - renderVertical(pnl, cfg); - System.out.println(pnl); + renderVertical(buf, cfg); + System.out.println(buf); } - private static void renderVertical(Panel pnl, FracBarConfig cfg) { - for (int i = 0; i < pnl.size().width(); i++) { - Canvas v = pnl.view(i, 0, 1, 8); + private static void renderVertical(Buffer buf, FracBarConfig cfg) { + for (int i = 0; i < buf.size().width(); i++) { + Canvas v = buf.view(i, 0, 1, 8); Bar b = new Bar(cfg).setValue(30 + i * 5.4d); b.render(v); } diff --git a/twinkle-chart/src/test/java/examples/MathPlotColorDemo.java b/twinkle-chart/src/test/java/examples/MathPlotColorDemo.java index 12648df..917e45a 100644 --- a/twinkle-chart/src/test/java/examples/MathPlotColorDemo.java +++ b/twinkle-chart/src/test/java/examples/MathPlotColorDemo.java @@ -6,7 +6,7 @@ import org.codejive.twinkle.ansi.Color; import org.codejive.twinkle.ansi.Style; import org.codejive.twinkle.core.text.Line; -import org.codejive.twinkle.core.widget.Panel; +import org.codejive.twinkle.core.widget.Buffer; import org.codejive.twinkle.core.widget.Size; import org.codejive.twinkle.widgets.Framed; import org.codejive.twinkle.widgets.graphs.plot.MathPlot; @@ -15,7 +15,7 @@ public class MathPlotColorDemo { public static void main(String[] args) throws InterruptedException { MathPlot p = MathPlot.of(Size.of(40, 20)).ranges(-2 * Math.PI, 2 * Math.PI, -2.0, 2.0); Framed f = Framed.of(p).title(Line.of(" Interfering Waves ")); - Panel pnl = Panel.of(42, 22); + Buffer buf = Buffer.of(42, 22); System.out.print(Ansi.hideCursor()); try { @@ -68,8 +68,8 @@ public static void main(String[] args) throws InterruptedException { Style.ofFgColor(Color.BasicColor.GREEN)); p.plot(x -> a2 * Math.sin(k2 * x + phase2), Style.ofFgColor(Color.BasicColor.RED)); - f.render(pnl); - System.out.println(pnl.toAnsiString()); + f.render(buf); + System.out.println(buf.toAnsiString()); Thread.sleep(20); } diff --git a/twinkle-chart/src/test/java/examples/MathPlotDemo.java b/twinkle-chart/src/test/java/examples/MathPlotDemo.java index 8019e0f..106b730 100644 --- a/twinkle-chart/src/test/java/examples/MathPlotDemo.java +++ b/twinkle-chart/src/test/java/examples/MathPlotDemo.java @@ -4,7 +4,7 @@ import java.util.Random; import org.codejive.twinkle.ansi.Ansi; import org.codejive.twinkle.core.text.Line; -import org.codejive.twinkle.core.widget.Panel; +import org.codejive.twinkle.core.widget.Buffer; import org.codejive.twinkle.core.widget.Size; import org.codejive.twinkle.widgets.Framed; import org.codejive.twinkle.widgets.graphs.plot.MathPlot; @@ -13,7 +13,7 @@ public class MathPlotDemo { public static void main(String[] args) throws InterruptedException { MathPlot p = MathPlot.of(Size.of(40, 20)).ranges(-2 * Math.PI, 2 * Math.PI, -2.0, 2.0); Framed f = Framed.of(p).title(Line.of(" Interfering Waves ")); - Panel pnl = Panel.of(42, 22); + Buffer buf = Buffer.of(42, 22); System.out.print(Ansi.hideCursor()); try { @@ -63,8 +63,8 @@ public static void main(String[] args) throws InterruptedException { // plot combined wave p.plot(x -> a1 * Math.sin(k1 * x + phase1) + a2 * Math.sin(k2 * x + phase2)); - f.render(pnl); - System.out.println(pnl); + f.render(buf); + System.out.println(buf); Thread.sleep(20); } diff --git a/twinkle-chart/src/test/java/examples/MathPlotFourDemo.java b/twinkle-chart/src/test/java/examples/MathPlotFourDemo.java index 6ac6930..5837637 100644 --- a/twinkle-chart/src/test/java/examples/MathPlotFourDemo.java +++ b/twinkle-chart/src/test/java/examples/MathPlotFourDemo.java @@ -8,8 +8,8 @@ import org.codejive.twinkle.ansi.Color; import org.codejive.twinkle.ansi.Style; import org.codejive.twinkle.core.text.Line; +import org.codejive.twinkle.core.widget.Buffer; import org.codejive.twinkle.core.widget.Canvas; -import org.codejive.twinkle.core.widget.Panel; import org.codejive.twinkle.core.widget.Size; import org.codejive.twinkle.core.widget.Widget; import org.codejive.twinkle.widgets.Framed; @@ -18,22 +18,22 @@ public class MathPlotFourDemo { public static void main(String[] args) throws InterruptedException, IOException { - Panel pnl = Panel.of(60, 40); + Buffer buf = Buffer.of(60, 40); AnimatingMathPlot p1 = new AnimatingMathPlot(Size.of(30, 20), " Interfering Waves 1 "); AnimatingMathPlot p2 = new AnimatingMathPlot(Size.of(30, 20), " Interfering Waves 2 "); AnimatingMathPlot p3 = new AnimatingMathPlot(Size.of(30, 20), " Interfering Waves 3 "); AnimatingMathPlot p4 = new AnimatingMathPlot(Size.of(30, 20), " Interfering Waves 4 "); - Canvas v1 = pnl.view(0, 0, 30, 20); - Canvas v2 = pnl.view(30, 0, 30, 20); - Canvas v3 = pnl.view(0, 20, 30, 20); - Canvas v4 = pnl.view(30, 20, 30, 20); + Canvas v1 = buf.view(0, 0, 30, 20); + Canvas v2 = buf.view(30, 0, 30, 20); + Canvas v3 = buf.view(0, 20, 30, 20); + Canvas v4 = buf.view(30, 20, 30, 20); PrintWriter pout = new PrintWriter(System.out); pout.print(Ansi.hideCursor()); try { for (int i = 0; i < 400; i++) { if (i > 0) { - pout.print(Ansi.cursorMove(Ansi.CURSOR_PREV_LINE, pnl.size().height())); + pout.print(Ansi.cursorMove(Ansi.CURSOR_PREV_LINE, buf.size().height())); } p1.update(); @@ -46,7 +46,7 @@ public static void main(String[] args) throws InterruptedException, IOException p3.render(v3); p4.render(v4); - pnl.toAnsi(pout); + buf.toAnsi(pout); pout.println(); Thread.sleep(20); diff --git a/twinkle-core/src/main/java/org/codejive/twinkle/core/text/StyledCodepointBuffer.java b/twinkle-core/src/main/java/org/codejive/twinkle/core/text/LineBuffer.java similarity index 84% rename from twinkle-core/src/main/java/org/codejive/twinkle/core/text/StyledCodepointBuffer.java rename to twinkle-core/src/main/java/org/codejive/twinkle/core/text/LineBuffer.java index f6cd2fa..75f513d 100644 --- a/twinkle-core/src/main/java/org/codejive/twinkle/core/text/StyledCodepointBuffer.java +++ b/twinkle-core/src/main/java/org/codejive/twinkle/core/text/LineBuffer.java @@ -3,21 +3,84 @@ 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 class StyledCodepointBuffer implements StyledBuffer { +public interface LineBuffer extends Printable { + + char REPLACEMENT_CHAR = '\uFFFD'; + + int length(); + + char charAt(int index); + + int codepointAt(int i); + + @NonNull String graphemeAt(int i); + + long styleStateAt(int i); + + @NonNull Style styleAt(int i); + + default void setCharAt(int index, @NonNull Style style, char c) { + setCharAt(index, style.state(), c); + } + + void setCharAt(int index, long styleState, char c); + + default void setCharAt(int index, @NonNull Style style, int cp) { + setCharAt(index, style.state(), cp); + } + + void setCharAt(int index, long styleState, int cp); + + default void setCharAt(int index, @NonNull Style style, @NonNull CharSequence grapheme) { + setCharAt(index, style.state(), grapheme); + } + + void setCharAt(int index, long styleState, @NonNull CharSequence grapheme); + + default int putStringAt(int index, @NonNull Style style, @NonNull CharSequence str) { + return putStringAt(index, style.state(), str); + } + + int putStringAt(int index, long styleState, @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 CodepointBuffer(0) { + @Override + public @NonNull CodepointBuffer resize(int newSize) { + if (newSize != 0) { + throw new UnsupportedOperationException("Cannot resize EMPTY"); + } + return this; + } + }; + + static @NonNull LineBuffer of(int width) { + return new CodepointBuffer(width); + } +} + +class CodepointBuffer implements LineBuffer { protected int[] cpBuffer; protected String[] graphemeBuffer; protected long[] styleBuffer; - public StyledCodepointBuffer(int size) { + public CodepointBuffer(int size) { cpBuffer = new int[size]; graphemeBuffer = new String[size]; styleBuffer = new long[size]; } - protected StyledCodepointBuffer(int[] cpBuffer, String[] graphemeBuffer, long[] styleBuffer) { + protected CodepointBuffer(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"); @@ -154,24 +217,6 @@ public int putStringAt(int index, long styleState, @NonNull CharSequence str) { return cpsCount; } - @Override - public int putStringAt(int index, @NonNull StyledCharSequence str) { - if (outside(index, str.length())) { - return str.length(); - } - int minIndex = 0; - int maxIndex = cpBuffer.length; - int startIndex = Math.max(index, minIndex); - int endIndex = Math.min(index + str.length(), maxIndex); - int strStart = Math.max(startIndex - index, 0); - int len = endIndex - startIndex; - for (int i = 0; i < len; i++) { - setCharAt_( - startIndex + i, str.styleStateAt(strStart + i), str.codepointAt(strStart + i)); - } - return str.length(); - } - @Override public int putStringAt(int index, @NonNull StyledIterator iter) { int minIndex = 0; @@ -198,7 +243,7 @@ public int putStringAt(int index, @NonNull StyledIterator iter) { } @Override - public @NonNull StyledCharSequence subSequence(int start, int end) { + public @NonNull CodepointBuffer subSequence(int start, int end) { if (start < 0 || end > length() || start > end) { throw new IndexOutOfBoundsException( "Invalid subsequence range: " + start + " to " + end); @@ -210,11 +255,11 @@ public int putStringAt(int index, @NonNull StyledIterator iter) { System.arraycopy(cpBuffer, start, subCpBuffer, 0, subLength); System.arraycopy(graphemeBuffer, start, subGraphemeBuffer, 0, subLength); System.arraycopy(styleBuffer, start, subStyleBuffer, 0, subLength); - return new StyledCodepointBuffer(subCpBuffer, subGraphemeBuffer, subStyleBuffer); + return new CodepointBuffer(subCpBuffer, subGraphemeBuffer, subStyleBuffer); } @Override - public @NonNull StyledCodepointBuffer resize(int newSize) { + public @NonNull CodepointBuffer resize(int newSize) { if (newSize == cpBuffer.length) { return this; } diff --git a/twinkle-core/src/main/java/org/codejive/twinkle/core/text/StyledBuffer.java b/twinkle-core/src/main/java/org/codejive/twinkle/core/text/StyledBuffer.java deleted file mode 100644 index 4461428..0000000 --- a/twinkle-core/src/main/java/org/codejive/twinkle/core/text/StyledBuffer.java +++ /dev/null @@ -1,56 +0,0 @@ -package org.codejive.twinkle.core.text; - -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 StyledBuffer extends StyledCharSequence, Printable { - - char REPLACEMENT_CHAR = '\uFFFD'; - - default void setCharAt(int index, @NonNull Style style, char c) { - setCharAt(index, style.state(), c); - } - - void setCharAt(int index, long styleState, char c); - - default void setCharAt(int index, @NonNull Style style, int cp) { - setCharAt(index, style.state(), cp); - } - - void setCharAt(int index, long styleState, int cp); - - default void setCharAt(int index, @NonNull Style style, @NonNull CharSequence grapheme) { - setCharAt(index, style.state(), grapheme); - } - - void setCharAt(int index, long styleState, @NonNull CharSequence grapheme); - - default int putStringAt(int index, @NonNull Style style, @NonNull CharSequence str) { - return putStringAt(index, style.state(), str); - } - - int putStringAt(int index, long styleState, @NonNull CharSequence str); - - int putStringAt(int index, @NonNull StyledCharSequence str); - - int putStringAt(int index, @NonNull StyledIterator iter); - - @NonNull StyledBuffer resize(int newSize); - - StyledBuffer EMPTY = - new StyledCodepointBuffer(0) { - @Override - public @NonNull StyledCodepointBuffer resize(int newSize) { - if (newSize != 0) { - throw new UnsupportedOperationException("Cannot resize EMPTY"); - } - return this; - } - }; - - static @NonNull StyledBuffer of(int width) { - return new StyledCodepointBuffer(width); - } -} diff --git a/twinkle-core/src/main/java/org/codejive/twinkle/core/text/StyledCharSequence.java b/twinkle-core/src/main/java/org/codejive/twinkle/core/text/StyledCharSequence.java deleted file mode 100644 index 0c56ee8..0000000 --- a/twinkle-core/src/main/java/org/codejive/twinkle/core/text/StyledCharSequence.java +++ /dev/null @@ -1,31 +0,0 @@ -package org.codejive.twinkle.core.text; - -import org.codejive.twinkle.ansi.Style; -import org.jspecify.annotations.NonNull; - -public interface StyledCharSequence { - - int length(); - - /** - * Returns the {@code char} value at the specified index. In contrast to the original {@link - * CharSequence#charAt(int)} specification, this method never throws an exception and always - * returns a valid character. If the index is out of bounds, it returns the Unicode replacement - * character. - * - * @param index the index of the {@code char} value to be returned - * @return the specified {@code char} value - */ - char charAt(int index); - - int codepointAt(int i); - - @NonNull String graphemeAt(int i); - - long styleStateAt(int i); - - @NonNull Style styleAt(int i); - - // @Override - @NonNull StyledCharSequence subSequence(int start, int end); -} diff --git a/twinkle-core/src/main/java/org/codejive/twinkle/core/widget/StyledBufferPanel.java b/twinkle-core/src/main/java/org/codejive/twinkle/core/widget/Buffer.java similarity index 69% rename from twinkle-core/src/main/java/org/codejive/twinkle/core/widget/StyledBufferPanel.java rename to twinkle-core/src/main/java/org/codejive/twinkle/core/widget/Buffer.java index ff6ae38..652a30a 100644 --- a/twinkle-core/src/main/java/org/codejive/twinkle/core/widget/StyledBufferPanel.java +++ b/twinkle-core/src/main/java/org/codejive/twinkle/core/widget/Buffer.java @@ -1,28 +1,61 @@ package org.codejive.twinkle.core.widget; -import static org.codejive.twinkle.core.text.StyledBuffer.REPLACEMENT_CHAR; +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; -import org.codejive.twinkle.core.text.StyledBuffer; -import org.codejive.twinkle.core.text.StyledCharSequence; +import org.codejive.twinkle.core.text.LineBuffer; +import org.codejive.twinkle.util.Printable; import org.codejive.twinkle.util.StyledIterator; import org.jspecify.annotations.NonNull; -public class StyledBufferPanel implements Panel { +public interface Buffer extends Canvas, Printable { + + @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)); + } + + @Override + @NonNull View view(@NonNull Rect rect); + + static @NonNull Buffer of(int width, int height) { + return of(Size.of(width, height)); + } + + static @NonNull Buffer of(@NonNull Size size) { + 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); + } + + interface View extends Buffer { + View moveTo(int x, int y); + + View moveBy(int dx, int dy); + } +} + +class BufferImpl implements Buffer { protected @NonNull Rect rect; - protected @NonNull StyledBuffer[] lines; + protected @NonNull LineBuffer[] lines; - public StyledBufferPanel(@NonNull Size size) { + public BufferImpl(@NonNull Size size) { this.rect = Rect.of(size); - this.lines = new StyledBuffer[size.height()]; + this.lines = new LineBuffer[size.height()]; for (int i = 0; i < size.height(); i++) { lines[i] = createBuffer(size.width()); } } - protected StyledBufferPanel(@NonNull Rect rect, @NonNull StyledBuffer[] lines) { + protected BufferImpl(@NonNull Rect rect, @NonNull LineBuffer[] lines) { this.rect = rect; this.lines = lines; } @@ -108,14 +141,6 @@ public int putStringAt(int x, int y, long styleState, @NonNull CharSequence str) return line(y).putStringAt(applyXOffset(x), styleState, str); } - @Override - public int putStringAt(int x, int y, @NonNull StyledCharSequence str) { - if (outside(x, y, str.length())) { - return str.length(); - } - return line(y).putStringAt(applyXOffset(x), str); - } - @Override public int putStringAt(int x, int y, @NonNull StyledIterator iter) { return line(y).putStringAt(applyXOffset(x), iter); @@ -145,11 +170,11 @@ public void copyTo(Canvas canvas, int x, int y) { } @Override - public @NonNull Panel resize(@NonNull Size newSize) { + public @NonNull Buffer resize(@NonNull Size newSize) { if (newSize.equals(size())) { return this; } - StyledBuffer[] newLines = new StyledBuffer[newSize.height()]; + 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()); @@ -163,16 +188,16 @@ public void copyTo(Canvas canvas, int x, int y) { return this; } - private @NonNull StyledBuffer createBuffer(int width) { - return StyledBuffer.of(width); + private @NonNull LineBuffer createBuffer(int width) { + return LineBuffer.of(width); } @Override - public @NonNull PanelView view(@NonNull Rect viewRect) { - return new StyledBufferPanelView(this, viewRect, lines); + public Buffer.@NonNull View view(@NonNull Rect viewRect) { + return new BufferViewImpl(this, viewRect, lines); } - private StyledBuffer line(int y) { + private LineBuffer line(int y) { y = applyYOffset(y); return lines[y]; } @@ -257,54 +282,52 @@ public String toString() { } return appendable; } +} - public static class StyledBufferPanelView extends StyledBufferPanel implements PanelView { - protected final @NonNull StyledBufferPanel parentPanel; +class BufferViewImpl extends BufferImpl implements Buffer.View { + protected final @NonNull BufferImpl parentPanel; - protected StyledBufferPanelView( - @NonNull StyledBufferPanel parentPanel, - @NonNull Rect rect, - @NonNull StyledBuffer[] lines) { - super(rect, lines); - this.parentPanel = parentPanel; - } + protected BufferViewImpl( + @NonNull BufferImpl parentPanel, @NonNull Rect rect, @NonNull LineBuffer[] lines) { + super(rect, lines); + this.parentPanel = parentPanel; + } - @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()))); - } + @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()))); + } - @Override - public @NonNull Panel resize(@NonNull Size newSize) { - if (newSize.equals(size())) { - return this; - } - Rect r = rect(); - rect = Rect.of(r.left(), r.top(), newSize); + @Override + public @NonNull Buffer resize(@NonNull Size newSize) { + if (newSize.equals(size())) { return this; } + Rect r = rect(); + rect = Rect.of(r.left(), r.top(), newSize); + return this; + } - @Override - public PanelView moveTo(int x, int y) { - Rect r = rect(); - rect = Rect.of(x, y, r.size()); - return this; - } + @Override + public View moveTo(int x, int y) { + Rect r = rect(); + rect = Rect.of(x, y, r.size()); + return this; + } - @Override - public PanelView moveBy(int dx, int dy) { - Rect r = rect(); - rect = Rect.of(r.left() + dx, r.top() + dy, r.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()); + return this; } } diff --git a/twinkle-core/src/main/java/org/codejive/twinkle/core/widget/Canvas.java b/twinkle-core/src/main/java/org/codejive/twinkle/core/widget/Canvas.java index cb12835..c47e96d 100644 --- a/twinkle-core/src/main/java/org/codejive/twinkle/core/widget/Canvas.java +++ b/twinkle-core/src/main/java/org/codejive/twinkle/core/widget/Canvas.java @@ -1,7 +1,6 @@ package org.codejive.twinkle.core.widget; import org.codejive.twinkle.ansi.Style; -import org.codejive.twinkle.core.text.StyledCharSequence; import org.codejive.twinkle.util.StyledIterator; import org.jspecify.annotations.NonNull; @@ -40,8 +39,6 @@ default int putStringAt(int x, int y, @NonNull Style style, @NonNull CharSequenc int putStringAt(int x, int y, long styleState, @NonNull CharSequence str); - int putStringAt(int x, int y, @NonNull StyledCharSequence str); - int putStringAt(int x, int y, @NonNull StyledIterator iter); default void drawHLineAt(int x, int y, int x2, Style style, char c) { diff --git a/twinkle-core/src/main/java/org/codejive/twinkle/core/widget/Panel.java b/twinkle-core/src/main/java/org/codejive/twinkle/core/widget/Panel.java deleted file mode 100644 index 21c519b..0000000 --- a/twinkle-core/src/main/java/org/codejive/twinkle/core/widget/Panel.java +++ /dev/null @@ -1,32 +0,0 @@ -package org.codejive.twinkle.core.widget; - -import org.codejive.twinkle.core.text.StyledBuffer; -import org.codejive.twinkle.util.Printable; -import org.jspecify.annotations.NonNull; - -public interface Panel extends Canvas, Printable { - - @NonNull Panel resize(@NonNull Size newSize); - - @Override - default @NonNull PanelView view(int left, int top, int width, int height) { - return view(new Rect(left, top, width, height)); - } - - @Override - @NonNull PanelView view(@NonNull Rect rect); - - static @NonNull Panel of(int width, int height) { - return of(Size.of(width, height)); - } - - static @NonNull Panel of(@NonNull Size size) { - return new StyledBufferPanel(size); - } - - static @NonNull Panel of(@NonNull StyledBuffer buffer) { - Rect rect = Rect.of(buffer.length(), 1); - StyledBuffer[] lines = new StyledBuffer[] {buffer}; - return new StyledBufferPanel(rect, lines); - } -} diff --git a/twinkle-core/src/main/java/org/codejive/twinkle/core/widget/PanelView.java b/twinkle-core/src/main/java/org/codejive/twinkle/core/widget/PanelView.java deleted file mode 100644 index 85acd08..0000000 --- a/twinkle-core/src/main/java/org/codejive/twinkle/core/widget/PanelView.java +++ /dev/null @@ -1,7 +0,0 @@ -package org.codejive.twinkle.core.widget; - -public interface PanelView extends Panel { - PanelView moveTo(int x, int y); - - PanelView moveBy(int dx, int dy); -} diff --git a/twinkle-core/src/main/java/org/codejive/twinkle/core/widget/Rectangular.java b/twinkle-core/src/main/java/org/codejive/twinkle/core/widget/Rectangular.java deleted file mode 100644 index 006728c..0000000 --- a/twinkle-core/src/main/java/org/codejive/twinkle/core/widget/Rectangular.java +++ /dev/null @@ -1,7 +0,0 @@ -package org.codejive.twinkle.core.widget; - -import org.jspecify.annotations.NonNull; - -public interface Rectangular { - @NonNull Rect rect(); -} diff --git a/twinkle-core/src/test/java/org/codejive/twinkle/core/text/StyledBufferTimings.java b/twinkle-core/src/test/java/org/codejive/twinkle/core/text/LineBufferTimings.java similarity index 91% rename from twinkle-core/src/test/java/org/codejive/twinkle/core/text/StyledBufferTimings.java rename to twinkle-core/src/test/java/org/codejive/twinkle/core/text/LineBufferTimings.java index 225735f..d9e0604 100644 --- a/twinkle-core/src/test/java/org/codejive/twinkle/core/text/StyledBufferTimings.java +++ b/twinkle-core/src/test/java/org/codejive/twinkle/core/text/LineBufferTimings.java @@ -1,6 +1,6 @@ package org.codejive.twinkle.core.text; -public class StyledBufferTimings { +public class LineBufferTimings { private static int iterations = 1_000_000; public static void main(String[] args) { @@ -39,13 +39,13 @@ public static void main(String[] args) { }); System.out.println("Timing simple strings:"); - timeSimpleString(StyledBuffer.of(1000)); + timeSimpleString(LineBuffer.of(1000)); System.out.println("Timing strings with surrogates:"); - timeStringWithSurrogates(StyledBuffer.of(1000)); + timeStringWithSurrogates(LineBuffer.of(1000)); } - private static void timeSimpleString(StyledBuffer buffer) { + private static void timeSimpleString(LineBuffer buffer) { titer( buffer.getClass().getSimpleName(), () -> { @@ -58,7 +58,7 @@ private static void timeSimpleString(StyledBuffer buffer) { }); } - private static void timeStringWithSurrogates(StyledBuffer buffer) { + private static void timeStringWithSurrogates(LineBuffer buffer) { titer( buffer.getClass().getSimpleName(), () -> { diff --git a/twinkle-core/src/test/java/org/codejive/twinkle/core/text/TestLine.java b/twinkle-core/src/test/java/org/codejive/twinkle/core/text/TestLine.java index 0c86f44..7c38816 100644 --- a/twinkle-core/src/test/java/org/codejive/twinkle/core/text/TestLine.java +++ b/twinkle-core/src/test/java/org/codejive/twinkle/core/text/TestLine.java @@ -4,7 +4,7 @@ import org.codejive.twinkle.ansi.Ansi; import org.codejive.twinkle.ansi.Style; -import org.codejive.twinkle.core.widget.Panel; +import org.codejive.twinkle.core.widget.Buffer; import org.codejive.twinkle.util.SequenceIterator; import org.codejive.twinkle.util.StyledIterator; import org.junit.jupiter.api.Test; @@ -13,7 +13,7 @@ public class TestLine { @Test public void testRenderSingleStyledSpan() { - Panel p = Panel.of(1, 1); + Buffer p = Buffer.of(1, 1); Line.of("A", Style.BOLD).render(p); assertThat(p.toString()).isEqualTo("A"); @@ -22,7 +22,7 @@ public void testRenderSingleStyledSpan() { @Test public void testRenderMultipleSpans() { - Panel p = Panel.of(3, 1); + Buffer p = Buffer.of(3, 1); Line.of(Span.of("A"), Span.of("B", Style.BOLD), Span.of("C")).render(p); assertThat(p.toString()).isEqualTo("ABC"); @@ -41,7 +41,7 @@ public void testRenderMultipleSpans() { @Test public void testOfStyledIterator() { StyledIterator iter = new StyledIterator(SequenceIterator.of("Line 1")); - Panel p = Panel.of(6, 1); + Buffer p = Buffer.of(6, 1); Line.of(iter).render(p); assertThat(p.toString()).isEqualTo("Line 1"); } diff --git a/twinkle-core/src/test/java/org/codejive/twinkle/core/text/TestStyledBuffer.java b/twinkle-core/src/test/java/org/codejive/twinkle/core/text/TestLineBuffer.java similarity index 88% rename from twinkle-core/src/test/java/org/codejive/twinkle/core/text/TestStyledBuffer.java rename to twinkle-core/src/test/java/org/codejive/twinkle/core/text/TestLineBuffer.java index 0f5043a..0bff2a6 100644 --- a/twinkle-core/src/test/java/org/codejive/twinkle/core/text/TestStyledBuffer.java +++ b/twinkle-core/src/test/java/org/codejive/twinkle/core/text/TestLineBuffer.java @@ -6,16 +6,16 @@ import org.codejive.twinkle.ansi.Style; import org.junit.jupiter.api.Test; -public class TestStyledBuffer { +public class TestLineBuffer { @Test public void testStyledBufferCreation() { - StyledBuffer buffer = StyledBuffer.of(10); + LineBuffer buffer = LineBuffer.of(10); assertThat(buffer.length()).isEqualTo(10); } @Test public void testStyledBufferPutGetChar() { - StyledBuffer buffer = StyledBuffer.of(10); + LineBuffer buffer = LineBuffer.of(10); for (int i = 0; i < buffer.length(); i++) { buffer.setCharAt(i, Style.ITALIC.state(), (char) ('a' + i)); } @@ -27,7 +27,7 @@ public void testStyledBufferPutGetChar() { @Test public void testStyledBufferPutCharToString() { - StyledBuffer buffer = StyledBuffer.of(10); + LineBuffer buffer = LineBuffer.of(10); for (int i = 0; i < buffer.length(); i++) { buffer.setCharAt(i, Style.ITALIC.state(), (char) ('a' + i)); } @@ -36,7 +36,7 @@ public void testStyledBufferPutCharToString() { @Test public void testStyledBufferPutCharToAnsiString() { - StyledBuffer buffer = StyledBuffer.of(10); + 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)); @@ -52,7 +52,7 @@ public void testStyledBufferPutCharToAnsiString() { @Test public void testStyledBufferPutCharToAnsiStringWithCurrentStyle() { - StyledBuffer buffer = StyledBuffer.of(10); + 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)); @@ -63,7 +63,7 @@ public void testStyledBufferPutCharToAnsiStringWithCurrentStyle() { @Test public void testStyledBufferPutCharToAnsiStringWithUnderAndOverflow() { - StyledBuffer buffer = StyledBuffer.of(10); + 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)); @@ -79,7 +79,7 @@ public void testStyledBufferPutCharToAnsiStringWithUnderAndOverflow() { @Test public void testStyledBufferPutStringGetChar() { - StyledBuffer buffer = StyledBuffer.of(10); + LineBuffer buffer = LineBuffer.of(10); buffer.putStringAt(0, Style.ITALIC, "abcdefghij"); for (int i = 0; i < buffer.length(); i++) { assertThat(buffer.charAt(i)).isEqualTo((char) ('a' + i)); diff --git a/twinkle-core/src/test/java/org/codejive/twinkle/core/text/TestSpan.java b/twinkle-core/src/test/java/org/codejive/twinkle/core/text/TestSpan.java index 0d182c7..6d42949 100644 --- a/twinkle-core/src/test/java/org/codejive/twinkle/core/text/TestSpan.java +++ b/twinkle-core/src/test/java/org/codejive/twinkle/core/text/TestSpan.java @@ -4,7 +4,7 @@ import org.codejive.twinkle.ansi.Ansi; import org.codejive.twinkle.ansi.Style; -import org.codejive.twinkle.core.widget.Panel; +import org.codejive.twinkle.core.widget.Buffer; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; @@ -32,7 +32,7 @@ public void testLengthZwjSequence() { @Test public void testSpansRender() { - Panel p = Panel.of(1, 1); + Buffer p = Buffer.of(1, 1); Span.of("A", Style.BOLD).render(p); assertThat(p.toString()).isEqualTo("A"); assertThat(p.toAnsiString()).isEqualTo(Ansi.STYLE_RESET + Ansi.style(Ansi.BOLD) + "A"); diff --git a/twinkle-core/src/test/java/org/codejive/twinkle/core/text/TestText.java b/twinkle-core/src/test/java/org/codejive/twinkle/core/text/TestText.java index e173877..d8d98a9 100644 --- a/twinkle-core/src/test/java/org/codejive/twinkle/core/text/TestText.java +++ b/twinkle-core/src/test/java/org/codejive/twinkle/core/text/TestText.java @@ -3,7 +3,7 @@ import static org.assertj.core.api.Assertions.assertThat; import org.codejive.twinkle.ansi.Style; -import org.codejive.twinkle.core.widget.Panel; +import org.codejive.twinkle.core.widget.Buffer; import org.codejive.twinkle.util.SequenceIterator; import org.codejive.twinkle.util.StyledIterator; import org.junit.jupiter.api.Test; @@ -12,26 +12,26 @@ public class TestText { @Test public void testOfSimpleString() { - Panel pnl = Panel.of(11, 1); - Text.of("Hello World").render(pnl); - assertThat(pnl.toString()).isEqualTo("Hello World"); + Buffer buf = Buffer.of(11, 1); + Text.of("Hello World").render(buf); + assertThat(buf.toString()).isEqualTo("Hello World"); } @Test public void testOfStyledString() { Style style = Style.BOLD; - Panel pnl = Panel.of(11, 1); - Text.of("Hello World", style).render(pnl); - assertThat(pnl.toAnsiString(Style.F_UNSTYLED)) + Buffer buf = Buffer.of(11, 1); + Text.of("Hello World", style).render(buf); + assertThat(buf.toAnsiString(Style.F_UNSTYLED)) .isEqualTo(style.toAnsiString() + "Hello World"); } @Test public void testOfStyleState() { Style style = Style.BOLD; - Panel pnl = Panel.of(11, 1); - Text.of("Hello World", style.state()).render(pnl); - assertThat(pnl.toAnsiString(Style.F_UNSTYLED)) + Buffer buf = Buffer.of(11, 1); + Text.of("Hello World", style.state()).render(buf); + assertThat(buf.toAnsiString(Style.F_UNSTYLED)) .isEqualTo(style.toAnsiString() + "Hello World"); } @@ -39,16 +39,16 @@ public void testOfStyleState() { public void testOfLines() { Line line1 = Line.of("Line 1"); Line line2 = Line.of("Line 2"); - Panel pnl = Panel.of(6, 2); - Text.of(line1, line2).render(pnl); - assertThat(pnl.toString()).isEqualTo("Line 1\nLine 2"); + Buffer buf = Buffer.of(6, 2); + Text.of(line1, line2).render(buf); + assertThat(buf.toString()).isEqualTo("Line 1\nLine 2"); } @Test public void testOfStyledIterator() { StyledIterator iter = new StyledIterator(SequenceIterator.of("Line 1\nLine 2")); - Panel pnl = Panel.of(6, 2); - Text.of(iter).render(pnl); - assertThat(pnl.toString()).isEqualTo("Line 1\nLine 2"); + Buffer buf = Buffer.of(6, 2); + Text.of(iter).render(buf); + assertThat(buf.toString()).isEqualTo("Line 1\nLine 2"); } } diff --git a/twinkle-core/src/test/java/org/codejive/twinkle/core/widget/TestPanel.java b/twinkle-core/src/test/java/org/codejive/twinkle/core/widget/TestBuffer.java similarity index 65% rename from twinkle-core/src/test/java/org/codejive/twinkle/core/widget/TestPanel.java rename to twinkle-core/src/test/java/org/codejive/twinkle/core/widget/TestBuffer.java index ca8d856..45b6769 100644 --- a/twinkle-core/src/test/java/org/codejive/twinkle/core/widget/TestPanel.java +++ b/twinkle-core/src/test/java/org/codejive/twinkle/core/widget/TestBuffer.java @@ -4,67 +4,67 @@ import org.codejive.twinkle.ansi.Color; import org.codejive.twinkle.ansi.Style; -import org.codejive.twinkle.core.text.StyledBuffer; +import org.codejive.twinkle.core.text.LineBuffer; import org.junit.jupiter.api.Test; -public class TestPanel { +public class TestBuffer { @Test public void testPanelCreation() { - Panel panel = Panel.of(10, 5); - Size size = panel.size(); + Buffer buffer = Buffer.of(10, 5); + Size size = buffer.size(); assertThat(size.width()).isEqualTo(10); assertThat(size.height()).isEqualTo(5); } @Test public void testPanelDefaultInnerContent() { - Panel panel = Panel.of(10, 5); - Size size = panel.size(); + Buffer buffer = Buffer.of(10, 5); + Size size = buffer.size(); for (int y = 0; y < size.height(); y++) { for (int x = 0; x < size.width(); x++) { - assertThat(panel.charAt(x, y)).isEqualTo('\0'); - assertThat(panel.styleAt(x, y)).isEqualTo(Style.UNSTYLED); + assertThat(buffer.charAt(x, y)).isEqualTo('\0'); + assertThat(buffer.styleAt(x, y)).isEqualTo(Style.UNSTYLED); } } } @Test public void testPanelDefaultOuterContent() { - Panel panel = Panel.of(10, 5); - Size size = panel.size(); + Buffer buffer = Buffer.of(10, 5); + Size size = buffer.size(); for (int y = -5; y < size.height() + 5; y++) { for (int x = -5; x < size.width() + 5; x++) { if (x >= 0 && x < size.width() && y >= 0 && y < size.height()) { continue; // Skip inner content } - assertThat(panel.charAt(x, y)).isEqualTo(StyledBuffer.REPLACEMENT_CHAR); - assertThat(panel.styleAt(x, y)).isEqualTo(Style.UNSTYLED); + assertThat(buffer.charAt(x, y)).isEqualTo(LineBuffer.REPLACEMENT_CHAR); + assertThat(buffer.styleAt(x, y)).isEqualTo(Style.UNSTYLED); } } } @Test public void testPanelNewContents() { - Panel panel = createPanel(); - Size size = panel.size(); + Buffer buffer = createPanel(); + Size size = buffer.size(); for (int y = 0; y < size.height(); y++) { for (int x = 0; x < size.width(); x++) { - assertThat(panel.charAt(x, y)).isEqualTo((char) ('A' + x + y * size.width())); - assertThat(panel.styleAt(x, y)).isEqualTo(Style.ofFgColor(Color.indexed(x))); + assertThat(buffer.charAt(x, y)).isEqualTo((char) ('A' + x + y * size.width())); + assertThat(buffer.styleAt(x, y)).isEqualTo(Style.ofFgColor(Color.indexed(x))); } } } @Test public void testPanelView() { - Panel panel = createPanel(); - Canvas view = panel.view(1, 1, 3, 3); + Buffer buffer = createPanel(); + Canvas 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++) { assertThat(view.charAt(x, y)) - .isEqualTo((char) ('G' + x + y * panel.size().width())); + .isEqualTo((char) ('G' + x + y * buffer.size().width())); assertThat(view.styleAt(x, y)).isEqualTo(Style.ofFgColor(Color.indexed(x + 1))); } } @@ -72,15 +72,15 @@ public void testPanelView() { @Test public void testPanelViewOutside() { - Panel panel = createPanel(); - Canvas view = panel.view(1, 1, 3, 3); + Buffer buffer = createPanel(); + Canvas 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(StyledBuffer.REPLACEMENT_CHAR); + assertThat(view.charAt(x, y)).isEqualTo(LineBuffer.REPLACEMENT_CHAR); assertThat(view.styleAt(x, y)).isEqualTo(Style.UNSTYLED); } } @@ -88,14 +88,14 @@ public void testPanelViewOutside() { @Test public void testPanelNestedView() { - Panel panel = createPanel(); - Panel view1 = panel.view(1, 1, 3, 3); - Panel view2 = view1.view(1, 1, 2, 2); + Buffer buffer = createPanel(); + Buffer view1 = buffer.view(1, 1, 3, 3); + Buffer view2 = view1.view(1, 1, 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((char) ('M' + x + y * panel.size().width())); + .isEqualTo((char) ('M' + x + y * buffer.size().width())); assertThat(view2.styleAt(x, y)).isEqualTo(Style.ofFgColor(Color.indexed(x + 2))); } } @@ -103,16 +103,16 @@ public void testPanelNestedView() { @Test public void testPanelNestedViewOutside() { - Panel panel = createPanel(); - Panel view1 = panel.view(1, 1, 3, 3); - Panel view2 = view1.view(1, 1, 2, 2); + Buffer buffer = createPanel(); + Buffer view1 = buffer.view(1, 1, 3, 3); + Buffer view2 = view1.view(1, 1, 2, 2); Size size = view2.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(view2.charAt(x, y)).isEqualTo(StyledBuffer.REPLACEMENT_CHAR); + assertThat(view2.charAt(x, y)).isEqualTo(LineBuffer.REPLACEMENT_CHAR); assertThat(view2.styleAt(x, y)).isEqualTo(Style.UNSTYLED); } } @@ -120,9 +120,9 @@ public void testPanelNestedViewOutside() { @Test public void testPanelNestedViewMoved() { - Panel panel = createPanel(); - PanelView view1 = panel.view(1, 1, 3, 3); - Panel view2 = view1.view(1, 1, 2, 2); + Buffer buffer = createPanel(); + Buffer.View view1 = buffer.view(1, 1, 3, 3); + Buffer view2 = view1.view(1, 1, 2, 2); view1.moveBy(1, 1); @@ -130,7 +130,7 @@ public void testPanelNestedViewMoved() { for (int y = 0; y < size.height(); y++) { for (int x = 0; x < size.width(); x++) { assertThat(view2.charAt(x, y)) - .isEqualTo((char) ('S' + x + y * panel.size().width())); + .isEqualTo((char) ('S' + x + y * buffer.size().width())); assertThat(view2.styleAt(x, y)).isEqualTo(Style.ofFgColor(Color.indexed(x + 3))); } } @@ -138,16 +138,16 @@ public void testPanelNestedViewMoved() { @Test public void testPanelNestedViewMovedFullyOutside() { - Panel panel = createPanel(); - PanelView view1 = panel.view(1, 1, 3, 3); - Panel view2 = view1.view(1, 1, 2, 2); + Buffer buffer = createPanel(); + Buffer.View view1 = buffer.view(1, 1, 3, 3); + Buffer view2 = view1.view(1, 1, 2, 2); view1.moveBy(10, 10); 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(StyledBuffer.REPLACEMENT_CHAR); + assertThat(view2.charAt(x, y)).isEqualTo(LineBuffer.REPLACEMENT_CHAR); assertThat(view2.styleAt(x, y)).isEqualTo(Style.UNSTYLED); } } @@ -155,9 +155,9 @@ public void testPanelNestedViewMovedFullyOutside() { @Test public void testPanelNestedViewMovedPartiallyOutside() { - Panel panel = createPanel(); - PanelView view1 = panel.view(1, 1, 3, 3); - Panel view2 = view1.view(1, 1, 2, 2); + Buffer buffer = createPanel(); + Buffer.View view1 = buffer.view(1, 1, 3, 3); + Buffer view2 = view1.view(1, 1, 2, 2); view1.moveTo(3, 3); @@ -169,26 +169,26 @@ public void testPanelNestedViewMovedPartiallyOutside() { assertThat(view2.styleAt(x, y)) .isEqualTo(Style.ofFgColor(Color.indexed(x + 4))); } else { - assertThat(view2.charAt(x, y)).isEqualTo(StyledBuffer.REPLACEMENT_CHAR); + assertThat(view2.charAt(x, y)).isEqualTo(LineBuffer.REPLACEMENT_CHAR); assertThat(view2.styleAt(x, y)).isEqualTo(Style.UNSTYLED); } } } } - private Panel createPanel() { - Panel panel = Panel.of(5, 5); - Size size = panel.size(); + private Buffer createPanel() { + 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++) { - panel.setCharAt( + buffer.setCharAt( x, y, Style.ofFgColor(Color.indexed(x)), (char) ('A' + x + y * size.width())); } } - return panel; + return buffer; } private void printCanvas(Canvas canvas) { From d980baf5c58410725460e741af02f9e70f1cc3e2 Mon Sep 17 00:00:00 2001 From: Tako Schotanus Date: Mon, 5 Jan 2026 16:43:31 +0100 Subject: [PATCH 04/11] refactor: moved bunch of classes around --- .../java/org/codejive/twinkle/widgets/graphs/bar/Bar.java | 2 +- .../twinkle/widgets/graphs/bar/FracBarRenderer.java | 4 ++-- .../org/codejive/twinkle/widgets/graphs/plot/MathPlot.java | 4 ++-- .../java/org/codejive/twinkle/widgets/graphs/plot/Plot.java | 6 +++--- twinkle-chart/src/test/java/examples/BarDemo.java | 4 ++-- twinkle-chart/src/test/java/examples/MathPlotColorDemo.java | 4 ++-- twinkle-chart/src/test/java/examples/MathPlotDemo.java | 4 ++-- twinkle-chart/src/test/java/examples/MathPlotFourDemo.java | 6 +++--- .../codejive/twinkle/core/decor/SimpleBorderRenderer.java | 2 +- .../org/codejive/twinkle/core/{widget => text}/Buffer.java | 5 +++-- .../org/codejive/twinkle/core/{widget => text}/Canvas.java | 4 +++- .../src/main/java/org/codejive/twinkle/core/text/Line.java | 1 - .../src/main/java/org/codejive/twinkle/core/text/Span.java | 1 - .../src/main/java/org/codejive/twinkle/core/text/Text.java | 1 - .../codejive/twinkle/core/{widget => util}/FlexRect.java | 2 +- .../org/codejive/twinkle/core/{widget => util}/Rect.java | 2 +- .../org/codejive/twinkle/core/{widget => util}/Size.java | 2 +- .../org/codejive/twinkle/core/{widget => util}/Sized.java | 2 +- .../java/org/codejive/twinkle/core/widget/Renderable.java | 2 ++ .../main/java/org/codejive/twinkle/core/widget/Widget.java | 2 ++ .../src/main/java/org/codejive/twinkle/widgets/Framed.java | 6 +++--- .../codejive/twinkle/core/{widget => text}/TestBuffer.java | 4 ++-- .../test/java/org/codejive/twinkle/core/text/TestLine.java | 1 - .../test/java/org/codejive/twinkle/core/text/TestSpan.java | 1 - .../test/java/org/codejive/twinkle/core/text/TestText.java | 1 - 25 files changed, 37 insertions(+), 36 deletions(-) rename twinkle-core/src/main/java/org/codejive/twinkle/core/{widget => text}/Buffer.java (98%) rename twinkle-core/src/main/java/org/codejive/twinkle/core/{widget => text}/Canvas.java (93%) rename twinkle-core/src/main/java/org/codejive/twinkle/core/{widget => util}/FlexRect.java (95%) rename twinkle-core/src/main/java/org/codejive/twinkle/core/{widget => util}/Rect.java (98%) rename twinkle-core/src/main/java/org/codejive/twinkle/core/{widget => util}/Size.java (96%) rename twinkle-core/src/main/java/org/codejive/twinkle/core/{widget => util}/Sized.java (69%) rename twinkle-core/src/test/java/org/codejive/twinkle/core/{widget => text}/TestBuffer.java (98%) diff --git a/twinkle-chart/src/main/java/org/codejive/twinkle/widgets/graphs/bar/Bar.java b/twinkle-chart/src/main/java/org/codejive/twinkle/widgets/graphs/bar/Bar.java index 465b398..e12617e 100644 --- a/twinkle-chart/src/main/java/org/codejive/twinkle/widgets/graphs/bar/Bar.java +++ b/twinkle-chart/src/main/java/org/codejive/twinkle/widgets/graphs/bar/Bar.java @@ -1,6 +1,6 @@ package org.codejive.twinkle.widgets.graphs.bar; -import org.codejive.twinkle.core.widget.Canvas; +import org.codejive.twinkle.core.text.Canvas; import org.codejive.twinkle.core.widget.Renderable; public class Bar implements Renderable { 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 9a75471..c67b212 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 @@ -1,8 +1,8 @@ package org.codejive.twinkle.widgets.graphs.bar; import org.codejive.twinkle.ansi.Style; -import org.codejive.twinkle.core.widget.Canvas; -import org.codejive.twinkle.core.widget.Size; +import org.codejive.twinkle.core.text.Canvas; +import org.codejive.twinkle.core.util.Size; import org.codejive.twinkle.widgets.graphs.bar.BarConfig.*; import org.codejive.twinkle.widgets.graphs.bar.FracBarConfig.*; diff --git a/twinkle-chart/src/main/java/org/codejive/twinkle/widgets/graphs/plot/MathPlot.java b/twinkle-chart/src/main/java/org/codejive/twinkle/widgets/graphs/plot/MathPlot.java index c7f4251..2127c97 100644 --- a/twinkle-chart/src/main/java/org/codejive/twinkle/widgets/graphs/plot/MathPlot.java +++ b/twinkle-chart/src/main/java/org/codejive/twinkle/widgets/graphs/plot/MathPlot.java @@ -2,8 +2,8 @@ import java.util.function.Function; import org.codejive.twinkle.ansi.Style; -import org.codejive.twinkle.core.widget.Canvas; -import org.codejive.twinkle.core.widget.Size; +import org.codejive.twinkle.core.text.Canvas; +import org.codejive.twinkle.core.util.Size; import org.codejive.twinkle.core.widget.Widget; import org.jspecify.annotations.NonNull; 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 804065a..a33bc1f 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 @@ -1,9 +1,9 @@ package org.codejive.twinkle.widgets.graphs.plot; import org.codejive.twinkle.ansi.Style; -import org.codejive.twinkle.core.widget.Buffer; -import org.codejive.twinkle.core.widget.Canvas; -import org.codejive.twinkle.core.widget.Size; +import org.codejive.twinkle.core.text.Buffer; +import org.codejive.twinkle.core.text.Canvas; +import org.codejive.twinkle.core.util.Size; import org.codejive.twinkle.core.widget.Widget; import org.jspecify.annotations.NonNull; diff --git a/twinkle-chart/src/test/java/examples/BarDemo.java b/twinkle-chart/src/test/java/examples/BarDemo.java index d3e1fde..e0cbe1f 100644 --- a/twinkle-chart/src/test/java/examples/BarDemo.java +++ b/twinkle-chart/src/test/java/examples/BarDemo.java @@ -1,7 +1,7 @@ package examples; -import org.codejive.twinkle.core.widget.Buffer; -import org.codejive.twinkle.core.widget.Canvas; +import org.codejive.twinkle.core.text.Buffer; +import org.codejive.twinkle.core.text.Canvas; import org.codejive.twinkle.widgets.graphs.bar.Bar; import org.codejive.twinkle.widgets.graphs.bar.BarConfig; import org.codejive.twinkle.widgets.graphs.bar.FracBarConfig; diff --git a/twinkle-chart/src/test/java/examples/MathPlotColorDemo.java b/twinkle-chart/src/test/java/examples/MathPlotColorDemo.java index 917e45a..c5ddffb 100644 --- a/twinkle-chart/src/test/java/examples/MathPlotColorDemo.java +++ b/twinkle-chart/src/test/java/examples/MathPlotColorDemo.java @@ -5,9 +5,9 @@ import org.codejive.twinkle.ansi.Ansi; import org.codejive.twinkle.ansi.Color; import org.codejive.twinkle.ansi.Style; +import org.codejive.twinkle.core.text.Buffer; import org.codejive.twinkle.core.text.Line; -import org.codejive.twinkle.core.widget.Buffer; -import org.codejive.twinkle.core.widget.Size; +import org.codejive.twinkle.core.util.Size; import org.codejive.twinkle.widgets.Framed; import org.codejive.twinkle.widgets.graphs.plot.MathPlot; diff --git a/twinkle-chart/src/test/java/examples/MathPlotDemo.java b/twinkle-chart/src/test/java/examples/MathPlotDemo.java index 106b730..4a335ae 100644 --- a/twinkle-chart/src/test/java/examples/MathPlotDemo.java +++ b/twinkle-chart/src/test/java/examples/MathPlotDemo.java @@ -3,9 +3,9 @@ import java.util.Random; import org.codejive.twinkle.ansi.Ansi; +import org.codejive.twinkle.core.text.Buffer; import org.codejive.twinkle.core.text.Line; -import org.codejive.twinkle.core.widget.Buffer; -import org.codejive.twinkle.core.widget.Size; +import org.codejive.twinkle.core.util.Size; import org.codejive.twinkle.widgets.Framed; import org.codejive.twinkle.widgets.graphs.plot.MathPlot; diff --git a/twinkle-chart/src/test/java/examples/MathPlotFourDemo.java b/twinkle-chart/src/test/java/examples/MathPlotFourDemo.java index 5837637..d3a4070 100644 --- a/twinkle-chart/src/test/java/examples/MathPlotFourDemo.java +++ b/twinkle-chart/src/test/java/examples/MathPlotFourDemo.java @@ -7,10 +7,10 @@ import org.codejive.twinkle.ansi.Ansi; import org.codejive.twinkle.ansi.Color; import org.codejive.twinkle.ansi.Style; +import org.codejive.twinkle.core.text.Buffer; +import org.codejive.twinkle.core.text.Canvas; import org.codejive.twinkle.core.text.Line; -import org.codejive.twinkle.core.widget.Buffer; -import org.codejive.twinkle.core.widget.Canvas; -import org.codejive.twinkle.core.widget.Size; +import org.codejive.twinkle.core.util.Size; import org.codejive.twinkle.core.widget.Widget; import org.codejive.twinkle.widgets.Framed; import org.codejive.twinkle.widgets.graphs.plot.MathPlot; 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 0c47928..d749f8d 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 @@ -1,7 +1,7 @@ package org.codejive.twinkle.core.decor; import org.codejive.twinkle.ansi.Style; -import org.codejive.twinkle.core.widget.Canvas; +import org.codejive.twinkle.core.text.Canvas; import org.codejive.twinkle.core.widget.Renderable; public class SimpleBorderRenderer implements Renderable { diff --git a/twinkle-core/src/main/java/org/codejive/twinkle/core/widget/Buffer.java b/twinkle-core/src/main/java/org/codejive/twinkle/core/text/Buffer.java similarity index 98% rename from twinkle-core/src/main/java/org/codejive/twinkle/core/widget/Buffer.java rename to twinkle-core/src/main/java/org/codejive/twinkle/core/text/Buffer.java index 652a30a..8252aef 100644 --- a/twinkle-core/src/main/java/org/codejive/twinkle/core/widget/Buffer.java +++ b/twinkle-core/src/main/java/org/codejive/twinkle/core/text/Buffer.java @@ -1,11 +1,12 @@ -package org.codejive.twinkle.core.widget; +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; -import org.codejive.twinkle.core.text.LineBuffer; +import org.codejive.twinkle.core.util.Rect; +import org.codejive.twinkle.core.util.Size; import org.codejive.twinkle.util.Printable; import org.codejive.twinkle.util.StyledIterator; import org.jspecify.annotations.NonNull; diff --git a/twinkle-core/src/main/java/org/codejive/twinkle/core/widget/Canvas.java b/twinkle-core/src/main/java/org/codejive/twinkle/core/text/Canvas.java similarity index 93% rename from twinkle-core/src/main/java/org/codejive/twinkle/core/widget/Canvas.java rename to twinkle-core/src/main/java/org/codejive/twinkle/core/text/Canvas.java index c47e96d..0547328 100644 --- a/twinkle-core/src/main/java/org/codejive/twinkle/core/widget/Canvas.java +++ b/twinkle-core/src/main/java/org/codejive/twinkle/core/text/Canvas.java @@ -1,6 +1,8 @@ -package org.codejive.twinkle.core.widget; +package org.codejive.twinkle.core.text; import org.codejive.twinkle.ansi.Style; +import org.codejive.twinkle.core.util.Rect; +import org.codejive.twinkle.core.util.Sized; import org.codejive.twinkle.util.StyledIterator; import org.jspecify.annotations.NonNull; diff --git a/twinkle-core/src/main/java/org/codejive/twinkle/core/text/Line.java b/twinkle-core/src/main/java/org/codejive/twinkle/core/text/Line.java index 35beb4d..d83ca54 100644 --- a/twinkle-core/src/main/java/org/codejive/twinkle/core/text/Line.java +++ b/twinkle-core/src/main/java/org/codejive/twinkle/core/text/Line.java @@ -4,7 +4,6 @@ import java.util.Collections; import java.util.List; import org.codejive.twinkle.ansi.Style; -import org.codejive.twinkle.core.widget.Canvas; import org.codejive.twinkle.core.widget.Renderable; import org.codejive.twinkle.util.StyledIterator; diff --git a/twinkle-core/src/main/java/org/codejive/twinkle/core/text/Span.java b/twinkle-core/src/main/java/org/codejive/twinkle/core/text/Span.java index 3a4bee2..7c78598 100644 --- a/twinkle-core/src/main/java/org/codejive/twinkle/core/text/Span.java +++ b/twinkle-core/src/main/java/org/codejive/twinkle/core/text/Span.java @@ -1,7 +1,6 @@ package org.codejive.twinkle.core.text; import org.codejive.twinkle.ansi.Style; -import org.codejive.twinkle.core.widget.Canvas; import org.codejive.twinkle.core.widget.Renderable; public class Span implements Renderable { diff --git a/twinkle-core/src/main/java/org/codejive/twinkle/core/text/Text.java b/twinkle-core/src/main/java/org/codejive/twinkle/core/text/Text.java index 236a4ec..de16e22 100644 --- a/twinkle-core/src/main/java/org/codejive/twinkle/core/text/Text.java +++ b/twinkle-core/src/main/java/org/codejive/twinkle/core/text/Text.java @@ -4,7 +4,6 @@ import java.util.Collections; import java.util.List; import org.codejive.twinkle.ansi.Style; -import org.codejive.twinkle.core.widget.Canvas; import org.codejive.twinkle.core.widget.Renderable; import org.codejive.twinkle.util.StyledIterator; diff --git a/twinkle-core/src/main/java/org/codejive/twinkle/core/widget/FlexRect.java b/twinkle-core/src/main/java/org/codejive/twinkle/core/util/FlexRect.java similarity index 95% rename from twinkle-core/src/main/java/org/codejive/twinkle/core/widget/FlexRect.java rename to twinkle-core/src/main/java/org/codejive/twinkle/core/util/FlexRect.java index dba741a..3dea4ec 100644 --- a/twinkle-core/src/main/java/org/codejive/twinkle/core/widget/FlexRect.java +++ b/twinkle-core/src/main/java/org/codejive/twinkle/core/util/FlexRect.java @@ -1,4 +1,4 @@ -package org.codejive.twinkle.core.widget; +package org.codejive.twinkle.core.util; import org.jspecify.annotations.NonNull; diff --git a/twinkle-core/src/main/java/org/codejive/twinkle/core/widget/Rect.java b/twinkle-core/src/main/java/org/codejive/twinkle/core/util/Rect.java similarity index 98% rename from twinkle-core/src/main/java/org/codejive/twinkle/core/widget/Rect.java rename to twinkle-core/src/main/java/org/codejive/twinkle/core/util/Rect.java index ac84885..cb9d037 100644 --- a/twinkle-core/src/main/java/org/codejive/twinkle/core/widget/Rect.java +++ b/twinkle-core/src/main/java/org/codejive/twinkle/core/util/Rect.java @@ -1,4 +1,4 @@ -package org.codejive.twinkle.core.widget; +package org.codejive.twinkle.core.util; import org.jspecify.annotations.NonNull; diff --git a/twinkle-core/src/main/java/org/codejive/twinkle/core/widget/Size.java b/twinkle-core/src/main/java/org/codejive/twinkle/core/util/Size.java similarity index 96% rename from twinkle-core/src/main/java/org/codejive/twinkle/core/widget/Size.java rename to twinkle-core/src/main/java/org/codejive/twinkle/core/util/Size.java index b298e48..bbcfd84 100644 --- a/twinkle-core/src/main/java/org/codejive/twinkle/core/widget/Size.java +++ b/twinkle-core/src/main/java/org/codejive/twinkle/core/util/Size.java @@ -1,4 +1,4 @@ -package org.codejive.twinkle.core.widget; +package org.codejive.twinkle.core.util; import java.util.Objects; import org.jspecify.annotations.NonNull; diff --git a/twinkle-core/src/main/java/org/codejive/twinkle/core/widget/Sized.java b/twinkle-core/src/main/java/org/codejive/twinkle/core/util/Sized.java similarity index 69% rename from twinkle-core/src/main/java/org/codejive/twinkle/core/widget/Sized.java rename to twinkle-core/src/main/java/org/codejive/twinkle/core/util/Sized.java index 8be2170..d23615b 100644 --- a/twinkle-core/src/main/java/org/codejive/twinkle/core/widget/Sized.java +++ b/twinkle-core/src/main/java/org/codejive/twinkle/core/util/Sized.java @@ -1,4 +1,4 @@ -package org.codejive.twinkle.core.widget; +package org.codejive.twinkle.core.util; import org.jspecify.annotations.NonNull; diff --git a/twinkle-core/src/main/java/org/codejive/twinkle/core/widget/Renderable.java b/twinkle-core/src/main/java/org/codejive/twinkle/core/widget/Renderable.java index ea80c46..5017481 100644 --- a/twinkle-core/src/main/java/org/codejive/twinkle/core/widget/Renderable.java +++ b/twinkle-core/src/main/java/org/codejive/twinkle/core/widget/Renderable.java @@ -1,5 +1,7 @@ package org.codejive.twinkle.core.widget; +import org.codejive.twinkle.core.text.Canvas; + public interface Renderable { void render(Canvas canvas); } diff --git a/twinkle-core/src/main/java/org/codejive/twinkle/core/widget/Widget.java b/twinkle-core/src/main/java/org/codejive/twinkle/core/widget/Widget.java index 289a6a8..1cf36c6 100644 --- a/twinkle-core/src/main/java/org/codejive/twinkle/core/widget/Widget.java +++ b/twinkle-core/src/main/java/org/codejive/twinkle/core/widget/Widget.java @@ -1,3 +1,5 @@ package org.codejive.twinkle.core.widget; +import org.codejive.twinkle.core.util.Sized; + public interface Widget extends Sized, Renderable {} diff --git a/twinkle-core/src/main/java/org/codejive/twinkle/widgets/Framed.java b/twinkle-core/src/main/java/org/codejive/twinkle/widgets/Framed.java index 3da0332..12cde3a 100644 --- a/twinkle-core/src/main/java/org/codejive/twinkle/widgets/Framed.java +++ b/twinkle-core/src/main/java/org/codejive/twinkle/widgets/Framed.java @@ -1,11 +1,11 @@ package org.codejive.twinkle.widgets; import org.codejive.twinkle.core.decor.SimpleBorderRenderer; +import org.codejive.twinkle.core.text.Canvas; import org.codejive.twinkle.core.text.Line; -import org.codejive.twinkle.core.widget.Canvas; -import org.codejive.twinkle.core.widget.Rect; +import org.codejive.twinkle.core.util.Rect; +import org.codejive.twinkle.core.util.Size; import org.codejive.twinkle.core.widget.Renderable; -import org.codejive.twinkle.core.widget.Size; import org.codejive.twinkle.core.widget.Widget; import org.jspecify.annotations.NonNull; import org.jspecify.annotations.Nullable; diff --git a/twinkle-core/src/test/java/org/codejive/twinkle/core/widget/TestBuffer.java b/twinkle-core/src/test/java/org/codejive/twinkle/core/text/TestBuffer.java similarity index 98% rename from twinkle-core/src/test/java/org/codejive/twinkle/core/widget/TestBuffer.java rename to twinkle-core/src/test/java/org/codejive/twinkle/core/text/TestBuffer.java index 45b6769..f3465b7 100644 --- a/twinkle-core/src/test/java/org/codejive/twinkle/core/widget/TestBuffer.java +++ b/twinkle-core/src/test/java/org/codejive/twinkle/core/text/TestBuffer.java @@ -1,10 +1,10 @@ -package org.codejive.twinkle.core.widget; +package org.codejive.twinkle.core.text; import static org.assertj.core.api.Assertions.assertThat; import org.codejive.twinkle.ansi.Color; import org.codejive.twinkle.ansi.Style; -import org.codejive.twinkle.core.text.LineBuffer; +import org.codejive.twinkle.core.util.Size; import org.junit.jupiter.api.Test; public class TestBuffer { diff --git a/twinkle-core/src/test/java/org/codejive/twinkle/core/text/TestLine.java b/twinkle-core/src/test/java/org/codejive/twinkle/core/text/TestLine.java index 7c38816..f75a83d 100644 --- a/twinkle-core/src/test/java/org/codejive/twinkle/core/text/TestLine.java +++ b/twinkle-core/src/test/java/org/codejive/twinkle/core/text/TestLine.java @@ -4,7 +4,6 @@ import org.codejive.twinkle.ansi.Ansi; import org.codejive.twinkle.ansi.Style; -import org.codejive.twinkle.core.widget.Buffer; import org.codejive.twinkle.util.SequenceIterator; import org.codejive.twinkle.util.StyledIterator; import org.junit.jupiter.api.Test; diff --git a/twinkle-core/src/test/java/org/codejive/twinkle/core/text/TestSpan.java b/twinkle-core/src/test/java/org/codejive/twinkle/core/text/TestSpan.java index 6d42949..e951de6 100644 --- a/twinkle-core/src/test/java/org/codejive/twinkle/core/text/TestSpan.java +++ b/twinkle-core/src/test/java/org/codejive/twinkle/core/text/TestSpan.java @@ -4,7 +4,6 @@ import org.codejive.twinkle.ansi.Ansi; import org.codejive.twinkle.ansi.Style; -import org.codejive.twinkle.core.widget.Buffer; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; diff --git a/twinkle-core/src/test/java/org/codejive/twinkle/core/text/TestText.java b/twinkle-core/src/test/java/org/codejive/twinkle/core/text/TestText.java index d8d98a9..c5e4e4e 100644 --- a/twinkle-core/src/test/java/org/codejive/twinkle/core/text/TestText.java +++ b/twinkle-core/src/test/java/org/codejive/twinkle/core/text/TestText.java @@ -3,7 +3,6 @@ import static org.assertj.core.api.Assertions.assertThat; import org.codejive.twinkle.ansi.Style; -import org.codejive.twinkle.core.widget.Buffer; import org.codejive.twinkle.util.SequenceIterator; import org.codejive.twinkle.util.StyledIterator; import org.junit.jupiter.api.Test; From d25a826d63153442716cda10eb491defc7ee1c6c Mon Sep 17 00:00:00 2001 From: Tako Schotanus Date: Mon, 5 Jan 2026 17:32:58 +0100 Subject: [PATCH 05/11] chore: added factory methods to `StyledIterator` --- .../codejive/twinkle/util/StyledIterator.java | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/twinkle-ansi/src/main/java/org/codejive/twinkle/util/StyledIterator.java b/twinkle-ansi/src/main/java/org/codejive/twinkle/util/StyledIterator.java index 0c3e52a..5698268 100644 --- a/twinkle-ansi/src/main/java/org/codejive/twinkle/util/StyledIterator.java +++ b/twinkle-ansi/src/main/java/org/codejive/twinkle/util/StyledIterator.java @@ -1,5 +1,6 @@ package org.codejive.twinkle.util; +import java.io.Reader; import java.util.NoSuchElementException; import org.codejive.twinkle.ansi.Ansi; import org.codejive.twinkle.ansi.Style; @@ -110,4 +111,28 @@ private void primeNext() { exhausted = true; primed = true; } + + public static StyledIterator of(CharSequence text) { + return of(text, Style.F_UNSTYLED); + } + + public static StyledIterator of(Reader input) { + return of(input, Style.F_UNSTYLED); + } + + public static StyledIterator of(SequenceIterator iter) { + return of(iter, Style.F_UNSTYLED); + } + + public static StyledIterator of(CharSequence text, long currentStyleState) { + return of(SequenceIterator.of(text), currentStyleState); + } + + public static StyledIterator of(Reader input, long currentStyleState) { + return of(SequenceIterator.of(input), currentStyleState); + } + + public static StyledIterator of(SequenceIterator iter, long currentStyleState) { + return new StyledIterator(iter, currentStyleState); + } } From f9f08c0892031cfa9687684350f7880e5569df6e Mon Sep 17 00:00:00 2001 From: Tako Schotanus Date: Mon, 5 Jan 2026 17:34:19 +0100 Subject: [PATCH 06/11] refactor: now using StyledIterators for all strings --- .../twinkle/core/text/LineBuffer.java | 35 +++++++++---------- 1 file changed, 17 insertions(+), 18 deletions(-) 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 index 75f513d..882163e 100644 --- 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 @@ -197,24 +197,7 @@ private void setCharAt_(int index, long styleState, @NonNull CharSequence graphe @Override public int putStringAt(int index, long styleState, @NonNull CharSequence str) { - if (outside(index, str.length())) { - return str.length(); - } - // TODO this code can be optimized by avoiding calculating codepointCount - // and simply looping until the end of the char sequence is reached - int cpsCount = codepointCount(str); - int minIndex = 0; - int maxIndex = cpBuffer.length; - int startIndex = Math.max(index, minIndex); - int strStart = Math.max(startIndex - index, 0); - int endIndex = Math.min(index + cpsCount, maxIndex); - int len = endIndex - startIndex; - for (int i = 0; i < len; ) { - int cp = codepointAt(str, strStart + i); - setCharAt_(startIndex + i, styleState, cp); - i += Character.charCount(cp); - } - return cpsCount; + return putStringAt(index, StyledIterator.of(str, styleState)); } @Override @@ -228,8 +211,13 @@ public int putStringAt(int index, @NonNull StyledIterator iter) { long style = iter.next(); int cp = iter.next(); if (cp == '\n') { + // We only deal with single lines here, so stop at newline break; } + if (iter.width() == 0) { + // We shouldn't be getting any of these from a StyledIterator, but just in case... + continue; + } if (cnt < len) { if (iter.isComplex()) { setCharAt_(startIndex + cnt, style, iter.sequence()); @@ -238,10 +226,21 @@ public int putStringAt(int index, @NonNull StyledIterator iter) { } } 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; } + private void setSkipAt(int index) { + cpBuffer[index] = -1; + graphemeBuffer[index] = null; + styleBuffer[index] = -1; + } + @Override public @NonNull CodepointBuffer subSequence(int start, int end) { if (start < 0 || end > length() || start > end) { From fdb28d1810b7d034733323725bb59dc50fe736c9 Mon Sep 17 00:00:00 2001 From: Tako Schotanus Date: Mon, 5 Jan 2026 18:04:15 +0100 Subject: [PATCH 07/11] feat: added style state `F_UNKNOWN` This will cause a style reset to be inserted if we change from an unknown state to a new state --- .../src/main/java/org/codejive/twinkle/ansi/Style.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/twinkle-ansi/src/main/java/org/codejive/twinkle/ansi/Style.java b/twinkle-ansi/src/main/java/org/codejive/twinkle/ansi/Style.java index c28c5e4..17e5342 100644 --- a/twinkle-ansi/src/main/java/org/codejive/twinkle/ansi/Style.java +++ b/twinkle-ansi/src/main/java/org/codejive/twinkle/ansi/Style.java @@ -19,6 +19,7 @@ public class Style implements Printable { private static final long IDX_CROSSEDOUT = 7; // private static final long DOUBLEUNDERLINE = 8; + public static final long F_UNKNOWN = -1L; public static final long F_UNSTYLED = 0L; public static final long F_BOLD = 1 << IDX_BOLD; public static final long F_FAINT = 1 << IDX_FAINT; @@ -509,6 +510,13 @@ public String toString() { @Override public @NonNull Appendable toAnsi(Appendable appendable, long currentStyleState) throws IOException { + if (state == F_UNKNOWN) { + // Do nothing, we keep the current state + return appendable; + } + if (currentStyleState == F_UNKNOWN) { + appendable.append(Ansi.STYLE_RESET); + } List styles = new ArrayList<>(); if ((currentStyleState & (F_BOLD | F_FAINT)) != (state & (F_BOLD | F_FAINT))) { // First we switch to NORMAL to clear both BOLD and FAINT From b43715aa995aa9fedf154094a70e07f56fc94101 Mon Sep 17 00:00:00 2001 From: Tako Schotanus Date: Mon, 5 Jan 2026 18:06:06 +0100 Subject: [PATCH 08/11] refactor: renamed `CodepointBuffer` to `LineBufferImpl` --- .../codejive/twinkle/core/text/LineBuffer.java | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) 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 index 882163e..7e215b9 100644 --- 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 @@ -54,9 +54,9 @@ default int putStringAt(int index, @NonNull Style style, @NonNull CharSequence s @NonNull LineBuffer resize(int newSize); LineBuffer EMPTY = - new CodepointBuffer(0) { + new LineBufferImpl(0) { @Override - public @NonNull CodepointBuffer resize(int newSize) { + public @NonNull LineBufferImpl resize(int newSize) { if (newSize != 0) { throw new UnsupportedOperationException("Cannot resize EMPTY"); } @@ -65,22 +65,22 @@ default int putStringAt(int index, @NonNull Style style, @NonNull CharSequence s }; static @NonNull LineBuffer of(int width) { - return new CodepointBuffer(width); + return new LineBufferImpl(width); } } -class CodepointBuffer implements LineBuffer { +class LineBufferImpl implements LineBuffer { protected int[] cpBuffer; protected String[] graphemeBuffer; protected long[] styleBuffer; - public CodepointBuffer(int size) { + public LineBufferImpl(int size) { cpBuffer = new int[size]; graphemeBuffer = new String[size]; styleBuffer = new long[size]; } - protected CodepointBuffer(int[] cpBuffer, String[] graphemeBuffer, long[] styleBuffer) { + 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"); @@ -242,7 +242,7 @@ private void setSkipAt(int index) { } @Override - public @NonNull CodepointBuffer subSequence(int start, int end) { + public @NonNull LineBufferImpl subSequence(int start, int end) { if (start < 0 || end > length() || start > end) { throw new IndexOutOfBoundsException( "Invalid subsequence range: " + start + " to " + end); @@ -254,11 +254,11 @@ private void setSkipAt(int index) { System.arraycopy(cpBuffer, start, subCpBuffer, 0, subLength); System.arraycopy(graphemeBuffer, start, subGraphemeBuffer, 0, subLength); System.arraycopy(styleBuffer, start, subStyleBuffer, 0, subLength); - return new CodepointBuffer(subCpBuffer, subGraphemeBuffer, subStyleBuffer); + return new LineBufferImpl(subCpBuffer, subGraphemeBuffer, subStyleBuffer); } @Override - public @NonNull CodepointBuffer resize(int newSize) { + public @NonNull LineBufferImpl resize(int newSize) { if (newSize == cpBuffer.length) { return this; } From 2d52f5511a215bb570dfafd4e9ffec5e92c849a9 Mon Sep 17 00:00:00 2001 From: Tako Schotanus Date: Mon, 5 Jan 2026 18:06:34 +0100 Subject: [PATCH 09/11] refactor: made mode `Printable` methods default --- .../org/codejive/twinkle/util/Printable.java | 17 ++++++++++++--- .../codejive/twinkle/core/text/Buffer.java | 21 ------------------- .../twinkle/core/text/LineBuffer.java | 20 ------------------ 3 files changed, 14 insertions(+), 44 deletions(-) diff --git a/twinkle-ansi/src/main/java/org/codejive/twinkle/util/Printable.java b/twinkle-ansi/src/main/java/org/codejive/twinkle/util/Printable.java index b32a588..9780e4e 100644 --- a/twinkle-ansi/src/main/java/org/codejive/twinkle/util/Printable.java +++ b/twinkle-ansi/src/main/java/org/codejive/twinkle/util/Printable.java @@ -11,7 +11,9 @@ public interface Printable { * * @return The ANSI string representation of the object. */ - @NonNull String toAnsiString(); + default @NonNull String toAnsiString() { + return toAnsiString(Style.F_UNKNOWN); + } /** * Outputs the object as an ANSI string, including ANSI escape codes for styles. This method @@ -20,7 +22,9 @@ public interface Printable { * @param appendable The Appendable to write the ANSI output to. * @return The Appendable passed as parameter. */ - @NonNull Appendable toAnsi(Appendable appendable) throws IOException; + default @NonNull Appendable toAnsi(Appendable appendable) throws IOException { + return toAnsi(appendable, Style.F_UNKNOWN); + } /** * Converts the object to an ANSI string, including ANSI escape codes for styles. This method @@ -56,7 +60,14 @@ public interface Printable { * @param currentStyleState The current style to start with. * @return The ANSI string representation of the object. */ - @NonNull String toAnsiString(long currentStyleState); + default @NonNull String toAnsiString(long currentStyleState) { + StringBuilder sb = new StringBuilder(); + try { + return toAnsi(sb, currentStyleState).toString(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } /** * Outputs the object as an ANSI string, including ANSI escape codes for styles. This method 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 8252aef..c2af447 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 @@ -3,7 +3,6 @@ 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; import org.codejive.twinkle.core.util.Rect; import org.codejive.twinkle.core.util.Size; @@ -238,26 +237,6 @@ public String toString() { return sb.toString(); } - @Override - public @NonNull String toAnsiString() { - // Assuming only single-width characters for capacity estimation - // plus 20 extra for escape codes and newline - int initialCapacity = (size().width() + 20) * size().height(); - StringBuilder sb = new StringBuilder(initialCapacity); - sb.append(Ansi.STYLE_RESET); - try { - return toAnsi(sb, Style.F_UNSTYLED).toString(); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - @Override - public @NonNull Appendable toAnsi(Appendable appendable) throws IOException { - appendable.append(Ansi.STYLE_RESET); - return toAnsi(appendable, Style.F_UNSTYLED); - } - @Override public @NonNull String toAnsiString(long currentStyleState) { // Assuming only single-width characters for capacity estimation 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 index 7e215b9..849b97f 100644 --- 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 @@ -1,7 +1,6 @@ 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; @@ -331,25 +330,6 @@ private boolean outside(int index, int length) { return sb.toString(); } - @Override - public @NonNull String toAnsiString() { - // 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).toString(); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - @Override - public @NonNull Appendable toAnsi(Appendable appendable) throws IOException { - appendable.append(Ansi.STYLE_RESET); - return toAnsi(appendable, Style.UNSTYLED.state()); - } - /** * Converts the buffer to an ANSI string, including ANSI escape codes for styles and colors. * This method takes into account the provided current style to generate a result that is as From 12363a9d61fe6172b8a53a47b97e27df9c64676c Mon Sep 17 00:00:00 2001 From: Tako Schotanus Date: Mon, 5 Jan 2026 21:48:30 +0100 Subject: [PATCH 10/11] refactor: `Text`, `Line` and `Span` are now `Printable` Instead of `Renderable`. This marks a shift away from using `Buffer` for rendering to preferring regular strings. --- .../java/org/codejive/twinkle/ansi/Style.java | 5 ++ .../codejive/twinkle/util/StyledIterator.java | 14 +++--- .../org/codejive/twinkle/core/text/Line.java | 32 ++++++++++--- .../twinkle/core/text/LineBuffer.java | 2 +- .../org/codejive/twinkle/core/text/Span.java | 46 +++++++++++++------ .../org/codejive/twinkle/core/text/Text.java | 33 ++++++++++--- .../org/codejive/twinkle/widgets/Framed.java | 4 +- .../codejive/twinkle/core/text/TestLine.java | 22 +++------ .../codejive/twinkle/core/text/TestSpan.java | 6 +-- .../codejive/twinkle/core/text/TestText.java | 29 +++++------- 10 files changed, 121 insertions(+), 72 deletions(-) diff --git a/twinkle-ansi/src/main/java/org/codejive/twinkle/ansi/Style.java b/twinkle-ansi/src/main/java/org/codejive/twinkle/ansi/Style.java index 17e5342..1d8a044 100644 --- a/twinkle-ansi/src/main/java/org/codejive/twinkle/ansi/Style.java +++ b/twinkle-ansi/src/main/java/org/codejive/twinkle/ansi/Style.java @@ -468,6 +468,10 @@ public int hashCode() { public String toString() { StringBuilder sb = new StringBuilder(); sb.append("StyleImpl{"); + if (state == F_UNKNOWN) { + sb.append("UNKNOWN}"); + return sb.toString(); + } if (isBold()) sb.append("bold, "); if (isFaint()) sb.append("faint, "); if (isItalic()) sb.append("italic, "); @@ -516,6 +520,7 @@ public String toString() { } if (currentStyleState == F_UNKNOWN) { appendable.append(Ansi.STYLE_RESET); + currentStyleState = F_UNSTYLED; } List styles = new ArrayList<>(); if ((currentStyleState & (F_BOLD | F_FAINT)) != (state & (F_BOLD | F_FAINT))) { diff --git a/twinkle-ansi/src/main/java/org/codejive/twinkle/util/StyledIterator.java b/twinkle-ansi/src/main/java/org/codejive/twinkle/util/StyledIterator.java index 5698268..277ad87 100644 --- a/twinkle-ansi/src/main/java/org/codejive/twinkle/util/StyledIterator.java +++ b/twinkle-ansi/src/main/java/org/codejive/twinkle/util/StyledIterator.java @@ -17,11 +17,11 @@ public class StyledIterator implements SequenceIterator { private boolean exhausted = false; /** - * Creates a StyledIterator that wraps the given SequenceIterator and starts with an unstyled - * initial state. + * Creates a StyledIterator that wraps the given SequenceIterator and starts with an unknown + * initial style state. */ - public StyledIterator(SequenceIterator delegate) { - this(delegate, Style.F_UNSTYLED); + protected StyledIterator(SequenceIterator delegate) { + this(delegate, Style.F_UNKNOWN); } /** @@ -113,15 +113,15 @@ private void primeNext() { } public static StyledIterator of(CharSequence text) { - return of(text, Style.F_UNSTYLED); + return of(text, Style.F_UNKNOWN); } public static StyledIterator of(Reader input) { - return of(input, Style.F_UNSTYLED); + return of(input, Style.F_UNKNOWN); } public static StyledIterator of(SequenceIterator iter) { - return of(iter, Style.F_UNSTYLED); + return of(iter, Style.F_UNKNOWN); } public static StyledIterator of(CharSequence text, long currentStyleState) { diff --git a/twinkle-core/src/main/java/org/codejive/twinkle/core/text/Line.java b/twinkle-core/src/main/java/org/codejive/twinkle/core/text/Line.java index d83ca54..baee037 100644 --- a/twinkle-core/src/main/java/org/codejive/twinkle/core/text/Line.java +++ b/twinkle-core/src/main/java/org/codejive/twinkle/core/text/Line.java @@ -1,15 +1,21 @@ package org.codejive.twinkle.core.text; +import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.List; import org.codejive.twinkle.ansi.Style; -import org.codejive.twinkle.core.widget.Renderable; +import org.codejive.twinkle.util.Printable; import org.codejive.twinkle.util.StyledIterator; +import org.jspecify.annotations.NonNull; -public class Line implements Renderable { +public class Line implements Printable { private final List spans; + public List spans() { + return Collections.unmodifiableList(spans); + } + public static Line of(String text) { return new Line((Span.of(text))); } @@ -34,7 +40,7 @@ protected Line(Span... spans) { public static Line of(StyledIterator iter) { List spans = new ArrayList<>(); StringBuilder sb = new StringBuilder(); - long currentStyleState = -1; + long currentStyleState = Style.F_UNKNOWN; while (iter.hasNext()) { long cp = iter.next(); if (cp == '\n') { @@ -56,11 +62,23 @@ public static Line of(StyledIterator iter) { } @Override - public void render(Canvas canvas) { - int x = 0; + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("Line{spans=["); + for (Span span : spans) { + sb.append(span.text()).append(", "); + } + sb.append("]}"); + return sb.toString(); + } + + @Override + public @NonNull Appendable toAnsi(Appendable appendable, long currentStyleState) + throws IOException { for (Span span : spans) { - span.render(canvas.view(x, 0, span.length(), 1)); - x += span.length(); + span.toAnsi(appendable, currentStyleState); + currentStyleState = span.style().state(); } + return appendable; } } 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 index 849b97f..ac20834 100644 --- 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 @@ -207,7 +207,6 @@ public int putStringAt(int index, @NonNull StyledIterator iter) { int len = maxIndex - startIndex; int cnt = 0; while (iter.hasNext()) { - long style = iter.next(); int cp = iter.next(); if (cp == '\n') { // We only deal with single lines here, so stop at newline @@ -218,6 +217,7 @@ public int putStringAt(int index, @NonNull StyledIterator iter) { continue; } if (cnt < len) { + long style = iter.styleState(); if (iter.isComplex()) { setCharAt_(startIndex + cnt, style, iter.sequence()); } else { diff --git a/twinkle-core/src/main/java/org/codejive/twinkle/core/text/Span.java b/twinkle-core/src/main/java/org/codejive/twinkle/core/text/Span.java index 7c78598..cb4d770 100644 --- a/twinkle-core/src/main/java/org/codejive/twinkle/core/text/Span.java +++ b/twinkle-core/src/main/java/org/codejive/twinkle/core/text/Span.java @@ -1,37 +1,55 @@ package org.codejive.twinkle.core.text; +import java.io.IOException; import org.codejive.twinkle.ansi.Style; -import org.codejive.twinkle.core.widget.Renderable; +import org.codejive.twinkle.util.Printable; +import org.jspecify.annotations.NonNull; -public class Span implements Renderable { - private final String text; - private final long styleState; +public class Span implements Printable { + private final @NonNull String text; + private final Style style; private final int length; - public static Span of(String text) { - return new Span(text, Style.F_UNSTYLED); + public static Span of(@NonNull String text) { + return new Span(text, Style.of(Style.F_UNSTYLED)); } - public static Span of(String text, Style style) { - return new Span(text, style.state()); + public static Span of(@NonNull String text, Style style) { + return new Span(text, style); } - public static Span of(String text, long styleState) { - return new Span(text, styleState); + public static Span of(@NonNull String text, long styleState) { + return new Span(text, Style.of(styleState)); } - protected Span(String text, long styleState) { + protected Span(@NonNull String text, Style style) { this.text = text; - this.styleState = styleState; + this.style = style; this.length = text.codePointCount(0, text.length()); } + public @NonNull String text() { + return text; + } + public int length() { return length; } + public @NonNull Style style() { + return style; + } + + @Override + public String toString() { + return "Span{text='" + text + "', style=" + style + "}"; + } + @Override - public void render(Canvas canvas) { - canvas.putStringAt(0, 0, styleState, text); + public @NonNull Appendable toAnsi(Appendable appendable, long currentStyleState) + throws IOException { + style.toAnsi(appendable, currentStyleState); + appendable.append(text); + return appendable; } } diff --git a/twinkle-core/src/main/java/org/codejive/twinkle/core/text/Text.java b/twinkle-core/src/main/java/org/codejive/twinkle/core/text/Text.java index de16e22..a9db003 100644 --- a/twinkle-core/src/main/java/org/codejive/twinkle/core/text/Text.java +++ b/twinkle-core/src/main/java/org/codejive/twinkle/core/text/Text.java @@ -1,13 +1,15 @@ package org.codejive.twinkle.core.text; +import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.List; import org.codejive.twinkle.ansi.Style; -import org.codejive.twinkle.core.widget.Renderable; +import org.codejive.twinkle.util.Printable; import org.codejive.twinkle.util.StyledIterator; +import org.jspecify.annotations.NonNull; -public class Text implements Renderable { +public class Text implements Printable { private final List lines; public static Text of(String text) { @@ -40,11 +42,30 @@ protected Text(Line... lines) { } @Override - public void render(Canvas canvas) { - int y = 0; + public @NonNull String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("Text{lines=[\n"); for (Line line : lines) { - line.render(canvas.view(0, y, canvas.size().width(), 1)); - y++; + sb.append(" ").append(line.toString()).append(",\n"); } + sb.append("]}"); + return sb.toString(); + } + + @Override + public @NonNull Appendable toAnsi(Appendable appendable, long currentStyleState) + throws IOException { + boolean first = true; + for (Line line : lines) { + if (!first) { + appendable.append('\n'); + } + line.toAnsi(appendable, currentStyleState); + if (!line.spans().isEmpty()) { + currentStyleState = line.spans().get(line.spans().size() - 1).style().state(); + } + first = false; + } + return appendable; } } diff --git a/twinkle-core/src/main/java/org/codejive/twinkle/widgets/Framed.java b/twinkle-core/src/main/java/org/codejive/twinkle/widgets/Framed.java index 12cde3a..8a63be2 100644 --- a/twinkle-core/src/main/java/org/codejive/twinkle/widgets/Framed.java +++ b/twinkle-core/src/main/java/org/codejive/twinkle/widgets/Framed.java @@ -1,5 +1,6 @@ package org.codejive.twinkle.widgets; +import org.codejive.twinkle.ansi.Style; import org.codejive.twinkle.core.decor.SimpleBorderRenderer; import org.codejive.twinkle.core.text.Canvas; import org.codejive.twinkle.core.text.Line; @@ -56,7 +57,8 @@ public void render(Canvas canvas) { borderRenderer.render(canvas); } if (title != null) { - title.render(canvas.view(2, 0, canvas.size().width() - 4, 1)); + Canvas view = canvas.view(2, 0, canvas.size().width() - 4, 1); + view.putStringAt(0, 0, Style.F_UNKNOWN, title.toAnsiString()); } if (widget != null) { widget.render(canvas.view(Rect.of(canvas.size()).grow(-1, -1, -1, -1))); diff --git a/twinkle-core/src/test/java/org/codejive/twinkle/core/text/TestLine.java b/twinkle-core/src/test/java/org/codejive/twinkle/core/text/TestLine.java index f75a83d..651ca1a 100644 --- a/twinkle-core/src/test/java/org/codejive/twinkle/core/text/TestLine.java +++ b/twinkle-core/src/test/java/org/codejive/twinkle/core/text/TestLine.java @@ -4,7 +4,6 @@ import org.codejive.twinkle.ansi.Ansi; import org.codejive.twinkle.ansi.Style; -import org.codejive.twinkle.util.SequenceIterator; import org.codejive.twinkle.util.StyledIterator; import org.junit.jupiter.api.Test; @@ -12,21 +11,15 @@ public class TestLine { @Test public void testRenderSingleStyledSpan() { - Buffer p = Buffer.of(1, 1); - Line.of("A", Style.BOLD).render(p); - - assertThat(p.toString()).isEqualTo("A"); - assertThat(p.toAnsiString()).isEqualTo(Ansi.STYLE_RESET + Ansi.style(Ansi.BOLD) + "A"); + Line l = Line.of("A", Style.BOLD); + assertThat(l.toAnsiString()).isEqualTo(Ansi.STYLE_RESET + Ansi.style(Ansi.BOLD) + "A"); } @Test public void testRenderMultipleSpans() { - Buffer p = Buffer.of(3, 1); - Line.of(Span.of("A"), Span.of("B", Style.BOLD), Span.of("C")).render(p); - - assertThat(p.toString()).isEqualTo("ABC"); + Line l = Line.of(Span.of("A"), Span.of("B", Style.BOLD), Span.of("C")); - String ansi = p.toAnsiString(); + String ansi = l.toAnsiString(); assertThat(ansi) .isEqualTo( Ansi.STYLE_RESET @@ -39,9 +32,8 @@ public void testRenderMultipleSpans() { @Test public void testOfStyledIterator() { - StyledIterator iter = new StyledIterator(SequenceIterator.of("Line 1")); - Buffer p = Buffer.of(6, 1); - Line.of(iter).render(p); - assertThat(p.toString()).isEqualTo("Line 1"); + StyledIterator iter = StyledIterator.of("Line 1"); + Line l = Line.of(iter); + assertThat(l.toAnsiString()).isEqualTo("Line 1"); } } diff --git a/twinkle-core/src/test/java/org/codejive/twinkle/core/text/TestSpan.java b/twinkle-core/src/test/java/org/codejive/twinkle/core/text/TestSpan.java index e951de6..a86976e 100644 --- a/twinkle-core/src/test/java/org/codejive/twinkle/core/text/TestSpan.java +++ b/twinkle-core/src/test/java/org/codejive/twinkle/core/text/TestSpan.java @@ -31,9 +31,7 @@ public void testLengthZwjSequence() { @Test public void testSpansRender() { - Buffer p = Buffer.of(1, 1); - Span.of("A", Style.BOLD).render(p); - assertThat(p.toString()).isEqualTo("A"); - assertThat(p.toAnsiString()).isEqualTo(Ansi.STYLE_RESET + Ansi.style(Ansi.BOLD) + "A"); + Span s = Span.of("A", Style.BOLD); + assertThat(s.toAnsiString()).isEqualTo(Ansi.STYLE_RESET + Ansi.style(Ansi.BOLD) + "A"); } } diff --git a/twinkle-core/src/test/java/org/codejive/twinkle/core/text/TestText.java b/twinkle-core/src/test/java/org/codejive/twinkle/core/text/TestText.java index c5e4e4e..73e2de6 100644 --- a/twinkle-core/src/test/java/org/codejive/twinkle/core/text/TestText.java +++ b/twinkle-core/src/test/java/org/codejive/twinkle/core/text/TestText.java @@ -2,8 +2,8 @@ import static org.assertj.core.api.Assertions.assertThat; +import org.codejive.twinkle.ansi.Ansi; import org.codejive.twinkle.ansi.Style; -import org.codejive.twinkle.util.SequenceIterator; import org.codejive.twinkle.util.StyledIterator; import org.junit.jupiter.api.Test; @@ -11,26 +11,23 @@ public class TestText { @Test public void testOfSimpleString() { - Buffer buf = Buffer.of(11, 1); - Text.of("Hello World").render(buf); - assertThat(buf.toString()).isEqualTo("Hello World"); + Text t = Text.of("Hello World"); + assertThat(t.toAnsiString()).isEqualTo(Ansi.STYLE_RESET + "Hello World"); } @Test public void testOfStyledString() { Style style = Style.BOLD; - Buffer buf = Buffer.of(11, 1); - Text.of("Hello World", style).render(buf); - assertThat(buf.toAnsiString(Style.F_UNSTYLED)) + Text t = Text.of("Hello World", style); + assertThat(t.toAnsiString(Style.F_UNSTYLED)) .isEqualTo(style.toAnsiString() + "Hello World"); } @Test public void testOfStyleState() { Style style = Style.BOLD; - Buffer buf = Buffer.of(11, 1); - Text.of("Hello World", style.state()).render(buf); - assertThat(buf.toAnsiString(Style.F_UNSTYLED)) + Text t = Text.of("Hello World", style.state()); + assertThat(t.toAnsiString(Style.F_UNSTYLED)) .isEqualTo(style.toAnsiString() + "Hello World"); } @@ -38,16 +35,14 @@ public void testOfStyleState() { public void testOfLines() { Line line1 = Line.of("Line 1"); Line line2 = Line.of("Line 2"); - Buffer buf = Buffer.of(6, 2); - Text.of(line1, line2).render(buf); - assertThat(buf.toString()).isEqualTo("Line 1\nLine 2"); + Text t = Text.of(line1, line2); + assertThat(t.toAnsiString()).isEqualTo(Ansi.STYLE_RESET + "Line 1\nLine 2"); } @Test public void testOfStyledIterator() { - StyledIterator iter = new StyledIterator(SequenceIterator.of("Line 1\nLine 2")); - Buffer buf = Buffer.of(6, 2); - Text.of(iter).render(buf); - assertThat(buf.toString()).isEqualTo("Line 1\nLine 2"); + StyledIterator iter = StyledIterator.of("Line 1\nLine 2"); + Text t = Text.of(iter); + assertThat(t.toAnsiString()).isEqualTo("Line 1\nLine 2"); } } From 6751c03ffc6c5434b87b9fccfe6d6862217f043a Mon Sep 17 00:00:00 2001 From: Tako Schotanus Date: Mon, 5 Jan 2026 21:49:44 +0100 Subject: [PATCH 11/11] chore: added `Unicode` utility class --- .../java/org/codejive/twinkle/util/Unicode.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 twinkle-ansi/src/main/java/org/codejive/twinkle/util/Unicode.java diff --git a/twinkle-ansi/src/main/java/org/codejive/twinkle/util/Unicode.java b/twinkle-ansi/src/main/java/org/codejive/twinkle/util/Unicode.java new file mode 100644 index 0000000..32e9296 --- /dev/null +++ b/twinkle-ansi/src/main/java/org/codejive/twinkle/util/Unicode.java @@ -0,0 +1,13 @@ +package org.codejive.twinkle.util; + +public class Unicode { + public static int visibleWidth(CharSequence text) { + int width = 0; + SequenceIterator si = SequenceIterator.of(text); + while (si.hasNext()) { + si.next(); + width += si.width(); + } + return width; + } +}