aboutsummaryrefslogtreecommitdiff
path: root/src/main/java
diff options
context:
space:
mode:
authorviciscat <51047087+viciscat@users.noreply.github.com>2025-07-26 23:31:43 +0200
committerGitHub <noreply@github.com>2025-07-26 17:31:43 -0400
commitf2b24066288fae2095ca92c166486d74b3b16ff1 (patch)
treeb8b23452b347245122d6689b0e44c7349a96a861 /src/main/java
parentfc8fd3425ce1c4d87aa8c494a9b16b4501fe0b19 (diff)
downloadSkyblocker-f2b24066288fae2095ca92c166486d74b3b16ff1.tar.gz
Skyblocker-f2b24066288fae2095ca92c166486d74b3b16ff1.tar.bz2
Skyblocker-f2b24066288fae2095ca92c166486d74b3b16ff1.zip
Rename item GUI (#1490)
* i need to change branch * done implementing this stuff * i am hilarious * better selecting and move text around * Update ARGBTextInput.java * fix NPE * small fix
Diffstat (limited to 'src/main/java')
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/custom/CustomItemNames.java45
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/custom/screen/name/ColorPopup.java133
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/custom/screen/name/CustomizeNameScreen.java464
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/custom/screen/name/visitor/BaseVisitor.java24
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/custom/screen/name/visitor/GetClickedPositionVisitor.java57
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/custom/screen/name/visitor/GetRenderWidthVisitor.java58
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/custom/screen/name/visitor/GetStyleVisitor.java46
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/custom/screen/name/visitor/InsertTextVisitor.java60
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/custom/screen/name/visitor/SetStyleVisitor.java57
-rw-r--r--src/main/java/de/hysky/skyblocker/utils/render/gui/ARGBTextInput.java34
-rw-r--r--src/main/java/de/hysky/skyblocker/utils/render/gui/ColorPickerWidget.java43
11 files changed, 995 insertions, 26 deletions
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/custom/CustomItemNames.java b/src/main/java/de/hysky/skyblocker/skyblock/item/custom/CustomItemNames.java
index 2cb7ca91..d1a6d4d5 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/item/custom/CustomItemNames.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/custom/CustomItemNames.java
@@ -5,15 +5,18 @@ import com.mojang.brigadier.CommandDispatcher;
import com.mojang.brigadier.arguments.StringArgumentType;
import de.hysky.skyblocker.annotations.Init;
import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.skyblock.item.custom.screen.name.CustomizeNameScreen;
import de.hysky.skyblocker.utils.Constants;
import de.hysky.skyblocker.utils.ItemUtils;
import de.hysky.skyblocker.utils.Utils;
+import de.hysky.skyblocker.utils.scheduler.Scheduler;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import net.fabricmc.fabric.api.client.command.v2.ClientCommandManager;
import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback;
import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource;
import net.minecraft.command.CommandRegistryAccess;
import net.minecraft.command.argument.TextArgumentType;
+import net.minecraft.item.ItemStack;
import net.minecraft.text.MutableText;
import net.minecraft.text.Style;
import net.minecraft.text.Text;
@@ -28,7 +31,7 @@ public class CustomItemNames {
dispatcher.register(ClientCommandManager.literal("skyblocker")
.then(ClientCommandManager.literal("custom")
.then(ClientCommandManager.literal("renameItem")
- .executes(context -> renameItem(context.getSource(), null))
+ .executes(context -> openScreen(context.getSource()))
.then(ClientCommandManager.argument("textComponent", TextArgumentType.text(registryAccess))
.executes(context -> renameItem(context.getSource(), context.getArgument("textComponent", Text.class))))
// greedy string will only consume the arg if the text component parsing fails.
@@ -36,24 +39,32 @@ public class CustomItemNames {
.executes(context -> renameItem(context.getSource(), Text.of(context.getArgument("basicText", String.class))))))));
}
+ private static int openScreen(FabricClientCommandSource source) {
+ if (!Utils.isOnSkyblock()) {
+ source.sendError(Constants.PREFIX.get().append(Text.translatable("skyblocker.customItemNames.notOnSkyblock")));
+ return 0;
+ }
+ ItemStack handStack = source.getPlayer().getMainHandStack();
+ if (handStack.isEmpty()) {
+ source.sendError(Constants.PREFIX.get().append(Text.translatable("skyblocker.customItemNames.noItem")));
+ return 0;
+ }
+ if (ItemUtils.getItemUuid(handStack).isEmpty()) {
+ source.sendError(Constants.PREFIX.get().append(Text.translatable("skyblocker.customItemNames.noItemUuid")));
+ return 0;
+ }
+ Scheduler.queueOpenScreen(new CustomizeNameScreen(handStack));
+ return Command.SINGLE_SUCCESS;
+ }
+
@SuppressWarnings("SameReturnValue")
private static int renameItem(FabricClientCommandSource source, Text text) {
if (Utils.isOnSkyblock()) {
String itemUuid = ItemUtils.getItemUuid(source.getPlayer().getMainHandStack());
if (!itemUuid.isEmpty()) {
- Object2ObjectOpenHashMap<String, Text> customItemNames = SkyblockerConfigManager.get().general.customItemNames;
-
- if (text == null) {
- if (customItemNames.containsKey(itemUuid)) {
- //Remove custom item name when the text argument isn't passed
- customItemNames.remove(itemUuid);
- SkyblockerConfigManager.save();
- source.sendFeedback(Constants.PREFIX.get().append(Text.translatable("skyblocker.customItemNames.removed")));
- } else {
- source.sendFeedback(Constants.PREFIX.get().append(Text.translatable("skyblocker.customItemNames.neverHad")));
- }
- } else {
+ SkyblockerConfigManager.update(config -> {
+ Object2ObjectOpenHashMap<String, Text> customItemNames = config.general.customItemNames;
//If the text is provided then set the item's custom name to it
//Set italic to false if it hasn't been changed (or was already false)
@@ -61,14 +72,14 @@ public class CustomItemNames {
((MutableText) text).setStyle(currentStyle.withItalic(currentStyle.isItalic()));
customItemNames.put(itemUuid, text);
- SkyblockerConfigManager.save();
- source.sendFeedback(Constants.PREFIX.get().append(Text.translatable("skyblocker.customItemNames.added")));
- }
+ });
+ source.sendFeedback(Constants.PREFIX.get().append(Text.translatable("skyblocker.customItemNames.added")));
+
} else {
source.sendError(Constants.PREFIX.get().append(Text.translatable("skyblocker.customItemNames.noItemUuid")));
}
} else {
- source.sendError(Constants.PREFIX.get().append(Text.translatable("skyblocker.customItemNames.unableToSetName")));
+ source.sendError(Constants.PREFIX.get().append(Text.translatable("skyblocker.customItemNames.notOnSkyblock")));
}
return Command.SINGLE_SUCCESS;
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/custom/screen/name/ColorPopup.java b/src/main/java/de/hysky/skyblocker/skyblock/item/custom/screen/name/ColorPopup.java
new file mode 100644
index 00000000..9c8ffa69
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/custom/screen/name/ColorPopup.java
@@ -0,0 +1,133 @@
+package de.hysky.skyblocker.skyblock.item.custom.screen.name;
+
+
+import de.hysky.skyblocker.utils.render.gui.ARGBTextInput;
+import de.hysky.skyblocker.utils.render.gui.AbstractPopupScreen;
+import de.hysky.skyblocker.utils.render.gui.ColorPickerWidget;
+import it.unimi.dsi.fastutil.ints.IntIntMutablePair;
+import it.unimi.dsi.fastutil.ints.IntIntPair;
+import net.minecraft.client.gui.DrawContext;
+import net.minecraft.client.gui.screen.Screen;
+import net.minecraft.client.gui.widget.*;
+import net.minecraft.text.Text;
+
+import java.util.function.IntConsumer;
+
+public class ColorPopup extends AbstractPopupScreen {
+
+ private final GridWidget layout = new GridWidget();
+
+ private final boolean gradient;
+ private final GradientConsumer gradientConsumer;
+ private final IntIntPair currentColor = new IntIntMutablePair(-1, -1);
+
+ private ColorPopup(Screen backgroundScreen, GradientConsumer gradientConsumer, boolean gradient) {
+ super(Text.literal("Color Popup"), backgroundScreen);
+ this.gradientConsumer = gradientConsumer;
+ this.gradient = gradient;
+ layout.getMainPositioner().alignHorizontalCenter();
+ }
+
+ private ColorPopup(Screen backgroundScreen, IntConsumer consumer) {
+ this(backgroundScreen, ((start, end) -> consumer.accept(start)), false);
+ }
+
+ public static ColorPopup create(Screen backgroundScreen, IntConsumer colorConsumer) {
+ return new ColorPopup(backgroundScreen, colorConsumer);
+ }
+
+ public static ColorPopup createGradient(Screen backgroundScreen, GradientConsumer gradientConsumer) {
+ return new ColorPopup(backgroundScreen, gradientConsumer, true);
+ }
+
+ @Override
+ protected void init() {
+ GridWidget.Adder adder = layout.createAdder(2);
+ addDrawableChild(adder.add(new TextWidget(Text.translatable("skyblocker.customItemNames.screen.customColorTitle"), textRenderer), 2));
+ if (gradient) {
+ createLayoutGradient(adder);
+ } else {
+ createLayout(adder);
+ }
+ adder.add(EmptyWidget.ofHeight(15), 2);
+ addDrawableChild(adder.add(ButtonWidget.builder(Text.translatable("gui.cancel"), b -> close()).build(), Positioner.create().alignRight().marginRight(2)));
+ addDrawableChild(adder.add(ButtonWidget.builder(Text.translatable("gui.done"), b -> {
+ gradientConsumer.accept(currentColor.firstInt(), currentColor.secondInt());
+ close();
+ }).build(), Positioner.create().alignLeft().marginLeft(2)));
+ super.init();
+ }
+
+ @Override
+ protected void refreshWidgetPositions() {
+ super.refreshWidgetPositions();
+ layout.refreshPositions();
+ layout.setPosition((width - layout.getWidth()) / 2, (height - layout.getHeight()) / 2);
+ }
+
+ @Override
+ public void renderBackground(DrawContext context, int mouseX, int mouseY, float delta) {
+ super.renderBackground(context, mouseX, mouseY, delta);
+ drawPopupBackground(context, layout.getX(), layout.getY(), layout.getWidth(), layout.getHeight());
+ }
+
+ private void createLayout(GridWidget.Adder adder) {
+ ColorPickerWidget colorPicker = new ColorPickerWidget(0, 0, 200, 100);
+ ARGBTextInput argb = new ARGBTextInput(0, 0, textRenderer, true, false);
+ addDrawableChild(colorPicker);
+ addDrawableChild(argb);
+
+ argb.setOnChange(color -> {
+ colorPicker.setRGBColor(color);
+ currentColor.first(color);
+ });
+ colorPicker.setOnColorChange((color, mouseRelease) -> {
+ argb.setARGBColor(color);
+ currentColor.first(color);
+ });
+
+ adder.add(colorPicker, 2);
+ adder.add(argb, 2);
+ }
+
+ private void createLayoutGradient(GridWidget.Adder adder) {
+ ColorPickerWidget colorPickerStart = new ColorPickerWidget(0, 0, 200, 100);
+ ARGBTextInput argbStart = new ARGBTextInput(0, 0, textRenderer, true, false);
+ ColorPickerWidget colorPickerEnd = new ColorPickerWidget(0, 0, 200, 100);
+ ARGBTextInput argbEnd = new ARGBTextInput(0, 0, textRenderer, true, false);
+ addDrawableChild(colorPickerStart);
+ addDrawableChild(argbStart);
+ addDrawableChild(colorPickerEnd);
+ addDrawableChild(argbEnd);
+
+ argbStart.setOnChange(color -> {
+ colorPickerStart.setRGBColor(color);
+ currentColor.first(color);
+ });
+ colorPickerStart.setOnColorChange((color, mouseRelease) -> {
+ argbStart.setARGBColor(color);
+ currentColor.first(color);
+ });
+ argbEnd.setOnChange(color -> {
+ colorPickerEnd.setRGBColor(color);
+ currentColor.second(color);
+ });
+ colorPickerEnd.setOnColorChange((color, mouseRelease) -> {
+ argbEnd.setARGBColor(color);
+ currentColor.second(color);
+ });
+
+ addDrawableChild(adder.add(new TextWidget(Text.translatable("skyblocker.customItemNames.screen.gradientStart"), textRenderer)));
+ addDrawableChild(adder.add(new TextWidget(Text.translatable("skyblocker.customItemNames.screen.gradientEnd"), textRenderer)));
+
+ adder.add(colorPickerStart);
+ adder.add(colorPickerEnd);
+ adder.add(argbStart);
+ adder.add(argbEnd);
+ }
+
+ @FunctionalInterface
+ public interface GradientConsumer {
+ void accept(int start, int end);
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/custom/screen/name/CustomizeNameScreen.java b/src/main/java/de/hysky/skyblocker/skyblock/item/custom/screen/name/CustomizeNameScreen.java
new file mode 100644
index 00000000..f643763b
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/custom/screen/name/CustomizeNameScreen.java
@@ -0,0 +1,464 @@
+package de.hysky.skyblocker.skyblock.item.custom.screen.name;
+
+import de.hysky.skyblocker.config.ConfigUtils;
+import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.debug.Debug;
+import de.hysky.skyblocker.skyblock.item.custom.screen.name.visitor.*;
+import de.hysky.skyblocker.utils.ItemUtils;
+import de.hysky.skyblocker.utils.OkLabColor;
+import net.minecraft.client.font.TextRenderer;
+import net.minecraft.client.gui.DrawContext;
+import net.minecraft.client.gui.screen.Screen;
+import net.minecraft.client.gui.screen.narration.NarrationMessageBuilder;
+import net.minecraft.client.gui.tooltip.Tooltip;
+import net.minecraft.client.gui.widget.*;
+import net.minecraft.client.render.RenderLayer;
+import net.minecraft.client.sound.SoundManager;
+import net.minecraft.item.ItemStack;
+import net.minecraft.text.*;
+import net.minecraft.util.*;
+import net.minecraft.util.math.ColorHelper;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.lwjgl.glfw.GLFW;
+
+import java.util.function.Predicate;
+
+public class CustomizeNameScreen extends Screen {
+ private static final int BORDER_SIZE = 12;
+ private static final Identifier BACKGROUND_TEXTURE = Identifier.ofVanilla("popup/background");
+
+ private final String uuid;
+
+ private Text text = Text.empty();
+ private String textString = "";
+
+ private TextField textField;
+ private FormattingButton[] formattingButtons;
+
+ private final GridWidget grid = new GridWidget();
+
+ private int selectionStart;
+ private int selectionEnd;
+
+ private @Nullable Style insertAs;
+
+ public CustomizeNameScreen(@NotNull ItemStack stack) {
+ super(Text.literal("Customize Item Name"));
+ uuid = ItemUtils.getItemUuid(stack);
+ setText(stack.getName().copy());
+ }
+
+ @Override
+ protected void init() {
+ if (uuid.isEmpty()) {
+ close();
+ return;
+ }
+ // the gui is a grid of 20 columns, should be 16 px each
+ textField = grid.add(new TextField(), 1, 0, 1, 20);
+ addDrawableChild(textField);
+ formattingButtons = new FormattingButton[]{
+ new FormattingButton("B", Formatting.BOLD, Style::isBold),
+ new FormattingButton("I", Formatting.ITALIC, Style::isItalic),
+ new FormattingButton("U", Formatting.UNDERLINE, Style::isUnderlined),
+ new FormattingButton("S", Formatting.STRIKETHROUGH, Style::isStrikethrough),
+ new FormattingButton("|||", Formatting.OBFUSCATED, Style::isObfuscated),
+ };
+ for (int i = 0; i < formattingButtons.length; i++) {
+ FormattingButton button = formattingButtons[i];
+ addDrawableChild(grid.add(button, 0, i));
+ }
+
+ int i = 0;
+ for (Formatting formatting : Formatting.values()) {
+ if (formatting.isColor()) {
+ addDrawableChild(grid.add(new ColorButton(formatting), 2, i++));
+ }
+ }
+
+ assert client != null;
+ addDrawableChild(grid.add(ButtonWidget.builder(Text.translatable("skyblocker.customItemNames.screen.customColor"), b ->
+ client.setScreen(ColorPopup.create(this, color -> setStyle(Style.EMPTY.withColor(color))))
+ ).size(48, 16).build(), 2, 17, 1, 3));
+ addDrawableChild(grid.add(ButtonWidget.builder(Text.translatable("skyblocker.customItemNames.screen.gradientColor"), b ->
+ client.setScreen(ColorPopup.createGradient(this, this::createGradient))
+ ).size(48, 16).build(), 3, 17, 1, 3));
+ addDrawableChild(grid.add(ButtonWidget.builder(Text.translatable("gui.cancel"), b -> close()).width(80).build(), 4, 0, 1, 10, Positioner.create().alignRight()));
+ addDrawableChild(grid.add(ButtonWidget.builder(Text.translatable("gui.done"), b -> {
+ SkyblockerConfigManager.update(config -> {
+ if (textString.isBlank()) config.general.customItemNames.remove(uuid);
+ else config.general.customItemNames.put(uuid, text.copy().setStyle(Style.EMPTY.withItalic(false).withColor(Formatting.WHITE)));
+ });
+ close();
+ }).width(80).build(), 4, 10, 1, 10, Positioner.create().alignLeft()));
+ addDrawableChild(grid.add(new TextWidget(20 * 16, textRenderer.fontHeight, Text.translatable("skyblocker.customItemNames.screen.howToRemove").formatted(Formatting.ITALIC, Formatting.GRAY), textRenderer).alignLeft(), 5, 0, 1, 20, Positioner.create().marginTop(2)));
+ refreshWidgetPositions();
+ }
+
+ @Override
+ protected void refreshWidgetPositions() {
+ grid.refreshPositions();
+ grid.setPosition((width - grid.getWidth()) / 2, (height - grid.getHeight()) / 2);
+ }
+
+ @Override
+ public void renderBackground(DrawContext context, int mouseX, int mouseY, float deltaTicks) {
+ context.drawGuiTexture(
+ RenderLayer::getGuiTextured,
+ BACKGROUND_TEXTURE,
+ grid.getX() - BORDER_SIZE,
+ grid.getY() - BORDER_SIZE,
+ grid.getWidth() + BORDER_SIZE * 2,
+ grid.getHeight() + BORDER_SIZE * 2);
+ }
+
+ @Override
+ public void render(DrawContext context, int mouseX, int mouseY, float deltaTicks) {
+ super.render(context, mouseX, mouseY, deltaTicks);
+ // little preview
+ context.drawCenteredTextWithShadow(textRenderer, text, width / 2, grid.getY() + grid.getHeight() + BORDER_SIZE + 6, -1);
+ if (Debug.debugEnabled()) {
+ context.drawTextWithShadow(textRenderer, Text.literal("Selection Start: " + selectionStart + ", Selection End: " + selectionEnd), 10, 10, -1);
+ context.drawTextWithShadow(textRenderer, Text.literal("Insert Style: " + (insertAs == null ? "null" : insertAs.toString())), 10, 20, -1);
+ }
+ }
+
+ /**
+ * Creates a gradient that goes from {@link CustomizeNameScreen#selectionStart} to {@link CustomizeNameScreen#selectionEnd}
+ * @param startColor the color at the start of the gradient
+ * @param endColor the color at the end of the gradient
+ */
+ private void createGradient(int startColor, int endColor) {
+ int previousSelectionStart = selectionStart;
+ int previousSelectionEnd = selectionEnd;
+
+ int selStart = Math.min(selectionStart, selectionEnd);
+ int selSize = Math.abs(selectionEnd - selectionStart);
+ if (selSize == 0) return;
+ if (selSize == 1) {
+ setStyle(Style.EMPTY.withColor(startColor));
+ } else {
+ for (int i = 0; i < selSize; i++) {
+ selectionStart = selStart + i;
+ selectionEnd = selStart + i + 1;
+ int color = OkLabColor.interpolate(startColor, endColor, (float) i / (selSize - 1));
+ setStyle(Style.EMPTY.withColor(color));
+ }
+ }
+ selectionStart = previousSelectionStart;
+ selectionEnd = previousSelectionEnd;
+ }
+
+ /**
+ * Sets the style of the selected text or the insert position if no text is selected.
+ * @param style the style to set
+ */
+ private void setStyle(Style style) {
+ if (selectionStart == selectionEnd) {
+ insertAs = style.withParent(insertAs == null ? Style.EMPTY : insertAs);
+ return;
+ }
+ SetStyleVisitor setStyleVisitor = new SetStyleVisitor(style, selectionStart, selectionEnd);
+ text.visit(setStyleVisitor, Style.EMPTY);
+ setText(setStyleVisitor.getNewText());
+ }
+
+ private void updateStyleButtons() {
+ GetStyleVisitor styleVisitor = new GetStyleVisitor(selectionStart, selectionEnd);
+ text.visit(styleVisitor, Style.EMPTY);
+ Style style = styleVisitor.getStyle();
+ for (FormattingButton button : formattingButtons) {
+ button.update(style);
+ }
+ }
+
+ /**
+ * Sets the text to be displayed in the text field and updates the textString to avoid calling getString() every time.
+ * @param text the text to set
+ */
+ public void setText(Text text) {
+ this.text = text;
+ textString = text.getString();
+ // called before init
+ if (textField != null) textField.updateMePrettyPlease = true;
+ }
+
+ @Override
+ public boolean charTyped(char chr, int modifiers) {
+ if (super.charTyped(chr, modifiers) || textField.isFocused()) return true;
+ setFocused(textField);
+ return textField.charTyped(chr, modifiers);
+ }
+
+ /**
+ * Inserts the given text at the current cursor position or replaces the selected text.
+ * If the text field is empty, it sets the text to the given string.
+ * @param str the text to insert
+ */
+ public void insertText(String str) {
+ str = StringHelper.stripInvalidChars(str);
+ if (text.getContent() == PlainTextContent.EMPTY) {
+ setText(Text.literal(str).setStyle(insertAs != null ? insertAs : Style.EMPTY));
+ } else {
+ InsertTextVisitor visitor = new InsertTextVisitor(str, insertAs, selectionStart, selectionEnd);
+ text.visit(visitor, Style.EMPTY);
+ setText(visitor.getNewText());
+ insertAs = null;
+ }
+
+ selectionStart = Math.min(selectionStart, selectionEnd) + str.length();
+ selectionEnd = selectionStart;
+ updateStyleButtons();
+ }
+
+ /**
+ * Moves the cursor left or right, depending on the direction.
+ * If shift is held, it will extend the selection.
+ * If ctrl is held, it will skip to the next word.
+ *
+ * @param left whether to move left or right
+ * @param shiftHeld whether shift is held
+ * @param ctrlHeld whether ctrl is held
+ */
+ private void moveCursor(boolean left, boolean shiftHeld, boolean ctrlHeld) {
+ if (left && selectionStart == 0 || (!left && selectionStart == textString.length())) return;
+ if (ctrlHeld) {
+ selectionStart = getWordSkipPosition(left);
+ } else {
+ selectionStart = Util.moveCursor(textString, selectionStart, left ? -1 : 1);
+ }
+ if (!shiftHeld) selectionEnd = selectionStart;
+ insertAs = null;
+ updateStyleButtons();
+ }
+
+ /**
+ * Erases the text at the current cursor position or the selected text.
+ * If the selection is not empty, it will remove the selected text.
+ * If the selection is empty, it will erase one character or word in the requested direction.
+ *
+ * @param left whether to erase left or right
+ * @param ctrlHeld whether ctrl is held
+ */
+ private void erase(boolean left, boolean ctrlHeld) {
+ if (selectionStart != selectionEnd) {
+ insertText("");
+ return;
+ }
+ moveCursor(left, true, ctrlHeld);
+ insertText("");
+ }
+
+ /**
+ * Skips one word in the requested direction from selectionStart
+ *
+ * @param left the direction
+ * @return the new position
+ */
+ private int getWordSkipPosition(boolean left) {
+ int i = selectionStart;
+
+ if (!left) {
+ int l = this.textString.length();
+ i = this.textString.indexOf(32, i);
+ if (i == -1) {
+ i = l;
+ } else {
+ while (i < l && this.textString.charAt(i) == ' ') {
+ i++;
+ }
+ }
+ } else {
+ while (i > 0 && this.textString.charAt(i - 1) == ' ') {
+ i--;
+ }
+
+ while (i > 0 && this.textString.charAt(i - 1) != ' ') {
+ i--;
+ }
+ }
+
+ return i;
+ }
+
+ private class FormattingButton extends PressableWidget {
+ private boolean enabled;
+ private final Formatting format;
+ private final Predicate<Style> isEnabled;
+
+ protected FormattingButton(Text message, Formatting format, Predicate<Style> isEnabled) {
+ super(0, 0, 16, 16, message);
+ this.format = format;
+ this.isEnabled = isEnabled;
+ setTooltip(Tooltip.of(ConfigUtils.FORMATTING_FORMATTER.apply(format))); // Yoink from config utils hehhehehehehe
+ }
+
+ protected FormattingButton(String str, Formatting format, Predicate<Style> isEnabled) {
+ this(Text.literal(str).formatted(format), format, isEnabled);
+ }
+
+ private void update(Style style) {
+ setEnabled(isEnabled.test(style));
+ }
+
+ @Override
+ public void onPress() {
+ setEnabled(!enabled);
+ switch (format) {
+ case BOLD -> setStyle(Style.EMPTY.withBold(enabled));
+ case ITALIC -> setStyle(Style.EMPTY.withItalic(enabled));
+ case UNDERLINE -> setStyle(Style.EMPTY.withUnderline(enabled));
+ case STRIKETHROUGH -> setStyle(Style.EMPTY.withStrikethrough(enabled));
+ case OBFUSCATED -> setStyle(Style.EMPTY.withObfuscated(enabled));
+ default -> throw new IllegalStateException("Unexpected value: " + format);
+ }
+ }
+
+ private void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ setMessage(getMessage().copy().withColor(enabled ? Colors.YELLOW : Colors.WHITE));
+ }
+
+ @Override
+ protected void appendClickableNarrations(NarrationMessageBuilder builder) {}
+ }
+
+ private class ColorButton extends PressableWidget {
+ private final Formatting color;
+ private final int intColor;
+
+ private ColorButton(Formatting format) {
+ super(0, 0, 16, 16, ConfigUtils.FORMATTING_FORMATTER.apply(format));
+ setTooltip(Tooltip.of(getMessage()));
+ this.color = format;
+ this.intColor = ColorHelper.fullAlpha(color.getColorValue());
+ }
+
+ @Override
+ public void onPress() {
+ setStyle(Style.EMPTY.withColor(color));
+ }
+
+ @Override
+ public void drawMessage(DrawContext context, TextRenderer textRenderer, int color) {
+ context.fill(getX() + 2, getY() + 2, getRight() - 2, getBottom() - 2, intColor);
+ }
+
+ @Override
+ protected void appendClickableNarrations(NarrationMessageBuilder builder) {}
+ }
+
+ /**
+ * Used to capture inputs and render the text. Most logic is done in the screen itself
+ */
+ private class TextField extends ClickableWidget {
+ private int renderedSelectionStart;
+ private int renderedSelectionEnd;
+ private boolean updateMePrettyPlease = false;
+
+ private int renderStart;
+ private int renderEnd;
+
+ private TextField() {
+ super(0, 0, 320, 20, Text.literal("TextField"));
+ }
+
+ @Override
+ protected void renderWidget(DrawContext context, int mouseX, int mouseY, float deltaTicks) {
+ if (renderedSelectionStart != selectionStart || renderedSelectionEnd != selectionEnd || updateMePrettyPlease) {
+ renderedSelectionStart = selectionStart;
+ renderedSelectionEnd = selectionEnd;
+ updateMePrettyPlease = false;
+ GetRenderWidthVisitor getRenderWidthVisitor = new GetRenderWidthVisitor(selectionStart, selectionEnd);
+ text.visit(getRenderWidthVisitor, Style.EMPTY);
+ renderStart = getRenderWidthVisitor.getWidths().firstInt();
+ renderEnd = getRenderWidthVisitor.getWidths().secondInt();
+ }
+
+ context.fill(getX(), getY(), getRight(), getBottom(), Colors.BLACK);
+ context.drawBorder(getX(), getY(), getWidth(), getHeight(), isFocused() ? Colors.WHITE : Colors.GRAY);
+ int textX = getTextX();
+ int textY = getY() + (getHeight() - textRenderer.fontHeight) / 2;
+
+ if (renderStart != renderEnd) {
+ context.fill(textX + renderStart, textY, textX + renderEnd, textY + textRenderer.fontHeight, Colors.BLUE);
+ }
+ if (isFocused()) context.drawVerticalLine(textX + (selectionStart < selectionEnd ? renderStart : renderEnd) - 1, textY - 1, textY + textRenderer.fontHeight, Colors.WHITE);
+
+ context.drawText(textRenderer, text, textX, textY, -1, false);
+ }
+
+ @Override
+ public boolean keyPressed(int keyCode, int scanCode, int modifiers) {
+ boolean captured = true;
+ switch (keyCode) {
+ case GLFW.GLFW_KEY_LEFT -> moveCursor(true, Screen.hasShiftDown(), Screen.hasControlDown());
+ case GLFW.GLFW_KEY_RIGHT -> moveCursor(false, Screen.hasShiftDown(), Screen.hasControlDown());
+ case GLFW.GLFW_KEY_BACKSPACE -> erase(true, Screen.hasControlDown());
+ case GLFW.GLFW_KEY_DELETE -> erase(false, Screen.hasControlDown());
+ default -> captured = false;
+ }
+ if (captured) return true;
+ assert client != null;
+ if (Screen.isSelectAll(keyCode)) {
+ selectionStart = 0;
+ selectionEnd = textString.length();
+ updateStyleButtons();
+ captured = true;
+ } else if (Screen.isCopy(keyCode)) {
+ client.keyboard.setClipboard(text.getString().substring(selectionStart, selectionEnd));
+ captured = true;
+ } else if (Screen.isPaste(keyCode)) {
+ String clipboard = client.keyboard.getClipboard();
+ if (!clipboard.isEmpty()) {
+ insertText(clipboard);
+ }
+ captured = true;
+ } else if (Screen.isCut(keyCode)) {
+ client.keyboard.setClipboard(text.getString().substring(selectionStart, selectionEnd));
+ insertText("");
+ captured = true;
+ }
+ return captured;
+ }
+
+ @Override
+ public boolean charTyped(char chr, int modifiers) {
+ if (!active) {
+ return false;
+ } else if (StringHelper.isValidChar(chr)) {
+ insertText(Character.toString(chr));
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ @Override
+ public void onClick(double mouseX, double mouseY) {
+ GetClickedPositionVisitor getClickedPositionVisitor = new GetClickedPositionVisitor((int) mouseX - getTextX());
+ text.visit(getClickedPositionVisitor, Style.EMPTY);
+ selectionStart = selectionEnd = getClickedPositionVisitor.getPosition() < 0 ? textString.length() : getClickedPositionVisitor.getPosition();
+ updateStyleButtons();
+ }
+
+ @Override
+ protected void onDrag(double mouseX, double mouseY, double deltaX, double deltaY) {
+ GetClickedPositionVisitor getClickedPositionVisitor = new GetClickedPositionVisitor((int) mouseX - getTextX());
+ text.visit(getClickedPositionVisitor, Style.EMPTY);
+ selectionStart = getClickedPositionVisitor.getPosition() < 0 ? textString.length() : getClickedPositionVisitor.getPosition();
+ updateStyleButtons();
+ }
+
+ private int getTextX() {
+ return getX() + 2;
+ }
+
+ @Override
+ public void playDownSound(SoundManager soundManager) {}
+
+ @Override
+ protected void appendClickableNarrations(NarrationMessageBuilder builder) {}
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/custom/screen/name/visitor/BaseVisitor.java b/src/main/java/de/hysky/skyblocker/skyblock/item/custom/screen/name/visitor/BaseVisitor.java
new file mode 100644
index 00000000..5d8d68b9
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/custom/screen/name/visitor/BaseVisitor.java
@@ -0,0 +1,24 @@
+package de.hysky.skyblocker.skyblock.item.custom.screen.name.visitor;
+
+import net.minecraft.text.StringVisitable;
+import net.minecraft.text.Style;
+
+import java.util.Optional;
+
+abstract class BaseVisitor implements StringVisitable.StyledVisitor<Void> {
+ protected int selStart;
+ protected int selSize;
+
+ BaseVisitor(int selectionStart, int selectionEnd) {
+ this.selStart = Math.min(selectionStart, selectionEnd);
+ this.selSize = Math.abs(selectionStart - selectionEnd);
+ }
+
+ @Override
+ public final Optional<Void> accept(Style style, String asString) {
+ visit(style, asString);
+ return Optional.empty();
+ }
+
+ protected abstract void visit(Style style, String asString);
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/custom/screen/name/visitor/GetClickedPositionVisitor.java b/src/main/java/de/hysky/skyblocker/skyblock/item/custom/screen/name/visitor/GetClickedPositionVisitor.java
new file mode 100644
index 00000000..27316839
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/custom/screen/name/visitor/GetClickedPositionVisitor.java
@@ -0,0 +1,57 @@
+package de.hysky.skyblocker.skyblock.item.custom.screen.name.visitor;
+
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.font.TextRenderer;
+import net.minecraft.text.*;
+
+import java.util.Optional;
+import java.util.concurrent.atomic.AtomicInteger;
+
+public class GetClickedPositionVisitor implements StringVisitable.StyledVisitor<Void> {
+ private final MutableText text = Text.empty();
+ private final TextRenderer textRenderer;
+ private int position = -1;
+ private final int x;
+
+ public GetClickedPositionVisitor(TextRenderer textRenderer, int x) {
+ this.x = x;
+ this.textRenderer = textRenderer;
+ }
+
+ public GetClickedPositionVisitor(int x) {
+ this(MinecraftClient.getInstance().textRenderer, x);
+ }
+
+ protected void visit(Style style, String asString) {
+ if (position >= 0) return; // already found position
+ if (asString.isEmpty()) return;
+ MutableText text1 = Text.literal(asString).setStyle(style);
+ int originalWidth = textRenderer.getWidth(text);
+ if (originalWidth + textRenderer.getWidth(text1) < x) { // if the text is smaller than the x position, we skip it and append it
+ text.append(text1);
+ return;
+ }
+ // the x position is within the text, we need to find the position
+ int currentWidth = 0;
+ AtomicInteger atomicInteger = new AtomicInteger(0);
+ OrderedText orderedText = visitor -> {
+ visitor.accept(0, style, asString.codePointAt(atomicInteger.get()));
+ return true;
+ };
+ while (atomicInteger.get() < asString.length() && originalWidth + currentWidth + textRenderer.getWidth(orderedText) / 2 <= x) {
+ currentWidth += textRenderer.getWidth(orderedText);
+ atomicInteger.incrementAndGet();
+ }
+ position = Math.max(text.getString().length() + atomicInteger.get(), 0);
+ }
+
+ public int getPosition() {
+ return position;
+ }
+
+ @Override
+ public Optional<Void> accept(Style style, String asString) {
+ visit(style, asString);
+ return Optional.empty();
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/custom/screen/name/visitor/GetRenderWidthVisitor.java b/src/main/java/de/hysky/skyblocker/skyblock/item/custom/screen/name/visitor/GetRenderWidthVisitor.java
new file mode 100644
index 00000000..3c2ec479
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/custom/screen/name/visitor/GetRenderWidthVisitor.java
@@ -0,0 +1,58 @@
+package de.hysky.skyblocker.skyblock.item.custom.screen.name.visitor;
+
+import it.unimi.dsi.fastutil.ints.IntIntMutablePair;
+import it.unimi.dsi.fastutil.ints.IntIntPair;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.font.TextRenderer;
+import net.minecraft.text.MutableText;
+import net.minecraft.text.Style;
+import net.minecraft.text.Text;
+
+/**
+ * Calculates the x coordinates of the selection start and end in a text.
+ */
+public class GetRenderWidthVisitor extends BaseVisitor {
+ private final IntIntMutablePair widths = new IntIntMutablePair(0, 0);
+ private final MutableText text = Text.empty();
+ private final TextRenderer textRenderer;
+
+ public GetRenderWidthVisitor(TextRenderer textRenderer, int selectionStar