diff --git a/runelite-client/src/main/java/net/runelite/client/game/chatbox/ChatboxTextInput.java b/runelite-client/src/main/java/net/runelite/client/game/chatbox/ChatboxTextInput.java index 3300002c4d..193e5667e9 100644 --- a/runelite-client/src/main/java/net/runelite/client/game/chatbox/ChatboxTextInput.java +++ b/runelite-client/src/main/java/net/runelite/client/game/chatbox/ChatboxTextInput.java @@ -24,7 +24,10 @@ */ package net.runelite.client.game.chatbox; +import com.google.common.base.Strings; +import com.google.common.primitives.Ints; import com.google.inject.Inject; +import java.awt.Point; import java.awt.Rectangle; import java.awt.Toolkit; import java.awt.datatransfer.DataFlavor; @@ -33,11 +36,15 @@ import java.awt.datatransfer.UnsupportedFlavorException; import java.awt.event.KeyEvent; import java.awt.event.MouseEvent; import java.io.IOException; +import java.util.ArrayList; +import java.util.List; import java.util.function.Consumer; import java.util.function.IntPredicate; import java.util.function.Predicate; import java.util.function.ToIntFunction; +import java.util.regex.Pattern; import javax.swing.SwingUtilities; +import lombok.AllArgsConstructor; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import net.runelite.api.FontID; @@ -57,6 +64,7 @@ import net.runelite.client.util.Text; public class ChatboxTextInput extends ChatboxInput implements KeyListener, MouseListener { private static final int CURSOR_FLASH_RATE_MILLIS = 1000; + private static final Pattern BREAK_MATCHER = Pattern.compile("[^a-zA-Z0-9']"); private final ChatboxPanelManager chatboxPanelManager; private final ClientThread clientThread; @@ -66,9 +74,20 @@ public class ChatboxTextInput extends ChatboxInput implements KeyListener, Mouse return i -> i >= 32 && i < 127; } + @AllArgsConstructor + private static class Line + { + private final int start; + private final int end; + private final String text; + } + @Getter private String prompt; + @Getter + private int lines; + private StringBuffer value = new StringBuffer(); @Getter @@ -98,9 +117,9 @@ public class ChatboxTextInput extends ChatboxInput implements KeyListener, Mouse @Getter private boolean built = false; - // This is a lambda so I can have atomic updates for it's captures - private ToIntFunction getCharOffset = null; + // These are lambdas for atomic updates private Predicate isInBounds = null; + private ToIntFunction getCharOffset = null; @Inject protected ChatboxTextInput(ChatboxPanelManager chatboxPanelManager, ClientThread clientThread) @@ -109,6 +128,16 @@ public class ChatboxTextInput extends ChatboxInput implements KeyListener, Mouse this.clientThread = clientThread; } + public ChatboxTextInput lines(int lines) + { + this.lines = lines; + if (built) + { + clientThread.invoke(this::update); + } + return this; + } + public ChatboxTextInput prompt(String prompt) { this.prompt = prompt; @@ -232,103 +261,209 @@ public class ChatboxTextInput extends ChatboxInput implements KeyListener, Mouse protected void buildEdit(int x, int y, int w, int h) { + final List editLines = new ArrayList<>(); + Widget container = chatboxPanelManager.getContainerWidget(); - String lt = Text.escapeJagex(value.substring(0, this.cursorStart)); - String mt = Text.escapeJagex(value.substring(this.cursorStart, this.cursorEnd)); - String rt = Text.escapeJagex(value.substring(this.cursorEnd)); - - Widget leftText = container.createChild(-1, WidgetType.TEXT); - Widget cursor = container.createChild(-1, WidgetType.RECTANGLE); - Widget middleText = container.createChild(-1, WidgetType.TEXT); - Widget rightText = container.createChild(-1, WidgetType.TEXT); - - leftText.setFontId(fontID); - FontTypeFace font = leftText.getFont(); + final Widget cursor = container.createChild(-1, WidgetType.RECTANGLE); + long start = System.currentTimeMillis(); + cursor.setOnTimerListener((JavaScriptCallback) ev -> + { + boolean on = (System.currentTimeMillis() - start) % CURSOR_FLASH_RATE_MILLIS > (CURSOR_FLASH_RATE_MILLIS / 2); + cursor.setOpacity(on ? 255 : 0); + }); + cursor.setTextColor(0xFFFFFF); + cursor.setHasListener(true); + cursor.setFilled(true); + cursor.setFontId(fontID); + FontTypeFace font = cursor.getFont(); if (h <= 0) { h = font.getBaseline(); } - int ltw = font.getTextWidth(lt); - int mtw = font.getTextWidth(mt); - int rtw = font.getTextWidth(rt); + final int oy = y; + final int ox = x; + final int oh = h; - int fullWidth = ltw + mtw + rtw; - - int ox = x; - if (w > 0) + int breakIndex = -1; + final StringBuilder sb = new StringBuilder(); + for (int i = 0; i < value.length(); i++) { - x += (w - fullWidth) / 2; - } - - int ltx = x; - int mtx = ltx + ltw; - int rtx = mtx + mtw; - - leftText.setText(lt); - leftText.setOriginalX(ltx); - leftText.setOriginalY(y); - leftText.setOriginalWidth(ltw); - leftText.setOriginalHeight(h); - leftText.revalidate(); - - if (!mt.isEmpty()) - { - cursor.setTextColor(0x113399); - } - else - { - cursor.setTextColor(0xFFFFFF); - long start = System.currentTimeMillis(); - cursor.setOnTimerListener((JavaScriptCallback) ev -> + int count = i - sb.length(); + final String c = value.charAt(i) + ""; + sb.append(c); + if (BREAK_MATCHER.matcher(c).matches()) { - boolean on = (System.currentTimeMillis() - start) % CURSOR_FLASH_RATE_MILLIS > (CURSOR_FLASH_RATE_MILLIS / 2); - cursor.setOpacity(on ? 255 : 0); - }); - cursor.setHasListener(true); + breakIndex = sb.length(); + } + + if (i == value.length() - 1) + { + Line line = new Line(count, count + sb.length() - 1, sb.toString()); + editLines.add(line); + break; + } + + if (font.getTextWidth(sb.toString() + value.charAt(i + 1)) < w) + { + continue; + } + + if (editLines.size() < this.lines - 1 || this.lines == 0) + { + if (breakIndex > 1) + { + String str = sb.substring(0, breakIndex); + Line line = new Line(count, count + str.length() - 1, str); + editLines.add(line); + + sb.replace(0, breakIndex, ""); + breakIndex = -1; + continue; + } + + Line line = new Line(count, count + sb.length() - 1, sb.toString()); + editLines.add(line); + sb.replace(0, sb.length(), ""); + } } - cursor.setFilled(true); - cursor.setOriginalX(mtx - 1); - cursor.setOriginalY(y); - cursor.setOriginalWidth(2 + mtw); - cursor.setOriginalHeight(h); - cursor.revalidate(); - middleText.setText(mt); - middleText.setFontId(fontID); - middleText.setOriginalX(mtx); - middleText.setOriginalY(y); - middleText.setOriginalWidth(mtw); - middleText.setOriginalHeight(h); - middleText.setTextColor(0xFFFFFF); - middleText.revalidate(); + Rectangle bounds = new Rectangle(container.getCanvasLocation().getX() + container.getWidth(), y, 0, editLines.size() * oh); + for (int i = 0; i < editLines.size() || i == 0; i++) + { + final Line line = editLines.size() > 0 ? editLines.get(i) : new Line(0, 0, ""); + final String text = line.text; + final int len = text.length(); - rightText.setText(rt); - rightText.setFontId(fontID); - rightText.setOriginalX(rtx); - rightText.setOriginalY(y); - rightText.setOriginalWidth(rtw); - rightText.setOriginalHeight(h); - rightText.revalidate(); + String lt = Text.escapeJagex(text); + String mt = ""; + String rt = ""; + + final boolean isStartLine = cursorOnLine(cursorStart, line.start, line.end) + || (cursorOnLine(cursorStart, line.start, line.end + 1) && i == editLines.size() - 1); + + final boolean isEndLine = cursorOnLine(cursorEnd, line.start, line.end); + + if (isStartLine || isEndLine || (cursorEnd > line.end && cursorStart < line.start)) + { + final int cIdx = Ints.constrainToRange(cursorStart - line.start, 0, len); + final int ceIdx = Ints.constrainToRange(cursorEnd - line.start, 0, len); + + lt = Text.escapeJagex(text.substring(0, cIdx)); + mt = Text.escapeJagex(text.substring(cIdx, ceIdx)); + rt = Text.escapeJagex(text.substring(ceIdx)); + } + + final int ltw = font.getTextWidth(lt); + final int mtw = font.getTextWidth(mt); + final int rtw = font.getTextWidth(rt); + final int fullWidth = ltw + mtw + rtw; + + int ltx = ox; + if (w > 0) + { + ltx += (w - fullWidth) / 2; + } + + final int mtx = ltx + ltw; + final int rtx = mtx + mtw; + + if (ltx < bounds.x) + { + bounds.setLocation(ltx, bounds.y); + } + + if (fullWidth > bounds.width) + { + bounds.setSize(fullWidth, bounds.height); + } + + if (editLines.size() == 0 || isStartLine) + { + cursor.setOriginalX(mtx - 1); + cursor.setOriginalY(y); + cursor.setOriginalWidth(2); + cursor.setOriginalHeight(h); + cursor.revalidate(); + } + + if (!Strings.isNullOrEmpty(lt)) + { + final Widget leftText = container.createChild(-1, WidgetType.TEXT); + leftText.setFontId(fontID); + leftText.setText(lt); + leftText.setOriginalX(ltx); + leftText.setOriginalY(y); + leftText.setOriginalWidth(ltw); + leftText.setOriginalHeight(h); + leftText.revalidate(); + } + + if (!Strings.isNullOrEmpty(mt)) + { + final Widget background = container.createChild(-1, WidgetType.RECTANGLE); + background.setTextColor(0x113399); + background.setFilled(true); + background.setOriginalX(mtx - 1); + background.setOriginalY(y); + background.setOriginalWidth(2 + mtw); + background.setOriginalHeight(h); + background.revalidate(); + + final Widget middleText = container.createChild(-1, WidgetType.TEXT); + middleText.setText(mt); + middleText.setFontId(fontID); + middleText.setOriginalX(mtx); + middleText.setOriginalY(y); + middleText.setOriginalWidth(mtw); + middleText.setOriginalHeight(h); + middleText.setTextColor(0xFFFFFF); + middleText.revalidate(); + } + + if (!Strings.isNullOrEmpty(rt)) + { + final Widget rightText = container.createChild(-1, WidgetType.TEXT); + rightText.setText(rt); + rightText.setFontId(fontID); + rightText.setOriginalX(rtx); + rightText.setOriginalY(y); + rightText.setOriginalWidth(rtw); + rightText.setOriginalHeight(h); + rightText.revalidate(); + } + + y += h; + } net.runelite.api.Point ccl = container.getCanvasLocation(); - int canvasX = ltx + ccl.getX(); - Rectangle bounds = new Rectangle(ccl.getX() + ox, ccl.getY() + y, w > 0 ? w : fullWidth, h); - String tsValue = value.toString(); - isInBounds = ev -> bounds.contains(ev.getPoint()); + isInBounds = ev -> bounds.contains(new Point(ev.getX() - ccl.getX(), ev.getY() - ccl.getY())); getCharOffset = ev -> { - if (fullWidth <= 0) + if (bounds.width <= 0) { return 0; } - int cx = ev.getX() - canvasX; + int cx = ev.getX() - ccl.getX() - ox; + int cy = ev.getY() - ccl.getY() - oy; - int charIndex = (tsValue.length() * cx) / fullWidth; + int currentLine = Ints.constrainToRange(cy / oh, 0, editLines.size() - 1); + + final Line line = editLines.get(currentLine); + final String tsValue = line.text; + int charIndex = tsValue.length(); + int fullWidth = font.getTextWidth(tsValue); + + int tx = ox; + if (w > 0) + { + tx += (w - fullWidth) / 2; + } + cx -= tx; // `i` is used to track max execution time incase there is a font with ligature width data that causes this to fail for (int i = tsValue.length(); i >= 0 && charIndex >= 0 && charIndex <= tsValue.length(); i--) @@ -353,19 +488,16 @@ public class ChatboxTextInput extends ChatboxInput implements KeyListener, Mouse break; } - if (charIndex < 0) - { - charIndex = 0; - } - if (charIndex > tsValue.length()) - { - charIndex = tsValue.length(); - } - - return charIndex; + charIndex = Ints.constrainToRange(charIndex, 0, tsValue.length()); + return line.start + charIndex; }; } + private boolean cursorOnLine(final int cursor, final int start, final int end) + { + return (cursor >= start) && (cursor <= end); + } + @Override protected void open() {