diff options
| author | viciscat <51047087+viciscat@users.noreply.github.com> | 2025-07-26 23:31:43 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-07-26 17:31:43 -0400 |
| commit | f2b24066288fae2095ca92c166486d74b3b16ff1 (patch) | |
| tree | b8b23452b347245122d6689b0e44c7349a96a861 /src/main/java | |
| parent | fc8fd3425ce1c4d87aa8c494a9b16b4501fe0b19 (diff) | |
| download | Skyblocker-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')
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 |
