From 13c7ba45ff201423eb8dba8a40cfb66ebb531439 Mon Sep 17 00:00:00 2001 From: Xander Date: Tue, 25 Apr 2023 16:28:41 +0100 Subject: Architectury! (#61) --- .../java/dev/isxander/yacl/gui/AbstractWidget.java | 107 ++++ .../dev/isxander/yacl/gui/CategoryListWidget.java | 99 ++++ .../java/dev/isxander/yacl/gui/CategoryWidget.java | 38 ++ .../isxander/yacl/gui/ElementListWidgetExt.java | 177 +++++++ .../isxander/yacl/gui/LowProfileButtonWidget.java | 28 + .../dev/isxander/yacl/gui/OptionListWidget.java | 570 +++++++++++++++++++++ .../isxander/yacl/gui/RequireRestartScreen.java | 21 + .../dev/isxander/yacl/gui/SearchFieldWidget.java | 66 +++ .../isxander/yacl/gui/TextScaledButtonWidget.java | 34 ++ .../dev/isxander/yacl/gui/TooltipButtonWidget.java | 33 ++ .../java/dev/isxander/yacl/gui/YACLScreen.java | 319 ++++++++++++ .../yacl/gui/controllers/ActionController.java | 120 +++++ .../yacl/gui/controllers/BooleanController.java | 157 ++++++ .../yacl/gui/controllers/ColorController.java | 221 ++++++++ .../yacl/gui/controllers/ControllerWidget.java | 170 ++++++ .../yacl/gui/controllers/LabelController.java | 193 +++++++ .../yacl/gui/controllers/ListEntryWidget.java | 135 +++++ .../yacl/gui/controllers/TickBoxController.java | 120 +++++ .../cycling/CyclingControllerElement.java | 60 +++ .../controllers/cycling/CyclingListController.java | 79 +++ .../gui/controllers/cycling/EnumController.java | 60 +++ .../controllers/cycling/ICyclingController.java | 38 ++ .../yacl/gui/controllers/package-info.java | 12 + .../controllers/slider/DoubleSliderController.java | 114 +++++ .../controllers/slider/FloatSliderController.java | 114 +++++ .../gui/controllers/slider/ISliderController.java | 54 ++ .../slider/IntegerSliderController.java | 111 ++++ .../controllers/slider/LongSliderController.java | 111 ++++ .../slider/SliderControllerElement.java | 164 ++++++ .../yacl/gui/controllers/slider/package-info.java | 10 + .../gui/controllers/string/IStringController.java | 44 ++ .../gui/controllers/string/StringController.java | 37 ++ .../string/StringControllerElement.java | 408 +++++++++++++++ .../string/number/DoubleFieldController.java | 104 ++++ .../string/number/FloatFieldController.java | 104 ++++ .../string/number/IntegerFieldController.java | 109 ++++ .../string/number/LongFieldController.java | 109 ++++ .../string/number/NumberFieldController.java | 69 +++ .../controllers/string/number/package-info.java | 10 + .../java/dev/isxander/yacl/gui/utils/GuiUtils.java | 41 ++ 40 files changed, 4570 insertions(+) create mode 100644 common/src/main/java/dev/isxander/yacl/gui/AbstractWidget.java create mode 100644 common/src/main/java/dev/isxander/yacl/gui/CategoryListWidget.java create mode 100644 common/src/main/java/dev/isxander/yacl/gui/CategoryWidget.java create mode 100644 common/src/main/java/dev/isxander/yacl/gui/ElementListWidgetExt.java create mode 100644 common/src/main/java/dev/isxander/yacl/gui/LowProfileButtonWidget.java create mode 100644 common/src/main/java/dev/isxander/yacl/gui/OptionListWidget.java create mode 100644 common/src/main/java/dev/isxander/yacl/gui/RequireRestartScreen.java create mode 100644 common/src/main/java/dev/isxander/yacl/gui/SearchFieldWidget.java create mode 100644 common/src/main/java/dev/isxander/yacl/gui/TextScaledButtonWidget.java create mode 100644 common/src/main/java/dev/isxander/yacl/gui/TooltipButtonWidget.java create mode 100644 common/src/main/java/dev/isxander/yacl/gui/YACLScreen.java create mode 100644 common/src/main/java/dev/isxander/yacl/gui/controllers/ActionController.java create mode 100644 common/src/main/java/dev/isxander/yacl/gui/controllers/BooleanController.java create mode 100644 common/src/main/java/dev/isxander/yacl/gui/controllers/ColorController.java create mode 100644 common/src/main/java/dev/isxander/yacl/gui/controllers/ControllerWidget.java create mode 100644 common/src/main/java/dev/isxander/yacl/gui/controllers/LabelController.java create mode 100644 common/src/main/java/dev/isxander/yacl/gui/controllers/ListEntryWidget.java create mode 100644 common/src/main/java/dev/isxander/yacl/gui/controllers/TickBoxController.java create mode 100644 common/src/main/java/dev/isxander/yacl/gui/controllers/cycling/CyclingControllerElement.java create mode 100644 common/src/main/java/dev/isxander/yacl/gui/controllers/cycling/CyclingListController.java create mode 100644 common/src/main/java/dev/isxander/yacl/gui/controllers/cycling/EnumController.java create mode 100644 common/src/main/java/dev/isxander/yacl/gui/controllers/cycling/ICyclingController.java create mode 100644 common/src/main/java/dev/isxander/yacl/gui/controllers/package-info.java create mode 100644 common/src/main/java/dev/isxander/yacl/gui/controllers/slider/DoubleSliderController.java create mode 100644 common/src/main/java/dev/isxander/yacl/gui/controllers/slider/FloatSliderController.java create mode 100644 common/src/main/java/dev/isxander/yacl/gui/controllers/slider/ISliderController.java create mode 100644 common/src/main/java/dev/isxander/yacl/gui/controllers/slider/IntegerSliderController.java create mode 100644 common/src/main/java/dev/isxander/yacl/gui/controllers/slider/LongSliderController.java create mode 100644 common/src/main/java/dev/isxander/yacl/gui/controllers/slider/SliderControllerElement.java create mode 100644 common/src/main/java/dev/isxander/yacl/gui/controllers/slider/package-info.java create mode 100644 common/src/main/java/dev/isxander/yacl/gui/controllers/string/IStringController.java create mode 100644 common/src/main/java/dev/isxander/yacl/gui/controllers/string/StringController.java create mode 100644 common/src/main/java/dev/isxander/yacl/gui/controllers/string/StringControllerElement.java create mode 100644 common/src/main/java/dev/isxander/yacl/gui/controllers/string/number/DoubleFieldController.java create mode 100644 common/src/main/java/dev/isxander/yacl/gui/controllers/string/number/FloatFieldController.java create mode 100644 common/src/main/java/dev/isxander/yacl/gui/controllers/string/number/IntegerFieldController.java create mode 100644 common/src/main/java/dev/isxander/yacl/gui/controllers/string/number/LongFieldController.java create mode 100644 common/src/main/java/dev/isxander/yacl/gui/controllers/string/number/NumberFieldController.java create mode 100644 common/src/main/java/dev/isxander/yacl/gui/controllers/string/number/package-info.java create mode 100644 common/src/main/java/dev/isxander/yacl/gui/utils/GuiUtils.java (limited to 'common/src/main/java/dev/isxander/yacl/gui') diff --git a/common/src/main/java/dev/isxander/yacl/gui/AbstractWidget.java b/common/src/main/java/dev/isxander/yacl/gui/AbstractWidget.java new file mode 100644 index 0000000..ae3c83b --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl/gui/AbstractWidget.java @@ -0,0 +1,107 @@ +package dev.isxander.yacl.gui; + +import com.mojang.blaze3d.systems.RenderSystem; +import com.mojang.blaze3d.vertex.PoseStack; +import dev.isxander.yacl.api.utils.Dimension; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.GuiComponent; +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.renderer.GameRenderer; +import net.minecraft.client.resources.sounds.SimpleSoundInstance; +import net.minecraft.sounds.SoundEvents; + +import java.awt.*; + +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 void postRender(PoseStack matrices, int mouseX, int mouseY, float delta) { + + } + + 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(PoseStack matrices, 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; + + RenderSystem.setShader(GameRenderer::getPositionTexShader); + RenderSystem.setShaderTexture(0, net.minecraft.client.gui.components.AbstractWidget.WIDGETS_LOCATION); + RenderSystem.setShaderColor(1.0F, 1.0F, 1.0F, 1.0F); + int i = !enabled ? 0 : hovered ? 2 : 1; + RenderSystem.enableBlend(); + RenderSystem.defaultBlendFunc(); + RenderSystem.enableDepthTest(); + GuiComponent.blit(matrices, x1, y1, 0, 0, 46 + i * 20, width / 2, height, 256, 256); + GuiComponent.blit(matrices, x1 + width / 2, y1, 0, 200 - width / 2f, 46 + i * 20, width / 2, height, 256, 256); + } + + 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/common/src/main/java/dev/isxander/yacl/gui/CategoryListWidget.java b/common/src/main/java/dev/isxander/yacl/gui/CategoryListWidget.java new file mode 100644 index 0000000..41286ff --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl/gui/CategoryListWidget.java @@ -0,0 +1,99 @@ +package dev.isxander.yacl.gui; + +import com.google.common.collect.ImmutableList; +import com.mojang.blaze3d.systems.RenderSystem; +import com.mojang.blaze3d.vertex.PoseStack; +import dev.isxander.yacl.api.ConfigCategory; +import dev.isxander.yacl.gui.utils.GuiUtils; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.components.events.GuiEventListener; +import net.minecraft.client.gui.narration.NarratableEntry; + +import java.util.List; + +public class CategoryListWidget extends ElementListWidgetExt { + private final YACLScreen yaclScreen; + + public CategoryListWidget(Minecraft client, YACLScreen yaclScreen, int screenWidth, int screenHeight) { + super(client, 0, 0, screenWidth / 3, yaclScreen.searchFieldWidget.getY() - 5, true); + this.yaclScreen = yaclScreen; + setRenderBackground(false); + setRenderTopAndBottom(false); + + for (ConfigCategory category : yaclScreen.config.categories()) { + addEntry(new CategoryEntry(category)); + } + } + + @Override + public void render(PoseStack matrices, int mouseX, int mouseY, float delta) { + GuiUtils.enableScissor(0, 0, width, height); + super.render(matrices, mouseX, mouseY, delta); + RenderSystem.disableScissor(); + } + + @Override + public int getRowWidth() { + return Math.min(width - width / 10, 396); + } + + @Override + public int getRowLeft() { + return super.getRowLeft() - 2; + } + + @Override + protected int getScrollbarPosition() { + return width - 2; + } + + @Override + protected void renderBackground(PoseStack matrices) { + + } + + public class CategoryEntry extends Entry { + private final CategoryWidget categoryButton; + public final int categoryIndex; + + public CategoryEntry(ConfigCategory category) { + this.categoryIndex = yaclScreen.config.categories().indexOf(category); + categoryButton = new CategoryWidget( + yaclScreen, + category, + categoryIndex, + getRowLeft(), 0, + getRowWidth(), 20 + ); + } + + @Override + public void render(PoseStack matrices, int index, int y, int x, int entryWidth, int entryHeight, int mouseX, int mouseY, boolean hovered, float tickDelta) { + if (mouseY > y1) { + mouseY = -20; + } + + categoryButton.setY(y); + categoryButton.render(matrices, mouseX, mouseY, tickDelta); + } + + public void postRender(PoseStack matrices, int mouseX, int mouseY, float tickDelta) { + categoryButton.renderHoveredTooltip(matrices); + } + + @Override + public int getItemHeight() { + return 21; + } + + @Override + public List children() { + return ImmutableList.of(categoryButton); + } + + @Override + public List narratables() { + return ImmutableList.of(categoryButton); + } + } +} diff --git a/common/src/main/java/dev/isxander/yacl/gui/CategoryWidget.java b/common/src/main/java/dev/isxander/yacl/gui/CategoryWidget.java new file mode 100644 index 0000000..60817a2 --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl/gui/CategoryWidget.java @@ -0,0 +1,38 @@ +package dev.isxander.yacl.gui; + +import dev.isxander.yacl.api.ConfigCategory; +import net.minecraft.client.sounds.SoundManager; + +public class CategoryWidget extends TooltipButtonWidget { + private final int categoryIndex; + + public CategoryWidget(YACLScreen screen, ConfigCategory category, int categoryIndex, int x, int y, int width, int height) { + super(screen, x, y, width, height, category.name(), category.tooltip(), btn -> { + screen.searchFieldWidget.setValue(""); + screen.changeCategory(categoryIndex); + }); + this.categoryIndex = categoryIndex; + } + + private boolean isCurrentCategory() { + return ((YACLScreen) screen).getCurrentCategoryIdx() == categoryIndex; + } + + @Override + protected int getTextureY() { + int i = 1; + if (!this.active) { + i = 0; + } else if (this.isHoveredOrFocused() || isCurrentCategory()) { + i = 2; + } + + return 46 + i * 20; + } + + @Override + public void playDownSound(SoundManager soundManager) { + if (!isCurrentCategory()) + super.playDownSound(soundManager); + } +} diff --git a/common/src/main/java/dev/isxander/yacl/gui/ElementListWidgetExt.java b/common/src/main/java/dev/isxander/yacl/gui/ElementListWidgetExt.java new file mode 100644 index 0000000..b177236 --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl/gui/ElementListWidgetExt.java @@ -0,0 +1,177 @@ +package dev.isxander.yacl.gui; + +import com.mojang.blaze3d.platform.InputConstants; +import com.mojang.blaze3d.vertex.PoseStack; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.components.ContainerObjectSelectionList; +import net.minecraft.client.gui.components.events.GuiEventListener; +import net.minecraft.util.Mth; +import org.jetbrains.annotations.Nullable; + +public class ElementListWidgetExt> extends ContainerObjectSelectionList { + protected final int x, y; + + private double smoothScrollAmount = getScrollAmount(); + private boolean returnSmoothAmount = false; + private final boolean doSmoothScrolling; + + public ElementListWidgetExt(Minecraft client, int x, int y, int width, int height, boolean smoothScrolling) { + super(client, width, height, y, y + height, 22); + this.x = this.x0 = x; + this.y = y; + this.x1 = this.x0 + width; + this.doSmoothScrolling = smoothScrolling; + } + + @Override + public boolean mouseScrolled(double mouseX, double mouseY, double amount) { + // default implementation bases scroll step from total height of entries, this is constant + this.setScrollAmount(this.getScrollAmount() - amount * 20); + return true; + } + + @Override + protected void renderBackground(PoseStack matrices) { + // render transparent background if in-game. + setRenderBackground(minecraft.level == null); + if (minecraft.level != null) + fill(matrices, x0, y0, x1, y1, 0x6B000000); + } + + @Override + protected int getScrollbarPosition() { + // default implementation does not respect left/right + return this.x1 - 2; + } + + @Override + public void render(PoseStack matrices, int mouseX, int mouseY, float delta) { + smoothScrollAmount = Mth.lerp(Minecraft.getInstance().getDeltaFrameTime() * 0.5, smoothScrollAmount, getScrollAmount()); + returnSmoothAmount = true; + super.render(matrices, mouseX, mouseY, delta); + returnSmoothAmount = false; + } + + /** + * 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 = getScrollAmount(); + } + + public void postRender(PoseStack matrices, int mouseX, int mouseY, float delta) { + for (E entry : children()) { + entry.postRender(matrices, mouseX, mouseY, delta); + } + } + + @Nullable + @Override + protected E getEntryAtPosition(double x, double y) { + y += getScrollAmount(); + + if (x < this.x0 || x > this.x1) + return null; + + int currentY = this.y0 - 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 = y0 + 4 - (int) this.getScrollAmount() + headerHeight; + for (int i = 0; i < children().size() && i < index; i++) + integer += children().get(i).getItemHeight(); + return integer; + } + + @Override + protected void renderList(PoseStack matrices, 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.y0 && top <= this.y1) { + this.renderItem(matrices, mouseX, mouseY, delta, i, left, top, right, entryHeight); + } + } + } + + /* END cloth config code */ + + 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 void postRender(PoseStack matrices, int mouseX, int mouseY, float delta) { + + } + + public int getItemHeight() { + return 22; + } + } +} diff --git a/common/src/main/java/dev/isxander/yacl/gui/LowProfileButtonWidget.java b/common/src/main/java/dev/isxander/yacl/gui/LowProfileButtonWidget.java new file mode 100644 index 0000000..e8bf59f --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl/gui/LowProfileButtonWidget.java @@ -0,0 +1,28 @@ +package dev.isxander.yacl.gui; + +import com.mojang.blaze3d.vertex.PoseStack; +import net.minecraft.client.Minecraft; +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(PoseStack matrices, int mouseX, int mouseY, float deltaTicks) { + if (!isHoveredOrFocused() || !active) { + int j = this.active ? 0xFFFFFF : 0xA0A0A0; + this.renderString(matrices, Minecraft.getInstance().font, j); + } else { + super.renderWidget(matrices, mouseX, mouseY, deltaTicks); + } + } +} diff --git a/common/src/main/java/dev/isxander/yacl/gui/OptionListWidget.java b/common/src/main/java/dev/isxander/yacl/gui/OptionListWidget.java new file mode 100644 index 0000000..a73ce43 --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl/gui/OptionListWidget.java @@ -0,0 +1,570 @@ +package dev.isxander.yacl.gui; + +import com.google.common.collect.ImmutableList; +import com.mojang.blaze3d.vertex.PoseStack; +import dev.isxander.yacl.api.*; +import dev.isxander.yacl.api.utils.Dimension; +import dev.isxander.yacl.impl.utils.YACLConstants; +import net.minecraft.ChatFormatting; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Font; +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; + +public class OptionListWidget extends ElementListWidgetExt { + private final YACLScreen yaclScreen; + private boolean singleCategory = false; + + private ImmutableList viewableChildren; + + public OptionListWidget(YACLScreen screen, Minecraft client, int width, int height) { + super(client, width / 3, 0, width / 3 * 2 + 1, height, true); + this.yaclScreen = screen; + + refreshOptions(); + + for (ConfigCategory category : screen.config.categories()) { + for (OptionGroup group : category.groups()) { + if (group instanceof ListOption listOption) { + listOption.addRefreshListener(() -> refreshListEntries(listOption, category)); + } + } + } + } + + public void refreshOptions() { + clearEntries(); + + List categories = new ArrayList<>(); + if (yaclScreen.getCurrentCategoryIdx() == -1) { + // -1 = no category, search in progress, so use all categories for search + categories.addAll(yaclScreen.config.categories()); + } else { + categories.add(yaclScreen.config.categories().get(yaclScreen.getCurrentCategoryIdx())); + } + singleCategory = categories.size() == 1; + + for (ConfigCategory category : categories) { + 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 getRowWidth() { + return Math.min(396, (int)(width / 1.3f)); + } + + @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, double amount) { + super.mouseScrolled(mouseX, mouseY, amount); + + for (Entry child : children()) { + if (child.mouseScrolled(mouseX, mouseY, amount)) + 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); + } + + @Override + protected int getScrollbarPosition() { + return x1 - (int)(width * 0.05f); + } + + 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; + } + + 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(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(PoseStack matrices, 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(matrices, mouseX, mouseY, tickDelta); + + if (resetButton != null) { + resetButton.setY(y); + resetButton.render(matrices, mouseX, mouseY, tickDelta); + } + } + + @Override + public void postRender(PoseStack matrices, int mouseX, int mouseY, float delta) { + widget.postRender(matrices, mouseX, mouseY, delta); + } + + @Override + public boolean mouseScrolled(double mouseX, double mouseY, double amount) { + return widget.mouseScrolled(mouseX, mouseY, amount); + } + + @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() { + String query = yaclScreen.searchFieldWidget.getQuery(); + return (groupSeparatorEntry == null || groupSeparatorEntry.isExpanded()) + && (yaclScreen.searchFieldWidget.isEmpty() + || (!singleCategory && categoryName.contains(query)) + || groupName.contains(query) + || widget.matchesSearch(query)); + } + + @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); + } + + @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(PoseStack matrices, 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(matrices, mouseX, mouseY, tickDelta); + + wrappedName.renderCentered(matrices, x + entryWidth / 2, y + getYPadding()); + } + + @Override + public void postRender(PoseStack matrices, int mouseX, int mouseY, float delta) { + if ((isHovered() && !expandMinimizeButton.isMouseOver(mouseX, mouseY)) || expandMinimizeButton.isFocused()) { + YACLScreen.renderMultilineTooltip(matrices, font, wrappedTooltip, getRowLeft() + getRowWidth() / 2, y - 3, y + getItemHeight() + 3, screen.width, screen.height); + } + } + + 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 yaclScreen.searchFieldWidget.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 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(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.insertNewEntryToTop(); + setExpanded(true); + }); + + updateExpandMinimizeText(); + minimizeIfUnavailable(); + } + + @Override + public void render(PoseStack matrices, 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(matrices, index, y, x, entryWidth, entryHeight, mouseX, mouseY, hovered, tickDelta); + + int buttonY = expandMinimizeButton.getY(); + + resetListButton.setY(buttonY); + addListButton.setY(buttonY); + + resetListButton.render(matrices, mouseX, mouseY, tickDelta); + addListButton.render(matrices, mouseX, mouseY, tickDelta); + } + + @Override + public void postRender(PoseStack matrices, int mouseX, int mouseY, float delta) { + minimizeIfUnavailable(); // cannot run in render because it *should* cause a ConcurrentModificationException (but doesn't) + + super.postRender(matrices, mouseX, mouseY, delta); + + addListButton.renderHoveredTooltip(matrices); + } + + 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; + } + + @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(PoseStack matrices, int index, int y, int x, int entryWidth, int entryHeight, int mouseX, int mouseY, boolean hovered, float tickDelta) { + drawCenteredString(matrices, Minecraft.getInstance().font, Component.translatable("yacl.list.empty").withStyle(ChatFormatting.DARK_GRAY, ChatFormatting.ITALIC), x + entryWidth / 2, y, -1); + } + + @Override + public boolean isViewable() { + String query = yaclScreen.searchFieldWidget.getQuery(); + return parent.isExpanded() && (yaclScreen.searchFieldWidget.isEmpty() + || (!singleCategory && categoryName.contains(query)) + || groupName.contains(query)); + } + + @Override + public int getItemHeight() { + return 11; + } + + @Override + public List children() { + return ImmutableList.of(); + } + + @Override + public List narratables() { + return ImmutableList.of(); + } + } +} diff --git a/common/src/main/java/dev/isxander/yacl/gui/RequireRestartScreen.java b/common/src/main/java/dev/isxander/yacl/gui/RequireRestartScreen.java new file mode 100644 index 0000000..18b6033 --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl/gui/RequireRestartScreen.java @@ -0,0 +1,21 @@ +package dev.isxander.yacl.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/common/src/main/java/dev/isxander/yacl/gui/SearchFieldWidget.java b/common/src/main/java/dev/isxander/yacl/gui/SearchFieldWidget.java new file mode 100644 index 0000000..5cf38e0 --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl/gui/SearchFieldWidget.java @@ -0,0 +1,66 @@ +package dev.isxander.yacl.gui; + +import com.mojang.blaze3d.vertex.PoseStack; +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.components.EditBox; +import net.minecraft.network.chat.Component; + +public class SearchFieldWidget extends EditBox { + private Component emptyText; + private final YACLScreen yaclScreen; + private final Font font; + + private boolean isEmpty = true; + + public SearchFieldWidget(YACLScreen yaclScreen, Font font, int x, int y, int width, int height, Component text, Component emptyText) { + super(font, x, y, width, height, text); + setResponder(string -> update()); + setFilter(string -> !string.endsWith(" ") && !string.startsWith(" ")); + this.yaclScreen = yaclScreen; + this.font = font; + this.emptyText = emptyText; + } + + @Override + public void renderWidget(PoseStack matrices, int mouseX, int mouseY, float delta) { + super.renderWidget(matrices, mouseX, mouseY, delta); + if (isVisible() && isEmpty()) { + font.drawShadow(matrices, emptyText, getX() + 4, this.getY() + (this.height - 8) / 2f, 0x707070); + } + } + + private void update() { + boolean wasEmpty = isEmpty; + isEmpty = getValue().isEmpty(); + + if (isEmpty && wasEmpty) + return; + + if (!isEmpty && yaclScreen.getCurrentCategoryIdx() != -1) + yaclScreen.changeCategory(-1); + if (isEmpty && yaclScreen.getCurrentCategoryIdx() == -1) + yaclScreen.changeCategory(0); + + yaclScreen.optionList.expandAllGroups(); + yaclScreen.optionList.recacheViewableChildren(); + + yaclScreen.optionList.setScrollAmount(0); + yaclScreen.categoryList.setScrollAmount(0); + } + + 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/common/src/main/java/dev/isxander/yacl/gui/TextScaledButtonWidget.java b/common/src/main/java/dev/isxander/yacl/gui/TextScaledButtonWidget.java new file mode 100644 index 0000000..b955912 --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl/gui/TextScaledButtonWidget.java @@ -0,0 +1,34 @@ +package dev.isxander.yacl.gui; + +import com.mojang.blaze3d.vertex.PoseStack; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.components.Tooltip; +import net.minecraft.network.chat.Component; +import net.minecraft.util.Mth; + +public class TextScaledButtonWidget extends Button { + public float textScale; + + public TextScaledButtonWidget(int x, int y, int width, int height, float textScale, Component message, OnPress onPress) { + super(x, y, width, height, message, onPress, DEFAULT_NARRATION); + this.textScale = textScale; + } + + public TextScaledButtonWidget(int x, int y, int width, int height, float textScale, Component message, OnPress onPress, Tooltip tooltip) { + this(x, y, width, height, textScale, message, onPress); + setTooltip(tooltip); + } + + @Override + public void renderString(PoseStack matrices, Font textRenderer, int color) { + Font font = Minecraft.getInstance().font; + + matrices.pushPose(); + matrices.translate(((this.getX() + this.width / 2f) - font.width(getMessage()) * textScale / 2), (float)this.getY() + (this.height - 8 * textScale) / 2f / textScale, 0); + matrices.scale(textScale, textScale, 1); + font.drawShadow(matrices, getMessage(), 0, 0, color | Mth.ceil(this.alpha * 255.0F) << 24); + matrices.popPose(); + } +} diff --git a/common/src/main/java/dev/isxander/yacl/gui/TooltipButtonWidget.java b/common/src/main/java/dev/isxander/yacl/gui/TooltipButtonWidget.java new file mode 100644 index 0000000..3b5b6fc --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl/gui/TooltipButtonWidget.java @@ -0,0 +1,33 @@ +package dev.isxander.yacl.gui; + +import com.mojang.blaze3d.vertex.PoseStack; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.components.MultiLineLabel; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.Component; + +public class TooltipButtonWidget extends TextScaledButtonWidget { + + protected final Screen screen; + protected MultiLineLabel wrappedDescription; + + public TooltipButtonWidget(Screen screen, int x, int y, int width, int height, Component message, float textScale, Component tooltip, OnPress onPress) { + super(x, y, width, height, textScale, message, onPress); + this.screen = screen; + setTooltip(tooltip); + } + + public TooltipButtonWidget(Screen screen, int x, int y, int width, int height, Component message, Component tooltip, OnPress onPress) { + this(screen, x, y, width, height, message, 1, tooltip, onPress); + } + + public void renderHoveredTooltip(PoseStack matrices) { + if (isHoveredOrFocused()) { + YACLScreen.renderMultilineTooltip(matrices, Minecraft.getInstance().font, wrappedDescription, getX() + width / 2, getY() - 4, getY() + height + 4, screen.width, screen.height); + } + } + + public void setTooltip(Component tooltip) { + wrappedDescription = MultiLineLabel.create(Minecraft.getInstance().font, tooltip, screen.width / 3 - 5); + } +} diff --git a/common/src/main/java/dev/isxander/yacl/gui/YACLScreen.java b/common/src/main/java/dev/isxander/yacl/gui/YACLScreen.java new file mode 100644 index 0000000..3600e61 --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl/gui/YACLScreen.java @@ -0,0 +1,319 @@ +package dev.isxander.yacl.gui; + +import com.mojang.blaze3d.systems.RenderSystem; +import com.mojang.blaze3d.vertex.*; +import dev.isxander.yacl.api.Option; +import dev.isxander.yacl.api.OptionFlag; +import dev.isxander.yacl.api.PlaceholderCategory; +import dev.isxander.yacl.api.YetAnotherConfigLib; +import dev.isxander.yacl.api.utils.Dimension; +import dev.isxander.yacl.api.utils.MutableDimension; +import dev.isxander.yacl.api.utils.OptionUtils; +import dev.isxander.yacl.gui.utils.GuiUtils; +import dev.isxander.yacl.impl.utils.YACLConstants; +import net.minecraft.ChatFormatting; +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.GuiComponent; +import net.minecraft.client.gui.components.MultiLineLabel; +import net.minecraft.client.gui.components.events.GuiEventListener; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.gui.screens.inventory.tooltip.TooltipRenderUtil; +import net.minecraft.client.renderer.GameRenderer; +import net.minecraft.network.chat.CommonComponents; +import net.minecraft.network.chat.Component; +import org.joml.Matrix4f; + +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; + +public class YACLScreen extends Screen { + public final YetAnotherConfigLib config; + private int currentCategoryIdx; + + private final Screen parent; + + public OptionListWidget optionList; + public CategoryListWidget categoryList; + public TooltipButtonWidget finishedSaveButton, cancelResetButton, undoButton; + public SearchFieldWidget searchFieldWidget; + + public Component saveButtonMessage, saveButtonTooltipMessage; + private int saveButtonMessageTime; + + + public YACLScreen(YetAnotherConfigLib config, Screen parent) { + super(config.title()); + this.config = config; + this.parent = parent; + this.currentCategoryIdx = 0; + } + + @Override + protected void init() { + int columnWidth = width / 3; + int padding = columnWidth / 20; + columnWidth = Math.min(columnWidth, 400); + int paddedWidth = columnWidth - padding * 2; + + MutableDimension actionDim = Dimension.ofInt(width / 3 / 2, height - padding - 20, paddedWidth, 20); + finishedSaveButton = new TooltipButtonWidget( + this, + actionDim.x() - actionDim.width() / 2, + actionDim.y(), + actionDim.width(), + actionDim.height(), + Component.empty(), + Component.empty(), + btn -> finishOrSave() + ); + actionDim.expand(-actionDim.width() / 2 - 2, 0).move(-actionDim.width() / 2 - 2, -22); + cancelResetButton = new TooltipButtonWidget( + this, + actionDim.x() - actionDim.width() / 2, + actionDim.y(), + actionDim.width(), + actionDim.height(), + Component.empty(), + Component.empty(), + btn -> cancelOrReset() + ); + actionDim.move(actionDim.width() + 4, 0); + undoButton = new TooltipButtonWidget( + this, + actionDim.x() - actionDim.width() / 2, + actionDim.y(), + actionDim.width(), + actionDim.height(), + Component.translatable("yacl.gui.undo"), + Component.translatable("yacl.gui.undo.tooltip"), + btn -> undo() + ); + + searchFieldWidget = new SearchFieldWidget( + this, + font, + width / 3 / 2 - paddedWidth / 2 + 1, + undoButton.getY() - 22, + paddedWidth - 2, 18, + Component.translatable("gui.recipebook.search_hint"), + Component.translatable("gui.recipebook.search_hint") + ); + + categoryList = new CategoryListWidget(minecraft, this, width, height); + addWidget(categoryList); + + updateActionAvailability(); + addRenderableWidget(searchFieldWidget); + addRenderableWidget(cancelResetButton); + addRenderableWidget(undoButton); + addRenderableWidget(finishedSaveButton); + + optionList = new OptionListWidget(this, minecraft, width, height); + addWidget(optionList); + + config.initConsumer().accept(this); + } + + @Override + public void render(PoseStack matrices, int mouseX, int mouseY, float delta) { + renderBackground(matrices); + + super.render(matrices, mouseX, mouseY, delta); + categoryList.render(matrices, mouseX, mouseY, delta); + searchFieldWidget.render(matrices, mouseX, mouseY, delta); + optionList.render(matrices, mouseX, mouseY, delta); + + categoryList.postRender(matrices, mouseX, mouseY, delta); + optionList.postRender(matrices, mouseX, mouseY, delta); + + for (GuiEventListener child : children()) { + if (child instanceof TooltipButtonWidget tooltipButtonWidget) { + tooltipButtonWidget.renderHoveredTooltip(matrices); + } + } + } + + protected void finishOrSave() { + saveButtonMessage = null; + + if (pendingChanges()) { + Set flags = new HashSet<>(); + OptionUtils.forEachOptions(config, option -> { + if (option.applyValue()) { + flags.addAll(option.flags()); + } + }); + OptionUtils.forEachOptions(config, option -> { + if (option.changed()) { + // if still changed after applying, reset to the current value from binding + // as something has gone wrong. + option.forgetPendingValue(); + YACLConstants.LOGGER.error("Option '{}' value mismatch after applying! Reset to binding's getter.", option.name().getString()); + } + }); + config.saveFunction().run(); + + flags.forEach(flag -> flag.accept(minecraft)); + } else onClose(); + } + + protected void cancelOrReset() { + if (pendingChanges()) { // if pending changes, button acts as a cancel button + OptionUtils.forEachOptions(config, Option::forgetPendingValue); + onClose(); + } else { // if not, button acts as a reset button + OptionUtils.forEachOptions(config, Option::requestSetDefault); + } + } + + protected void undo() { + OptionUtils.forEachOptions(config, Option::forgetPendingValue); + } + + @Override + public boolean keyPressed(int keyCode, int scanCode, int modifiers) { + if (optionList.keyPressed(keyCode, scanCode, modifiers)) { + return true; + } + + return super.keyPressed(keyCode, scanCode, modifiers); + } + + @Override + public boolean charTyped(char chr, int modifiers) { + if (optionList.charTyped(chr, modifiers)) { + return true; + } + + return super.charTyped(chr, modifiers); + } + + public void changeCategory(int idx) { + if (idx == currentCategoryIdx) + return; + + if (idx != -1 && config.categories().get(idx) instanceof PlaceholderCategory placeholderCategory) { + minecraft.setScreen(placeholderCategory.screen().apply(minecraft, this)); + } else { + currentCategoryIdx = idx; + optionList.refreshOptions(); + } + } + + public int getCurrentCategoryIdx() { + return currentCategoryIdx; + } + + private void updateActionAvailability() { + boolean pendingChanges = pendingChanges(); + + undoButton.active = pendingChanges; + finishedSaveButton.setMessage(pendingChanges ? Component.translatable("yacl.gui.save") : GuiUtils.translatableFallback("yacl.gui.done", CommonComponents.GUI_DONE)); + finishedSaveButton.setTooltip(pendingChanges ? Component.translatable("yacl.gui.save.tooltip") : Component.translatable("yacl.gui.finished.tooltip")); + cancelResetButton.setMessage(pendingChanges ? GuiUtils.translatableFallback("yacl.gui.cancel", CommonComponents.GUI_CANCEL) : Component.translatable("controls.reset")); + cancelResetButton.setTooltip(pendingChanges ? Component.translatable("yacl.gui.cancel.tooltip") : Component.translatable("yacl.gui.reset.tooltip")); + } + + @Override + public void tick() { + searchFieldWidget.tick(); + + updateActionAvailability(); + + if (saveButtonMessage != null) { + if (saveButtonMessageTime > 140) { + saveButtonMessage = null; + saveButtonTooltipMessage = null; + saveButtonMessageTime = 0; + } else { + saveButtonMessageTime++; + finishedSaveButton.setMessage(saveButtonMessage); + if (saveButtonTooltipMessage != null) { + finishedSaveButton.setTooltip(saveButtonTooltipMessage); + } + } + } + } + + private void setSaveButtonMessage(Component message, Component tooltip) { + saveButtonMessage = message; + saveButtonTooltipMessage = tooltip; + saveButtonMessageTime = 0; + } + + private boolean pendingChanges() { + AtomicBoolean pendingChanges = new AtomicBoolean(false); + OptionUtils.consumeOptions(config, (option) -> { + if (option.changed()) { + pendingChanges.set(true); + return true; + } + return false; + }); + + return pendingChanges.get(); + } + + @Override + public boolean shouldCloseOnEsc() { + if (pendingChanges()) { + setSaveButtonMessage(Component.translatable("yacl.gui.save_before_exit").withStyle(ChatFormatting.RED), Component.translatable("yacl.gui.save_before_exit.tooltip")); + return false; + } + return true; + } + + @Override + public void onClose() { + minecraft.setScreen(parent); + } + + public static void renderMultilineTooltip(PoseStack matrices, Font font, MultiLineLabel text, int centerX, int yAbove, int yBelow, int screenWidth, int screenHeight) { + if (text.getLineCount() > 0) { + int maxWidth = text.getWidth(); + int lineHeight = font.lineHeight + 1; + int height = text.getLineCount() * lineHeight - 1; + + int belowY = yBelow + 12; + int aboveY = yAbove - height + 12; + int maxBelow = screenHeight - (belowY + height); + int minAbove = aboveY - height; + int y = aboveY; + if (minAbove < 8) + y = maxBelow > minAbove ? belowY : aboveY; + + int x = Math.max(centerX - text.getWidth() / 2 - 12, -6); + + int drawX = x + 12; + int drawY = y - 12; + + matrices.pushPose(); + Tesselator tesselator = Tesselator.getInstance(); + BufferBuilder bufferBuilder = tesselator.getBuilder(); + RenderSystem.setShader(GameRenderer::getPositionColorShader); + bufferBuilder.begin(VertexFormat.Mode.QUADS, DefaultVertexFormat.POSITION_COLOR); + Matrix4f matrix4f = matrices.last().pose(); + TooltipRenderUtil.renderTooltipBackground( + GuiComponent::fillGradient, + matrix4f, + bufferBuilder, + drawX, + drawY, +