606 lines
21 KiB
Java
606 lines
21 KiB
Java
/*
|
|
* Decompiled with CFR 0.152.
|
|
*
|
|
* Could not load the following classes:
|
|
* org.jspecify.annotations.Nullable
|
|
*/
|
|
package net.minecraft.client.gui.components;
|
|
|
|
import com.mojang.blaze3d.platform.cursor.CursorTypes;
|
|
import java.util.ArrayList;
|
|
import java.util.List;
|
|
import java.util.Objects;
|
|
import java.util.function.Consumer;
|
|
import java.util.function.Predicate;
|
|
import net.minecraft.ChatFormatting;
|
|
import net.minecraft.client.Minecraft;
|
|
import net.minecraft.client.gui.Font;
|
|
import net.minecraft.client.gui.GuiGraphics;
|
|
import net.minecraft.client.gui.components.AbstractWidget;
|
|
import net.minecraft.client.gui.components.WidgetSprites;
|
|
import net.minecraft.client.gui.narration.NarratedElementType;
|
|
import net.minecraft.client.gui.narration.NarrationElementOutput;
|
|
import net.minecraft.client.input.CharacterEvent;
|
|
import net.minecraft.client.input.KeyEvent;
|
|
import net.minecraft.client.input.MouseButtonEvent;
|
|
import net.minecraft.client.renderer.RenderPipelines;
|
|
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.Identifier;
|
|
import net.minecraft.util.FormattedCharSequence;
|
|
import net.minecraft.util.Mth;
|
|
import net.minecraft.util.StringUtil;
|
|
import net.minecraft.util.Util;
|
|
import org.jspecify.annotations.Nullable;
|
|
|
|
public class EditBox
|
|
extends AbstractWidget {
|
|
private static final WidgetSprites SPRITES = new WidgetSprites(Identifier.withDefaultNamespace("widget/text_field"), Identifier.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 String CURSOR_APPEND_CHARACTER = "_";
|
|
public static final int DEFAULT_TEXT_COLOR = -2039584;
|
|
public static final Style DEFAULT_HINT_STYLE = Style.EMPTY.withColor(ChatFormatting.DARK_GRAY);
|
|
public static final Style SEARCH_HINT_STYLE = Style.EMPTY.applyFormats(ChatFormatting.GRAY, ChatFormatting.ITALIC);
|
|
private static final int CURSOR_BLINK_INTERVAL_MS = 300;
|
|
private final Font font;
|
|
private String value = "";
|
|
private int maxLength = 32;
|
|
private boolean bordered = true;
|
|
private boolean canLoseFocus = true;
|
|
private boolean isEditable = true;
|
|
private boolean centered = false;
|
|
private boolean textShadow = true;
|
|
private boolean invertHighlightedTextColor = true;
|
|
private int displayPos;
|
|
private int cursorPos;
|
|
private int highlightPos;
|
|
private int textColor = -2039584;
|
|
private int textColorUneditable = -9408400;
|
|
private @Nullable String suggestion;
|
|
private @Nullable Consumer<String> responder;
|
|
private Predicate<String> filter = Objects::nonNull;
|
|
private final List<TextFormatter> formatters = new ArrayList<TextFormatter>();
|
|
private @Nullable Component hint;
|
|
private long focusedTime = Util.getMillis();
|
|
private int textX;
|
|
private int textY;
|
|
|
|
public EditBox(Font font, int width, int height, Component narration) {
|
|
this(font, 0, 0, width, height, narration);
|
|
}
|
|
|
|
public EditBox(Font font, int x, int y, int width, int height, Component narration) {
|
|
this(font, x, y, width, height, null, narration);
|
|
}
|
|
|
|
public EditBox(Font font, int x, int y, int width, int height, @Nullable EditBox oldBox, Component narration) {
|
|
super(x, y, width, height, narration);
|
|
this.font = font;
|
|
if (oldBox != null) {
|
|
this.setValue(oldBox.getValue());
|
|
}
|
|
this.updateTextPosition();
|
|
}
|
|
|
|
public void setResponder(Consumer<String> responder) {
|
|
this.responder = responder;
|
|
}
|
|
|
|
public void addFormatter(TextFormatter formatter) {
|
|
this.formatters.add(formatter);
|
|
}
|
|
|
|
@Override
|
|
protected MutableComponent createNarrationMessage() {
|
|
Component message = this.getMessage();
|
|
return Component.translatable("gui.narrate.editBox", message, this.value);
|
|
}
|
|
|
|
public void setValue(String value) {
|
|
if (!this.filter.test(value)) {
|
|
return;
|
|
}
|
|
this.value = value.length() > this.maxLength ? value.substring(0, this.maxLength) : value;
|
|
this.moveCursorToEnd(false);
|
|
this.setHighlightPos(this.cursorPos);
|
|
this.onValueChange(value);
|
|
}
|
|
|
|
public String getValue() {
|
|
return this.value;
|
|
}
|
|
|
|
public String getHighlighted() {
|
|
int start = Math.min(this.cursorPos, this.highlightPos);
|
|
int end = Math.max(this.cursorPos, this.highlightPos);
|
|
return this.value.substring(start, end);
|
|
}
|
|
|
|
@Override
|
|
public void setX(int x) {
|
|
super.setX(x);
|
|
this.updateTextPosition();
|
|
}
|
|
|
|
@Override
|
|
public void setY(int y) {
|
|
super.setY(y);
|
|
this.updateTextPosition();
|
|
}
|
|
|
|
public void setFilter(Predicate<String> filter) {
|
|
this.filter = filter;
|
|
}
|
|
|
|
public void insertText(String input) {
|
|
String newValue;
|
|
int start = Math.min(this.cursorPos, this.highlightPos);
|
|
int end = Math.max(this.cursorPos, this.highlightPos);
|
|
int maxInsertionLength = this.maxLength - this.value.length() - (start - end);
|
|
if (maxInsertionLength <= 0) {
|
|
return;
|
|
}
|
|
String text = StringUtil.filterText(input);
|
|
int insertionLength = text.length();
|
|
if (maxInsertionLength < insertionLength) {
|
|
if (Character.isHighSurrogate(text.charAt(maxInsertionLength - 1))) {
|
|
--maxInsertionLength;
|
|
}
|
|
text = text.substring(0, maxInsertionLength);
|
|
insertionLength = maxInsertionLength;
|
|
}
|
|
if (!this.filter.test(newValue = new StringBuilder(this.value).replace(start, end, text).toString())) {
|
|
return;
|
|
}
|
|
this.value = newValue;
|
|
this.setCursorPosition(start + insertionLength);
|
|
this.setHighlightPos(this.cursorPos);
|
|
this.onValueChange(this.value);
|
|
}
|
|
|
|
private void onValueChange(String value) {
|
|
if (this.responder != null) {
|
|
this.responder.accept(value);
|
|
}
|
|
this.updateTextPosition();
|
|
}
|
|
|
|
private void deleteText(int dir, boolean wholeWord) {
|
|
if (wholeWord) {
|
|
this.deleteWords(dir);
|
|
} else {
|
|
this.deleteChars(dir);
|
|
}
|
|
}
|
|
|
|
public void deleteWords(int dir) {
|
|
if (this.value.isEmpty()) {
|
|
return;
|
|
}
|
|
if (this.highlightPos != this.cursorPos) {
|
|
this.insertText("");
|
|
return;
|
|
}
|
|
this.deleteCharsToPos(this.getWordPosition(dir));
|
|
}
|
|
|
|
public void deleteChars(int dir) {
|
|
this.deleteCharsToPos(this.getCursorPos(dir));
|
|
}
|
|
|
|
public void deleteCharsToPos(int pos) {
|
|
int end;
|
|
if (this.value.isEmpty()) {
|
|
return;
|
|
}
|
|
if (this.highlightPos != this.cursorPos) {
|
|
this.insertText("");
|
|
return;
|
|
}
|
|
int start = Math.min(pos, this.cursorPos);
|
|
if (start == (end = Math.max(pos, this.cursorPos))) {
|
|
return;
|
|
}
|
|
String newValue = new StringBuilder(this.value).delete(start, end).toString();
|
|
if (!this.filter.test(newValue)) {
|
|
return;
|
|
}
|
|
this.value = newValue;
|
|
this.moveCursorTo(start, false);
|
|
}
|
|
|
|
public int getWordPosition(int dir) {
|
|
return this.getWordPosition(dir, this.getCursorPosition());
|
|
}
|
|
|
|
private int getWordPosition(int dir, int from) {
|
|
return this.getWordPosition(dir, from, true);
|
|
}
|
|
|
|
private int getWordPosition(int dir, int from, boolean stripSpaces) {
|
|
int result = from;
|
|
boolean reverse = dir < 0;
|
|
int abs = Math.abs(dir);
|
|
for (int i = 0; i < abs; ++i) {
|
|
if (reverse) {
|
|
while (stripSpaces && result > 0 && this.value.charAt(result - 1) == ' ') {
|
|
--result;
|
|
}
|
|
while (result > 0 && this.value.charAt(result - 1) != ' ') {
|
|
--result;
|
|
}
|
|
continue;
|
|
}
|
|
int length = this.value.length();
|
|
if ((result = this.value.indexOf(32, result)) == -1) {
|
|
result = length;
|
|
continue;
|
|
}
|
|
while (stripSpaces && result < length && this.value.charAt(result) == ' ') {
|
|
++result;
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
public void moveCursor(int dir, boolean hasShiftDown) {
|
|
this.moveCursorTo(this.getCursorPos(dir), hasShiftDown);
|
|
}
|
|
|
|
private int getCursorPos(int dir) {
|
|
return Util.offsetByCodepoints(this.value, this.cursorPos, dir);
|
|
}
|
|
|
|
public void moveCursorTo(int dir, boolean extendSelection) {
|
|
this.setCursorPosition(dir);
|
|
if (!extendSelection) {
|
|
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 hasShiftDown) {
|
|
this.moveCursorTo(0, hasShiftDown);
|
|
}
|
|
|
|
public void moveCursorToEnd(boolean hasShiftDown) {
|
|
this.moveCursorTo(this.value.length(), hasShiftDown);
|
|
}
|
|
|
|
@Override
|
|
public boolean keyPressed(KeyEvent event) {
|
|
if (!this.isActive() || !this.isFocused()) {
|
|
return false;
|
|
}
|
|
switch (event.key()) {
|
|
case 263: {
|
|
if (event.hasControlDownWithQuirk()) {
|
|
this.moveCursorTo(this.getWordPosition(-1), event.hasShiftDown());
|
|
} else {
|
|
this.moveCursor(-1, event.hasShiftDown());
|
|
}
|
|
return true;
|
|
}
|
|
case 262: {
|
|
if (event.hasControlDownWithQuirk()) {
|
|
this.moveCursorTo(this.getWordPosition(1), event.hasShiftDown());
|
|
} else {
|
|
this.moveCursor(1, event.hasShiftDown());
|
|
}
|
|
return true;
|
|
}
|
|
case 259: {
|
|
if (this.isEditable) {
|
|
this.deleteText(-1, event.hasControlDownWithQuirk());
|
|
}
|
|
return true;
|
|
}
|
|
case 261: {
|
|
if (this.isEditable) {
|
|
this.deleteText(1, event.hasControlDownWithQuirk());
|
|
}
|
|
return true;
|
|
}
|
|
case 268: {
|
|
this.moveCursorToStart(event.hasShiftDown());
|
|
return true;
|
|
}
|
|
case 269: {
|
|
this.moveCursorToEnd(event.hasShiftDown());
|
|
return true;
|
|
}
|
|
}
|
|
if (event.isSelectAll()) {
|
|
this.moveCursorToEnd(false);
|
|
this.setHighlightPos(0);
|
|
return true;
|
|
}
|
|
if (event.isCopy()) {
|
|
Minecraft.getInstance().keyboardHandler.setClipboard(this.getHighlighted());
|
|
return true;
|
|
}
|
|
if (event.isPaste()) {
|
|
if (this.isEditable()) {
|
|
this.insertText(Minecraft.getInstance().keyboardHandler.getClipboard());
|
|
}
|
|
return true;
|
|
}
|
|
if (event.isCut()) {
|
|
Minecraft.getInstance().keyboardHandler.setClipboard(this.getHighlighted());
|
|
if (this.isEditable()) {
|
|
this.insertText("");
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
public boolean canConsumeInput() {
|
|
return this.isActive() && this.isFocused() && this.isEditable();
|
|
}
|
|
|
|
@Override
|
|
public boolean charTyped(CharacterEvent event) {
|
|
if (!this.canConsumeInput()) {
|
|
return false;
|
|
}
|
|
if (event.isAllowedChatCharacter()) {
|
|
if (this.isEditable) {
|
|
this.insertText(event.codepointAsString());
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private int findClickedPositionInText(MouseButtonEvent event) {
|
|
int positionInText = Math.min(Mth.floor(event.x()) - this.textX, this.getInnerWidth());
|
|
String displayed = this.value.substring(this.displayPos);
|
|
return this.displayPos + this.font.plainSubstrByWidth(displayed, positionInText).length();
|
|
}
|
|
|
|
private void selectWord(MouseButtonEvent event) {
|
|
int clickedPosition = this.findClickedPositionInText(event);
|
|
int wordStart = this.getWordPosition(-1, clickedPosition);
|
|
int wordEnd = this.getWordPosition(1, clickedPosition);
|
|
this.moveCursorTo(wordStart, false);
|
|
this.moveCursorTo(wordEnd, true);
|
|
}
|
|
|
|
@Override
|
|
public void onClick(MouseButtonEvent event, boolean doubleClick) {
|
|
if (doubleClick) {
|
|
this.selectWord(event);
|
|
} else {
|
|
this.moveCursorTo(this.findClickedPositionInText(event), event.hasShiftDown());
|
|
}
|
|
}
|
|
|
|
@Override
|
|
protected void onDrag(MouseButtonEvent event, double dx, double dy) {
|
|
this.moveCursorTo(this.findClickedPositionInText(event), true);
|
|
}
|
|
|
|
@Override
|
|
public void playDownSound(SoundManager soundManager) {
|
|
}
|
|
|
|
@Override
|
|
public void renderWidget(GuiGraphics graphics, int mouseX, int mouseY, float a) {
|
|
if (!this.isVisible()) {
|
|
return;
|
|
}
|
|
if (this.isBordered()) {
|
|
Identifier sprite = SPRITES.get(this.isActive(), this.isFocused());
|
|
graphics.blitSprite(RenderPipelines.GUI_TEXTURED, sprite, this.getX(), this.getY(), this.getWidth(), this.getHeight());
|
|
}
|
|
int color = this.isEditable ? this.textColor : this.textColorUneditable;
|
|
int relCursorPos = this.cursorPos - this.displayPos;
|
|
String displayed = this.font.plainSubstrByWidth(this.value.substring(this.displayPos), this.getInnerWidth());
|
|
boolean cursorOnScreen = relCursorPos >= 0 && relCursorPos <= displayed.length();
|
|
boolean showCursor = this.isFocused() && (Util.getMillis() - this.focusedTime) / 300L % 2L == 0L && cursorOnScreen;
|
|
int drawX = this.textX;
|
|
int relHighlightPos = Mth.clamp(this.highlightPos - this.displayPos, 0, displayed.length());
|
|
if (!displayed.isEmpty()) {
|
|
String half = cursorOnScreen ? displayed.substring(0, relCursorPos) : displayed;
|
|
FormattedCharSequence charSequence = this.applyFormat(half, this.displayPos);
|
|
graphics.drawString(this.font, charSequence, drawX, this.textY, color, this.textShadow);
|
|
drawX += this.font.width(charSequence) + 1;
|
|
}
|
|
boolean insert = this.cursorPos < this.value.length() || this.value.length() >= this.getMaxLength();
|
|
int cursorX = drawX;
|
|
if (!cursorOnScreen) {
|
|
cursorX = relCursorPos > 0 ? this.textX + this.width : this.textX;
|
|
} else if (insert) {
|
|
--cursorX;
|
|
--drawX;
|
|
}
|
|
if (!displayed.isEmpty() && cursorOnScreen && relCursorPos < displayed.length()) {
|
|
graphics.drawString(this.font, this.applyFormat(displayed.substring(relCursorPos), this.cursorPos), drawX, this.textY, color, this.textShadow);
|
|
}
|
|
if (this.hint != null && displayed.isEmpty() && !this.isFocused()) {
|
|
graphics.drawString(this.font, this.hint, drawX, this.textY, color);
|
|
}
|
|
if (!insert && this.suggestion != null) {
|
|
graphics.drawString(this.font, this.suggestion, cursorX - 1, this.textY, -8355712, this.textShadow);
|
|
}
|
|
if (relHighlightPos != relCursorPos) {
|
|
int highlightX = this.textX + this.font.width(displayed.substring(0, relHighlightPos));
|
|
graphics.textHighlight(Math.min(cursorX, this.getX() + this.width), this.textY - 1, Math.min(highlightX - 1, this.getX() + this.width), this.textY + 1 + this.font.lineHeight, this.invertHighlightedTextColor);
|
|
}
|
|
if (showCursor) {
|
|
if (insert) {
|
|
graphics.fill(cursorX, this.textY - 1, cursorX + 1, this.textY + 1 + this.font.lineHeight, color);
|
|
} else {
|
|
graphics.drawString(this.font, CURSOR_APPEND_CHARACTER, cursorX, this.textY, color, this.textShadow);
|
|
}
|
|
}
|
|
if (this.isHovered()) {
|
|
graphics.requestCursor(this.isEditable() ? CursorTypes.IBEAM : CursorTypes.NOT_ALLOWED);
|
|
}
|
|
}
|
|
|
|
private FormattedCharSequence applyFormat(String text, int offset) {
|
|
for (TextFormatter formatter : this.formatters) {
|
|
FormattedCharSequence formattedCharSequence = formatter.format(text, offset);
|
|
if (formattedCharSequence == null) continue;
|
|
return formattedCharSequence;
|
|
}
|
|
return FormattedCharSequence.forward(text, Style.EMPTY);
|
|
}
|
|
|
|
private void updateTextPosition() {
|
|
if (this.font == null) {
|
|
return;
|
|
}
|
|
String displayed = this.font.plainSubstrByWidth(this.value.substring(this.displayPos), this.getInnerWidth());
|
|
this.textX = this.getX() + (this.isCentered() ? (this.getWidth() - this.font.width(displayed)) / 2 : (this.bordered ? 4 : 0));
|
|
this.textY = this.bordered ? this.getY() + (this.height - 8) / 2 : this.getY();
|
|
}
|
|
|
|
public void setMaxLength(int maxLength) {
|
|
this.maxLength = maxLength;
|
|
if (this.value.length() > maxLength) {
|
|
this.value = this.value.substring(0, maxLength);
|
|
this.onValueChange(this.value);
|
|
}
|
|
}
|
|
|
|
private int getMaxLength() {
|
|
return this.maxLength;
|
|
}
|
|
|
|
public int getCursorPosition() {
|
|
return this.cursorPos;
|
|
}
|
|
|
|
public boolean isBordered() {
|
|
return this.bordered;
|
|
}
|
|
|
|
public void setBordered(boolean bordered) {
|
|
this.bordered = bordered;
|
|
this.updateTextPosition();
|
|
}
|
|
|
|
public void setTextColor(int textColor) {
|
|
this.textColor = textColor;
|
|
}
|
|
|
|
public void setTextColorUneditable(int textColorUneditable) {
|
|
this.textColorUneditable = textColorUneditable;
|
|
}
|
|
|
|
@Override
|
|
public void setFocused(boolean focused) {
|
|
if (!this.canLoseFocus && !focused) {
|
|
return;
|
|
}
|
|
super.setFocused(focused);
|
|
if (focused) {
|
|
this.focusedTime = Util.getMillis();
|
|
}
|
|
}
|
|
|
|
private boolean isEditable() {
|
|
return this.isEditable;
|
|
}
|
|
|
|
public void setEditable(boolean isEditable) {
|
|
this.isEditable = isEditable;
|
|
}
|
|
|
|
private boolean isCentered() {
|
|
return this.centered;
|
|
}
|
|
|
|
public void setCentered(boolean centered) {
|
|
this.centered = centered;
|
|
this.updateTextPosition();
|
|
}
|
|
|
|
public void setTextShadow(boolean textShadow) {
|
|
this.textShadow = textShadow;
|
|
}
|
|
|
|
public void setInvertHighlightedTextColor(boolean invertHighlightedTextColor) {
|
|
this.invertHighlightedTextColor = invertHighlightedTextColor;
|
|
}
|
|
|
|
public int getInnerWidth() {
|
|
return this.isBordered() ? this.width - 8 : this.width;
|
|
}
|
|
|
|
public void setHighlightPos(int pos) {
|
|
this.highlightPos = Mth.clamp(pos, 0, this.value.length());
|
|
this.scrollTo(this.highlightPos);
|
|
}
|
|
|
|
private void scrollTo(int pos) {
|
|
if (this.font == null) {
|
|
return;
|
|
}
|
|
this.displayPos = Math.min(this.displayPos, this.value.length());
|
|
int innerWidth = this.getInnerWidth();
|
|
String displayed = this.font.plainSubstrByWidth(this.value.substring(this.displayPos), innerWidth);
|
|
int lastPos = displayed.length() + this.displayPos;
|
|
if (pos == this.displayPos) {
|
|
this.displayPos -= this.font.plainSubstrByWidth(this.value, innerWidth, true).length();
|
|
}
|
|
if (pos > lastPos) {
|
|
this.displayPos += pos - lastPos;
|
|
} else if (pos <= this.displayPos) {
|
|
this.displayPos -= this.displayPos - pos;
|
|
}
|
|
this.displayPos = Mth.clamp(this.displayPos, 0, this.value.length());
|
|
}
|
|
|
|
public void setCanLoseFocus(boolean canLoseFocus) {
|
|
this.canLoseFocus = canLoseFocus;
|
|
}
|
|
|
|
public boolean isVisible() {
|
|
return this.visible;
|
|
}
|
|
|
|
public void setVisible(boolean visible) {
|
|
this.visible = visible;
|
|
}
|
|
|
|
public void setSuggestion(@Nullable String suggestion) {
|
|
this.suggestion = suggestion;
|
|
}
|
|
|
|
public int getScreenX(int charIndex) {
|
|
if (charIndex > this.value.length()) {
|
|
return this.getX();
|
|
}
|
|
return this.getX() + this.font.width(this.value.substring(0, charIndex));
|
|
}
|
|
|
|
@Override
|
|
public void updateWidgetNarration(NarrationElementOutput output) {
|
|
output.add(NarratedElementType.TITLE, (Component)this.createNarrationMessage());
|
|
}
|
|
|
|
public void setHint(Component hint) {
|
|
boolean hasNoStyle = hint.getStyle().equals(Style.EMPTY);
|
|
this.hint = hasNoStyle ? hint.copy().withStyle(DEFAULT_HINT_STYLE) : hint;
|
|
}
|
|
|
|
@FunctionalInterface
|
|
public static interface TextFormatter {
|
|
public @Nullable FormattedCharSequence format(String var1, int var2);
|
|
}
|
|
}
|
|
|