From 04fe933f4c24817100f3101f088accf55a621f8a Mon Sep 17 00:00:00 2001 From: isxander Date: Thu, 11 Apr 2024 18:43:06 +0100 Subject: Extremely fragile and broken multiversion build with stonecutter --- .../dev/isxander/yacl3/gui/AbstractWidget.java | 100 ++++ .../isxander/yacl3/gui/DescriptionWithName.java | 11 + .../isxander/yacl3/gui/ElementListWidgetExt.java | 274 ++++++++++ .../isxander/yacl3/gui/LowProfileButtonWidget.java | 28 + .../yacl3/gui/OptionDescriptionWidget.java | 222 ++++++++ .../dev/isxander/yacl3/gui/OptionListWidget.java | 578 +++++++++++++++++++++ .../isxander/yacl3/gui/RequireRestartScreen.java | 21 + .../dev/isxander/yacl3/gui/SearchFieldWidget.java | 61 +++ .../isxander/yacl3/gui/TextScaledButtonWidget.java | 34 ++ .../isxander/yacl3/gui/TooltipButtonWidget.java | 21 + .../dev/isxander/yacl3/gui/ValueFormatters.java | 21 + .../java/dev/isxander/yacl3/gui/YACLScreen.java | 426 +++++++++++++++ .../java/dev/isxander/yacl3/gui/YACLTooltip.java | 23 + .../isxander/yacl3/gui/YACLTooltipPositioner.java | 48 ++ .../yacl3/gui/controllers/ActionController.java | 120 +++++ .../yacl3/gui/controllers/BooleanController.java | 164 ++++++ .../yacl3/gui/controllers/ColorController.java | 220 ++++++++ .../yacl3/gui/controllers/ControllerWidget.java | 148 ++++++ .../yacl3/gui/controllers/LabelController.java | 193 +++++++ .../yacl3/gui/controllers/ListEntryWidget.java | 128 +++++ .../yacl3/gui/controllers/TickBoxController.java | 119 +++++ .../cycling/CyclingControllerElement.java | 60 +++ .../controllers/cycling/CyclingListController.java | 86 +++ .../gui/controllers/cycling/EnumController.java | 48 ++ .../controllers/cycling/ICyclingController.java | 38 ++ .../dropdown/AbstractDropdownController.java | 87 ++++ .../AbstractDropdownControllerElement.java | 248 +++++++++ .../dropdown/DropdownStringController.java | 34 ++ .../dropdown/DropdownStringControllerElement.java | 31 ++ .../dropdown/EnumDropdownController.java | 92 ++++ .../dropdown/EnumDropdownControllerElement.java | 25 + .../gui/controllers/dropdown/ItemController.java | 68 +++ .../dropdown/ItemControllerElement.java | 87 ++++ .../yacl3/gui/controllers/package-info.java | 12 + .../controllers/slider/DoubleSliderController.java | 119 +++++ .../controllers/slider/FloatSliderController.java | 119 +++++ .../gui/controllers/slider/ISliderController.java | 54 ++ .../slider/IntegerSliderController.java | 116 +++++ .../controllers/slider/LongSliderController.java | 116 +++++ .../slider/SliderControllerElement.java | 157 ++++++ .../yacl3/gui/controllers/slider/package-info.java | 10 + .../gui/controllers/string/IStringController.java | 44 ++ .../gui/controllers/string/StringController.java | 37 ++ .../string/StringControllerElement.java | 466 +++++++++++++++++ .../string/number/DoubleFieldController.java | 111 ++++ .../string/number/FloatFieldController.java | 111 ++++ .../string/number/IntegerFieldController.java | 111 ++++ .../string/number/LongFieldController.java | 111 ++++ .../string/number/NumberFieldController.java | 80 +++ .../controllers/string/number/package-info.java | 10 + .../isxander/yacl3/gui/image/ImageRenderer.java | 11 + .../yacl3/gui/image/ImageRendererFactory.java | 24 + .../yacl3/gui/image/ImageRendererManager.java | 120 +++++ .../yacl3/gui/image/YACLImageReloadListener.java | 110 ++++ .../image/impl/AnimatedDynamicTextureImage.java | 286 ++++++++++ .../yacl3/gui/image/impl/DynamicTextureImage.java | 72 +++ .../yacl3/gui/image/impl/ResourceTextureImage.java | 56 ++ .../isxander/yacl3/gui/tab/ListHolderWidget.java | 116 +++++ .../yacl3/gui/tab/ScrollableNavigationBar.java | 120 +++++ .../java/dev/isxander/yacl3/gui/tab/TabExt.java | 14 + .../yacl3/gui/utils/ButtonTextureRenderer.java | 34 ++ .../dev/isxander/yacl3/gui/utils/GuiUtils.java | 32 ++ .../yacl3/gui/utils/ItemRegistryHelper.java | 116 +++++ .../isxander/yacl3/gui/utils/UndoRedoHelper.java | 42 ++ 64 files changed, 6801 insertions(+) create mode 100644 src/main/java/dev/isxander/yacl3/gui/AbstractWidget.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/DescriptionWithName.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/ElementListWidgetExt.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/LowProfileButtonWidget.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/OptionDescriptionWidget.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/OptionListWidget.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/RequireRestartScreen.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/SearchFieldWidget.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/TextScaledButtonWidget.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/TooltipButtonWidget.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/ValueFormatters.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/YACLScreen.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/YACLTooltip.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/YACLTooltipPositioner.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/controllers/ActionController.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/controllers/BooleanController.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/controllers/ColorController.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/controllers/ControllerWidget.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/controllers/LabelController.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/controllers/ListEntryWidget.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/controllers/TickBoxController.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/controllers/cycling/CyclingControllerElement.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/controllers/cycling/CyclingListController.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/controllers/cycling/EnumController.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/controllers/cycling/ICyclingController.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/controllers/dropdown/AbstractDropdownController.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/controllers/dropdown/AbstractDropdownControllerElement.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/controllers/dropdown/DropdownStringController.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/controllers/dropdown/DropdownStringControllerElement.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/controllers/dropdown/EnumDropdownController.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/controllers/dropdown/EnumDropdownControllerElement.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/controllers/dropdown/ItemController.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/controllers/dropdown/ItemControllerElement.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/controllers/package-info.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/controllers/slider/DoubleSliderController.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/controllers/slider/FloatSliderController.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/controllers/slider/ISliderController.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/controllers/slider/IntegerSliderController.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/controllers/slider/LongSliderController.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/controllers/slider/SliderControllerElement.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/controllers/slider/package-info.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/controllers/string/IStringController.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/controllers/string/StringController.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/controllers/string/StringControllerElement.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/controllers/string/number/DoubleFieldController.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/controllers/string/number/FloatFieldController.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/controllers/string/number/IntegerFieldController.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/controllers/string/number/LongFieldController.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/controllers/string/number/NumberFieldController.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/controllers/string/number/package-info.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/image/ImageRenderer.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/image/ImageRendererFactory.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/image/ImageRendererManager.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/image/YACLImageReloadListener.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/image/impl/AnimatedDynamicTextureImage.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/image/impl/DynamicTextureImage.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/image/impl/ResourceTextureImage.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/tab/ListHolderWidget.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/tab/ScrollableNavigationBar.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/tab/TabExt.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/utils/ButtonTextureRenderer.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/utils/GuiUtils.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/utils/ItemRegistryHelper.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/utils/UndoRedoHelper.java (limited to 'src/main/java/dev/isxander/yacl3/gui') diff --git a/src/main/java/dev/isxander/yacl3/gui/AbstractWidget.java b/src/main/java/dev/isxander/yacl3/gui/AbstractWidget.java new file mode 100644 index 0000000..6f92749 --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/gui/AbstractWidget.java @@ -0,0 +1,100 @@ +package dev.isxander.yacl3.gui; + +import dev.isxander.yacl3.api.utils.Dimension; +import dev.isxander.yacl3.gui.utils.ButtonTextureRenderer; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.Renderable; +import net.minecraft.client.gui.components.events.GuiEventListener; +import net.minecraft.client.gui.narration.NarratableEntry; +import net.minecraft.client.gui.narration.NarrationElementOutput; +import net.minecraft.client.resources.sounds.SimpleSoundInstance; +import net.minecraft.sounds.SoundEvents; + +import java.awt.Color; + +public abstract class AbstractWidget implements GuiEventListener, Renderable, NarratableEntry { + protected final Minecraft client = Minecraft.getInstance(); + protected final Font textRenderer = client.font; + protected final int inactiveColor = 0xFFA0A0A0; + + private Dimension dim; + + public AbstractWidget(Dimension dim) { + this.dim = dim; + } + + public boolean canReset() { + return false; + } + + @Override + public boolean isMouseOver(double mouseX, double mouseY) { + if (dim == null) return false; + return this.dim.isPointInside((int) mouseX, (int) mouseY); + } + + public void setDimension(Dimension dim) { + this.dim = dim; + } + + public Dimension getDimension() { + return dim; + } + + @Override + public NarrationPriority narrationPriority() { + return NarrationPriority.NONE; + } + + public void unfocus() { + + } + + public boolean matchesSearch(String query) { + return true; + } + + @Override + public void updateNarration(NarrationElementOutput builder) { + + } + + protected void drawButtonRect(GuiGraphics graphics, int x1, int y1, int x2, int y2, boolean hovered, boolean enabled) { + if (x1 > x2) { + int xx1 = x1; + x1 = x2; + x2 = xx1; + } + if (y1 > y2) { + int yy1 = y1; + y1 = y2; + y2 = yy1; + } + int width = x2 - x1; + int height = y2 - y1; + + ButtonTextureRenderer.render(graphics, x1, y1, width, height, enabled, hovered); + } + + protected void drawOutline(GuiGraphics graphics, int x1, int y1, int x2, int y2, int width, int color) { + graphics.fill(x1, y1, x2, y1 + width, color); + graphics.fill(x2, y1, x2 - width, y2, color); + graphics.fill(x1, y2, x2, y2 - width, color); + graphics.fill(x1, y1, x1 + width, y2, color); + } + + protected int multiplyColor(int hex, float amount) { + Color color = new Color(hex, true); + + return new Color(Math.max((int)(color.getRed() * amount), 0), + Math.max((int)(color.getGreen() * amount), 0), + Math.max((int)(color.getBlue() * amount), 0), + color.getAlpha()).getRGB(); + } + + public void playDownSound() { + Minecraft.getInstance().getSoundManager().play(SimpleSoundInstance.forUI(SoundEvents.UI_BUTTON_CLICK, 1.0F)); + } +} diff --git a/src/main/java/dev/isxander/yacl3/gui/DescriptionWithName.java b/src/main/java/dev/isxander/yacl3/gui/DescriptionWithName.java new file mode 100644 index 0000000..6ad72e8 --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/gui/DescriptionWithName.java @@ -0,0 +1,11 @@ +package dev.isxander.yacl3.gui; + +import dev.isxander.yacl3.api.OptionDescription; +import net.minecraft.ChatFormatting; +import net.minecraft.network.chat.Component; + +public record DescriptionWithName(Component name, OptionDescription description) { + public static DescriptionWithName of(Component name, OptionDescription description) { + return new DescriptionWithName(name.copy().withStyle(ChatFormatting.BOLD), description); + } +} diff --git a/src/main/java/dev/isxander/yacl3/gui/ElementListWidgetExt.java b/src/main/java/dev/isxander/yacl3/gui/ElementListWidgetExt.java new file mode 100644 index 0000000..742125b --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/gui/ElementListWidgetExt.java @@ -0,0 +1,274 @@ +package dev.isxander.yacl3.gui; + +import com.mojang.blaze3d.platform.InputConstants; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.AbstractWidget; +import net.minecraft.client.gui.components.ContainerObjectSelectionList; +import net.minecraft.client.gui.components.events.GuiEventListener; +import net.minecraft.client.gui.layouts.LayoutElement; +import net.minecraft.client.gui.navigation.ScreenRectangle; +import net.minecraft.util.Mth; +import org.jetbrains.annotations.Nullable; + +import java.util.function.Consumer; + +public class ElementListWidgetExt> extends ContainerObjectSelectionList implements LayoutElement { + protected static final int SCROLLBAR_WIDTH = 6; + + private double smoothScrollAmount = getScrollAmount(); + private boolean returnSmoothAmount = false; + private final boolean doSmoothScrolling; + private boolean usingScrollbar; + + public ElementListWidgetExt(Minecraft client, int x, int y, int width, int height, boolean smoothScrolling) { + /*? if >1.20.2 {*/ + super(client, width, x, y, height); + /*? } else {*//* + super(client, width, height, y, y + height, 22); + this.x0 = x; + this.x1 = x + width; + *//*?}*/ + this.doSmoothScrolling = smoothScrolling; + setRenderHeader(false, 0); + } + + @Override + public boolean mouseScrolled(double mouseX, double mouseY, /*? if >1.20.2 {*/ double horizontal, /*?}*/ double vertical) { + double scroll = vertical; + /*? if >1.20.2 {*/ + scroll += horizontal; + /*?}*/ + + // default implementation bases scroll step from total height of entries, this is constant + this.setScrollAmount(this.getScrollAmount() - scroll * 20); + return true; + } + + @Override + protected int getScrollbarPosition() { + // default implementation does not respect left/right + return this.getX() + this.getWidth() - SCROLLBAR_WIDTH; + } + + @Override + /*? if >1.20.2 { */ + public void renderWidget(GuiGraphics graphics, int mouseX, int mouseY, float delta) + /*?} else { *//* + public void render(GuiGraphics graphics, int mouseX, int mouseY, float delta) + *//*?}*/ + { + if (usingScrollbar) { + resetSmoothScrolling(); + } + + smoothScrollAmount = Mth.lerp(Minecraft.getInstance().getDeltaFrameTime() * 0.5, smoothScrollAmount, getScrollAmount()); + returnSmoothAmount = true; + + + graphics.enableScissor(this.getX(), this.getY(), this.getX() + this.getWidth(), this.getY() + this.getHeight()); + + /*? if >1.20.2 { */ + super.renderWidget(graphics, mouseX, mouseY, delta); + /*?} else { *//* + super.render(graphics, mouseX, mouseY, delta); + *//*?}*/ + + graphics.disableScissor(); + + returnSmoothAmount = false; + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + if (button == 0 && mouseX >= getScrollbarPosition() && mouseX < getScrollbarPosition() + SCROLLBAR_WIDTH) { + usingScrollbar = true; + } + + return super.mouseClicked(mouseX, mouseY, button); + } + + @Override + public boolean mouseReleased(double mouseX, double mouseY, int button) { + if (button == 0) { + usingScrollbar = false; + } + + return super.mouseReleased(mouseX, mouseY, button); + } + + public void updateDimensions(ScreenRectangle rectangle) { + this.setX(rectangle.left()); + this.setY(rectangle.top()); + this.setWidth(rectangle.width()); + this.setHeight(rectangle.height()); + } + + /** + * awful code to only use smooth scroll state when rendering, + * not other code that needs target scroll amount + */ + @Override + public double getScrollAmount() { + if (returnSmoothAmount && doSmoothScrolling) + return smoothScrollAmount; + + return super.getScrollAmount(); + } + + protected void resetSmoothScrolling() { + this.smoothScrollAmount = super.getScrollAmount(); + } + + @Nullable + @Override + protected E getEntryAtPosition(double x, double y) { + y += getScrollAmount(); + + if (x < this.getX() || x > this.getX() + this.getWidth()) + return null; + + int currentY = this.getY() - headerHeight + 4; + for (E entry : children()) { + if (y >= currentY && y <= currentY + entry.getItemHeight()) { + return entry; + } + + currentY += entry.getItemHeight(); + } + + return null; + } + + /* + below code is licensed from cloth-config under LGPL3 + modified to inherit vanilla's EntryListWidget and use yarn mappings + + code is responsible for having dynamic item heights + */ + + @Override + protected int getMaxPosition() { + return children().stream().map(E::getItemHeight).reduce(0, Integer::sum) + headerHeight; + } + + @Override + protected void centerScrollOn(E entry) { + double d = (this.height) / -2d; + for (int i = 0; i < this.children().indexOf(entry) && i < this.getItemCount(); i++) + d += children().get(i).getItemHeight(); + this.setScrollAmount(d); + } + + @Override + protected int getRowTop(int index) { + int integer = getY() + 4 - (int) this.getScrollAmount() + headerHeight; + for (int i = 0; i < children().size() && i < index; i++) + integer += children().get(i).getItemHeight(); + return integer; + } + + @Override + /*? if >1.20.4 {*//* + protected void renderListItems(GuiGraphics graphics, int mouseX, int mouseY, float delta) + *//*? } else {*/ + protected void renderList(GuiGraphics graphics, int mouseX, int mouseY, float delta) + /*?}*/ + { + int left = this.getRowLeft(); + int right = this.getRowWidth(); + int count = this.getItemCount(); + + for(int i = 0; i < count; ++i) { + E entry = children().get(i); + int top = this.getRowTop(i); + int bottom = top + entry.getItemHeight(); + int entryHeight = entry.getItemHeight() - 4; + if (bottom >= this.getY() && top <= this.getY() + this.getHeight()) { + this.renderItem(graphics, mouseX, mouseY, delta, i, left, top, right, entryHeight); + } + } + } + + /* END cloth config code */ + + @Override + public void visitWidgets(Consumer consumer) { + } + + public abstract static class Entry> extends ContainerObjectSelectionList.Entry { + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + for (GuiEventListener child : this.children()) { + if (child.mouseClicked(mouseX, mouseY, button)) { + if (button == InputConstants.MOUSE_BUTTON_LEFT) + this.setDragging(true); + return true; + } + } + + return false; + } + + @Override + public boolean mouseDragged(double mouseX, double mouseY, int button, double deltaX, double deltaY) { + if (isDragging() && button == InputConstants.MOUSE_BUTTON_LEFT) { + for (GuiEventListener child : this.children()) { + if (child.mouseDragged(mouseX, mouseY, button, deltaX, deltaY)) + return true; + } + } + return false; + } + + public int getItemHeight() { + return 22; + } + } + + /*? if <1.20.3 {*//* + @Override + public int getX() { + return x0; + } + + @Override + public int getY() { + return y0; + } + + @Override + public void setX(int x) { + int width = this.getWidth(); + x0 = x; + x1 = x + width; + } + + @Override + public void setY(int y) { + int height = this.getHeight(); + y0 = y; + y1 = y + height; + } + + public void setWidth(int width) { + x1 = x0 + width; + this.width = width; + } + + public void setHeight(int height) { + y1 = y0 + height; + this.height = height; + } + + @Override + public int getWidth() { + return width; + } + + @Override + public int getHeight() { + return height; + } + *//*?}*/ +} diff --git a/src/main/java/dev/isxander/yacl3/gui/LowProfileButtonWidget.java b/src/main/java/dev/isxander/yacl3/gui/LowProfileButtonWidget.java new file mode 100644 index 0000000..3f5822f --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/gui/LowProfileButtonWidget.java @@ -0,0 +1,28 @@ +package dev.isxander.yacl3.gui; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.components.Tooltip; +import net.minecraft.network.chat.Component; + +public class LowProfileButtonWidget extends Button { + public LowProfileButtonWidget(int x, int y, int width, int height, Component message, OnPress onPress) { + super(x, y, width, height, message, onPress, DEFAULT_NARRATION); + } + + public LowProfileButtonWidget(int x, int y, int width, int height, Component message, OnPress onPress, Tooltip tooltip) { + this(x, y, width, height, message, onPress); + setTooltip(tooltip); + } + + @Override + public void renderWidget(GuiGraphics graphics, int mouseX, int mouseY, float deltaTicks) { + if (!isHoveredOrFocused() || !active) { + int j = this.active ? 0xFFFFFF : 0xA0A0A0; + this.renderString(graphics, Minecraft.getInstance().font, j); + } else { + super.renderWidget(graphics, mouseX, mouseY, deltaTicks); + } + } +} diff --git a/src/main/java/dev/isxander/yacl3/gui/OptionDescriptionWidget.java b/src/main/java/dev/isxander/yacl3/gui/OptionDescriptionWidget.java new file mode 100644 index 0000000..4ca3ad3 --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/gui/OptionDescriptionWidget.java @@ -0,0 +1,222 @@ +package dev.isxander.yacl3.gui; + +import com.mojang.blaze3d.Blaze3D; +import com.mojang.blaze3d.platform.InputConstants; +import dev.isxander.yacl3.gui.image.ImageRenderer; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.ComponentPath; +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.AbstractWidget; +import net.minecraft.client.gui.narration.NarratedElementType; +import net.minecraft.client.gui.narration.NarrationElementOutput; +import net.minecraft.client.gui.navigation.FocusNavigationEvent; +import net.minecraft.client.gui.navigation.ScreenRectangle; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.Style; +import net.minecraft.util.FormattedCharSequence; +import net.minecraft.util.Mth; +import org.jetbrains.annotations.Nullable; + +import java.util.List; +import java.util.Optional; +import java.util.function.Supplier; + +public class OptionDescriptionWidget extends AbstractWidget { + private static final int AUTO_SCROLL_TIMER = 1500; + private static final float AUTO_SCROLL_SPEED = 1; // lines per second + + private @Nullable DescriptionWithName description; + private List wrappedText; + + private static final Minecraft minecraft = Minecraft.getInstance(); + private static final Font font = minecraft.font; + + private Supplier dimensions; + + private float targetScrollAmount, currentScrollAmount; + private int maxScrollAmount; + private int descriptionY; + + private int lastInteractionTime; + private boolean scrollingBackward; + + public OptionDescriptionWidget(Supplier dimensions, @Nullable DescriptionWithName description) { + super(0, 0, 0, 0, description == null ? Component.empty() : description.name()); + this.dimensions = dimensions; + this.setOptionDescription(description); + } + + @Override + public void renderWidget(GuiGraphics graphics, int mouseX, int mouseY, float delta) { + if (description == null) return; + + currentScrollAmount = Mth.lerp(delta * 0.5f, currentScrollAmount, targetScrollAmount); + + ScreenRectangle dimensions = this.dimensions.get(); + this.setX(dimensions.left()); + this.setY(dimensions.top()); + this.width = dimensions.width(); + this.height = dimensions.height(); + + int y = getY(); + + int nameWidth = font.width(description.name()); + if (nameWidth > getWidth()) { + renderScrollingString(graphics, font, description.name(), getX(), y, getX() + getWidth(), y + font.lineHeight, -1); + } else { + graphics.drawString(font, description.name(), getX(), y, 0xFFFFFF); + } + + y += 5 + font.lineHeight; + + graphics.enableScissor(getX(), y, getX() + getWidth(), getY() + getHeight()); + + y -= (int)currentScrollAmount; + + if (description.description().image().isDone()) { + var image = description.description().image().join(); + if (image.isPresent()) { + y += image.get().render(graphics, getX(), y, getWidth(), delta) + 5; + } + } + + if (wrappedText == null) + wrappedText = font.split(description.description().text(), getWidth()); + + descriptionY = y; + for (var line : wrappedText) { + graphics.drawString(font, line, getX(), y, 0xFFFFFF); + y += font.lineHeight; + } + + graphics.disableScissor(); + + maxScrollAmount = Math.max(0, y + (int)currentScrollAmount - getY() - getHeight()); + + if (isHoveredOrFocused()) { + lastInteractionTime = currentTimeMS(); + } + Style hoveredStyle = getDescStyle(mouseX, mouseY); + if (hoveredStyle != null && hoveredStyle.getHoverEvent() != null) { + graphics.renderComponentHoverEffect(font, hoveredStyle, mouseX, mouseY); + } + + if (isFocused()) { + graphics.renderOutline(getX(), getY(), getWidth(), getHeight(), -1); + } + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + Style clickedStyle = getDescStyle((int) mouseX, (int) mouseY); + if (clickedStyle != null && clickedStyle.getClickEvent() != null) { + if (minecraft.screen.handleComponentClicked(clickedStyle)) { + playDownSound(minecraft.getSoundManager()); + return true; + } + return false; + } + + return false; + } + + @Override + public boolean mouseScrolled(double mouseX, double mouseY, /*? if >1.20.2 {*/ double horizontal, /*?}*/ double vertical) { + if (isMouseOver(mouseX, mouseY)) { + targetScrollAmount = Mth.clamp(targetScrollAmount - (int) vertical * 10, 0, maxScrollAmount); + lastInteractionTime = currentTimeMS(); + return true; + } + return false; + } + + @Override + public boolean keyPressed(int keyCode, int scanCode, int modifiers) { + if (isFocused()) { + switch (keyCode) { + case InputConstants.KEY_UP -> + targetScrollAmount = Mth.clamp(targetScrollAmount - 10, 0, maxScrollAmount); + case InputConstants.KEY_DOWN -> + targetScrollAmount = Mth.clamp(targetScrollAmount + 10, 0, maxScrollAmount); + default -> { + return false; + } + } + return true; + } + return false; + } + + public void tick() { + if (description != null) { + description.description().image() + .getNow(Optional.empty()) + .ifPresent(ImageRenderer::tick); + } + + float pxPerTick = AUTO_SCROLL_SPEED / 20f * font.lineHeight; + if (maxScrollAmount > 0 && currentTimeMS() - lastInteractionTime > AUTO_SCROLL_TIMER) { + if (scrollingBackward) { + pxPerTick *= -1; + if (targetScrollAmount + pxPerTick < 0) { + scrollingBackward = false; + lastInteractionTime = currentTimeMS(); + } + } else { + if (targetScrollAmount + pxPerTick > maxScrollAmount) { + scrollingBackward = true; + lastInteractionTime = currentTimeMS(); + } + } + + targetScrollAmount = Mth.clamp(targetScrollAmount + pxPerTick, 0, maxScrollAmount); + } + } + + private Style getDescStyle(int mouseX, int mouseY) { + if (!clicked(mouseX, mouseY)) + return null; + + int x = mouseX - getX(); + int y = mouseY - descriptionY; + + if (x < 0 || x > getX() + getWidth()) return null; + if (y < 0 || y > getY() + getHeight()) return null; + + int line = y / font.lineHeight; + + if (line >= wrappedText.size()) return null; + + return font.getSplitter().componentStyleAtWidth(wrappedText.get(line), x); + } + + @Override + protected void updateWidgetNarration(NarrationElementOutput builder) { + if (description != null) { + builder.add(NarratedElementType.TITLE, description.name()); + builder.add(NarratedElementType.HINT, description.description().text()); + } + + } + + public void setOptionDescription(DescriptionWithName description) { + this.description = description; + this.wrappedText = null; + this.targetScrollAmount = 0; + this.currentScrollAmount = 0; + this.lastInteractionTime = currentTimeMS(); + } + + private int currentTimeMS() { + return (int)(Blaze3D.getTime() * 1000); + } + + @Nullable + @Override + public ComponentPath nextFocusPath(FocusNavigationEvent event) { + // prevents focusing on this widget + return null; + } + +} diff --git a/src/main/java/dev/isxander/yacl3/gui/OptionListWidget.java b/src/main/java/dev/isxander/yacl3/gui/OptionListWidget.java new file mode 100644 index 0000000..f699f0c --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/gui/OptionListWidget.java @@ -0,0 +1,578 @@ +package dev.isxander.yacl3.gui; + +import com.google.common.collect.ImmutableList; +import dev.isxander.yacl3.api.*; +import dev.isxander.yacl3.api.utils.Dimension; +import dev.isxander.yacl3.impl.utils.YACLConstants; +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.MultiLineLabel; +import net.minecraft.client.gui.components.events.GuiEventListener; +import net.minecraft.client.gui.narration.NarratableEntry; +import net.minecraft.client.gui.narration.NarratedElementType; +import net.minecraft.client.gui.narration.NarrationElementOutput; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.Component; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.function.Consumer; + +public class OptionListWidget extends ElementListWidgetExt { + private final YACLScreen yaclScreen; + private final ConfigCategory category; + private ImmutableList viewableChildren; + private String searchQuery = ""; + private final Consumer hoverEvent; + private DescriptionWithName lastHoveredOption; + + public OptionListWidget(YACLScreen screen, ConfigCategory category, Minecraft client, int x, int y, int width, int height, Consumer hoverEvent) { + super(client, x, y, width, height, true); + this.yaclScreen = screen; + this.category = category; + this.hoverEvent = hoverEvent; + + refreshOptions(); + + for (OptionGroup group : category.groups()) { + if (group instanceof ListOption listOption) { + listOption.addRefreshListener(() -> refreshListEntries(listOption, category)); + } + } + } + + public void refreshOptions() { + clearEntries(); + + for (OptionGroup group : category.groups()) { + GroupSeparatorEntry groupSeparatorEntry; + if (!group.isRoot()) { + groupSeparatorEntry = group instanceof ListOption listOption + ? new ListGroupSeparatorEntry(listOption, yaclScreen) + : new GroupSeparatorEntry(group, yaclScreen); + addEntry(groupSeparatorEntry); + } else { + groupSeparatorEntry = null; + } + + List optionEntries = new ArrayList<>(); + + // add empty entry to make sure users know it's empty not just bugging out + if (groupSeparatorEntry instanceof ListGroupSeparatorEntry listGroupSeparatorEntry) { + if (listGroupSeparatorEntry.listOption.options().isEmpty()) { + EmptyListLabel emptyListLabel = new EmptyListLabel(listGroupSeparatorEntry, category); + addEntry(emptyListLabel); + optionEntries.add(emptyListLabel); + } + } + + for (Option option : group.options()) { + OptionEntry entry = new OptionEntry(option, category, group, groupSeparatorEntry, option.controller().provideWidget(yaclScreen, getDefaultEntryDimension())); + addEntry(entry); + optionEntries.add(entry); + } + + if (groupSeparatorEntry != null) { + groupSeparatorEntry.setChildEntries(optionEntries); + } + } + + recacheViewableChildren(); + setScrollAmount(0); + resetSmoothScrolling(); + } + + private void refreshListEntries(ListOption listOption, ConfigCategory category) { + // find group separator for group + ListGroupSeparatorEntry groupSeparator = super.children().stream().filter(e -> e instanceof ListGroupSeparatorEntry gs && gs.group == listOption).map(ListGroupSeparatorEntry.class::cast).findAny().orElse(null); + + if (groupSeparator == null) { + YACLConstants.LOGGER.warn("Can't find group seperator to refresh list option entries for list option " + listOption.name()); + return; + } + + for (Entry entry : groupSeparator.childEntries) + super.removeEntry(entry); + groupSeparator.childEntries.clear(); + + // if no entries, below loop won't run where addEntryBelow() recaches viewable children + if (listOption.options().isEmpty()) { + EmptyListLabel emptyListLabel; + addEntryBelow(groupSeparator, emptyListLabel = new EmptyListLabel(groupSeparator, category)); + groupSeparator.childEntries.add(emptyListLabel); + return; + } + + Entry lastEntry = groupSeparator; + for (ListOptionEntry listOptionEntry : listOption.options()) { + OptionEntry optionEntry = new OptionEntry(listOptionEntry, category, listOption, groupSeparator, listOptionEntry.controller().provideWidget(yaclScreen, getDefaultEntryDimension())); + addEntryBelow(lastEntry, optionEntry); + groupSeparator.childEntries.add(optionEntry); + lastEntry = optionEntry; + } + } + + public Dimension getDefaultEntryDimension() { + return Dimension.ofInt(getRowLeft(), 0, getRowWidth(), 20); + } + + public void expandAllGroups() { + for (Entry entry : super.children()) { + if (entry instanceof GroupSeparatorEntry groupSeparatorEntry) { + groupSeparatorEntry.setExpanded(true); + } + } + } + + @Override + public int getRowLeft() { + return super.getRowLeft() - SCROLLBAR_WIDTH; + } + + @Override + public int getRowWidth() { + return getWidth() - SCROLLBAR_WIDTH - 20; // 10 padding each side + } + + public void updateSearchQuery(String query) { + this.searchQuery = query; + expandAllGroups(); + recacheViewableChildren(); + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + for (Entry child : children()) { + if (child != getEntryAtPosition(mouseX, mouseY) && child instanceof OptionEntry optionEntry) + optionEntry.widget.unfocus(); + } + + return super.mouseClicked(mouseX, mouseY, button); + } + + @Override + public boolean mouseScrolled(double mouseX, double mouseY, /*? if >1.20.2 {*/ double horizontal, /*?}*/ double vertical) { + super.mouseScrolled(mouseX, mouseY, /*? if >1.20.2 {*/ horizontal, /*?}*/ vertical); + + for (Entry child : children()) { + if (child.mouseScrolled(mouseX, mouseY, /*? if >1.20.2 {*/ horizontal, /*?}*/ vertical)) + break; + } + + return true; + } + + @Override + public boolean keyPressed(int keyCode, int scanCode, int modifiers) { + for (Entry child : children()) { + if (child.keyPressed(keyCode, scanCode, modifiers)) + return true; + } + + return super.keyPressed(keyCode, scanCode, modifiers); + } + + @Override + public boolean charTyped(char chr, int modifiers) { + for (Entry child : children()) { + if (child.charTyped(chr, modifiers)) + return true; + } + + return super.charTyped(chr, modifiers); + } + + public void recacheViewableChildren() { + this.viewableChildren = ImmutableList.copyOf(super.children().stream().filter(Entry::isViewable).toList()); + + // update y positions before they need to be rendered are rendered + int i = 0; + for (Entry entry : viewableChildren) { + if (entry instanceof OptionEntry optionEntry) + optionEntry.widget.setDimension(optionEntry.widget.getDimension().withY(getRowTop(i))); + i++; + } + } + + @Override + public List children() { + return viewableChildren; + } + + public void addEntry(int index, Entry entry) { + super.children().add(index, entry); + recacheViewableChildren(); + } + + public void addEntryBelow(Entry below, Entry entry) { + int idx = super.children().indexOf(below) + 1; + + if (idx == 0) + throw new IllegalStateException("The entry to insert below does not exist!"); + + addEntry(idx, entry); + } + + public void addEntryBelowWithoutScroll(Entry below, Entry entry) { + double d = (double)this.getMaxScroll() - this.getScrollAmount(); + addEntryBelow(below, entry); + setScrollAmount(getMaxScroll() - d); + } + + @Override + public boolean removeEntryFromTop(Entry entry) { + boolean ret = super.removeEntryFromTop(entry); + recacheViewableChildren(); + return ret; + } + + @Override + public boolean removeEntry(Entry entry) { + boolean ret = super.removeEntry(entry); + recacheViewableChildren(); + return ret; + } + + private void setHoverDescription(DescriptionWithName description) { + if (description != lastHoveredOption) { + lastHoveredOption = description; + hoverEvent.accept(description); + } + } + + /*? if >1.20.4 {*//* + @Override + protected void renderListBackground(GuiGraphics guiGraphics) { + } + *//*?}*/ + + public abstract class Entry extends ElementListWidgetExt.Entry { + public boolean isViewable() { + return true; + } + + protected boolean isHovered() { + return Objects.equals(getHovered(), this); + } + } + + public class OptionEntry extends Entry { + public final Option option; + public final ConfigCategory category; + public final OptionGroup group; + + public final @Nullable GroupSeparatorEntry groupSeparatorEntry; + + public final AbstractWidget widget; + + private final TextScaledButtonWidget resetButton; + + private final String categoryName; + private final String groupName; + + public OptionEntry(Option option, ConfigCategory category, OptionGroup group, @Nullable GroupSeparatorEntry groupSeparatorEntry, AbstractWidget widget) { + this.option = option; + this.category = category; + this.group = group; + this.groupSeparatorEntry = groupSeparatorEntry; + this.widget = widget; + this.categoryName = category.name().getString().toLowerCase(); + this.groupName = group.name().getString().toLowerCase(); + if (option.canResetToDefault() && this.widget.canReset()) { + this.widget.setDimension(this.widget.getDimension().expanded(-20, 0)); + this.resetButton = new TextScaledButtonWidget(yaclScreen, widget.getDimension().xLimit(), -50, 20, 20, 2f, Component.literal("\u21BB"), button -> { + option.requestSetDefault(); + }); + option.addListener((opt, val) -> this.resetButton.active = !opt.isPendingValueDefault() && opt.available()); + this.resetButton.active = !option.isPendingValueDefault() && option.available(); + } else { + this.resetButton = null; + } + } + + @Override + public void render(GuiGraphics graphics, int index, int y, int x, int entryWidth, int entryHeight, int mouseX, int mouseY, boolean hovered, float tickDelta) { + widget.setDimension(widget.getDimension().withY(y)); + + widget.render(graphics, mouseX, mouseY, tickDelta); + + if (resetButton != null) { + resetButton.setY(y); + resetButton.render(graphics, mouseX, mouseY, tickDelta); + } + + if (isHovered()) { + setHoverDescription(DescriptionWithName.of(option.name(), option.description())); + } + } + + @Override + public boolean mouseScrolled(double mouseX, double mouseY, /*? if >1.20.2 {*/ double horizontal, /*?}*/ double vertical) { + return widget.mouseScrolled(mouseX, mouseY, /*? if >1.20.2 {*/ horizontal, /*?}*/ vertical); + } + + @Override + public boolean keyPressed(int keyCode, int scanCode, int modifiers) { + return widget.keyPressed(keyCode, scanCode, modifiers); + } + + @Override + public boolean charTyped(char chr, int modifiers) { + return widget.charTyped(chr, modifiers); + } + + @Override + public boolean isViewable() { + return (groupSeparatorEntry == null || groupSeparatorEntry.isExpanded()) + && (searchQuery.isEmpty() + || groupName.contains(searchQuery) + || widget.matchesSearch(searchQuery)); + } + + @Override + public int getItemHeight() { + return Math.max(widget.getDimension().height(), resetButton != null ? resetButton.getHeight() : 0) + 2; + } + + @Override + public void setFocused(boolean focused) { + super.setFocused(focused); + if (focused) + setHoverDescription(DescriptionWithName.of(option.name(), option.description())); + } + + @Override + public List narratables() { + if (resetButton == null) + return ImmutableList.of(widget); + + return ImmutableList.of(widget, resetButton); + } + + @Override + public List children() { + if (resetButton == null) + return ImmutableList.of(widget); + + return ImmutableList.of(widget, resetButton); + } + } + + public class GroupSeparatorEntry extends Entry { + protected final OptionGroup group; + protected final MultiLineLabel wrappedName; + protected final MultiLineLabel wrappedTooltip; + + protected final LowProfileButtonWidget expandMinimizeButton; + + protected final Screen screen; + protected final Font font = Minecraft.getInstance().font; + + protected boolean groupExpanded; + + protected List childEntries = new ArrayList<>(); + + private int y; + + private GroupSeparatorEntry(OptionGroup group, Screen screen) { + this.group = group; + this.screen = screen; + this.wrappedName = MultiLineLabel.create(font, group.name(), getRowWidth() - 45); + this.wrappedTooltip = MultiLineLabel.create(font, group.tooltip(), screen.width / 3 * 2 - 10); + this.groupExpanded = !group.collapsed(); + this.expandMinimizeButton = new LowProfileButtonWidget(0, 0, 20, 20, Component.empty(), btn -> onExpandButtonPress()); + updateExpandMinimizeText(); + } + + @Override + public void render(GuiGraphics graphics, int index, int y, int x, int entryWidth, int entryHeight, int mouseX, int mouseY, boolean hovered, float tickDelta) { + this.y = y; + + int buttonY = y + entryHeight / 2 - expandMinimizeButton.getHeight() / 2 + 1; + + expandMinimizeButton.setY(buttonY); + expandMinimizeButton.setX(x); + expandMinimizeButton.render(graphics, mouseX, mouseY, tickDelta); + + wrappedName.renderCentered(graphics, x + entryWidth / 2, y + getYPadding()); + + if (isHovered()) { + setHoverDescription(DescriptionWithName.of(group.name(), group.description())); + } + } + + public boolean isExpanded() { + return groupExpanded; + } + + public void setExpanded(boolean expanded) { + if (this.groupExpanded == expanded) + return; + + this.groupExpanded = expanded; + updateExpandMinimizeText(); + recacheViewableChildren(); + } + + protected void onExpandButtonPress() { + setExpanded(!isExpanded()); + } + + protected void updateExpandMinimizeText() { + expandMinimizeButton.setMessage(Component.literal(isExpanded() ? "▼" : "▶")); + } + + public void setChildEntries(List childEntries) { + this.childEntries.clear(); + this.childEntries.addAll(childEntries); + } + + @Override + public boolean isViewable() { + return searchQuery.isEmpty() || childEntries.stream().anyMatch(Entry::isViewable); + } + + @Override + public int getItemHeight() { + return Math.max(wrappedName.getLineCount(), 1) * font.lineHeight + getYPadding() * 2; + } + + private int getYPadding() { + return 6; + } + + @Override + public void setFocused(boolean focused) { + super.setFocused(focused); + if (focused) + setHoverDescription(DescriptionWithName.of(group.name(), group.description())); + } + + @Override + public List narratables() { + return ImmutableList.of(new NarratableEntry() { + @Override + public NarrationPriority narrationPriority() { + return NarrationPriority.HOVERED; + } + + @Override + public void updateNarration(NarrationElementOutput builder) { + builder.add(NarratedElementType.TITLE, group.name()); + builder.add(NarratedElementType.HINT, group.tooltip()); + } + }); + } + + @Override + public List children() { + return ImmutableList.of(expandMinimizeButton); + } + } + + public class ListGroupSeparatorEntry extends GroupSeparatorEntry { + private final ListOption listOption; + private final TextScaledButtonWidget resetListButton; + private final TooltipButtonWidget addListButton; + + private ListGroupSeparatorEntry(ListOption group, Screen screen) { + super(group, screen); + this.listOption = group; + + this.resetListButton = new TextScaledButtonWidget(screen, getRowRight() - 20, -50, 20, 20, 2f, Component.literal("\u21BB"), button -> { + group.requestSetDefault(); + }); + group.addListener((opt, val) -> this.resetListButton.active = !opt.isPendingValueDefault() && opt.available()); + this.resetListButton.active = !group.isPendingValueDefault() && group.available(); + + + this.addListButton = new TooltipButtonWidget(yaclScreen, resetListButton.getX() - 20, -50, 20, 20, Component.literal("+"), Component.translatable("yacl.list.add_top"), btn -> { + group.insertNewEntry(); + setExpanded(true); + }); + + updateExpandMinimizeText(); + minimizeIfUnavailable(); + } + + @Override + public void render(GuiGraphics graphics, int index, int y, int x, int entryWidth, int entryHeight, int mouseX, int mouseY, boolean hovered, float tickDelta) { + updateExpandMinimizeText(); // update every render because option could become available/unavailable at any time + + super.render(graphics, index, y, x, entryWidth, entryHeight, mouseX, mouseY, hovered, tickDelta); + + int buttonY = expandMinimizeButton.getY(); + + resetListButton.setY(buttonY); + addListButton.setY(buttonY); + + resetListButton.render(graphics, mouseX, mouseY, tickDelta); + addListButton.render(graphics, mouseX, mouseY, tickDelta); + } + + private void minimizeIfUnavailable() { + if (!listOption.available() && isExpanded()) { + setExpanded(false); + } + } + + @Override + protected void updateExpandMinimizeText() { + super.updateExpandMinimizeText(); + expandMinimizeButton.active = listOption == null || listOption.available(); + if (addListButton != null) + addListButton.active = expandMinimizeButton.active && listOption.numberOfEntries() < listOption.maximumNumberOfEntries(); + } + + @Override + public void setExpanded(boolean expanded) { + super.setExpanded(listOption.available() && expanded); + } + + @Override + public List children() { + return ImmutableList.of(expandMinimizeButton, addListButton, resetListButton); + } + } + + public class EmptyListLabel extends Entry { + private final ListGroupSeparatorEntry parent; + private final String groupName; + private final String categoryName; + + public EmptyListLabel(ListGroupSeparatorEntry parent, ConfigCategory category) { + this.parent = parent; + this.groupName = parent.group.name().getString().toLowerCase(); + this.categoryName = category.name().getString().toLowerCase(); + } + + @Override + public void render(GuiGraphics graphics, int index, int y, int x, int entryWidth, int entryHeight, int mouseX, int mouseY, boolean hovered, float tickDelta) { + graphics.drawCenteredString(Minecraft.getInstance().font, Component.translatable("yacl.list.empty").withStyle(ChatFormatting.DARK_GRAY, ChatFormatting.ITALIC), x + entryWidth / 2, y, -1); + } + + @Override + public boolean isViewable() { + return parent.isExpanded() && (searchQuery.isEmpty() || groupName.contains(searchQuery)); + } + + @Override + public int getItemHeight() { + return 11; + } + + @Override + public List children() { + return ImmutableList.of(); + } + + @Override + public List narratables() { + return ImmutableList.of(); + } + } +} diff --git a/src/main/java/dev/isxander/yacl3/gui/RequireRestartScreen.java b/src/main/java/dev/isxander/yacl3/gui/RequireRestartScreen.java new file mode 100644 index 0000000..5ba4b03 --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/gui/RequireRestartScreen.java @@ -0,0 +1,21 @@ +package dev.isxander.yacl3.gui; + +import net.minecraft.ChatFormatting; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.screens.ConfirmScreen; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.Component; + +public class RequireRestartScreen extends ConfirmScreen { + public RequireRestartScreen(Screen parent) { + super(option -> { + if (option) Minecraft.getInstance().stop(); + else Minecraft.getInstance().setScreen(parent); + }, + Component.translatable("yacl.restart.title").withStyle(ChatFormatting.RED, ChatFormatting.BOLD), + Component.translatable("yacl.restart.message"), + Component.translatable("yacl.restart.yes"), + Component.translatable("yacl.restart.no") + ); + } +} diff --git a/src/main/java/dev/isxander/yacl3/gui/SearchFieldWidget.java b/src/main/java/dev/isxander/yacl3/gui/SearchFieldWidget.java new file mode 100644 index 0000000..a666886 --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/gui/SearchFieldWidget.java @@ -0,0 +1,61 @@ +package dev.isxander.yacl3.gui; + +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.EditBox; +import net.minecraft.network.chat.Component; + +import java.util.function.Consumer; + +public class SearchFieldWidget extends EditBox { + private Component emptyText; + private final YACLScreen yaclScreen; + private final Font font; + private final Consumer updateConsumer; + + private boolean isEmpty = true; + + public SearchFieldWidget(YACLScreen yaclScreen, Font font, int x, int y, int width, int height, Component text, Component emptyText, Consumer updateConsumer) { + super(font, x, y, width, height, text); + setResponder(this::update); + setFilter(string -> !string.endsWith(" ") && !string.startsWith(" ")); + this.yaclScreen = yaclScreen; + this.font = font; + this.emptyText = emptyText; + this.updateConsumer = updateConsumer; + } + + @Override + public void renderWidget(GuiGraphics graphics, int mouseX, int mouseY, float delta) { + super.renderWidget(graphics, mouseX, mouseY, delta); + if (isVisible() && isEmpty()) { + graphics.drawString(font, emptyText, getX() + 4, this.getY() + (this.height - 8) / 2, 0x707070, true); + } + } + + private void update(String query) { + boolean wasEmpty = isEmpty; + isEmpty = query.isEmpty(); + + if (isEmpty && wasEmpty) + return; + + updateConsumer.accept(query); + } + + public String getQuery() { + return getValue().toLowerCase(); + } + + public boolean isEmpty() { + return isEmpty; + } + + public Component getEmptyText() { + return emptyText; + } + + public void setEmptyText(Component emptyText) { + this.emptyText = emptyText; + } +} diff --git a/src/main/java/dev/isxander/yacl3/gui/TextScaledButtonWidget.java b/src/main/java/dev/isxander/yacl3/gui/TextScaledButtonWidget.java new file mode 100644 index 0000000..6ad0d1c --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/gui/TextScaledButtonWidget.java @@ -0,0 +1,34 @@ +package dev.isxander.yacl3.gui; + +import com.mojang.blaze3d.vertex.PoseStack; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.Component; +import net.minecraft.util.Mth; + +public class TextScaledButtonWidget extends TooltipButtonWidget { + public float textScale; + + public TextScaledButtonWidget(Screen screen, int x, int y, int width, int height, float textScale, Component message, Component tooltip, OnPress onPress) { + super(screen, x, y, width, height, message, tooltip, onPress); + this.textScale = textScale; + } + + public TextScaledButtonWidget(Screen screen, int x, int y, int width, int height, float textScale, Component message, OnPress onPress) { + this(screen, x, y, width, height, textScale, message, null, onPress); + } + + @Override + public void renderString(GuiGraphics graphics, Font textRenderer, int color) { + Font font = Minecraft.getInstance().font; + PoseStack pose = graphics.pose(); + + pose.pushPose(); + pose.translate(((this.getX() + this.width / 2f) - font.width(getMessage()) * textScale / 2), (float)this.getY() + (this.height - 8 * textScale) / 2f / textScale, 0); + pose.scale(textScale, textScale, 1); + graphics.drawString(font, getMessage(), 0, 0, color | Mth.ceil(this.alpha * 255.0F) << 24, true); + pose.popPose(); + } +} diff --git a/src/main/java/dev/isxander/yacl3/gui/TooltipButtonWidget.java b/src/main/java/dev/isxander/yacl3/gui/TooltipButtonWidget.java new file mode 100644 index 0000000..f439301 --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/gui/TooltipButtonWidget.java @@ -0,0 +1,21 @@ +package dev.isxander.yacl3.gui; + +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.components.Tooltip; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.gui.screens.inventory.tooltip.ClientTooltipPositioner; +import net.minecraft.network.chat.Component; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class TooltipButtonWidget extends Button { + + protected final Screen screen; + + public TooltipButtonWidget(Screen screen, int x, int y, int width, int height, Component message, Component tooltip, OnPress onPress) { + super(x, y, width, height, message, onPress, DEFAULT_NARRATION); + this.screen = screen; + if (tooltip != null) + setTooltip(new YACLTooltip(tooltip, this)); + } +} diff --git a/src/main/java/dev/isxander/yacl3/gui/ValueFormatters.java b/src/main/java/dev/isxander/yacl3/gui/ValueFormatters.java new file mode 100644 index 0000000..988b257 --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/gui/ValueFormatters.java @@ -0,0 +1,21 @@ +package dev.isxander.yacl3.gui; + +import dev.isxander.yacl3.api.controller.ValueFormatter; +import net.minecraft.network.chat.Component; + +public final class ValueFormatters { + public static ValueFormatter percent(int decimalPlaces) { + return new PercentFormatter(decimalPlaces); + } + + public record PercentFormatter(int decimalPlaces) implements ValueFormatter { + public PercentFormatter() { + this(1); + } + + @Override + public Component format(Float value) { + return Component.literal(String.format("%." + decimalPlaces + "f%%", value * 100)); + } + } +} diff --git a/src/main/java/dev/isxander/yacl3/gui/YACLScreen.java b/src/main/java/dev/isxander/yacl3/gui/YACLScreen.java new file mode 100644 index 0000000..e88c144 --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/gui/YACLScreen.java @@ -0,0 +1,426 @@ +package dev.isxander.yacl3.gui; + +import com.mojang.blaze3d.systems.RenderSystem; +import com.mojang.blaze3d.vertex.*; +import com.mojang.math.Axis; +import dev.isxander.yacl3.api.*; +import dev.isxander.yacl3.api.utils.Dimension; +import dev.isxander.yacl3.api.utils.MutableDimension; +import dev.isxander.yacl3.api.utils.OptionUtils; +import dev.isxander.yacl3.gui.tab.ScrollableNavigationBar; +import dev.isxander.yacl3.gui.tab.ListHolderWidget; +import dev.isxander.yacl3.gui.tab.TabExt; +import dev.isxander.yacl3.gui.utils.GuiUtils; +import dev.isxander.yacl3.impl.utils.YACLConstants; +import net.minecraft.ChatFormatting; +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.Button; +import net.minecraft.client.gui.components.MultiLineLabel; +import net.minecraft.client.gui.components.Tooltip; +import net.minecraft.client.gui.components.tabs.Tab; +import net.minecraft.client.gui.components.tabs.TabManager; +import net.minecraft.client.gui.components.tabs.TabNavigationBar; +import net.minecraft.client.gui.navigation.ScreenRectangle; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.gui.screens.inventory.tooltip.TooltipRenderUtil; +import net.minecraft.client.gui.screens.worldselection.CreateWorldScreen; +import net.minecraft.client.renderer.GameRenderer; +import net.minecraft.network.chat.CommonComponents; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import org.jetbrains.annotations.Nullable; + +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Consumer; + +public class YACLScreen extends Screen { + public final YetAnotherConfigLib config; + + private final Screen parent; + + pub