640 lines
18 KiB
Java
640 lines
18 KiB
Java
package net.minecraft.client.gui.components;
|
|
|
|
import java.util.Objects;
|
|
import java.util.function.BiFunction;
|
|
import java.util.function.Consumer;
|
|
import java.util.function.Predicate;
|
|
import net.fabricmc.api.EnvType;
|
|
import net.fabricmc.api.Environment;
|
|
import net.minecraft.Util;
|
|
import net.minecraft.client.Minecraft;
|
|
import net.minecraft.client.gui.Font;
|
|
import net.minecraft.client.gui.GuiGraphics;
|
|
import net.minecraft.client.gui.narration.NarratedElementType;
|
|
import net.minecraft.client.gui.narration.NarrationElementOutput;
|
|
import net.minecraft.client.gui.screens.Screen;
|
|
import net.minecraft.client.renderer.RenderType;
|
|
import net.minecraft.client.sounds.SoundManager;
|
|
import net.minecraft.network.chat.Component;
|
|
import net.minecraft.network.chat.MutableComponent;
|
|
import net.minecraft.network.chat.Style;
|
|
import net.minecraft.resources.ResourceLocation;
|
|
import net.minecraft.util.FormattedCharSequence;
|
|
import net.minecraft.util.Mth;
|
|
import net.minecraft.util.StringUtil;
|
|
import org.jetbrains.annotations.Nullable;
|
|
|
|
@Environment(EnvType.CLIENT)
|
|
public class EditBox extends AbstractWidget {
|
|
private static final WidgetSprites SPRITES = new WidgetSprites(
|
|
ResourceLocation.withDefaultNamespace("widget/text_field"), ResourceLocation.withDefaultNamespace("widget/text_field_highlighted")
|
|
);
|
|
public static final int BACKWARDS = -1;
|
|
public static final int FORWARDS = 1;
|
|
private static final int CURSOR_INSERT_WIDTH = 1;
|
|
private static final int CURSOR_INSERT_COLOR = -3092272;
|
|
private static final String CURSOR_APPEND_CHARACTER = "_";
|
|
public static final int DEFAULT_TEXT_COLOR = 14737632;
|
|
private static final int CURSOR_BLINK_INTERVAL_MS = 300;
|
|
private final Font font;
|
|
/**
|
|
* Has the current text being edited on the textbox.
|
|
*/
|
|
private String value = "";
|
|
private int maxLength = 32;
|
|
private boolean bordered = true;
|
|
/**
|
|
* if true the textbox can lose focus by clicking elsewhere on the screen
|
|
*/
|
|
private boolean canLoseFocus = true;
|
|
/**
|
|
* If this value is true along with isFocused, keyTyped will process the keys.
|
|
*/
|
|
private boolean isEditable = true;
|
|
/**
|
|
* The current character index that should be used as start of the rendered text.
|
|
*/
|
|
private int displayPos;
|
|
private int cursorPos;
|
|
/**
|
|
* other selection position, maybe the same as the cursor
|
|
*/
|
|
private int highlightPos;
|
|
private int textColor = 14737632;
|
|
private int textColorUneditable = 7368816;
|
|
@Nullable
|
|
private String suggestion;
|
|
@Nullable
|
|
private Consumer<String> responder;
|
|
/**
|
|
* Called to check if the text is valid
|
|
*/
|
|
private Predicate<String> filter = Objects::nonNull;
|
|
private BiFunction<String, Integer, FormattedCharSequence> formatter = (string, integer) -> FormattedCharSequence.forward(string, Style.EMPTY);
|
|
@Nullable
|
|
private Component hint;
|
|
private long focusedTime = Util.getMillis();
|
|
|
|
public EditBox(Font font, int width, int height, Component message) {
|
|
this(font, 0, 0, width, height, message);
|
|
}
|
|
|
|
public EditBox(Font font, int x, int y, int width, int height, Component message) {
|
|
this(font, x, y, width, height, null, message);
|
|
}
|
|
|
|
public EditBox(Font font, int x, int y, int width, int height, @Nullable EditBox editBox, Component message) {
|
|
super(x, y, width, height, message);
|
|
this.font = font;
|
|
if (editBox != null) {
|
|
this.setValue(editBox.getValue());
|
|
}
|
|
}
|
|
|
|
public void setResponder(Consumer<String> responder) {
|
|
this.responder = responder;
|
|
}
|
|
|
|
public void setFormatter(BiFunction<String, Integer, FormattedCharSequence> textFormatter) {
|
|
this.formatter = textFormatter;
|
|
}
|
|
|
|
@Override
|
|
protected MutableComponent createNarrationMessage() {
|
|
Component component = this.getMessage();
|
|
return Component.translatable("gui.narrate.editBox", component, this.value);
|
|
}
|
|
|
|
/**
|
|
* Sets the text of the textbox, and moves the cursor to the end.
|
|
*/
|
|
public void setValue(String text) {
|
|
if (this.filter.test(text)) {
|
|
if (text.length() > this.maxLength) {
|
|
this.value = text.substring(0, this.maxLength);
|
|
} else {
|
|
this.value = text;
|
|
}
|
|
|
|
this.moveCursorToEnd(false);
|
|
this.setHighlightPos(this.cursorPos);
|
|
this.onValueChange(text);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the contents of the textbox
|
|
*/
|
|
public String getValue() {
|
|
return this.value;
|
|
}
|
|
|
|
/**
|
|
* Returns the text between the cursor and selectionEnd.
|
|
*/
|
|
public String getHighlighted() {
|
|
int i = Math.min(this.cursorPos, this.highlightPos);
|
|
int j = Math.max(this.cursorPos, this.highlightPos);
|
|
return this.value.substring(i, j);
|
|
}
|
|
|
|
public void setFilter(Predicate<String> validator) {
|
|
this.filter = validator;
|
|
}
|
|
|
|
/**
|
|
* Adds the given text after the cursor, or replaces the currently selected text if there is a selection.
|
|
*/
|
|
public void insertText(String textToWrite) {
|
|
int i = Math.min(this.cursorPos, this.highlightPos);
|
|
int j = Math.max(this.cursorPos, this.highlightPos);
|
|
int k = this.maxLength - this.value.length() - (i - j);
|
|
if (k > 0) {
|
|
String string = StringUtil.filterText(textToWrite);
|
|
int l = string.length();
|
|
if (k < l) {
|
|
if (Character.isHighSurrogate(string.charAt(k - 1))) {
|
|
k--;
|
|
}
|
|
|
|
string = string.substring(0, k);
|
|
l = k;
|
|
}
|
|
|
|
String string2 = new StringBuilder(this.value).replace(i, j, string).toString();
|
|
if (this.filter.test(string2)) {
|
|
this.value = string2;
|
|
this.setCursorPosition(i + l);
|
|
this.setHighlightPos(this.cursorPos);
|
|
this.onValueChange(this.value);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void onValueChange(String newText) {
|
|
if (this.responder != null) {
|
|
this.responder.accept(newText);
|
|
}
|
|
}
|
|
|
|
private void deleteText(int count) {
|
|
if (Screen.hasControlDown()) {
|
|
this.deleteWords(count);
|
|
} else {
|
|
this.deleteChars(count);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Deletes the given number of words from the current cursor's position, unless there is currently a selection, in which case the selection is deleted instead.
|
|
*/
|
|
public void deleteWords(int num) {
|
|
if (!this.value.isEmpty()) {
|
|
if (this.highlightPos != this.cursorPos) {
|
|
this.insertText("");
|
|
} else {
|
|
this.deleteCharsToPos(this.getWordPosition(num));
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Deletes the given number of characters from the current cursor's position, unless there is currently a selection, in which case the selection is deleted instead.
|
|
*/
|
|
public void deleteChars(int num) {
|
|
this.deleteCharsToPos(this.getCursorPos(num));
|
|
}
|
|
|
|
public void deleteCharsToPos(int num) {
|
|
if (!this.value.isEmpty()) {
|
|
if (this.highlightPos != this.cursorPos) {
|
|
this.insertText("");
|
|
} else {
|
|
int i = Math.min(num, this.cursorPos);
|
|
int j = Math.max(num, this.cursorPos);
|
|
if (i != j) {
|
|
String string = new StringBuilder(this.value).delete(i, j).toString();
|
|
if (this.filter.test(string)) {
|
|
this.value = string;
|
|
this.moveCursorTo(i, false);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets the starting index of the word at the specified number of words away from the cursor position.
|
|
*/
|
|
public int getWordPosition(int numWords) {
|
|
return this.getWordPosition(numWords, this.getCursorPosition());
|
|
}
|
|
|
|
/**
|
|
* Gets the starting index of the word at a distance of the specified number of words away from the given position.
|
|
*/
|
|
private int getWordPosition(int numWords, int pos) {
|
|
return this.getWordPosition(numWords, pos, true);
|
|
}
|
|
|
|
/**
|
|
* Like getNthWordFromPos (which wraps this), but adds option for skipping consecutive spaces
|
|
*/
|
|
private int getWordPosition(int numWords, int pos, boolean skipConsecutiveSpaces) {
|
|
int i = pos;
|
|
boolean bl = numWords < 0;
|
|
int j = Math.abs(numWords);
|
|
|
|
for (int k = 0; k < j; k++) {
|
|
if (!bl) {
|
|
int l = this.value.length();
|
|
i = this.value.indexOf(32, i);
|
|
if (i == -1) {
|
|
i = l;
|
|
} else {
|
|
while (skipConsecutiveSpaces && i < l && this.value.charAt(i) == ' ') {
|
|
i++;
|
|
}
|
|
}
|
|
} else {
|
|
while (skipConsecutiveSpaces && i > 0 && this.value.charAt(i - 1) == ' ') {
|
|
i--;
|
|
}
|
|
|
|
while (i > 0 && this.value.charAt(i - 1) != ' ') {
|
|
i--;
|
|
}
|
|
}
|
|
}
|
|
|
|
return i;
|
|
}
|
|
|
|
public void moveCursor(int delta, boolean select) {
|
|
this.moveCursorTo(this.getCursorPos(delta), select);
|
|
}
|
|
|
|
private int getCursorPos(int delta) {
|
|
return Util.offsetByCodepoints(this.value, this.cursorPos, delta);
|
|
}
|
|
|
|
public void moveCursorTo(int delta, boolean select) {
|
|
this.setCursorPosition(delta);
|
|
if (!select) {
|
|
this.setHighlightPos(this.cursorPos);
|
|
}
|
|
|
|
this.onValueChange(this.value);
|
|
}
|
|
|
|
public void setCursorPosition(int pos) {
|
|
this.cursorPos = Mth.clamp(pos, 0, this.value.length());
|
|
this.scrollTo(this.cursorPos);
|
|
}
|
|
|
|
public void moveCursorToStart(boolean select) {
|
|
this.moveCursorTo(0, select);
|
|
}
|
|
|
|
public void moveCursorToEnd(boolean select) {
|
|
this.moveCursorTo(this.value.length(), select);
|
|
}
|
|
|
|
@Override
|
|
public boolean keyPressed(int keyCode, int scanCode, int modifiers) {
|
|
if (this.isActive() && this.isFocused()) {
|
|
switch (keyCode) {
|
|
case 259:
|
|
if (this.isEditable) {
|
|
this.deleteText(-1);
|
|
}
|
|
|
|
return true;
|
|
case 260:
|
|
case 264:
|
|
case 265:
|
|
case 266:
|
|
case 267:
|
|
default:
|
|
if (Screen.isSelectAll(keyCode)) {
|
|
this.moveCursorToEnd(false);
|
|
this.setHighlightPos(0);
|
|
return true;
|
|
} else if (Screen.isCopy(keyCode)) {
|
|
Minecraft.getInstance().keyboardHandler.setClipboard(this.getHighlighted());
|
|
return true;
|
|
} else if (Screen.isPaste(keyCode)) {
|
|
if (this.isEditable()) {
|
|
this.insertText(Minecraft.getInstance().keyboardHandler.getClipboard());
|
|
}
|
|
|
|
return true;
|
|
} else {
|
|
if (Screen.isCut(keyCode)) {
|
|
Minecraft.getInstance().keyboardHandler.setClipboard(this.getHighlighted());
|
|
if (this.isEditable()) {
|
|
this.insertText("");
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
case 261:
|
|
if (this.isEditable) {
|
|
this.deleteText(1);
|
|
}
|
|
|
|
return true;
|
|
case 262:
|
|
if (Screen.hasControlDown()) {
|
|
this.moveCursorTo(this.getWordPosition(1), Screen.hasShiftDown());
|
|
} else {
|
|
this.moveCursor(1, Screen.hasShiftDown());
|
|
}
|
|
|
|
return true;
|
|
case 263:
|
|
if (Screen.hasControlDown()) {
|
|
this.moveCursorTo(this.getWordPosition(-1), Screen.hasShiftDown());
|
|
} else {
|
|
this.moveCursor(-1, Screen.hasShiftDown());
|
|
}
|
|
|
|
return true;
|
|
case 268:
|
|
this.moveCursorToStart(Screen.hasShiftDown());
|
|
return true;
|
|
case 269:
|
|
this.moveCursorToEnd(Screen.hasShiftDown());
|
|
return true;
|
|
}
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
public boolean canConsumeInput() {
|
|
return this.isActive() && this.isFocused() && this.isEditable();
|
|
}
|
|
|
|
@Override
|
|
public boolean charTyped(char codePoint, int modifiers) {
|
|
if (!this.canConsumeInput()) {
|
|
return false;
|
|
} else if (StringUtil.isAllowedChatCharacter(codePoint)) {
|
|
if (this.isEditable) {
|
|
this.insertText(Character.toString(codePoint));
|
|
}
|
|
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onClick(double mouseX, double mouseY) {
|
|
int i = Mth.floor(mouseX) - this.getX();
|
|
if (this.bordered) {
|
|
i -= 4;
|
|
}
|
|
|
|
String string = this.font.plainSubstrByWidth(this.value.substring(this.displayPos), this.getInnerWidth());
|
|
this.moveCursorTo(this.font.plainSubstrByWidth(string, i).length() + this.displayPos, Screen.hasShiftDown());
|
|
}
|
|
|
|
@Override
|
|
public void playDownSound(SoundManager handler) {
|
|
}
|
|
|
|
@Override
|
|
public void renderWidget(GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTick) {
|
|
if (this.isVisible()) {
|
|
if (this.isBordered()) {
|
|
ResourceLocation resourceLocation = SPRITES.get(this.isActive(), this.isFocused());
|
|
guiGraphics.blitSprite(RenderType::guiTextured, resourceLocation, this.getX(), this.getY(), this.getWidth(), this.getHeight());
|
|
}
|
|
|
|
int i = this.isEditable ? this.textColor : this.textColorUneditable;
|
|
int j = this.cursorPos - this.displayPos;
|
|
String string = this.font.plainSubstrByWidth(this.value.substring(this.displayPos), this.getInnerWidth());
|
|
boolean bl = j >= 0 && j <= string.length();
|
|
boolean bl2 = this.isFocused() && (Util.getMillis() - this.focusedTime) / 300L % 2L == 0L && bl;
|
|
int k = this.bordered ? this.getX() + 4 : this.getX();
|
|
int l = this.bordered ? this.getY() + (this.height - 8) / 2 : this.getY();
|
|
int m = k;
|
|
int n = Mth.clamp(this.highlightPos - this.displayPos, 0, string.length());
|
|
if (!string.isEmpty()) {
|
|
String string2 = bl ? string.substring(0, j) : string;
|
|
m = guiGraphics.drawString(this.font, (FormattedCharSequence)this.formatter.apply(string2, this.displayPos), k, l, i);
|
|
}
|
|
|
|
boolean bl3 = this.cursorPos < this.value.length() || this.value.length() >= this.getMaxLength();
|
|
int o = m;
|
|
if (!bl) {
|
|
o = j > 0 ? k + this.width : k;
|
|
} else if (bl3) {
|
|
o = m - 1;
|
|
m--;
|
|
}
|
|
|
|
if (!string.isEmpty() && bl && j < string.length()) {
|
|
guiGraphics.drawString(this.font, (FormattedCharSequence)this.formatter.apply(string.substring(j), this.cursorPos), m, l, i);
|
|
}
|
|
|
|
if (this.hint != null && string.isEmpty() && !this.isFocused()) {
|
|
guiGraphics.drawString(this.font, this.hint, m, l, i);
|
|
}
|
|
|
|
if (!bl3 && this.suggestion != null) {
|
|
guiGraphics.drawString(this.font, this.suggestion, o - 1, l, -8355712);
|
|
}
|
|
|
|
if (bl2) {
|
|
if (bl3) {
|
|
guiGraphics.fill(RenderType.guiOverlay(), o, l - 1, o + 1, l + 1 + 9, -3092272);
|
|
} else {
|
|
guiGraphics.drawString(this.font, "_", o, l, i);
|
|
}
|
|
}
|
|
|
|
if (n != j) {
|
|
int p = k + this.font.width(string.substring(0, n));
|
|
this.renderHighlight(guiGraphics, o, l - 1, p - 1, l + 1 + 9);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void renderHighlight(GuiGraphics guiGraphics, int minX, int minY, int maxX, int maxY) {
|
|
if (minX < maxX) {
|
|
int i = minX;
|
|
minX = maxX;
|
|
maxX = i;
|
|
}
|
|
|
|
if (minY < maxY) {
|
|
int i = minY;
|
|
minY = maxY;
|
|
maxY = i;
|
|
}
|
|
|
|
if (maxX > this.getX() + this.width) {
|
|
maxX = this.getX() + this.width;
|
|
}
|
|
|
|
if (minX > this.getX() + this.width) {
|
|
minX = this.getX() + this.width;
|
|
}
|
|
|
|
guiGraphics.fill(RenderType.guiTextHighlight(), minX, minY, maxX, maxY, -16776961);
|
|
}
|
|
|
|
/**
|
|
* Sets the maximum length for the text in this text box. If the current text is longer than this length, the current text will be trimmed.
|
|
*/
|
|
public void setMaxLength(int length) {
|
|
this.maxLength = length;
|
|
if (this.value.length() > length) {
|
|
this.value = this.value.substring(0, length);
|
|
this.onValueChange(this.value);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the maximum number of character that can be contained in this textbox.
|
|
*/
|
|
private int getMaxLength() {
|
|
return this.maxLength;
|
|
}
|
|
|
|
/**
|
|
* Returns the current position of the cursor.
|
|
*/
|
|
public int getCursorPosition() {
|
|
return this.cursorPos;
|
|
}
|
|
|
|
/**
|
|
* Gets whether the background and outline of this text box should be drawn (true if so).
|
|
*/
|
|
public boolean isBordered() {
|
|
return this.bordered;
|
|
}
|
|
|
|
/**
|
|
* Sets whether the background and outline of this text box should be drawn.
|
|
*/
|
|
public void setBordered(boolean enableBackgroundDrawing) {
|
|
this.bordered = enableBackgroundDrawing;
|
|
}
|
|
|
|
/**
|
|
* Sets the color to use when drawing this text box's text. A different color is used if this text box is disabled.
|
|
*/
|
|
public void setTextColor(int color) {
|
|
this.textColor = color;
|
|
}
|
|
|
|
/**
|
|
* Sets the color to use for text in this text box when this text box is disabled.
|
|
*/
|
|
public void setTextColorUneditable(int color) {
|
|
this.textColorUneditable = color;
|
|
}
|
|
|
|
@Override
|
|
public void setFocused(boolean focused) {
|
|
if (this.canLoseFocus || focused) {
|
|
super.setFocused(focused);
|
|
if (focused) {
|
|
this.focusedTime = Util.getMillis();
|
|
}
|
|
}
|
|
}
|
|
|
|
private boolean isEditable() {
|
|
return this.isEditable;
|
|
}
|
|
|
|
/**
|
|
* Sets whether this text box is enabled. Disabled text boxes cannot be typed in.
|
|
*/
|
|
public void setEditable(boolean enabled) {
|
|
this.isEditable = enabled;
|
|
}
|
|
|
|
/**
|
|
* Returns the width of the textbox depending on if background drawing is enabled.
|
|
*/
|
|
public int getInnerWidth() {
|
|
return this.isBordered() ? this.width - 8 : this.width;
|
|
}
|
|
|
|
/**
|
|
* Sets the position of the selection anchor (the selection anchor and the cursor position mark the edges of the selection). If the anchor is set beyond the bounds of the current text, it will be put back inside.
|
|
*/
|
|
public void setHighlightPos(int position) {
|
|
this.highlightPos = Mth.clamp(position, 0, this.value.length());
|
|
this.scrollTo(this.highlightPos);
|
|
}
|
|
|
|
private void scrollTo(int position) {
|
|
if (this.font != null) {
|
|
this.displayPos = Math.min(this.displayPos, this.value.length());
|
|
int i = this.getInnerWidth();
|
|
String string = this.font.plainSubstrByWidth(this.value.substring(this.displayPos), i);
|
|
int j = string.length() + this.displayPos;
|
|
if (position == this.displayPos) {
|
|
this.displayPos = this.displayPos - this.font.plainSubstrByWidth(this.value, i, true).length();
|
|
}
|
|
|
|
if (position > j) {
|
|
this.displayPos += position - j;
|
|
} else if (position <= this.displayPos) {
|
|
this.displayPos = this.displayPos - (this.displayPos - position);
|
|
}
|
|
|
|
this.displayPos = Mth.clamp(this.displayPos, 0, this.value.length());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sets whether this text box loses focus when something other than it is clicked.
|
|
*/
|
|
public void setCanLoseFocus(boolean canLoseFocus) {
|
|
this.canLoseFocus = canLoseFocus;
|
|
}
|
|
|
|
/**
|
|
* Returns {@code true} if this textbox is visible.
|
|
*/
|
|
public boolean isVisible() {
|
|
return this.visible;
|
|
}
|
|
|
|
/**
|
|
* Sets whether this textbox is visible.
|
|
*/
|
|
public void setVisible(boolean isVisible) {
|
|
this.visible = isVisible;
|
|
}
|
|
|
|
public void setSuggestion(@Nullable String suggestion) {
|
|
this.suggestion = suggestion;
|
|
}
|
|
|
|
public int getScreenX(int charNum) {
|
|
return charNum > this.value.length() ? this.getX() : this.getX() + this.font.width(this.value.substring(0, charNum));
|
|
}
|
|
|
|
@Override
|
|
public void updateWidgetNarration(NarrationElementOutput narrationElementOutput) {
|
|
narrationElementOutput.add(NarratedElementType.TITLE, this.createNarrationMessage());
|
|
}
|
|
|
|
public void setHint(Component hint) {
|
|
this.hint = hint;
|
|
}
|
|
}
|