ChatboxTextInput: support line wrapping

This commit is contained in:
Ron Young
2019-01-25 00:18:49 -06:00
committed by Max Weber
parent af9733f2e2
commit b7a4033712

View File

@@ -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<MouseEvent> getCharOffset = null;
// These are lambdas for atomic updates
private Predicate<MouseEvent> isInBounds = null;
private ToIntFunction<MouseEvent> 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<Line> 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()
{