From b251d1f444859f960fddf8a2c04ae965874c93d8 Mon Sep 17 00:00:00 2001 From: NotNekodev <97458908+NotNekodev@users.noreply.github.com> Date: Sun, 30 Nov 2025 14:20:47 +0100 Subject: [PATCH 01/27] Add undo button in VOptionScreen.java --- .../vulkanmod/config/gui/VOptionScreen.java | 37 +++++++++++++++++-- .../net/vulkanmod/config/option/Option.java | 16 +++++++- .../vulkanmod/config/option/OptionPage.java | 18 ++++++++- .../assets/vulkanmod/lang/en_us.json | 1 + 4 files changed, 66 insertions(+), 6 deletions(-) diff --git a/src/main/java/net/vulkanmod/config/gui/VOptionScreen.java b/src/main/java/net/vulkanmod/config/gui/VOptionScreen.java index fcf879d71e..b6d2f11014 100644 --- a/src/main/java/net/vulkanmod/config/gui/VOptionScreen.java +++ b/src/main/java/net/vulkanmod/config/gui/VOptionScreen.java @@ -41,10 +41,12 @@ public class VOptionScreen extends Screen { private VButtonWidget doneButton; private VButtonWidget applyButton; + private VButtonWidget undoButton; private final List pageButtons = Lists.newArrayList(); private final List buttons = Lists.newArrayList(); + public VOptionScreen(Component title, Screen parent) { super(title); this.parent = parent; @@ -83,20 +85,19 @@ private void addPages() { @Override protected void init() { this.addPages(); + this.captureOriginalState(); int top = 40; int bottom = 60; int itemHeight = 20; int leftMargin = 100; -// int listWidth = (int) (this.width * 0.65f); int listWidth = Math.min((int) (this.width * 0.65f), 420); int listHeight = this.height - top - bottom; this.buildLists(leftMargin, top, listWidth, listHeight, itemHeight); int x = leftMargin + listWidth + 10; -// int width = Math.min(this.width - this.tooltipX - 10, 200); int width = this.width - x - 10; int y = 50; @@ -113,6 +114,21 @@ protected void init() { buildPage(); this.applyButton.active = false; + this.undoButton.active = false; + } + + private void captureOriginalState() { + for (OptionPage page : this.optionPages) { + page.captureOriginalState(); + } + } + + private void undo() { + for (OptionPage page : this.optionPages) { + page.resetToOriginalState(); + } + + buildPage(); } private void buildLists(int left, int top, int listWidth, int listHeight, int itemHeight) { @@ -146,7 +162,6 @@ private void buildPage() { this.pageButtons.clear(); this.clearWidgets(); -// this.addPageButtons(20, 6, 60, 20, false); this.addPageButtons(10, 40, 80, 22, true); VOptionList currentList = this.optionPages.get(this.currentListIdx).getOptionList(); @@ -180,6 +195,17 @@ private void addButtons() { button -> this.applyOptions() ); + buttonWidth = minecraft.font.width(Component.translatable("vulkanmod.options.buttons.undo")) + 2 * padding; + x0 -= (buttonWidth + buttonMargin); + this.undoButton = new VButtonWidget( + x0, y0, + buttonWidth, buttonHeight, + Component.translatable("vulkanmod.options.buttons.undo"), + button -> { + undo(); + } + ); + buttonWidth = minecraft.font.width(Component.translatable("vulkanmod.options.buttons.kofi")) + 10; x0 = (this.width - buttonWidth - rightMargin); this.supportButton = new VButtonWidget( @@ -192,10 +218,12 @@ private void addButtons() { this.buttons.add(this.applyButton); this.buttons.add(this.doneButton); this.buttons.add(this.supportButton); + this.buttons.add(this.undoButton); this.addWidget(this.applyButton); this.addWidget(this.doneButton); this.addWidget(this.supportButton); + this.addWidget(this.undoButton); } @Override @@ -292,6 +320,7 @@ private void updateState() { } this.applyButton.active = modified; + this.undoButton.active = modified; } private void setOptionList(int i) { @@ -310,4 +339,4 @@ private void applyOptions() { Initializer.CONFIG.write(); } -} +} \ No newline at end of file diff --git a/src/main/java/net/vulkanmod/config/option/Option.java b/src/main/java/net/vulkanmod/config/option/Option.java index a1425ccf80..274c4cac3a 100644 --- a/src/main/java/net/vulkanmod/config/option/Option.java +++ b/src/main/java/net/vulkanmod/config/option/Option.java @@ -16,6 +16,7 @@ public abstract class Option { protected T value; protected T newValue; + protected T originalValue; protected Function translator; @@ -91,6 +92,19 @@ public void apply() { this.value = this.newValue; } + public void captureOriginalState() { + this.originalValue = this.value; + } + + public void resetToOriginalState() { + if (this.originalValue != null) { + this.newValue = this.originalValue; + + if (onChange != null) + onChange.run(); + } + } + public T getNewValue() { return this.newValue; } @@ -107,4 +121,4 @@ public Option setTooltip(Component text) { public Component getTooltip() { return this.tooltip; } -} +} \ No newline at end of file diff --git a/src/main/java/net/vulkanmod/config/option/OptionPage.java b/src/main/java/net/vulkanmod/config/option/OptionPage.java index 8d783fe4e1..069ebef856 100644 --- a/src/main/java/net/vulkanmod/config/option/OptionPage.java +++ b/src/main/java/net/vulkanmod/config/option/OptionPage.java @@ -41,4 +41,20 @@ public void applyOptionChanges() { } } } -} + + public void captureOriginalState() { + for (var block : this.optionBlocks) { + for (var option : block.options()) { + option.captureOriginalState(); + } + } + } + + public void resetToOriginalState() { + for (var block : this.optionBlocks) { + for (var option : block.options()) { + option.resetToOriginalState(); + } + } + } +} \ No newline at end of file diff --git a/src/main/resources/assets/vulkanmod/lang/en_us.json b/src/main/resources/assets/vulkanmod/lang/en_us.json index 09be191c71..167bcd7c0c 100644 --- a/src/main/resources/assets/vulkanmod/lang/en_us.json +++ b/src/main/resources/assets/vulkanmod/lang/en_us.json @@ -7,6 +7,7 @@ "vulkanmod.options.pages.other": "Other", "vulkanmod.options.buttons.apply": "Apply", + "vulkanmod.options.buttons.undo": "Undo", "vulkanmod.options.buttons.kofi": "Support me", "vulkanmod.options.advCulling": "Advanced Chunk Culling", From b2e7a4af4e3a4e286f0679512c5fdd219bd9f72a Mon Sep 17 00:00:00 2001 From: NotNekodev <97458908+NotNekodev@users.noreply.github.com> Date: Sun, 30 Nov 2025 14:41:31 +0100 Subject: [PATCH 02/27] Change position of tooltips to be slightly below the option and aligned to the right edge of the option --- .../vulkanmod/config/gui/VOptionScreen.java | 34 ++++++++----------- 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/src/main/java/net/vulkanmod/config/gui/VOptionScreen.java b/src/main/java/net/vulkanmod/config/gui/VOptionScreen.java index b6d2f11014..453fb1b814 100644 --- a/src/main/java/net/vulkanmod/config/gui/VOptionScreen.java +++ b/src/main/java/net/vulkanmod/config/gui/VOptionScreen.java @@ -33,8 +33,6 @@ public class VOptionScreen extends Screen { private int currentListIdx = 0; - private int tooltipX; - private int tooltipY; private int tooltipWidth; private VButtonWidget supportButton; @@ -99,16 +97,11 @@ protected void init() { int x = leftMargin + listWidth + 10; int width = this.width - x - 10; - int y = 50; if (width < 200) { - x = 100; width = listWidth; - y = this.height - bottom + 10; } - this.tooltipX = x; - this.tooltipY = y; this.tooltipWidth = width; buildPage(); @@ -271,9 +264,16 @@ public void render(GuiGraphics guiGraphics, int mouseX, int mouseY, float delta) currentList.renderWidget(mouseX, mouseY); renderButtons(mouseX, mouseY); - List list = getHoveredButtonTooltip(currentList, mouseX, mouseY); - if (list != null) { - this.renderTooltip(list, this.tooltipX, this.tooltipY); + VAbstractWidget hoveredWidget = currentList.getHoveredWidget(mouseX, mouseY); + if (hoveredWidget != null) { + List tooltip = getWidgetTooltip(hoveredWidget); + if (tooltip != null) { + int padding = 3; + int tooltipWidth = GuiRenderer.getMaxTextWidth(this.font, tooltip); + int tooltipX = hoveredWidget.getX() + hoveredWidget.getWidth() - tooltipWidth - padding; + int tooltipY = hoveredWidget.getY() + hoveredWidget.getHeight() + 3 + 1; // 3 is the padding inside of renderTooltip and 1 is the custom padding + this.renderTooltip(tooltip, tooltipX, tooltipY); + } } } @@ -301,16 +301,12 @@ private void renderTooltip(List list, int x, int y) { } } - private List getHoveredButtonTooltip(VOptionList buttonList, int mouseX, int mouseY) { - VAbstractWidget widget = buttonList.getHoveredWidget(mouseX, mouseY); - if (widget != null) { - var tooltip = widget.getTooltip(); - if (tooltip == null) - return null; + private List getWidgetTooltip(VAbstractWidget widget) { + var tooltip = widget.getTooltip(); + if (tooltip == null) + return null; - return this.font.split(tooltip, this.tooltipWidth); - } - return null; + return this.font.split(tooltip, this.tooltipWidth); } private void updateState() { From 2b37f17dc765702c027a659456b24329629f2891 Mon Sep 17 00:00:00 2001 From: NotNekodev <97458908+NotNekodev@users.noreply.github.com> Date: Sun, 30 Nov 2025 16:39:20 +0100 Subject: [PATCH 03/27] make colors more welcoming, add VGuiConstants.java add hovering colors --- .../vulkanmod/config/gui/VGuiConstants.java | 14 ++++ .../vulkanmod/config/gui/VOptionScreen.java | 49 ++++++----- .../config/gui/widget/VAbstractWidget.java | 25 ++++-- .../config/gui/widget/VButtonWidget.java | 83 ++++++++++++++----- 4 files changed, 122 insertions(+), 49 deletions(-) create mode 100644 src/main/java/net/vulkanmod/config/gui/VGuiConstants.java diff --git a/src/main/java/net/vulkanmod/config/gui/VGuiConstants.java b/src/main/java/net/vulkanmod/config/gui/VGuiConstants.java new file mode 100644 index 0000000000..fa34faf60a --- /dev/null +++ b/src/main/java/net/vulkanmod/config/gui/VGuiConstants.java @@ -0,0 +1,14 @@ +package net.vulkanmod.config.gui; + +import net.vulkanmod.vulkan.util.ColorUtil; + +public class VGuiConstants { + public static final int COLOR_WHITE = ColorUtil.ARGB.pack(1f, 1f, 1f, 1f); + public static final int COLOR_BLACK = ColorUtil.ARGB.pack(0f, 0f, 0f, 1f); + public static final int COLOR_GRAY = ColorUtil.ARGB.pack(0.6f, 0.6f, 0.6f, 1f); + public static final int COLOR_RED = ColorUtil.ARGB.pack(0.59f, 0.18f, 0.17f, 1f); + + public static final int WIDGET_HEIGHT = 20; + public static final int WIDGET_MARGIN = 5; + +} diff --git a/src/main/java/net/vulkanmod/config/gui/VOptionScreen.java b/src/main/java/net/vulkanmod/config/gui/VOptionScreen.java index 453fb1b814..b515f15c7e 100644 --- a/src/main/java/net/vulkanmod/config/gui/VOptionScreen.java +++ b/src/main/java/net/vulkanmod/config/gui/VOptionScreen.java @@ -24,7 +24,6 @@ import java.util.List; public class VOptionScreen extends Screen { - public final static int RED = ColorUtil.ARGB.pack(0.3f, 0.0f, 0.0f, 0.8f); final ResourceLocation ICON = ResourceLocation.fromNamespaceAndPath("vulkanmod", "vlogo_transparent.png"); private final Screen parent; @@ -90,7 +89,8 @@ protected void init() { int itemHeight = 20; int leftMargin = 100; - int listWidth = Math.min((int) (this.width * 0.65f), 420); + int rightMargin = 20; + int listWidth = this.width - rightMargin - leftMargin; int listHeight = this.height - top - bottom; this.buildLists(leftMargin, top, listWidth, listHeight, itemHeight); @@ -107,7 +107,7 @@ protected void init() { buildPage(); this.applyButton.active = false; - this.undoButton.active = false; + this.undoButton.visible = false; } private void captureOriginalState() { @@ -155,7 +155,7 @@ private void buildPage() { this.pageButtons.clear(); this.clearWidgets(); - this.addPageButtons(10, 40, 80, 22, true); + this.addPageButtons(10, 40, 80, VGuiConstants.WIDGET_HEIGHT, true); VOptionList currentList = this.optionPages.get(this.currentListIdx).getOptionList(); this.addWidget(currentList); @@ -165,34 +165,32 @@ private void buildPage() { private void addButtons() { int rightMargin = 20; - int buttonHeight = 20; int padding = 10; - int buttonMargin = 5; int buttonWidth = minecraft.font.width(CommonComponents.GUI_DONE) + 2 * padding; int x0 = (this.width - buttonWidth - rightMargin); - int y0 = this.height - buttonHeight - 7; + int y0 = this.height - VGuiConstants.WIDGET_HEIGHT - 7; this.doneButton = new VButtonWidget( x0, y0, - buttonWidth, buttonHeight, + buttonWidth, VGuiConstants.WIDGET_HEIGHT, CommonComponents.GUI_DONE, button -> this.minecraft.setScreen(this.parent) ); buttonWidth = minecraft.font.width(Component.translatable("vulkanmod.options.buttons.apply")) + 2 * padding; - x0 -= (buttonWidth + buttonMargin); + x0 -= (buttonWidth + VGuiConstants.WIDGET_MARGIN); this.applyButton = new VButtonWidget( x0, y0, - buttonWidth, buttonHeight, + buttonWidth, VGuiConstants.WIDGET_HEIGHT, Component.translatable("vulkanmod.options.buttons.apply"), button -> this.applyOptions() ); buttonWidth = minecraft.font.width(Component.translatable("vulkanmod.options.buttons.undo")) + 2 * padding; - x0 -= (buttonWidth + buttonMargin); + x0 -= (buttonWidth + VGuiConstants.WIDGET_MARGIN); this.undoButton = new VButtonWidget( x0, y0, - buttonWidth, buttonHeight, + buttonWidth, VGuiConstants.WIDGET_HEIGHT, Component.translatable("vulkanmod.options.buttons.undo"), button -> { undo(); @@ -203,7 +201,7 @@ private void addButtons() { x0 = (this.width - buttonWidth - rightMargin); this.supportButton = new VButtonWidget( x0, 6, - buttonWidth, buttonHeight, + buttonWidth, VGuiConstants.WIDGET_HEIGHT, Component.translatable("vulkanmod.options.buttons.kofi"), button -> Util.getPlatform().openUri("https://ko-fi.com/xcollateral") ); @@ -264,7 +262,19 @@ public void render(GuiGraphics guiGraphics, int mouseX, int mouseY, float delta) currentList.renderWidget(mouseX, mouseY); renderButtons(mouseX, mouseY); - VAbstractWidget hoveredWidget = currentList.getHoveredWidget(mouseX, mouseY); + VAbstractWidget hoveredWidget = null; + + for (var b : buttons) { + if (b.isMouseOver(mouseX, mouseY)) { + hoveredWidget = b; + break; + } + } + + if (hoveredWidget == null) { + hoveredWidget = currentList.getHoveredWidget(mouseX, mouseY); + } + if (hoveredWidget != null) { List tooltip = getWidgetTooltip(hoveredWidget); if (tooltip != null) { @@ -279,6 +289,7 @@ public void render(GuiGraphics guiGraphics, int mouseX, int mouseY, float delta) public void renderButtons(int mouseX, int mouseY) { for (VButtonWidget button : buttons) { + button.updateState(mouseX, mouseY); button.render(mouseX, mouseY); } } @@ -287,12 +298,10 @@ private void renderTooltip(List list, int x, int y) { int padding = 3; int width = GuiRenderer.getMaxTextWidth(this.font, list); int height = list.size() * 10; - float intensity = 0.05f; - int color = ColorUtil.ARGB.pack(intensity, intensity, intensity, 0.6f); - GuiRenderer.fill(x - padding, y - padding, x + width + padding, y + height + padding, color); + GuiRenderer.fill(x - padding, y - padding, x + width + padding, y + height + padding, + ColorUtil.ARGB.pack(0.05f, 0.05f, 0.05f, 0.6f)); - color = RED; - GuiRenderer.renderBorder(x - padding, y - padding, x + width + padding, y + height + padding, 1, color); + GuiRenderer.renderBorder(x - padding, y - padding, x + width + padding, y + height + padding, 1, VGuiConstants.COLOR_RED); int yOffset = 0; for (var text : list) { @@ -316,7 +325,7 @@ private void updateState() { } this.applyButton.active = modified; - this.undoButton.active = modified; + this.undoButton.visible = modified; } private void setOptionList(int i) { diff --git a/src/main/java/net/vulkanmod/config/gui/widget/VAbstractWidget.java b/src/main/java/net/vulkanmod/config/gui/widget/VAbstractWidget.java index 0a76bd6ff3..2c86906457 100644 --- a/src/main/java/net/vulkanmod/config/gui/widget/VAbstractWidget.java +++ b/src/main/java/net/vulkanmod/config/gui/widget/VAbstractWidget.java @@ -7,6 +7,7 @@ import net.minecraft.network.chat.Component; import net.minecraft.sounds.SoundEvents; import net.vulkanmod.config.gui.GuiElement; +import net.vulkanmod.config.gui.VGuiConstants; import net.vulkanmod.config.gui.render.GuiRenderer; import net.vulkanmod.vulkan.util.ColorUtil; @@ -20,6 +21,7 @@ public abstract class VAbstractWidget extends GuiElement { public void render(double mX, double mY) { this.updateState(mX, mY); this.renderWidget(mX, mY); + this.renderHovering(0, 0); } public void renderWidget(double mX, double mY) { @@ -35,16 +37,17 @@ protected void onDrag(double mX, double mY, double f, double g) { } protected void renderHovering(int xPadding, int yPadding) { + if (this.isFocused() || !this.isActive()) + return; + float hoverMultiplier = this.getHoverMultiplier(200); + int borderColor = ColorUtil.ARGB.multiplyAlpha(VGuiConstants.COLOR_RED, hoverMultiplier); + int backgroundColor = ColorUtil.ARGB.multiplyAlpha(VGuiConstants.COLOR_RED, 0.3f * hoverMultiplier); if (hoverMultiplier > 0.0f) { -// int color = ColorUtil.ARGB.pack(0.5f, 0.5f, 0.5f, hoverMultiplier * 0.2f); - int color = ColorUtil.ARGB.pack(0.3f, 0.0f, 0.0f, hoverMultiplier * 0.2f); -// int color = ColorUtil.ARGB.multiplyAlpha(VOptionScreen.RED, hoverMultiplier); - GuiRenderer.fill(this.x - xPadding, this.y - yPadding, this.x + this.width + xPadding, this.y + this.height + yPadding, color); - -// color = ColorUtil.ARGB.pack(1.0f, 1.0f, 1.0f, hoverMultiplier * 0.8f); - color = ColorUtil.ARGB.pack(0.3f, 0.0f, 0.0f, hoverMultiplier * 0.8f); + GuiRenderer.fill(this.x - xPadding, this.y - yPadding, + this.x + this.width + xPadding, this.y + this.height + yPadding, + backgroundColor); int x0 = this.x - xPadding; int x1 = this.x + this.width + xPadding; @@ -52,7 +55,7 @@ protected void renderHovering(int xPadding, int yPadding) { int y1 = this.y + height + yPadding; int border = 1; - GuiRenderer.renderBorder(x0, y0, x1, y1, border, color); + GuiRenderer.renderBorder(x0, y0, x1, y1, border, borderColor); } } @@ -105,6 +108,12 @@ public boolean mouseDragged(MouseButtonEvent event, double d, double e) { } } + @Override + public void updateState(double mX, double mY) { + super.updateState(mX, mY); + + } + public void playDownSound(SoundManager soundManager) { soundManager.play(SimpleSoundInstance.forUI(SoundEvents.UI_BUTTON_CLICK, 1.0F)); } diff --git a/src/main/java/net/vulkanmod/config/gui/widget/VButtonWidget.java b/src/main/java/net/vulkanmod/config/gui/widget/VButtonWidget.java index 3055259d60..18dbf5bf2a 100644 --- a/src/main/java/net/vulkanmod/config/gui/widget/VButtonWidget.java +++ b/src/main/java/net/vulkanmod/config/gui/widget/VButtonWidget.java @@ -1,12 +1,16 @@ package net.vulkanmod.config.gui.widget; import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.ComponentPath; import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.navigation.FocusNavigationEvent; import net.minecraft.network.chat.Component; import net.minecraft.util.Mth; +import net.vulkanmod.config.gui.VGuiConstants; import net.vulkanmod.config.gui.render.GuiRenderer; import net.vulkanmod.vulkan.VRenderSystem; import net.vulkanmod.vulkan.util.ColorUtil; +import org.jetbrains.annotations.Nullable; import java.util.function.Consumer; @@ -24,38 +28,75 @@ public VButtonWidget(int x, int y, int width, int height, Component message, Con } public void renderWidget(double mouseX, double mouseY) { - Minecraft minecraftClient = Minecraft.getInstance(); - Font textRenderer = minecraftClient.font; + if (!this.isVisible()) return; + + int backgroundColor = this.isActive() + ? ColorUtil.ARGB.multiplyAlpha(VGuiConstants.COLOR_BLACK, 0.45f) + : ColorUtil.ARGB.multiplyAlpha(VGuiConstants.COLOR_BLACK, 0.3f); + int textColor = this.isActive() + ? VGuiConstants.COLOR_WHITE + : VGuiConstants.COLOR_GRAY; + int selectionOutlineColor = ColorUtil.ARGB.multiplyAlpha(VGuiConstants.COLOR_RED, 0.8f); + int selectionFillColor = ColorUtil.ARGB.multiplyAlpha(VGuiConstants.COLOR_RED, 0.2f); + + GuiRenderer.fill(this.x, this.y, this.x + this.width, this.y + this.height, backgroundColor); + + if (this.selected) { + GuiRenderer.fill(this.x, this.y, this.x + 2, this.y + this.height, selectionOutlineColor); + GuiRenderer.fill(this.x, this.y, this.x + this.width, this.y + this.height, selectionFillColor); + } - int xPadding = 0; - int yPadding = 0; + // this is down here because of layering + GuiRenderer.drawCenteredString( + Minecraft.getInstance().font, + this.message, + this.x + this.width / 2, (this.y + this.height / 2) - 4, + textColor | (Mth.ceil(this.alpha * 255.0f) << 24)); + } - int color = ColorUtil.ARGB.pack(0.0f, 0.0f, 0.0f, this.active ? 0.45f : 0.3f); - GuiRenderer.fill(this.x - xPadding, this.y - yPadding, this.x + this.width + xPadding, this.y + this.height + yPadding, color); + public void onClick(double mX, double mY) { + this.onPress.accept(this); + } - if (this.active) { - this.renderHovering(0, 0); - } + private void doAction() { + this.onPress.accept(this); + this.playDownSound(Minecraft.getInstance().getSoundManager()); + } - int j = this.active ? 0xFFFFFF : 0xA0A0A0; - GuiRenderer.drawCenteredString(textRenderer, this.message, this.x + this.width / 2, this.y + (this.height - 8) / 2, j | Mth.ceil(this.alpha * 255.0f) << 24); + public void setSelected(boolean selected) { + this.selected = selected; + } + public Component getMessage() { + return message; + } - if(this.selected) { - color = ColorUtil.ARGB.pack(0.3f, 0.0f, 0.0f, 1.0f); - GuiRenderer.fillBox(this.x, this.y, (int) 1.5f, this.height, color); + public void setMessage(Component message) { + this.message = message; + } - color = ColorUtil.ARGB.pack(0.3f, 0.0f, 0.0f, 0.2f); - GuiRenderer.fillBox(this.x, this.y, this.width, this.height, color); - } + public boolean isVisible() { + return visible; } - public void setSelected(boolean selected) { - this.selected = selected; + public void setVisible(boolean visible) { + this.visible = visible; } - public void onClick(double mX, double mY) { - this.onPress.accept(this); + @Override + public boolean isActive() { + return active; + } + + public void setActive(boolean active) { + this.active = active; + } + + @Override + public @Nullable ComponentPath nextFocusPath(FocusNavigationEvent event) { + if (!this.active || !this.visible) + return null; + return super.nextFocusPath(event); } } From 54caebf85b6c73c7986c162dfed8e56e7c217a64 Mon Sep 17 00:00:00 2001 From: NotNekodev <97458908+NotNekodev@users.noreply.github.com> Date: Sun, 30 Nov 2025 18:57:51 +0100 Subject: [PATCH 04/27] rewrite tooltip system, to support different translations based on different values --- .../vulkanmod/config/gui/VOptionScreen.java | 1 + .../net/vulkanmod/config/option/Option.java | 29 +++++++++++++++---- .../net/vulkanmod/config/option/Options.java | 25 +++++++++++----- .../assets/vulkanmod/lang/en_us.json | 4 +-- 4 files changed, 43 insertions(+), 16 deletions(-) diff --git a/src/main/java/net/vulkanmod/config/gui/VOptionScreen.java b/src/main/java/net/vulkanmod/config/gui/VOptionScreen.java index b515f15c7e..42fda32aaf 100644 --- a/src/main/java/net/vulkanmod/config/gui/VOptionScreen.java +++ b/src/main/java/net/vulkanmod/config/gui/VOptionScreen.java @@ -295,6 +295,7 @@ public void renderButtons(int mouseX, int mouseY) { } private void renderTooltip(List list, int x, int y) { + if (list.isEmpty()) return; int padding = 3; int width = GuiRenderer.getMaxTextWidth(this.font, list); int height = list.size() * 10; diff --git a/src/main/java/net/vulkanmod/config/option/Option.java b/src/main/java/net/vulkanmod/config/option/Option.java index 274c4cac3a..be35f5c9ce 100644 --- a/src/main/java/net/vulkanmod/config/option/Option.java +++ b/src/main/java/net/vulkanmod/config/option/Option.java @@ -19,10 +19,23 @@ public abstract class Option { protected T originalValue; protected Function translator; + protected Function tooltipTranslator; protected boolean active; protected Runnable onChange; + public Option(Component name, Consumer setter, Supplier getter, Function translator, Function tooltip) { + this.name = name; + + this.onApply = setter; + this.valueSupplier = getter; + + this.translator = translator; + this.tooltipTranslator = tooltip; + + this.newValue = this.value = this.valueSupplier.get(); + } + public Option(Component name, Consumer setter, Supplier getter, Function translator) { this.name = name; @@ -58,6 +71,11 @@ public Option setTranslator(Function translator) { return this; } + public Option setTooltip(Function tooltipTranslator) { + this.tooltipTranslator = tooltipTranslator; + return this; + } + public Option setActive(boolean active) { this.active = active; return this; @@ -113,12 +131,11 @@ public Component getDisplayedValue() { return this.translator.apply(this.newValue); } - public Option setTooltip(Component text) { - this.tooltip = text; - return this; - } - public Component getTooltip() { - return this.tooltip; + if (this.tooltipTranslator != null) { + return this.tooltipTranslator.apply(this.newValue); + } else { + return Component.empty(); + } } } \ No newline at end of file diff --git a/src/main/java/net/vulkanmod/config/option/Options.java b/src/main/java/net/vulkanmod/config/option/Options.java index 560e6d2c93..55761e8f24 100644 --- a/src/main/java/net/vulkanmod/config/option/Options.java +++ b/src/main/java/net/vulkanmod/config/option/Options.java @@ -210,7 +210,13 @@ public static OptionBlock[] getGraphicsOpts() { case LightMode.SUB_BLOCK -> "vulkanmod.options.ao.subBlock"; default -> "vulkanmod.options.unknown"; })) - .setTooltip(Component.translatable("vulkanmod.options.ao.subBlock.tooltip")), + .setTooltip(value -> switch (value) { + case LightMode.FLAT -> Component.empty(); + case LightMode.SMOOTH -> Component.empty(); + case LightMode.SUB_BLOCK -> Component.translatable("vulkanmod.options.ao.subBlock.tooltip"); + default -> Component.empty(); + } + ), new RangeOption(Component.translatable("options.biomeBlendRadius"), 0, 7, 1, value -> { @@ -258,11 +264,14 @@ public static OptionBlock[] getOptimizationOpts() { case 10 -> "options.off"; default -> "vulkanmod.options.unknown"; })) - .setTooltip(Component.translatable("vulkanmod.options.advCulling.tooltip")), + .setTooltip(value -> Component.translatable(switch (value) { + case 1, 2, 3 -> "vulkanmod.options.advCulling.tooltip"; + default -> ""; + })), new SwitchOption(Component.translatable("vulkanmod.options.entityCulling"), value -> config.entityCulling = value, () -> config.entityCulling) - .setTooltip(Component.translatable("vulkanmod.options.entityCulling.tooltip")), + .setTooltip(value -> Component.translatable("vulkanmod.options.entityCulling.tooltip")), new SwitchOption(Component.translatable("vulkanmod.options.uniqueOpaqueLayer"), value -> { config.uniqueOpaqueLayer = value; @@ -270,18 +279,18 @@ public static OptionBlock[] getOptimizationOpts() { minecraft.levelRenderer.allChanged(); }, () -> config.uniqueOpaqueLayer) - .setTooltip(Component.translatable("vulkanmod.options.uniqueOpaqueLayer.tooltip")), + .setTooltip(value -> Component.translatable("vulkanmod.options.uniqueOpaqueLayer.tooltip")), new SwitchOption(Component.translatable("vulkanmod.options.backfaceCulling"), value -> { config.backFaceCulling = value; Minecraft.getInstance().levelRenderer.allChanged(); }, () -> config.backFaceCulling) - .setTooltip(Component.translatable("vulkanmod.options.backfaceCulling.tooltip")), + .setTooltip(value -> Component.translatable("vulkanmod.options.backfaceCulling.tooltip")), new SwitchOption(Component.translatable("vulkanmod.options.indirectDraw"), value -> config.indirectDraw = value, () -> config.indirectDraw) - .setTooltip(Component.translatable("vulkanmod.options.indirectDraw.tooltip")), + .setTooltip(value -> Component.translatable("vulkanmod.options.indirectDraw.tooltip")), }) }; @@ -309,7 +318,7 @@ public static OptionBlock[] getOtherOpts() { config.frameQueueSize = value; Renderer.scheduleSwapChainUpdate(); }, () -> config.frameQueueSize) - .setTooltip(Component.translatable("vulkanmod.options.frameQueue.tooltip")), + .setTooltip(value -> Component.translatable("vulkanmod.options.frameQueue.tooltip")), new SwitchOption(Component.translatable("vulkanmod.options.textureAnimations"), value -> { config.textureAnimations = value; @@ -327,7 +336,7 @@ public static OptionBlock[] getOtherOpts() { : DeviceManager.suitableDevices.get( value).deviceName) ) - .setTooltip(Component.nullToEmpty("%s: %s".formatted( + .setTooltip(value -> Component.nullToEmpty("%s: %s".formatted( Component.translatable("vulkanmod.options.deviceSelector.tooltip").getString(), DeviceManager.device.deviceName))) }) diff --git a/src/main/resources/assets/vulkanmod/lang/en_us.json b/src/main/resources/assets/vulkanmod/lang/en_us.json index 167bcd7c0c..0421bef4ad 100644 --- a/src/main/resources/assets/vulkanmod/lang/en_us.json +++ b/src/main/resources/assets/vulkanmod/lang/en_us.json @@ -16,8 +16,8 @@ "vulkanmod.options.advCulling.normal": "Normal", "vulkanmod.options.advCulling.tooltip": "Use a culling algorithm that might improve performance by reducing the number of non visible chunk sections rendered.", - "vulkanmod.options.ao.subBlock": "ON (Sub-block)", - "vulkanmod.options.ao.subBlock.tooltip": "ON (Sub-block): Enables smooth lighting for non full block (experimental).", + "vulkanmod.options.ao.subBlock": "Sub Block", + "vulkanmod.options.ao.subBlock.tooltip": "Enables smooth lighting for non full block (experimental).", "vulkanmod.options.deviceSelector": "Device selector", "vulkanmod.options.deviceSelector.auto": "Auto", From 2aad69dcdd0e153dec97b2bc9caa207703e356be Mon Sep 17 00:00:00 2001 From: NotNekodev <97458908+NotNekodev@users.noreply.github.com> Date: Sun, 30 Nov 2025 21:18:49 +0100 Subject: [PATCH 05/27] more compact page button layout --- .../java/net/vulkanmod/config/gui/VOptionScreen.java | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/main/java/net/vulkanmod/config/gui/VOptionScreen.java b/src/main/java/net/vulkanmod/config/gui/VOptionScreen.java index 42fda32aaf..383ab9166e 100644 --- a/src/main/java/net/vulkanmod/config/gui/VOptionScreen.java +++ b/src/main/java/net/vulkanmod/config/gui/VOptionScreen.java @@ -130,7 +130,7 @@ private void buildLists(int left, int top, int listWidth, int listHeight, int it } } - private void addPageButtons(int x0, int y0, int width, int height, boolean verticalLayout) { + private void addPageButtons(int x0, int y0, int width, int height) { int x = x0; int y = y0; for (int i = 0; i < this.optionPages.size(); ++i) { @@ -141,10 +141,7 @@ private void addPageButtons(int x0, int y0, int width, int height, boolean verti this.pageButtons.add(widget); this.addWidget(widget); - if (verticalLayout) - y += height + 1; - else - x += width + 1; + y += height; } this.pageButtons.get(this.currentListIdx).setSelected(true); @@ -155,7 +152,7 @@ private void buildPage() { this.pageButtons.clear(); this.clearWidgets(); - this.addPageButtons(10, 40, 80, VGuiConstants.WIDGET_HEIGHT, true); + this.addPageButtons(10, 40, 80, VGuiConstants.WIDGET_HEIGHT); VOptionList currentList = this.optionPages.get(this.currentListIdx).getOptionList(); this.addWidget(currentList); From 8efec30d2da1b29a19696e02c601abde01107a20 Mon Sep 17 00:00:00 2001 From: NotNekodev <97458908+NotNekodev@users.noreply.github.com> Date: Sun, 30 Nov 2025 21:36:43 +0100 Subject: [PATCH 06/27] fix hovering of the undo button even when its not visible --- .../java/net/vulkanmod/config/gui/widget/VAbstractWidget.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/net/vulkanmod/config/gui/widget/VAbstractWidget.java b/src/main/java/net/vulkanmod/config/gui/widget/VAbstractWidget.java index 2c86906457..87545531ae 100644 --- a/src/main/java/net/vulkanmod/config/gui/widget/VAbstractWidget.java +++ b/src/main/java/net/vulkanmod/config/gui/widget/VAbstractWidget.java @@ -37,7 +37,7 @@ protected void onDrag(double mX, double mY, double f, double g) { } protected void renderHovering(int xPadding, int yPadding) { - if (this.isFocused() || !this.isActive()) + if (this.isFocused() || !this.isActive() || !this.visible || this.focused) return; float hoverMultiplier = this.getHoverMultiplier(200); From 9da51eca3815c368547f7e7c84074b62c9d5b4bb Mon Sep 17 00:00:00 2001 From: NotNekodev <97458908+NotNekodev@users.noreply.github.com> Date: Sun, 30 Nov 2025 22:04:09 +0100 Subject: [PATCH 07/27] make scrolling strings for options that are too long --- .../vulkanmod/config/gui/VOptionScreen.java | 1 + .../config/gui/render/GuiRenderer.java | 78 ++++++++++++++++++- .../config/gui/{ => util}/VGuiConstants.java | 2 +- .../gui/widget/CyclingOptionWidget.java | 2 +- .../config/gui/widget/VAbstractWidget.java | 2 +- .../config/gui/widget/VButtonWidget.java | 4 +- 6 files changed, 82 insertions(+), 7 deletions(-) rename src/main/java/net/vulkanmod/config/gui/{ => util}/VGuiConstants.java (92%) diff --git a/src/main/java/net/vulkanmod/config/gui/VOptionScreen.java b/src/main/java/net/vulkanmod/config/gui/VOptionScreen.java index 383ab9166e..5eacf9a216 100644 --- a/src/main/java/net/vulkanmod/config/gui/VOptionScreen.java +++ b/src/main/java/net/vulkanmod/config/gui/VOptionScreen.java @@ -13,6 +13,7 @@ import net.minecraft.util.FormattedCharSequence; import net.vulkanmod.Initializer; import net.vulkanmod.config.gui.render.GuiRenderer; +import net.vulkanmod.config.gui.util.VGuiConstants; import net.vulkanmod.config.gui.widget.VAbstractWidget; import net.vulkanmod.config.gui.widget.VButtonWidget; import net.vulkanmod.config.option.OptionPage; diff --git a/src/main/java/net/vulkanmod/config/gui/render/GuiRenderer.java b/src/main/java/net/vulkanmod/config/gui/render/GuiRenderer.java index e268dab921..3ab7171b64 100644 --- a/src/main/java/net/vulkanmod/config/gui/render/GuiRenderer.java +++ b/src/main/java/net/vulkanmod/config/gui/render/GuiRenderer.java @@ -2,15 +2,19 @@ import com.mojang.blaze3d.pipeline.RenderPipeline; import com.mojang.blaze3d.vertex.*; +import net.minecraft.Util; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.Font; import net.minecraft.client.gui.GuiGraphics; import net.minecraft.client.gui.render.TextureSetup; import net.minecraft.network.chat.Component; import net.minecraft.util.FormattedCharSequence; +import net.minecraft.util.Mth; import org.joml.Matrix3x2f; +import java.util.HashMap; import java.util.List; +import java.util.Map; public abstract class GuiRenderer { @@ -19,6 +23,60 @@ public abstract class GuiRenderer { public static PoseStack pose; public static BufferBuilder bufferBuilder; + // Scrolling text state management + private static final Map scrollingTextStates = new HashMap<>(); + private static final float SCROLL_SPEED = 30.0f; // pixels per second + private static final float SCROLL_PAUSE_DURATION = 1.0f; // seconds to pause at ends + + private static class ScrollingTextState { + float scrollOffset = 0.0f; + long lastUpdateTime = System.currentTimeMillis(); + boolean scrollingForward = true; + float pauseTimer = 0.0f; + + void update(float textWidth, float maxWidth) { + long currentTime = System.currentTimeMillis(); + float deltaTime = (currentTime - lastUpdateTime) / 1000.0f; + lastUpdateTime = currentTime; + + if (textWidth <= maxWidth) { + scrollOffset = 0.0f; + return; + } + + // Handle pause at ends + if (pauseTimer > 0) { + pauseTimer -= deltaTime; + return; + } + + float maxScroll = textWidth - maxWidth; + + if (scrollingForward) { + scrollOffset += SCROLL_SPEED * deltaTime; + if (scrollOffset >= maxScroll) { + scrollOffset = maxScroll; + scrollingForward = false; + pauseTimer = SCROLL_PAUSE_DURATION; + } + } else { + scrollOffset -= SCROLL_SPEED * deltaTime; + if (scrollOffset <= 0) { + scrollOffset = 0; + scrollingForward = true; + pauseTimer = SCROLL_PAUSE_DURATION; + } + } + } + + void reset() { + scrollOffset = 0.0f; + scrollingForward = true; + pauseTimer = 0.0f; + lastUpdateTime = System.currentTimeMillis(); + } + } + public static void enableScissor(int i, int j, int k, int l) { guiGraphics.enableScissor(i, j, k, l); } @@ -80,6 +138,24 @@ public static void drawCenteredString(Font font, Component component, int x, int guiGraphics.drawString(font, formattedCharSequence, x - font.width(formattedCharSequence) / 2, y, color); } + public static void drawScrollingString(Font font, Component component, int x, int y, int maxWidth, int color) { + int textWidth = font.width(component); + if (textWidth <= maxWidth) { + drawCenteredString(font, component, x, y, color); + } else { + int x0 = x - maxWidth / 2, x1 = x + maxWidth / 2; + int scrollAmount = textWidth - maxWidth; + double currentTimeInSeconds = (double) Util.getMillis() / 1000.0; + double scrollSpeed = Math.max(scrollAmount * 0.5, 3.0); + double scrollingOffset = Math.sin((Math.PI / 2) * Math.cos((Math.PI * 2) * currentTimeInSeconds / scrollSpeed)) / 2.0 + 0.5; + double horizontalScroll = Mth.lerp(scrollingOffset, 0.0, scrollAmount); + + enableScissor(x0 - 1, 0, x1, Minecraft.getInstance().getWindow().getScreenHeight()); + drawString(font, component, (int) (x0 - horizontalScroll), y, color); + disableScissor(); + } + } + public static int getMaxTextWidth(Font font, List list) { int maxWidth = 0; for (var text : list) { @@ -98,4 +174,4 @@ renderPipeline, textureSetup, new Matrix3x2f(), vertices, color, guiGraphics.sci ) ); } -} +} \ No newline at end of file diff --git a/src/main/java/net/vulkanmod/config/gui/VGuiConstants.java b/src/main/java/net/vulkanmod/config/gui/util/VGuiConstants.java similarity index 92% rename from src/main/java/net/vulkanmod/config/gui/VGuiConstants.java rename to src/main/java/net/vulkanmod/config/gui/util/VGuiConstants.java index fa34faf60a..419cde2f06 100644 --- a/src/main/java/net/vulkanmod/config/gui/VGuiConstants.java +++ b/src/main/java/net/vulkanmod/config/gui/util/VGuiConstants.java @@ -1,4 +1,4 @@ -package net.vulkanmod.config.gui; +package net.vulkanmod.config.gui.util; import net.vulkanmod.vulkan.util.ColorUtil; diff --git a/src/main/java/net/vulkanmod/config/gui/widget/CyclingOptionWidget.java b/src/main/java/net/vulkanmod/config/gui/widget/CyclingOptionWidget.java index 6ec5157d2b..f1401e8097 100644 --- a/src/main/java/net/vulkanmod/config/gui/widget/CyclingOptionWidget.java +++ b/src/main/java/net/vulkanmod/config/gui/widget/CyclingOptionWidget.java @@ -39,7 +39,7 @@ public void renderControls(double mouseX, double mouseY) { Font textRenderer = Minecraft.getInstance().font; int x = this.controlX + this.controlWidth / 2; int y = this.y + (this.height - 9) / 2; - GuiRenderer.drawCenteredString(textRenderer, this.getDisplayedValue(), x, y, color); + GuiRenderer.drawScrollingString(textRenderer, this.getDisplayedValue(), x, y, (int) (rightButton.x - (leftButton.x + leftButton.width) - 12), color); this.leftButton.renderButton(mouseX, mouseY); this.rightButton.renderButton(mouseX, mouseY); diff --git a/src/main/java/net/vulkanmod/config/gui/widget/VAbstractWidget.java b/src/main/java/net/vulkanmod/config/gui/widget/VAbstractWidget.java index 87545531ae..5597d90ddd 100644 --- a/src/main/java/net/vulkanmod/config/gui/widget/VAbstractWidget.java +++ b/src/main/java/net/vulkanmod/config/gui/widget/VAbstractWidget.java @@ -7,7 +7,7 @@ import net.minecraft.network.chat.Component; import net.minecraft.sounds.SoundEvents; import net.vulkanmod.config.gui.GuiElement; -import net.vulkanmod.config.gui.VGuiConstants; +import net.vulkanmod.config.gui.util.VGuiConstants; import net.vulkanmod.config.gui.render.GuiRenderer; import net.vulkanmod.vulkan.util.ColorUtil; diff --git a/src/main/java/net/vulkanmod/config/gui/widget/VButtonWidget.java b/src/main/java/net/vulkanmod/config/gui/widget/VButtonWidget.java index 18dbf5bf2a..291a88648c 100644 --- a/src/main/java/net/vulkanmod/config/gui/widget/VButtonWidget.java +++ b/src/main/java/net/vulkanmod/config/gui/widget/VButtonWidget.java @@ -2,13 +2,11 @@ import net.minecraft.client.Minecraft; import net.minecraft.client.gui.ComponentPath; -import net.minecraft.client.gui.Font; import net.minecraft.client.gui.navigation.FocusNavigationEvent; import net.minecraft.network.chat.Component; import net.minecraft.util.Mth; -import net.vulkanmod.config.gui.VGuiConstants; +import net.vulkanmod.config.gui.util.VGuiConstants; import net.vulkanmod.config.gui.render.GuiRenderer; -import net.vulkanmod.vulkan.VRenderSystem; import net.vulkanmod.vulkan.util.ColorUtil; import org.jetbrains.annotations.Nullable; From 701cf4c77077ed164dc8669731df7d2dbd4ce363 Mon Sep 17 00:00:00 2001 From: NotNekodev <97458908+NotNekodev@users.noreply.github.com> Date: Mon, 1 Dec 2025 17:02:53 +0100 Subject: [PATCH 08/27] Apply italicised option name for options, where the changes havent been applied yet --- .../net/vulkanmod/config/gui/VOptionScreen.java | 2 ++ .../vulkanmod/config/gui/widget/OptionWidget.java | 15 ++++++++++++++- .../java/net/vulkanmod/config/option/Option.java | 1 + 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/main/java/net/vulkanmod/config/gui/VOptionScreen.java b/src/main/java/net/vulkanmod/config/gui/VOptionScreen.java index 5eacf9a216..02dd966aad 100644 --- a/src/main/java/net/vulkanmod/config/gui/VOptionScreen.java +++ b/src/main/java/net/vulkanmod/config/gui/VOptionScreen.java @@ -341,6 +341,8 @@ private void applyOptions() { page.applyOptionChanges(); } + this.captureOriginalState(); + Initializer.CONFIG.write(); } } \ No newline at end of file diff --git a/src/main/java/net/vulkanmod/config/gui/widget/OptionWidget.java b/src/main/java/net/vulkanmod/config/gui/widget/OptionWidget.java index b3ee793459..80f9bde4a7 100644 --- a/src/main/java/net/vulkanmod/config/gui/widget/OptionWidget.java +++ b/src/main/java/net/vulkanmod/config/gui/widget/OptionWidget.java @@ -72,7 +72,20 @@ public void renderWidget(double mouseX, double mouseY) { color = this.active ? 0xFFFFFFFF : 0xFFA0A0A0; Font textRenderer = minecraftClient.font; - GuiRenderer.drawString(textRenderer, this.getName().getVisualOrderText(), this.x + 8, this.y + (this.height - 8) / 2, color); + Component nameComp = this.getName(); + + if (this.option.isChanged()) { + nameComp = nameComp.copy().withStyle(style -> style.withItalic(true)); + } + + GuiRenderer.drawString( + textRenderer, + nameComp.getVisualOrderText(), + this.x + 8, + this.y + (this.height - 8) / 2, + color + ); + this.renderControls(mouseX, mouseY); } diff --git a/src/main/java/net/vulkanmod/config/option/Option.java b/src/main/java/net/vulkanmod/config/option/Option.java index be35f5c9ce..4a441806fc 100644 --- a/src/main/java/net/vulkanmod/config/option/Option.java +++ b/src/main/java/net/vulkanmod/config/option/Option.java @@ -3,6 +3,7 @@ import net.minecraft.network.chat.Component; import net.vulkanmod.config.gui.widget.OptionWidget; +import java.util.Objects; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; From d797d1b5949aad10efce74b8a9f48087699c4467 Mon Sep 17 00:00:00 2001 From: NotNekodev <97458908+NotNekodev@users.noreply.github.com> Date: Mon, 1 Dec 2025 17:28:30 +0100 Subject: [PATCH 09/27] Make the icon and the pageButtons more compact --- .../java/net/vulkanmod/config/gui/VOptionScreen.java | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/main/java/net/vulkanmod/config/gui/VOptionScreen.java b/src/main/java/net/vulkanmod/config/gui/VOptionScreen.java index 02dd966aad..244fcb0192 100644 --- a/src/main/java/net/vulkanmod/config/gui/VOptionScreen.java +++ b/src/main/java/net/vulkanmod/config/gui/VOptionScreen.java @@ -85,11 +85,11 @@ protected void init() { this.addPages(); this.captureOriginalState(); - int top = 40; + int top = 36; int bottom = 60; int itemHeight = 20; - int leftMargin = 100; + int leftMargin = 95; int rightMargin = 20; int listWidth = this.width - rightMargin - leftMargin; int listHeight = this.height - top - bottom; @@ -153,7 +153,7 @@ private void buildPage() { this.pageButtons.clear(); this.clearWidgets(); - this.addPageButtons(10, 40, 80, VGuiConstants.WIDGET_HEIGHT); + this.addPageButtons(10, 36, 80, VGuiConstants.WIDGET_HEIGHT); VOptionList currentList = this.optionPages.get(this.currentListIdx).getOptionList(); this.addWidget(currentList); @@ -251,8 +251,12 @@ public void render(GuiGraphics guiGraphics, int mouseX, int mouseY, float delta) GuiRenderer.guiGraphics = guiGraphics; VRenderSystem.enableBlend(); - int size = minecraft.font.lineHeight * 4; + int iconBackgroundColor = ColorUtil.ARGB.multiplyAlpha(VGuiConstants.COLOR_BLACK, 0.45f); + int iconBackgroundWidth = 90; + int iconBackgroundHeight = (minecraft.font.lineHeight * 4); + guiGraphics.fill(10, 4, iconBackgroundWidth, iconBackgroundHeight, iconBackgroundColor); + int size = minecraft.font.lineHeight * 4; guiGraphics.blit(RenderPipelines.GUI_TEXTURED, ICON, 30, 4, 0f, 0f, size, size, size, size); VOptionList currentList = this.optionPages.get(this.currentListIdx).getOptionList(); From a5ace88d053605f3baa7e73f1eac5b078eb1ca1b Mon Sep 17 00:00:00 2001 From: NotNekodev <97458908+NotNekodev@users.noreply.github.com> Date: Mon, 1 Dec 2025 20:14:27 +0100 Subject: [PATCH 10/27] initial revision for the search area + some ui positioning improvements and some scaling improvements --- .../net/vulkanmod/config/gui/VOptionList.java | 4 +- .../vulkanmod/config/gui/VOptionScreen.java | 48 ++++-- .../config/gui/widget/VAbstractWidget.java | 2 + .../config/gui/widget/VTextInputWidget.java | 140 ++++++++++++++++++ .../assets/vulkanmod/lang/en_us.json | 4 +- 5 files changed, 179 insertions(+), 19 deletions(-) create mode 100644 src/main/java/net/vulkanmod/config/gui/widget/VTextInputWidget.java diff --git a/src/main/java/net/vulkanmod/config/gui/VOptionList.java b/src/main/java/net/vulkanmod/config/gui/VOptionList.java index 6e353f2f46..619238c4df 100644 --- a/src/main/java/net/vulkanmod/config/gui/VOptionList.java +++ b/src/main/java/net/vulkanmod/config/gui/VOptionList.java @@ -31,7 +31,7 @@ public VOptionList(int x, int y, int width, int height, int itemHeight) { this.width = width; this.height = height; - this.itemWidth = (int) (0.95f * this.width); + this.itemWidth = this.width - 7; this.itemHeight = itemHeight; this.itemMargin = 3; this.totalItemHeight = this.itemHeight + this.itemMargin; @@ -238,7 +238,7 @@ public void renderWidget(int mouseX, int mouseY) { } protected int getScrollbarPosition() { - return this.x + this.itemWidth + 5; + return this.x + this.width; } public VAbstractWidget getHoveredWidget(double mouseX, double mouseY) { diff --git a/src/main/java/net/vulkanmod/config/gui/VOptionScreen.java b/src/main/java/net/vulkanmod/config/gui/VOptionScreen.java index 244fcb0192..3c63fab9af 100644 --- a/src/main/java/net/vulkanmod/config/gui/VOptionScreen.java +++ b/src/main/java/net/vulkanmod/config/gui/VOptionScreen.java @@ -16,6 +16,7 @@ import net.vulkanmod.config.gui.util.VGuiConstants; import net.vulkanmod.config.gui.widget.VAbstractWidget; import net.vulkanmod.config.gui.widget.VButtonWidget; +import net.vulkanmod.config.gui.widget.VTextInputWidget; import net.vulkanmod.config.option.OptionPage; import net.vulkanmod.config.option.Options; import net.vulkanmod.vulkan.VRenderSystem; @@ -41,6 +42,8 @@ public class VOptionScreen extends Screen { private VButtonWidget applyButton; private VButtonWidget undoButton; + private VTextInputWidget searchField; + private final List pageButtons = Lists.newArrayList(); private final List buttons = Lists.newArrayList(); @@ -85,12 +88,12 @@ protected void init() { this.addPages(); this.captureOriginalState(); - int top = 36; + int top = 29; int bottom = 60; int itemHeight = 20; - int leftMargin = 95; - int rightMargin = 20; + int leftMargin = 94; + int rightMargin = 3; int listWidth = this.width - rightMargin - leftMargin; int listHeight = this.height - top - bottom; @@ -162,7 +165,7 @@ private void buildPage() { } private void addButtons() { - int rightMargin = 20; + int rightMargin = 10; int padding = 10; int buttonWidth = minecraft.font.width(CommonComponents.GUI_DONE) + 2 * padding; int x0 = (this.width - buttonWidth - rightMargin); @@ -194,25 +197,38 @@ private void addButtons() { undo(); } ); + this.searchField = new VTextInputWidget( + 94, 4, + x0 - 19, VGuiConstants.WIDGET_HEIGHT, + Component.translatable("vulkanmod.options.searchFieldPlaceholder"), + widget -> { + System.out.println("Searched in graphic settings: " + widget.getInput()); + } + ); - buttonWidth = minecraft.font.width(Component.translatable("vulkanmod.options.buttons.kofi")) + 10; + + buttonWidth = minecraft.font.width(Component.translatable("vulkanmod.options.buttons.kofi")) + padding; x0 = (this.width - buttonWidth - rightMargin); this.supportButton = new VButtonWidget( - x0, 6, + x0, 4, buttonWidth, VGuiConstants.WIDGET_HEIGHT, Component.translatable("vulkanmod.options.buttons.kofi"), button -> Util.getPlatform().openUri("https://ko-fi.com/xcollateral") ); + this.buttons.add(this.applyButton); this.buttons.add(this.doneButton); this.buttons.add(this.supportButton); this.buttons.add(this.undoButton); + this.addWidget(this.applyButton); this.addWidget(this.doneButton); this.addWidget(this.supportButton); this.addWidget(this.undoButton); + + this.addWidget(this.searchField); } @Override @@ -257,12 +273,19 @@ public void render(GuiGraphics guiGraphics, int mouseX, int mouseY, float delta) guiGraphics.fill(10, 4, iconBackgroundWidth, iconBackgroundHeight, iconBackgroundColor); int size = minecraft.font.lineHeight * 4; - guiGraphics.blit(RenderPipelines.GUI_TEXTURED, ICON, 30, 4, 0f, 0f, size, size, size, size); + int iconX = 10 + (iconBackgroundWidth - 10 - size) / 2; + int iconY = 4 + (iconBackgroundHeight - 4 - size) / 2; + guiGraphics.blit(RenderPipelines.GUI_TEXTURED, ICON, iconX, iconY, 0f, 0f, size, size, size, size); VOptionList currentList = this.optionPages.get(this.currentListIdx).getOptionList(); currentList.updateState(mouseX, mouseY); currentList.renderWidget(mouseX, mouseY); - renderButtons(mouseX, mouseY); + for (VButtonWidget button : buttons) { + button.updateState(mouseX, mouseY); + button.render(mouseX, mouseY); + } + searchField.updateState(mouseX, mouseY); + searchField.render(mouseX, mouseY); VAbstractWidget hoveredWidget = null; @@ -283,19 +306,12 @@ public void render(GuiGraphics guiGraphics, int mouseX, int mouseY, float delta) int padding = 3; int tooltipWidth = GuiRenderer.getMaxTextWidth(this.font, tooltip); int tooltipX = hoveredWidget.getX() + hoveredWidget.getWidth() - tooltipWidth - padding; - int tooltipY = hoveredWidget.getY() + hoveredWidget.getHeight() + 3 + 1; // 3 is the padding inside of renderTooltip and 1 is the custom padding + int tooltipY = hoveredWidget.getY() + hoveredWidget.getHeight() + 3 + 1; this.renderTooltip(tooltip, tooltipX, tooltipY); } } } - public void renderButtons(int mouseX, int mouseY) { - for (VButtonWidget button : buttons) { - button.updateState(mouseX, mouseY); - button.render(mouseX, mouseY); - } - } - private void renderTooltip(List list, int x, int y) { if (list.isEmpty()) return; int padding = 3; diff --git a/src/main/java/net/vulkanmod/config/gui/widget/VAbstractWidget.java b/src/main/java/net/vulkanmod/config/gui/widget/VAbstractWidget.java index 5597d90ddd..6745f48108 100644 --- a/src/main/java/net/vulkanmod/config/gui/widget/VAbstractWidget.java +++ b/src/main/java/net/vulkanmod/config/gui/widget/VAbstractWidget.java @@ -1,6 +1,7 @@ package net.vulkanmod.config.gui.widget; import net.minecraft.client.Minecraft; +import net.minecraft.client.input.KeyEvent; import net.minecraft.client.input.MouseButtonEvent; import net.minecraft.client.resources.sounds.SimpleSoundInstance; import net.minecraft.client.sounds.SoundManager; @@ -10,6 +11,7 @@ import net.vulkanmod.config.gui.util.VGuiConstants; import net.vulkanmod.config.gui.render.GuiRenderer; import net.vulkanmod.vulkan.util.ColorUtil; +import org.lwjgl.glfw.GLFW; public abstract class VAbstractWidget extends GuiElement { public boolean active = true; diff --git a/src/main/java/net/vulkanmod/config/gui/widget/VTextInputWidget.java b/src/main/java/net/vulkanmod/config/gui/widget/VTextInputWidget.java new file mode 100644 index 0000000000..fcfc18f16c --- /dev/null +++ b/src/main/java/net/vulkanmod/config/gui/widget/VTextInputWidget.java @@ -0,0 +1,140 @@ +package net.vulkanmod.config.gui.widget; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.ComponentPath; +import net.minecraft.client.gui.navigation.FocusNavigationEvent; +import net.minecraft.client.input.KeyEvent; +import net.minecraft.network.chat.Component; +import net.minecraft.util.Mth; +import net.vulkanmod.config.gui.render.GuiRenderer; +import net.vulkanmod.config.gui.util.VGuiConstants; +import net.vulkanmod.vulkan.util.ColorUtil; +import org.jetbrains.annotations.Nullable; +import org.lwjgl.glfw.GLFW; + +import java.util.Objects; +import java.util.function.Consumer; + +public class VTextInputWidget extends VAbstractWidget { + boolean selected = false; + Consumer onSearch; // when the search is "activated", like pressing enter + String searchTerm = ""; + Component message; + Component placeholder; + + public VTextInputWidget(int x, int y, int width, int height, Component placeholder, Consumer onSearch) { + this.setPosition(x, y, width, height); + + this.message = placeholder; + this.placeholder = placeholder; + this.onSearch = onSearch; + } + + public void renderWidget(double mouseX, double mouseY) { + if (!this.isVisible()) return; + int backgroundColor = this.isActive() + ? ColorUtil.ARGB.multiplyAlpha(VGuiConstants.COLOR_BLACK, 0.45f) + : ColorUtil.ARGB.multiplyAlpha(VGuiConstants.COLOR_BLACK, 0.3f); + + int textColor = VGuiConstants.COLOR_WHITE; + + if (this.message.getString().equals(this.placeholder.getString())) { + textColor = VGuiConstants.COLOR_GRAY; + } + + int selectionOutlineColor = ColorUtil.ARGB.multiplyAlpha(VGuiConstants.COLOR_RED, 0.8f); + int selectionFillColor = ColorUtil.ARGB.multiplyAlpha(VGuiConstants.COLOR_RED, 0.2f); + + GuiRenderer.fill(this.x, this.y, this.x + this.width, this.y + this.height, backgroundColor); + + if (this.selected) { + GuiRenderer.fill(this.x, this.y, this.x + 2, this.y + this.height, selectionOutlineColor); + GuiRenderer.fill(this.x, this.y, this.x + this.width, this.y + this.height, selectionFillColor); + } + + // this is down here because of layering + GuiRenderer.drawString( + Minecraft.getInstance().font, + this.message, + this.x + 8, (this.y + this.height / 2) - 4, + textColor | (Mth.ceil(255.0f) << 24)); + + } + + @Override + public boolean keyPressed(KeyEvent keyEvent) { + if (keyEvent.key() == GLFW.GLFW_KEY_ENTER || keyEvent.key() == GLFW.GLFW_KEY_KP_ENTER) { + this.onSearch.accept(this); + return true; + } else { + if (this.message.getString().equals(this.placeholder.getString())) { + this.message = Component.empty(); + } + + if (keyEvent.key() == GLFW.GLFW_KEY_BACKSPACE) { + String string = this.message.getString(); + if (!string.isEmpty()) { + string = string.substring(0, string.length() - 1); + } + this.message = Component.literal(string); + } else { + if (!keyEvent.hasShiftDown()) { + String string = this.message.getString(); + String name = GLFW.glfwGetKeyName(keyEvent.key(), keyEvent.scancode()); + if (name == null) { + return false; + } + string += name; + this.message = Component.literal(string); + } else { + String string = this.message.getString(); + String name = Objects.requireNonNull(GLFW.glfwGetKeyName(keyEvent.key(), keyEvent.scancode())).toUpperCase(); + string += name; + this.message = Component.literal(string); + } + } + + return true; + } + } + + public String getInput() { + return this.message.getString(); + } + + public void setSelected(boolean selected) { + this.selected = selected; + } + + public Component getMessage() { + return message; + } + + public void setMessage(Component message) { + this.message = message; + } + + public boolean isVisible() { + return visible; + } + + public void setVisible(boolean visible) { + this.visible = visible; + } + + @Override + public boolean isActive() { + return active; + } + + public void setActive(boolean active) { + this.active = active; + } + + @Override + public @Nullable ComponentPath nextFocusPath(FocusNavigationEvent event) { + if (!this.active || !this.visible) + return null; + return super.nextFocusPath(event); + } +} diff --git a/src/main/resources/assets/vulkanmod/lang/en_us.json b/src/main/resources/assets/vulkanmod/lang/en_us.json index 0421bef4ad..310967ad4a 100644 --- a/src/main/resources/assets/vulkanmod/lang/en_us.json +++ b/src/main/resources/assets/vulkanmod/lang/en_us.json @@ -47,5 +47,7 @@ "vulkanmod.options.builderThreads": "Chunk Builder Threads", "vulkanmod.options.builderThreads.auto": "Auto", - "vulkanmod.options.textureAnimations": "Texture Animations" + "vulkanmod.options.textureAnimations": "Texture Animations", + + "vulkanmod.options.searchFieldPlaceholder": "Search Graphics Settings" } \ No newline at end of file From 59d5413d645290babe30fbb289f6b6cf1d38ddde Mon Sep 17 00:00:00 2001 From: NotNekodev <97458908+NotNekodev@users.noreply.github.com> Date: Mon, 1 Dec 2025 20:32:49 +0100 Subject: [PATCH 11/27] fix warnings in the java code --- .../net/vulkanmod/config/gui/GuiElement.java | 8 ++- .../net/vulkanmod/config/gui/VOptionList.java | 13 +---- .../vulkanmod/config/gui/VOptionScreen.java | 57 ++++++++----------- .../gui/widget/CyclingOptionWidget.java | 32 +++++------ .../config/gui/widget/OptionWidget.java | 34 +---------- .../config/gui/widget/RangeOptionWidget.java | 6 -- .../config/gui/widget/VAbstractWidget.java | 3 +- .../config/gui/widget/VButtonWidget.java | 18 +----- .../config/gui/widget/VTextInputWidget.java | 18 +----- 9 files changed, 55 insertions(+), 134 deletions(-) diff --git a/src/main/java/net/vulkanmod/config/gui/GuiElement.java b/src/main/java/net/vulkanmod/config/gui/GuiElement.java index 04b85d2a1b..a50316cab9 100644 --- a/src/main/java/net/vulkanmod/config/gui/GuiElement.java +++ b/src/main/java/net/vulkanmod/config/gui/GuiElement.java @@ -7,7 +7,7 @@ import net.minecraft.client.gui.narration.NarrationElementOutput; import net.minecraft.client.gui.navigation.FocusNavigationEvent; import net.minecraft.client.gui.navigation.ScreenRectangle; -import net.minecraft.client.input.MouseButtonEvent; +import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; public abstract class GuiElement implements GuiEventListener, NarratableEntry { @@ -22,6 +22,7 @@ public abstract class GuiElement implements GuiEventListener, NarratableEntry { protected int hoverTime; protected long hoverStopTime; + @SuppressWarnings("unused") // this will surely be used some day public void setPosition(int x, int y) { this.x = x; this.y = y; @@ -34,6 +35,7 @@ public void setPosition(int x, int y, int width, int height) { this.height = height; } + @SuppressWarnings("unused") // this will surely be used someday public void resize(int width, int height) { this.width = width; this.height = height; @@ -102,7 +104,7 @@ public ComponentPath getCurrentFocusPath() { } @Override - public ScreenRectangle getRectangle() { + public @NotNull ScreenRectangle getRectangle() { return GuiEventListener.super.getRectangle(); } @@ -117,7 +119,7 @@ public boolean isFocused() { } @Override - public NarrationPriority narrationPriority() { + public @NotNull NarrationPriority narrationPriority() { return NarrationPriority.NONE; } diff --git a/src/main/java/net/vulkanmod/config/gui/VOptionList.java b/src/main/java/net/vulkanmod/config/gui/VOptionList.java index 619238c4df..4f7e181230 100644 --- a/src/main/java/net/vulkanmod/config/gui/VOptionList.java +++ b/src/main/java/net/vulkanmod/config/gui/VOptionList.java @@ -37,6 +37,7 @@ public VOptionList(int x, int y, int width, int height, int itemHeight) { this.totalItemHeight = this.itemHeight + this.itemMargin; } + @SuppressWarnings("unused") public void addButton(OptionWidget widget) { this.addEntry(new Entry(widget, this.itemMargin)); } @@ -59,23 +60,13 @@ public void addAll(OptionBlock[] blocks) { } } - public void addAll(Option[] options) { - for (Option option : options) { - int x0 = this.x; - int width = this.itemWidth; - int height = this.itemHeight; - - this.addEntry(new Entry(option.createOptionWidget(x0, 0, width, height), this.itemMargin)); -// this.addEntry(new Entry(options[i].createOptionWidget(width / 2 - 155, 0, 200, 20))); - } - } - private void addEntry(Entry entry) { this.children.add(entry); this.listLength += entry.getTotalHeight(); } + @SuppressWarnings("unused") public void clearEntries() { this.listLength = 0; this.children.clear(); diff --git a/src/main/java/net/vulkanmod/config/gui/VOptionScreen.java b/src/main/java/net/vulkanmod/config/gui/VOptionScreen.java index 3c63fab9af..9c3247d5d4 100644 --- a/src/main/java/net/vulkanmod/config/gui/VOptionScreen.java +++ b/src/main/java/net/vulkanmod/config/gui/VOptionScreen.java @@ -2,6 +2,7 @@ import com.google.common.collect.Lists; import net.minecraft.Util; +import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiGraphics; import net.minecraft.client.gui.components.events.GuiEventListener; import net.minecraft.client.gui.screens.Screen; @@ -36,9 +37,6 @@ public class VOptionScreen extends Screen { private int tooltipWidth; - private VButtonWidget supportButton; - - private VButtonWidget doneButton; private VButtonWidget applyButton; private VButtonWidget undoButton; @@ -134,29 +132,25 @@ private void buildLists(int left, int top, int listWidth, int listHeight, int it } } - private void addPageButtons(int x0, int y0, int width, int height) { - int x = x0; - int y = y0; + private void buildPage() { + this.buttons.clear(); + this.pageButtons.clear(); + this.clearWidgets(); + + int x = 10; + int y = 36; for (int i = 0; i < this.optionPages.size(); ++i) { var page = this.optionPages.get(i); final int finalIdx = i; - VButtonWidget widget = new VButtonWidget(x, y, width, height, Component.nullToEmpty(page.name), button -> this.setOptionList(finalIdx)); + VButtonWidget widget = new VButtonWidget(x, y, 80, VGuiConstants.WIDGET_HEIGHT, Component.nullToEmpty(page.name), button -> this.setOptionList(finalIdx)); this.buttons.add(widget); this.pageButtons.add(widget); this.addWidget(widget); - y += height; + y += VGuiConstants.WIDGET_HEIGHT; } this.pageButtons.get(this.currentListIdx).setSelected(true); - } - - private void buildPage() { - this.buttons.clear(); - this.pageButtons.clear(); - this.clearWidgets(); - - this.addPageButtons(10, 36, 80, VGuiConstants.WIDGET_HEIGHT); VOptionList currentList = this.optionPages.get(this.currentListIdx).getOptionList(); this.addWidget(currentList); @@ -167,18 +161,18 @@ private void buildPage() { private void addButtons() { int rightMargin = 10; int padding = 10; - int buttonWidth = minecraft.font.width(CommonComponents.GUI_DONE) + 2 * padding; + int buttonWidth = Minecraft.getInstance().font.width(CommonComponents.GUI_DONE) + 2 * padding; int x0 = (this.width - buttonWidth - rightMargin); int y0 = this.height - VGuiConstants.WIDGET_HEIGHT - 7; - this.doneButton = new VButtonWidget( + VButtonWidget doneButton = new VButtonWidget( x0, y0, buttonWidth, VGuiConstants.WIDGET_HEIGHT, CommonComponents.GUI_DONE, - button -> this.minecraft.setScreen(this.parent) + button -> Minecraft.getInstance().setScreen(this.parent) ); - buttonWidth = minecraft.font.width(Component.translatable("vulkanmod.options.buttons.apply")) + 2 * padding; + buttonWidth = Minecraft.getInstance().font.width(Component.translatable("vulkanmod.options.buttons.apply")) + 2 * padding; x0 -= (buttonWidth + VGuiConstants.WIDGET_MARGIN); this.applyButton = new VButtonWidget( x0, y0, @@ -187,29 +181,28 @@ private void addButtons() { button -> this.applyOptions() ); - buttonWidth = minecraft.font.width(Component.translatable("vulkanmod.options.buttons.undo")) + 2 * padding; + buttonWidth = Minecraft.getInstance().font.width(Component.translatable("vulkanmod.options.buttons.undo")) + 2 * padding; x0 -= (buttonWidth + VGuiConstants.WIDGET_MARGIN); this.undoButton = new VButtonWidget( x0, y0, buttonWidth, VGuiConstants.WIDGET_HEIGHT, Component.translatable("vulkanmod.options.buttons.undo"), - button -> { - undo(); - } + button -> undo() + ); this.searchField = new VTextInputWidget( 94, 4, x0 - 19, VGuiConstants.WIDGET_HEIGHT, Component.translatable("vulkanmod.options.searchFieldPlaceholder"), widget -> { - System.out.println("Searched in graphic settings: " + widget.getInput()); + } ); - buttonWidth = minecraft.font.width(Component.translatable("vulkanmod.options.buttons.kofi")) + padding; + buttonWidth = Minecraft.getInstance().font.width(Component.translatable("vulkanmod.options.buttons.kofi")) + padding; x0 = (this.width - buttonWidth - rightMargin); - this.supportButton = new VButtonWidget( + VButtonWidget supportButton = new VButtonWidget( x0, 4, buttonWidth, VGuiConstants.WIDGET_HEIGHT, Component.translatable("vulkanmod.options.buttons.kofi"), @@ -218,14 +211,14 @@ private void addButtons() { this.buttons.add(this.applyButton); - this.buttons.add(this.doneButton); - this.buttons.add(this.supportButton); + this.buttons.add(doneButton); + this.buttons.add(supportButton); this.buttons.add(this.undoButton); this.addWidget(this.applyButton); - this.addWidget(this.doneButton); - this.addWidget(this.supportButton); + this.addWidget(doneButton); + this.addWidget(supportButton); this.addWidget(this.undoButton); this.addWidget(this.searchField); @@ -259,7 +252,7 @@ public boolean mouseReleased(MouseButtonEvent event) { @Override public void onClose() { - this.minecraft.setScreen(this.parent); + Minecraft.getInstance().setScreen(this.parent); } @Override diff --git a/src/main/java/net/vulkanmod/config/gui/widget/CyclingOptionWidget.java b/src/main/java/net/vulkanmod/config/gui/widget/CyclingOptionWidget.java index f1401e8097..b9fe2a69b1 100644 --- a/src/main/java/net/vulkanmod/config/gui/widget/CyclingOptionWidget.java +++ b/src/main/java/net/vulkanmod/config/gui/widget/CyclingOptionWidget.java @@ -8,10 +8,11 @@ import net.vulkanmod.config.option.CyclingOption; import net.vulkanmod.render.shader.CustomRenderPipelines; import net.vulkanmod.vulkan.util.ColorUtil; +import org.jetbrains.annotations.NotNull; public class CyclingOptionWidget extends OptionWidget> { - private Button leftButton; - private Button rightButton; + private final Button leftButton; + private final Button rightButton; private boolean focused; @@ -24,11 +25,6 @@ public CyclingOptionWidget(CyclingOption option, int x, int y, int width, int // updateDisplayedValue(option.getValueText()); } - @Override - protected int getYImage(boolean hovered) { - return 0; - } - public void renderControls(double mouseX, double mouseY) { this.renderBars(); @@ -39,7 +35,7 @@ public void renderControls(double mouseX, double mouseY) { Font textRenderer = Minecraft.getInstance().font; int x = this.controlX + this.controlWidth / 2; int y = this.y + (this.height - 9) / 2; - GuiRenderer.drawScrollingString(textRenderer, this.getDisplayedValue(), x, y, (int) (rightButton.x - (leftButton.x + leftButton.width) - 12), color); + GuiRenderer.drawScrollingString(textRenderer, this.getDisplayedValue(), x, y, (rightButton.x - (leftButton.x + leftButton.width) - 12), color); this.leftButton.renderButton(mouseX, mouseY); this.rightButton.renderButton(mouseX, mouseY); @@ -137,7 +133,13 @@ else if (this.active) { color = INACTIVE_COLOR; } - float h = f; + float[][] vertices = getVertices(f); + + + GuiRenderer.submitPolygon(CustomRenderPipelines.GUI_TRIANGLES, TextureSetup.noTexture(), vertices, color); + } + + private float[] @NotNull [] getVertices(float f) { float w = f - 1.0f; float yC = y + height * 0.5f; float xC = x + width * 0.5f; @@ -146,20 +148,18 @@ else if (this.active) { if (this.direction == Direction.LEFT) { vertices = new float[][]{ {xC - w, yC}, - {xC + w, yC + h}, - {xC + w, yC - h}, + {xC + w, yC + f}, + {xC + w, yC - f}, }; } else { vertices = new float[][]{ {xC + w, yC}, - {xC - w, yC - h}, - {xC - w, yC + h}, + {xC - w, yC - f}, + {xC - w, yC + f}, }; } - - - GuiRenderer.submitPolygon(CustomRenderPipelines.GUI_TRIANGLES, TextureSetup.noTexture(), vertices, color); + return vertices; } enum Direction { diff --git a/src/main/java/net/vulkanmod/config/gui/widget/OptionWidget.java b/src/main/java/net/vulkanmod/config/gui/widget/OptionWidget.java index 80f9bde4a7..bd93245b96 100644 --- a/src/main/java/net/vulkanmod/config/gui/widget/OptionWidget.java +++ b/src/main/java/net/vulkanmod/config/gui/widget/OptionWidget.java @@ -5,13 +5,11 @@ import net.minecraft.client.gui.narration.NarratableEntry; import net.minecraft.client.gui.narration.NarrationElementOutput; import net.minecraft.client.input.MouseButtonEvent; -import net.minecraft.client.resources.sounds.SimpleSoundInstance; -import net.minecraft.client.sounds.SoundManager; import net.minecraft.network.chat.Component; -import net.minecraft.sounds.SoundEvents; import net.vulkanmod.config.gui.render.GuiRenderer; import net.vulkanmod.config.option.Option; import net.vulkanmod.vulkan.util.ColorUtil; +import org.jetbrains.annotations.NotNull; public abstract class OptionWidget> extends VAbstractWidget implements NarratableEntry { @@ -52,15 +50,9 @@ public void render(double mouseX, double mouseY) { this.renderWidget(mouseX, mouseY); } - public void updateState() { - - } - public void renderWidget(double mouseX, double mouseY) { Minecraft minecraftClient = Minecraft.getInstance(); - int i = this.getYImage(this.isHovered()); - int xPadding = 0; int yPadding = 0; @@ -90,20 +82,6 @@ public void renderWidget(double mouseX, double mouseY) { this.renderControls(mouseX, mouseY); } - protected int getYImage(boolean hovered) { - int i = 1; - if (!this.active) { - i = 0; - } else if (hovered) { - i = 2; - } - return i; - } - - public boolean isHovered() { - return this.hovered || this.focused; - } - protected abstract void renderControls(double mouseX, double mouseY); public abstract void onClick(double mouseX, double mouseY); @@ -112,10 +90,6 @@ public boolean isHovered() { protected abstract void onDrag(double mouseX, double mouseY, double deltaX, double deltaY); - protected boolean isValidClickButton(int button) { - return button == 0; - } - @Override public boolean mouseDragged(MouseButtonEvent event, double deltaX, double deltaY) { if (this.isValidClickButton(event.button())) { @@ -184,7 +158,7 @@ public Component getTooltip() { } @Override - public NarrationPriority narrationPriority() { + public @NotNull NarrationPriority narrationPriority() { if (this.focused) { return NarrationPriority.FOCUSED; } @@ -198,8 +172,4 @@ public NarrationPriority narrationPriority() { public final void updateNarration(NarrationElementOutput narrationElementOutput) { } - public void playDownSound(SoundManager soundManager) { - soundManager.play(SimpleSoundInstance.forUI(SoundEvents.UI_BUTTON_CLICK, 1.0f)); - } - } diff --git a/src/main/java/net/vulkanmod/config/gui/widget/RangeOptionWidget.java b/src/main/java/net/vulkanmod/config/gui/widget/RangeOptionWidget.java index 277386177b..a9459be2a5 100644 --- a/src/main/java/net/vulkanmod/config/gui/widget/RangeOptionWidget.java +++ b/src/main/java/net/vulkanmod/config/gui/widget/RangeOptionWidget.java @@ -8,7 +8,6 @@ import net.minecraft.util.Mth; import net.vulkanmod.config.gui.render.GuiRenderer; import net.vulkanmod.config.option.RangeOption; -import net.vulkanmod.vulkan.VRenderSystem; import net.vulkanmod.vulkan.util.ColorUtil; import org.lwjgl.glfw.GLFW; @@ -24,11 +23,6 @@ public RangeOptionWidget(RangeOption option, int x, int y, int width, int height } - @Override - protected int getYImage(boolean hovered) { - return 0; - } - @Override protected void renderControls(double mouseX, double mouseY) { int valueX = this.controlX + (int) (this.value * (this.controlWidth)); diff --git a/src/main/java/net/vulkanmod/config/gui/widget/VAbstractWidget.java b/src/main/java/net/vulkanmod/config/gui/widget/VAbstractWidget.java index 6745f48108..d84c530064 100644 --- a/src/main/java/net/vulkanmod/config/gui/widget/VAbstractWidget.java +++ b/src/main/java/net/vulkanmod/config/gui/widget/VAbstractWidget.java @@ -1,7 +1,6 @@ package net.vulkanmod.config.gui.widget; import net.minecraft.client.Minecraft; -import net.minecraft.client.input.KeyEvent; import net.minecraft.client.input.MouseButtonEvent; import net.minecraft.client.resources.sounds.SimpleSoundInstance; import net.minecraft.client.sounds.SoundManager; @@ -11,7 +10,6 @@ import net.vulkanmod.config.gui.util.VGuiConstants; import net.vulkanmod.config.gui.render.GuiRenderer; import net.vulkanmod.vulkan.util.ColorUtil; -import org.lwjgl.glfw.GLFW; public abstract class VAbstractWidget extends GuiElement { public boolean active = true; @@ -38,6 +36,7 @@ public void onRelease(double mX, double mY) { protected void onDrag(double mX, double mY, double f, double g) { } + @SuppressWarnings("SameParameterValue") // I just want code without warnings :^ protected void renderHovering(int xPadding, int yPadding) { if (this.isFocused() || !this.isActive() || !this.visible || this.focused) return; diff --git a/src/main/java/net/vulkanmod/config/gui/widget/VButtonWidget.java b/src/main/java/net/vulkanmod/config/gui/widget/VButtonWidget.java index 291a88648c..50745bcd8a 100644 --- a/src/main/java/net/vulkanmod/config/gui/widget/VButtonWidget.java +++ b/src/main/java/net/vulkanmod/config/gui/widget/VButtonWidget.java @@ -34,6 +34,7 @@ public void renderWidget(double mouseX, double mouseY) { int textColor = this.isActive() ? VGuiConstants.COLOR_WHITE : VGuiConstants.COLOR_GRAY; + //noinspection DuplicatedCode int selectionOutlineColor = ColorUtil.ARGB.multiplyAlpha(VGuiConstants.COLOR_RED, 0.8f); int selectionFillColor = ColorUtil.ARGB.multiplyAlpha(VGuiConstants.COLOR_RED, 0.2f); @@ -56,31 +57,14 @@ public void onClick(double mX, double mY) { this.onPress.accept(this); } - private void doAction() { - this.onPress.accept(this); - this.playDownSound(Minecraft.getInstance().getSoundManager()); - } - public void setSelected(boolean selected) { this.selected = selected; } - public Component getMessage() { - return message; - } - - public void setMessage(Component message) { - this.message = message; - } - public boolean isVisible() { return visible; } - public void setVisible(boolean visible) { - this.visible = visible; - } - @Override public boolean isActive() { return active; diff --git a/src/main/java/net/vulkanmod/config/gui/widget/VTextInputWidget.java b/src/main/java/net/vulkanmod/config/gui/widget/VTextInputWidget.java index fcfc18f16c..3d18a7a66f 100644 --- a/src/main/java/net/vulkanmod/config/gui/widget/VTextInputWidget.java +++ b/src/main/java/net/vulkanmod/config/gui/widget/VTextInputWidget.java @@ -18,7 +18,6 @@ public class VTextInputWidget extends VAbstractWidget { boolean selected = false; Consumer onSearch; // when the search is "activated", like pressing enter - String searchTerm = ""; Component message; Component placeholder; @@ -42,6 +41,7 @@ public void renderWidget(double mouseX, double mouseY) { textColor = VGuiConstants.COLOR_GRAY; } + //noinspection DuplicatedCode int selectionOutlineColor = ColorUtil.ARGB.multiplyAlpha(VGuiConstants.COLOR_RED, 0.8f); int selectionFillColor = ColorUtil.ARGB.multiplyAlpha(VGuiConstants.COLOR_RED, 0.2f); @@ -65,7 +65,6 @@ public void renderWidget(double mouseX, double mouseY) { public boolean keyPressed(KeyEvent keyEvent) { if (keyEvent.key() == GLFW.GLFW_KEY_ENTER || keyEvent.key() == GLFW.GLFW_KEY_KP_ENTER) { this.onSearch.accept(this); - return true; } else { if (this.message.getString().equals(this.placeholder.getString())) { this.message = Component.empty(); @@ -94,34 +93,23 @@ public boolean keyPressed(KeyEvent keyEvent) { } } - return true; } + return true; } public String getInput() { return this.message.getString(); } + @SuppressWarnings("unused") public void setSelected(boolean selected) { this.selected = selected; } - public Component getMessage() { - return message; - } - - public void setMessage(Component message) { - this.message = message; - } - public boolean isVisible() { return visible; } - public void setVisible(boolean visible) { - this.visible = visible; - } - @Override public boolean isActive() { return active; From 51de9bc0c53abadad3b3da4a3e49049cc268769c Mon Sep 17 00:00:00 2001 From: NotNekodev <97458908+NotNekodev@users.noreply.github.com> Date: Mon, 1 Dec 2025 21:21:14 +0100 Subject: [PATCH 12/27] first real search version --- .../net/vulkanmod/config/gui/VOptionList.java | 55 ++++++++--- .../vulkanmod/config/gui/VOptionScreen.java | 99 +++++++++++++++++-- .../config/gui/util/SearchHelper.java | 19 ++++ .../config/gui/widget/VTextInputWidget.java | 4 + .../net/vulkanmod/config/option/Option.java | 4 + .../vulkanmod/config/option/OptionPage.java | 2 +- 6 files changed, 159 insertions(+), 24 deletions(-) create mode 100644 src/main/java/net/vulkanmod/config/gui/util/SearchHelper.java diff --git a/src/main/java/net/vulkanmod/config/gui/VOptionList.java b/src/main/java/net/vulkanmod/config/gui/VOptionList.java index 4f7e181230..69f6650466 100644 --- a/src/main/java/net/vulkanmod/config/gui/VOptionList.java +++ b/src/main/java/net/vulkanmod/config/gui/VOptionList.java @@ -2,8 +2,10 @@ import com.mojang.blaze3d.opengl.GlStateManager; import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import net.minecraft.client.Minecraft; import net.minecraft.client.gui.components.events.GuiEventListener; import net.minecraft.client.input.MouseButtonEvent; +import net.minecraft.network.chat.Component; import net.minecraft.util.Mth; import net.vulkanmod.config.gui.render.GuiRenderer; import net.vulkanmod.config.gui.widget.OptionWidget; @@ -39,7 +41,7 @@ public VOptionList(int x, int y, int width, int height, int itemHeight) { @SuppressWarnings("unused") public void addButton(OptionWidget widget) { - this.addEntry(new Entry(widget, this.itemMargin)); + this.addEntry(new Entry(widget, this.itemMargin, null)); } public void addAll(OptionBlock[] blocks) { @@ -48,21 +50,24 @@ public void addAll(OptionBlock[] blocks) { int width = this.itemWidth; int height = this.itemHeight; + // add a header (this is MOSTLY for the search) + String title = block.title(); + if (title != null && !title.isEmpty()) { + this.addEntry(new Entry(null, 8, title)); + } + var options = block.options(); for (Option option : options) { - int margin = this.itemMargin; - - this.addEntry(new Entry(option.createOptionWidget(x0, 0, width, height), margin)); + this.addEntry(new Entry(option.createOptionWidget(x0, 0, width, height), margin, null)); } - this.addEntry(new Entry(null, 12)); + this.addEntry(new Entry(null, 12, null)); } } private void addEntry(Entry entry) { this.children.add(entry); - this.listLength += entry.getTotalHeight(); } @@ -254,13 +259,11 @@ protected void renderList(int mouseX, int mouseY) { int rowTop = this.y - (int) this.getScrollAmount(); for (int j = 0; j < itemCount; ++j) { - int rowBottom = rowTop + this.itemHeight; - VOptionList.Entry entry = this.getEntry(j); - if (rowBottom >= this.y && rowTop <= (this.y + this.height)) { - boolean updateState = this.focused == null; - entry.render(rowTop, mouseX, mouseY, updateState); + if (rowTop + entry.getTotalHeight() >= this.y && rowTop <= (this.y + this.height)) { + boolean updateState = this.focused == null; + entry.render(rowTop, mouseX, mouseY, updateState, this.x); } rowTop += entry.getTotalHeight(); @@ -278,13 +281,28 @@ protected boolean isValidClickButton(int i) { protected static class Entry implements GuiEventListener { final VAbstractWidget widget; final int margin; + final String headerTitle; - private Entry(OptionWidget widget, int margin) { + private Entry(OptionWidget widget, int margin, String headerTitle) { this.widget = widget; this.margin = margin; + this.headerTitle = headerTitle; } - public void render(int y, int mouseX, int mouseY, boolean updateState) { + public void render(int y, int mouseX, int mouseY, boolean updateState, int listX) { + // if there is a title, RENDER IT!!! + if (headerTitle != null && !headerTitle.isEmpty()) { + int headerY = y + 4; + GuiRenderer.drawString( + Minecraft.getInstance().font, + Component.literal(headerTitle), + listX + 8, + headerY, + 0xFFFFFFFF + ); + return; + } + if (widget == null) return; @@ -297,6 +315,9 @@ public void render(int y, int mouseX, int mouseY, boolean updateState) { } public int getTotalHeight() { + if (headerTitle != null && !headerTitle.isEmpty()) { + return Minecraft.getInstance().font.lineHeight + margin; + } if (widget != null) return widget.height + margin; else @@ -305,16 +326,19 @@ public int getTotalHeight() { @Override public boolean mouseClicked(MouseButtonEvent event, boolean bl) { + if (widget == null) return false; return widget.mouseClicked(event, bl); } @Override public boolean mouseReleased(MouseButtonEvent event) { + if (widget == null) return false; return widget.mouseReleased(event); } @Override public boolean mouseDragged(MouseButtonEvent event, double deltaX, double deltaY) { + if (widget == null) return false; return widget.mouseDragged(event, deltaX, deltaY); } @@ -325,7 +349,8 @@ public boolean isFocused() { @Override public void setFocused(boolean bl) { - widget.setFocused(bl); + if (widget != null) + widget.setFocused(bl); } } -} +} \ No newline at end of file diff --git a/src/main/java/net/vulkanmod/config/gui/VOptionScreen.java b/src/main/java/net/vulkanmod/config/gui/VOptionScreen.java index 9c3247d5d4..1ebe0e337a 100644 --- a/src/main/java/net/vulkanmod/config/gui/VOptionScreen.java +++ b/src/main/java/net/vulkanmod/config/gui/VOptionScreen.java @@ -14,12 +14,15 @@ import net.minecraft.util.FormattedCharSequence; import net.vulkanmod.Initializer; import net.vulkanmod.config.gui.render.GuiRenderer; +import net.vulkanmod.config.gui.util.SearchHelper; import net.vulkanmod.config.gui.util.VGuiConstants; import net.vulkanmod.config.gui.widget.VAbstractWidget; import net.vulkanmod.config.gui.widget.VButtonWidget; import net.vulkanmod.config.gui.widget.VTextInputWidget; +import net.vulkanmod.config.option.CyclingOption; import net.vulkanmod.config.option.OptionPage; import net.vulkanmod.config.option.Options; +import net.vulkanmod.config.option.Option; import net.vulkanmod.vulkan.VRenderSystem; import net.vulkanmod.vulkan.util.ColorUtil; @@ -32,8 +35,10 @@ public class VOptionScreen extends Screen { private final Screen parent; private final List optionPages; + private OptionPage searchResultsPage; private int currentListIdx = 0; + private boolean isSearchActive = false; private int tooltipWidth; @@ -132,6 +137,72 @@ private void buildLists(int left, int top, int listWidth, int listHeight, int it } } + private void performSearch(String query) { + if (query == null || query.trim().isEmpty()) { + isSearchActive = false; + this.currentListIdx = 0; + buildPage(); + return; + } + + String searchTerm = query.toLowerCase().trim(); + List searchResults = new ArrayList<>(); + + for (OptionPage page : this.optionPages) { + List> matchingOptions = new ArrayList<>(); + + for (OptionBlock block : page.optionBlocks) { + for (Option option : block.options()) { + boolean matches = false; + + String optionName = option.getName().getString().toLowerCase(); + String optionTooltip = option.getTooltip() != null ? + option.getTooltip().getString().toLowerCase() : ""; + String displayedValue = option.getDisplayedValue().getString().toLowerCase(); + + if (optionName.contains(searchTerm) || + optionTooltip.contains(searchTerm) || + displayedValue.contains(searchTerm)) { + matches = true; + } + + else if (option instanceof CyclingOption cycling) { + if (SearchHelper.matchesAnyValue(cycling, searchTerm)) { + matches = true; + } + } + + if (matches) { + matchingOptions.add(option); + } + } + } + + if (!matchingOptions.isEmpty()) { + searchResults.add(new OptionBlock("§l" + page.name, + matchingOptions.toArray(new Option[0]))); + searchResults.add(new OptionBlock("", new Option[0])); + } + } + + searchResultsPage = new OptionPage( + "Search Results", + searchResults.toArray(new OptionBlock[0]) + ); + + int top = 29; + int itemHeight = 20; + int leftMargin = 94; + int rightMargin = 3; + int listWidth = this.width - rightMargin - leftMargin; + int listHeight = this.height - top - 60; + + searchResultsPage.createList(leftMargin, top, listWidth, listHeight, itemHeight); + + isSearchActive = true; + buildPage(); + } + private void buildPage() { this.buttons.clear(); this.pageButtons.clear(); @@ -150,10 +221,16 @@ private void buildPage() { y += VGuiConstants.WIDGET_HEIGHT; } - this.pageButtons.get(this.currentListIdx).setSelected(true); - - VOptionList currentList = this.optionPages.get(this.currentListIdx).getOptionList(); - this.addWidget(currentList); + if (!isSearchActive) { + this.pageButtons.get(this.currentListIdx).setSelected(true); + VOptionList currentList = this.optionPages.get(this.currentListIdx).getOptionList(); + this.addWidget(currentList); + } else { + if (searchResultsPage != null) { + VOptionList searchList = searchResultsPage.getOptionList(); + this.addWidget(searchList); + } + } this.addButtons(); } @@ -194,9 +271,7 @@ private void addButtons() { 94, 4, x0 - 19, VGuiConstants.WIDGET_HEIGHT, Component.translatable("vulkanmod.options.searchFieldPlaceholder"), - widget -> { - - } + widget -> performSearch(widget.getInput()) ); @@ -270,9 +345,16 @@ public void render(GuiGraphics guiGraphics, int mouseX, int mouseY, float delta) int iconY = 4 + (iconBackgroundHeight - 4 - size) / 2; guiGraphics.blit(RenderPipelines.GUI_TEXTURED, ICON, iconX, iconY, 0f, 0f, size, size, size, size); - VOptionList currentList = this.optionPages.get(this.currentListIdx).getOptionList(); + VOptionList currentList; + if (isSearchActive && searchResultsPage != null) { + currentList = searchResultsPage.getOptionList(); + } else { + currentList = this.optionPages.get(this.currentListIdx).getOptionList(); + } + currentList.updateState(mouseX, mouseY); currentList.renderWidget(mouseX, mouseY); + for (VButtonWidget button : buttons) { button.updateState(mouseX, mouseY); button.render(mouseX, mouseY); @@ -342,6 +424,7 @@ private void updateState() { private void setOptionList(int i) { this.currentListIdx = i; + this.isSearchActive = false; this.buildPage(); diff --git a/src/main/java/net/vulkanmod/config/gui/util/SearchHelper.java b/src/main/java/net/vulkanmod/config/gui/util/SearchHelper.java new file mode 100644 index 0000000000..4ec106685b --- /dev/null +++ b/src/main/java/net/vulkanmod/config/gui/util/SearchHelper.java @@ -0,0 +1,19 @@ +package net.vulkanmod.config.gui.util; + +import net.minecraft.network.chat.Component; +import net.vulkanmod.config.option.CyclingOption; + +import java.util.function.Function; + +public class SearchHelper { + public static boolean matchesAnyValue(CyclingOption cycling, String searchTerm) { + Function translator = cycling.getTranslator(); + for (T value : cycling.getValues()) { + String translated = translator.apply(value).getString().toLowerCase(); + if (translated.contains(searchTerm)) { + return true; + } + } + return false; + } +} diff --git a/src/main/java/net/vulkanmod/config/gui/widget/VTextInputWidget.java b/src/main/java/net/vulkanmod/config/gui/widget/VTextInputWidget.java index 3d18a7a66f..75eb9f99cd 100644 --- a/src/main/java/net/vulkanmod/config/gui/widget/VTextInputWidget.java +++ b/src/main/java/net/vulkanmod/config/gui/widget/VTextInputWidget.java @@ -101,6 +101,10 @@ public String getInput() { return this.message.getString(); } + public void setInput(String input) { + this.message = Component.literal(input); + } + @SuppressWarnings("unused") public void setSelected(boolean selected) { this.selected = selected; diff --git a/src/main/java/net/vulkanmod/config/option/Option.java b/src/main/java/net/vulkanmod/config/option/Option.java index 4a441806fc..42b211b9db 100644 --- a/src/main/java/net/vulkanmod/config/option/Option.java +++ b/src/main/java/net/vulkanmod/config/option/Option.java @@ -72,6 +72,10 @@ public Option setTranslator(Function translator) { return this; } + public Function getTranslator() { + return translator; + } + public Option setTooltip(Function tooltipTranslator) { this.tooltipTranslator = tooltipTranslator; return this; diff --git a/src/main/java/net/vulkanmod/config/option/OptionPage.java b/src/main/java/net/vulkanmod/config/option/OptionPage.java index 069ebef856..bb9e1f0d92 100644 --- a/src/main/java/net/vulkanmod/config/option/OptionPage.java +++ b/src/main/java/net/vulkanmod/config/option/OptionPage.java @@ -5,7 +5,7 @@ public class OptionPage { public final String name; - OptionBlock[] optionBlocks; + public OptionBlock[] optionBlocks; private VOptionList optionList; public OptionPage(String name, OptionBlock[] optionBlocks) { From cf86e8f0fe542f9f23acc4f5e69c7e43252f0861 Mon Sep 17 00:00:00 2001 From: NotNekodev <97458908+NotNekodev@users.noreply.github.com> Date: Mon, 1 Dec 2025 21:47:54 +0100 Subject: [PATCH 13/27] add some styling for selection --- .../vulkanmod/config/gui/VOptionScreen.java | 79 ++++++++++++ .../config/gui/widget/VTextInputWidget.java | 113 ++++++++++-------- 2 files changed, 144 insertions(+), 48 deletions(-) diff --git a/src/main/java/net/vulkanmod/config/gui/VOptionScreen.java b/src/main/java/net/vulkanmod/config/gui/VOptionScreen.java index 1ebe0e337a..cd8ec75c32 100644 --- a/src/main/java/net/vulkanmod/config/gui/VOptionScreen.java +++ b/src/main/java/net/vulkanmod/config/gui/VOptionScreen.java @@ -6,6 +6,7 @@ import net.minecraft.client.gui.GuiGraphics; import net.minecraft.client.gui.components.events.GuiEventListener; import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.input.KeyEvent; import net.minecraft.client.input.MouseButtonEvent; import net.minecraft.client.renderer.RenderPipelines; import net.minecraft.network.chat.CommonComponents; @@ -25,6 +26,7 @@ import net.vulkanmod.config.option.Option; import net.vulkanmod.vulkan.VRenderSystem; import net.vulkanmod.vulkan.util.ColorUtil; +import org.lwjgl.glfw.GLFW; import java.util.ArrayList; import java.util.List; @@ -111,6 +113,8 @@ protected void init() { this.tooltipWidth = width; + addButtonsWithSearchBar(); + buildPage(); this.applyButton.active = false; @@ -267,6 +271,64 @@ private void addButtons() { button -> undo() ); + + buttonWidth = Minecraft.getInstance().font.width(Component.translatable("vulkanmod.options.buttons.kofi")) + padding; + x0 = (this.width - buttonWidth - rightMargin); + VButtonWidget supportButton = new VButtonWidget( + x0, 4, + buttonWidth, VGuiConstants.WIDGET_HEIGHT, + Component.translatable("vulkanmod.options.buttons.kofi"), + button -> Util.getPlatform().openUri("https://ko-fi.com/xcollateral") + ); + + + this.buttons.add(this.applyButton); + this.buttons.add(doneButton); + this.buttons.add(supportButton); + this.buttons.add(this.undoButton); + + + this.addWidget(this.applyButton); + this.addWidget(doneButton); + this.addWidget(supportButton); + this.addWidget(this.undoButton); + + this.addWidget(this.searchField); + } + + private void addButtonsWithSearchBar() { + int rightMargin = 10; + int padding = 10; + int buttonWidth = Minecraft.getInstance().font.width(CommonComponents.GUI_DONE) + 2 * padding; + int x0 = (this.width - buttonWidth - rightMargin); + int y0 = this.height - VGuiConstants.WIDGET_HEIGHT - 7; + + VButtonWidget doneButton = new VButtonWidget( + x0, y0, + buttonWidth, VGuiConstants.WIDGET_HEIGHT, + CommonComponents.GUI_DONE, + button -> Minecraft.getInstance().setScreen(this.parent) + ); + + buttonWidth = Minecraft.getInstance().font.width(Component.translatable("vulkanmod.options.buttons.apply")) + 2 * padding; + x0 -= (buttonWidth + VGuiConstants.WIDGET_MARGIN); + this.applyButton = new VButtonWidget( + x0, y0, + buttonWidth, VGuiConstants.WIDGET_HEIGHT, + Component.translatable("vulkanmod.options.buttons.apply"), + button -> this.applyOptions() + ); + + buttonWidth = Minecraft.getInstance().font.width(Component.translatable("vulkanmod.options.buttons.undo")) + 2 * padding; + x0 -= (buttonWidth + VGuiConstants.WIDGET_MARGIN); + this.undoButton = new VButtonWidget( + x0, y0, + buttonWidth, VGuiConstants.WIDGET_HEIGHT, + Component.translatable("vulkanmod.options.buttons.undo"), + button -> undo() + + ); + this.searchField = new VTextInputWidget( 94, 4, x0 - 19, VGuiConstants.WIDGET_HEIGHT, @@ -426,6 +488,9 @@ private void setOptionList(int i) { this.currentListIdx = i; this.isSearchActive = false; + this.searchField.setInput(""); + this.searchField.setFocused(false); + this.buildPage(); this.pageButtons.get(i).setSelected(true); @@ -441,4 +506,18 @@ private void applyOptions() { Initializer.CONFIG.write(); } + + @Override + public boolean keyPressed(KeyEvent keyEvent) { + if (keyEvent.key() == GLFW.GLFW_KEY_ESCAPE && this.isSearchActive) { + this.isSearchActive = false; + this.searchField.setInput(""); + this.searchField.setFocused(false); + this.buildPage(); + this.pageButtons.get(this.currentListIdx).setSelected(true); + return true; + } + + return super.keyPressed(keyEvent); + } } \ No newline at end of file diff --git a/src/main/java/net/vulkanmod/config/gui/widget/VTextInputWidget.java b/src/main/java/net/vulkanmod/config/gui/widget/VTextInputWidget.java index 75eb9f99cd..47745d05db 100644 --- a/src/main/java/net/vulkanmod/config/gui/widget/VTextInputWidget.java +++ b/src/main/java/net/vulkanmod/config/gui/widget/VTextInputWidget.java @@ -4,42 +4,41 @@ import net.minecraft.client.gui.ComponentPath; import net.minecraft.client.gui.navigation.FocusNavigationEvent; import net.minecraft.client.input.KeyEvent; +import net.minecraft.client.input.MouseButtonEvent; import net.minecraft.network.chat.Component; -import net.minecraft.util.Mth; import net.vulkanmod.config.gui.render.GuiRenderer; import net.vulkanmod.config.gui.util.VGuiConstants; import net.vulkanmod.vulkan.util.ColorUtil; import org.jetbrains.annotations.Nullable; import org.lwjgl.glfw.GLFW; -import java.util.Objects; import java.util.function.Consumer; public class VTextInputWidget extends VAbstractWidget { boolean selected = false; Consumer onSearch; // when the search is "activated", like pressing enter - Component message; - Component placeholder; + private String text; + private final Component placeholder; public VTextInputWidget(int x, int y, int width, int height, Component placeholder, Consumer onSearch) { this.setPosition(x, y, width, height); - this.message = placeholder; this.placeholder = placeholder; this.onSearch = onSearch; + this.text = ""; } + @Override public void renderWidget(double mouseX, double mouseY) { if (!this.isVisible()) return; - int backgroundColor = this.isActive() - ? ColorUtil.ARGB.multiplyAlpha(VGuiConstants.COLOR_BLACK, 0.45f) - : ColorUtil.ARGB.multiplyAlpha(VGuiConstants.COLOR_BLACK, 0.3f); - int textColor = VGuiConstants.COLOR_WHITE; + boolean hasText = !this.text.isEmpty(); - if (this.message.getString().equals(this.placeholder.getString())) { - textColor = VGuiConstants.COLOR_GRAY; - } + int backgroundColor = this.focused || this.selected + ? ColorUtil.ARGB.multiplyAlpha(VGuiConstants.COLOR_BLACK, 0.3f) + : ColorUtil.ARGB.multiplyAlpha(VGuiConstants.COLOR_BLACK, 0.45f); + + int textColor = hasText ? VGuiConstants.COLOR_WHITE : VGuiConstants.COLOR_GRAY; //noinspection DuplicatedCode int selectionOutlineColor = ColorUtil.ARGB.multiplyAlpha(VGuiConstants.COLOR_RED, 0.8f); @@ -47,62 +46,56 @@ public void renderWidget(double mouseX, double mouseY) { GuiRenderer.fill(this.x, this.y, this.x + this.width, this.y + this.height, backgroundColor); - if (this.selected) { - GuiRenderer.fill(this.x, this.y, this.x + 2, this.y + this.height, selectionOutlineColor); + if (this.selected || this.focused) { + GuiRenderer.renderBorder(x, y, x + width, y + height, 1, selectionOutlineColor); GuiRenderer.fill(this.x, this.y, this.x + this.width, this.y + this.height, selectionFillColor); } - // this is down here because of layering + Component displayText = hasText ? Component.literal(this.text) : this.placeholder; + GuiRenderer.drawString( Minecraft.getInstance().font, - this.message, - this.x + 8, (this.y + this.height / 2) - 4, - textColor | (Mth.ceil(255.0f) << 24)); - + displayText, + this.x + 8, + this.y + (this.height - 8) / 2, + textColor | 0xFF000000 + ); } @Override public boolean keyPressed(KeyEvent keyEvent) { - if (keyEvent.key() == GLFW.GLFW_KEY_ENTER || keyEvent.key() == GLFW.GLFW_KEY_KP_ENTER) { + if (!this.focused && !this.selected) return false; + + if (keyEvent.key() == GLFW.GLFW_KEY_ENTER || keyEvent.key() == GLFW.GLFW_KEY_KP_ENTER) { this.onSearch.accept(this); - } else { - if (this.message.getString().equals(this.placeholder.getString())) { - this.message = Component.empty(); - } + return true; + } - if (keyEvent.key() == GLFW.GLFW_KEY_BACKSPACE) { - String string = this.message.getString(); - if (!string.isEmpty()) { - string = string.substring(0, string.length() - 1); - } - this.message = Component.literal(string); - } else { - if (!keyEvent.hasShiftDown()) { - String string = this.message.getString(); - String name = GLFW.glfwGetKeyName(keyEvent.key(), keyEvent.scancode()); - if (name == null) { - return false; - } - string += name; - this.message = Component.literal(string); - } else { - String string = this.message.getString(); - String name = Objects.requireNonNull(GLFW.glfwGetKeyName(keyEvent.key(), keyEvent.scancode())).toUpperCase(); - string += name; - this.message = Component.literal(string); - } + if (keyEvent.key() == GLFW.GLFW_KEY_BACKSPACE) { + if (!this.text.isEmpty()) { + this.text = this.text.substring(0, this.text.length() - 1); + this.onSearch.accept(this); // live search } + return true; + } + String keyName = GLFW.glfwGetKeyName(keyEvent.key(), keyEvent.scancode()); + if (keyName != null && keyName.length() == 1) { + if (keyEvent.hasShiftDown()) keyName = keyName.toUpperCase(); + this.text += keyName; + this.onSearch.accept(this); + return true; } - return true; + + return false; } public String getInput() { - return this.message.getString(); + return this.text; } public void setInput(String input) { - this.message = Component.literal(input); + this.text = input != null ? input : ""; } @SuppressWarnings("unused") @@ -129,4 +122,28 @@ public void setActive(boolean active) { return null; return super.nextFocusPath(event); } + + @Override + public boolean mouseClicked(MouseButtonEvent event, boolean bl) { + if (!this.active || !this.visible) return false; + + boolean clicked = this.clicked(event.x(), event.y()); + if (clicked) { + this.setFocused(true); + this.selected = true; + return true; + } else { + this.setFocused(false); + this.selected = false; + return false; + } + } + + @Override + public void setFocused(boolean focused) { + super.setFocused(focused); + if (!focused) { + this.selected = false; + } + } } From 0c353b4bdc643b1010b5ee518aad0281e961096b Mon Sep 17 00:00:00 2001 From: NotNekodev <97458908+NotNekodev@users.noreply.github.com> Date: Mon, 1 Dec 2025 22:05:21 +0100 Subject: [PATCH 14/27] add selection and such UX + a blinking cursor --- .../config/gui/widget/VTextInputWidget.java | 135 +++++++++++++++--- 1 file changed, 113 insertions(+), 22 deletions(-) diff --git a/src/main/java/net/vulkanmod/config/gui/widget/VTextInputWidget.java b/src/main/java/net/vulkanmod/config/gui/widget/VTextInputWidget.java index 47745d05db..7d6b85aa99 100644 --- a/src/main/java/net/vulkanmod/config/gui/widget/VTextInputWidget.java +++ b/src/main/java/net/vulkanmod/config/gui/widget/VTextInputWidget.java @@ -1,5 +1,6 @@ package net.vulkanmod.config.gui.widget; +import net.minecraft.Util; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.ComponentPath; import net.minecraft.client.gui.navigation.FocusNavigationEvent; @@ -20,6 +21,13 @@ public class VTextInputWidget extends VAbstractWidget { private String text; private final Component placeholder; + private int cursorPos = 0; + private int selectionEnd = 0; + private long lastBlinkTime = 0; + private boolean showCursor = true; + + private static final int CURSOR_BLINK_INTERVAL = 500; // ms + public VTextInputWidget(int x, int y, int width, int height, Component placeholder, Consumer onSearch) { this.setPosition(x, y, width, height); @@ -33,56 +41,129 @@ public void renderWidget(double mouseX, double mouseY) { if (!this.isVisible()) return; boolean hasText = !this.text.isEmpty(); + boolean isFocused = this.focused || this.selected; - int backgroundColor = this.focused || this.selected - ? ColorUtil.ARGB.multiplyAlpha(VGuiConstants.COLOR_BLACK, 0.3f) - : ColorUtil.ARGB.multiplyAlpha(VGuiConstants.COLOR_BLACK, 0.45f); + int backgroundColor = ColorUtil.ARGB.multiplyAlpha(VGuiConstants.COLOR_BLACK, 0.45f); int textColor = hasText ? VGuiConstants.COLOR_WHITE : VGuiConstants.COLOR_GRAY; - //noinspection DuplicatedCode - int selectionOutlineColor = ColorUtil.ARGB.multiplyAlpha(VGuiConstants.COLOR_RED, 0.8f); - int selectionFillColor = ColorUtil.ARGB.multiplyAlpha(VGuiConstants.COLOR_RED, 0.2f); - GuiRenderer.fill(this.x, this.y, this.x + this.width, this.y + this.height, backgroundColor); - if (this.selected || this.focused) { - GuiRenderer.renderBorder(x, y, x + width, y + height, 1, selectionOutlineColor); - GuiRenderer.fill(this.x, this.y, this.x + this.width, this.y + this.height, selectionFillColor); + if (isFocused && cursorPos != selectionEnd) { + int start = Math.min(cursorPos, selectionEnd); + int end = Math.max(cursorPos, selectionEnd); + String before = text.substring(0, start); + String selected = text.substring(start, end); + + int xBefore = this.x + 8 + Minecraft.getInstance().font.width(before); + int xSelected = Minecraft.getInstance().font.width(selected); + + int selColor = ColorUtil.ARGB.multiplyAlpha(VGuiConstants.COLOR_RED, 0.55f); + GuiRenderer.fill(xBefore, this.y + 4, xBefore + xSelected, this.y + this.height - 4, selColor); } Component displayText = hasText ? Component.literal(this.text) : this.placeholder; + GuiRenderer.drawString(Minecraft.getInstance().font, displayText, + this.x + 8, this.y + (this.height - 8) / 2, textColor | 0xFF000000); - GuiRenderer.drawString( - Minecraft.getInstance().font, - displayText, - this.x + 8, - this.y + (this.height - 8) / 2, - textColor | 0xFF000000 - ); + if (isFocused && showCursor) { + String beforeCursor = text.substring(0, cursorPos); + int cursorX = this.x + 8 + Minecraft.getInstance().font.width(beforeCursor); + + GuiRenderer.fill(cursorX, this.y + 6, cursorX + 1, this.y + this.height - 6, + VGuiConstants.COLOR_WHITE); + } + + if (isFocused) { + int borderColor = ColorUtil.ARGB.multiplyAlpha(VGuiConstants.COLOR_RED, 0.8f); + GuiRenderer.renderBorder(this.x, this.y, this.x + this.width, this.y + this.height, 1, borderColor); + } + + if (isFocused) { + long time = Util.getMillis(); + if (time - lastBlinkTime > CURSOR_BLINK_INTERVAL) { + showCursor = !showCursor; + lastBlinkTime = time; + } + } else { + showCursor = true; + } } @Override public boolean keyPressed(KeyEvent keyEvent) { if (!this.focused && !this.selected) return false; + boolean shift = keyEvent.hasShiftDown(); + boolean ctrl = keyEvent.hasControlDown(); + if (keyEvent.key() == GLFW.GLFW_KEY_ENTER || keyEvent.key() == GLFW.GLFW_KEY_KP_ENTER) { this.onSearch.accept(this); return true; } + if (cursorPos != selectionEnd) { + int start = Math.min(cursorPos, selectionEnd); + int end = Math.max(cursorPos, selectionEnd); + + if (keyEvent.key() == GLFW.GLFW_KEY_BACKSPACE || keyEvent.key() == GLFW.GLFW_KEY_DELETE) { + this.text = text.substring(0, start) + text.substring(end); + cursorPos = start; + selectionEnd = start; + this.onSearch.accept(this); + return true; + } + } + if (keyEvent.key() == GLFW.GLFW_KEY_BACKSPACE) { - if (!this.text.isEmpty()) { - this.text = this.text.substring(0, this.text.length() - 1); - this.onSearch.accept(this); // live search + if (cursorPos > 0) { + this.text = text.substring(0, cursorPos - 1) + text.substring(cursorPos); + cursorPos--; + selectionEnd = cursorPos; + this.onSearch.accept(this); + } + return true; + } + + if (keyEvent.key() == GLFW.GLFW_KEY_DELETE) { + if (cursorPos < text.length()) { + this.text = text.substring(0, cursorPos) + text.substring(cursorPos + 1); + this.onSearch.accept(this); } return true; } + if (ctrl && keyEvent.key() == GLFW.GLFW_KEY_A) { + cursorPos = text.length(); + selectionEnd = 0; + return true; + } + + if (keyEvent.key() == GLFW.GLFW_KEY_LEFT) { + if (cursorPos > 0) cursorPos--; + if (!shift) selectionEnd = cursorPos; + return true; + } + if (keyEvent.key() == GLFW.GLFW_KEY_RIGHT) { + if (cursorPos < text.length()) cursorPos++; + if (!shift) selectionEnd = cursorPos; + return true; + } + String keyName = GLFW.glfwGetKeyName(keyEvent.key(), keyEvent.scancode()); if (keyName != null && keyName.length() == 1) { - if (keyEvent.hasShiftDown()) keyName = keyName.toUpperCase(); - this.text += keyName; + char c = keyEvent.hasShiftDown() ? keyName.toUpperCase().charAt(0) : keyName.charAt(0); + + if (cursorPos != selectionEnd) { + int start = Math.min(cursorPos, selectionEnd); + int end = Math.max(cursorPos, selectionEnd); + this.text = text.substring(0, start) + c + text.substring(end); + cursorPos = start + 1; + } else { + this.text = text.substring(0, cursorPos) + c + text.substring(cursorPos); + cursorPos++; + } + selectionEnd = cursorPos; this.onSearch.accept(this); return true; } @@ -131,6 +212,16 @@ public boolean mouseClicked(MouseButtonEvent event, boolean bl) { if (clicked) { this.setFocused(true); this.selected = true; + + int relX = (int) event.x() - (this.x + 8); + int pos = 0; + for (int i = 0; i < text.length(); i++) { + if (Minecraft.getInstance().font.width(text.substring(0, i + 1)) > relX) break; + pos = i + 1; + } + cursorPos = pos; + selectionEnd = pos; + return true; } else { this.setFocused(false); From 68c0198b379d6ae17af5803fda0972be4631284b Mon Sep 17 00:00:00 2001 From: NotNekodev <97458908+NotNekodev@users.noreply.github.com> Date: Mon, 1 Dec 2025 22:20:31 +0100 Subject: [PATCH 15/27] add a keybind for returning to the vanilla settings (Shift + P) and a keybind for focusing the search field (Ctrl + L) --- .../vulkanmod/config/gui/VOptionScreen.java | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/main/java/net/vulkanmod/config/gui/VOptionScreen.java b/src/main/java/net/vulkanmod/config/gui/VOptionScreen.java index cd8ec75c32..7481c2c576 100644 --- a/src/main/java/net/vulkanmod/config/gui/VOptionScreen.java +++ b/src/main/java/net/vulkanmod/config/gui/VOptionScreen.java @@ -6,6 +6,7 @@ import net.minecraft.client.gui.GuiGraphics; import net.minecraft.client.gui.components.events.GuiEventListener; import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.gui.screens.options.VideoSettingsScreen; import net.minecraft.client.input.KeyEvent; import net.minecraft.client.input.MouseButtonEvent; import net.minecraft.client.renderer.RenderPipelines; @@ -509,6 +510,14 @@ private void applyOptions() { @Override public boolean keyPressed(KeyEvent keyEvent) { + if (keyEvent.hasControlDown() && keyEvent.key() == GLFW.GLFW_KEY_L) { + this.setFocused(searchField); + searchField.setFocused(true); + searchField.setSelected(true); + + return true; + } + if (keyEvent.key() == GLFW.GLFW_KEY_ESCAPE && this.isSearchActive) { this.isSearchActive = false; this.searchField.setInput(""); @@ -518,6 +527,15 @@ public boolean keyPressed(KeyEvent keyEvent) { return true; } + + if (!this.isSearchActive + && keyEvent.key() == GLFW.GLFW_KEY_P + && keyEvent.hasShiftDown()) { + Minecraft.getInstance().setScreen(new VideoSettingsScreen(this, Minecraft.getInstance(), Minecraft.getInstance().options)); + + return true; + } + return super.keyPressed(keyEvent); } } \ No newline at end of file From a50e6c76cd07657d864452edc84ab18e622e9b35 Mon Sep 17 00:00:00 2001 From: NotNekodev <97458908+NotNekodev@users.noreply.github.com> Date: Tue, 2 Dec 2025 18:34:29 +0100 Subject: [PATCH 16/27] fix a bug, where you cant change to the vanilla screen when there is something in the text box --- src/main/java/net/vulkanmod/config/gui/VOptionScreen.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/net/vulkanmod/config/gui/VOptionScreen.java b/src/main/java/net/vulkanmod/config/gui/VOptionScreen.java index 7481c2c576..1968b9c678 100644 --- a/src/main/java/net/vulkanmod/config/gui/VOptionScreen.java +++ b/src/main/java/net/vulkanmod/config/gui/VOptionScreen.java @@ -528,12 +528,12 @@ public boolean keyPressed(KeyEvent keyEvent) { } - if (!this.isSearchActive + if (!this.searchField.focused && keyEvent.key() == GLFW.GLFW_KEY_P && keyEvent.hasShiftDown()) { Minecraft.getInstance().setScreen(new VideoSettingsScreen(this, Minecraft.getInstance(), Minecraft.getInstance().options)); - return true; + return false; } return super.keyPressed(keyEvent); From b7436efd4614a172d66940764cdcd57ad78845cc Mon Sep 17 00:00:00 2001 From: NotNekodev <97458908+NotNekodev@users.noreply.github.com> Date: Tue, 2 Dec 2025 18:46:20 +0100 Subject: [PATCH 17/27] add fovEffectScale option --- src/main/java/net/vulkanmod/config/option/Options.java | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/main/java/net/vulkanmod/config/option/Options.java b/src/main/java/net/vulkanmod/config/option/Options.java index 55761e8f24..37b0d155b5 100644 --- a/src/main/java/net/vulkanmod/config/option/Options.java +++ b/src/main/java/net/vulkanmod/config/option/Options.java @@ -2,6 +2,7 @@ import com.mojang.blaze3d.platform.Window; import net.minecraft.client.*; +import net.minecraft.network.chat.CommonComponents; import net.minecraft.network.chat.Component; import net.minecraft.server.level.ParticleStatus; import net.vulkanmod.Initializer; @@ -142,6 +143,11 @@ public static OptionBlock[] getVideoOpts() { new SwitchOption(Component.translatable("options.viewBobbing"), (value) -> minecraftOptions.bobView().set(value), () -> minecraftOptions.bobView().get()), + new RangeOption(Component.translatable("options.fovEffectScale"), + 0, 100, 1, + (value) -> minecraftOptions.fovEffectScale().set(value / 100d), + () -> (int) (minecraftOptions.fovEffectScale().get() * 100)) + .setTooltip(value -> Component.translatable("options.fovEffectScale.tooltip")), new CyclingOption<>(Component.translatable("options.attackIndicator"), AttackIndicatorStatus.values(), value -> minecraftOptions.attackIndicator().set(value), @@ -246,7 +252,9 @@ public static OptionBlock[] getGraphicsOpts() { }, () -> minecraftOptions.mipmapLevels().get()) .setTranslator(value -> Component.nullToEmpty(value.toString())) - }) + })/*, + + */ }; } From cb4e1df40727e59811648bd3760e2125eb493775 Mon Sep 17 00:00:00 2001 From: NotNekodev <97458908+NotNekodev@users.noreply.github.com> Date: Tue, 2 Dec 2025 18:53:08 +0100 Subject: [PATCH 18/27] add glint options (glintSpeed and glintStrength) + change vulkanmod.options.refreshRate to "Fullscreen Refresh Rate", because it only applies in fullscreen --- src/main/java/net/vulkanmod/config/option/Options.java | 10 ++++++++++ src/main/resources/assets/vulkanmod/lang/en_us.json | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/main/java/net/vulkanmod/config/option/Options.java b/src/main/java/net/vulkanmod/config/option/Options.java index 37b0d155b5..0ec1951f58 100644 --- a/src/main/java/net/vulkanmod/config/option/Options.java +++ b/src/main/java/net/vulkanmod/config/option/Options.java @@ -148,6 +148,16 @@ public static OptionBlock[] getVideoOpts() { (value) -> minecraftOptions.fovEffectScale().set(value / 100d), () -> (int) (minecraftOptions.fovEffectScale().get() * 100)) .setTooltip(value -> Component.translatable("options.fovEffectScale.tooltip")), + new RangeOption(Component.translatable("options.glintSpeed"), + 0, 100, 1, + (value) -> minecraftOptions.glintSpeed().set(value / 100d), + () -> (int) (minecraftOptions.glintSpeed().get() * 100)) + .setTooltip(value -> Component.translatable("options.glintSpeed.tooltip")), + new RangeOption(Component.translatable("options.glintStrength"), + 0, 100, 1, + (value) -> minecraftOptions.glintStrength().set(value / 100d), + () -> (int) (minecraftOptions.glintStrength().get() * 100)) + .setTooltip(value -> Component.translatable("options.glintStrength.tooltip")), new CyclingOption<>(Component.translatable("options.attackIndicator"), AttackIndicatorStatus.values(), value -> minecraftOptions.attackIndicator().set(value), diff --git a/src/main/resources/assets/vulkanmod/lang/en_us.json b/src/main/resources/assets/vulkanmod/lang/en_us.json index 310967ad4a..241746fcf0 100644 --- a/src/main/resources/assets/vulkanmod/lang/en_us.json +++ b/src/main/resources/assets/vulkanmod/lang/en_us.json @@ -35,7 +35,7 @@ "vulkanmod.options.indirectDraw": "Indirect Draw", "vulkanmod.options.indirectDraw.tooltip": "Reduces CPU overhead but might increases GPU overhead.", - "vulkanmod.options.refreshRate": "Refresh Rate", + "vulkanmod.options.refreshRate": "Fullscreen Refresh Rate", "vulkanmod.options.uniqueOpaqueLayer": "Unique opaque layer", "vulkanmod.options.uniqueOpaqueLayer.tooltip": "Use a unique render layer for opaque terrain to improve performance.", From f4fc07c49dd3d845d132424cced419788f3fd09a Mon Sep 17 00:00:00 2001 From: NotNekodev <97458908+NotNekodev@users.noreply.github.com> Date: Tue, 2 Dec 2025 19:27:07 +0100 Subject: [PATCH 19/27] WindowMode, VideoMode and all that has been rewritten and Config has also changed --- src/main/java/net/vulkanmod/Initializer.java | 9 +- .../java/net/vulkanmod/config/Config.java | 138 +++-- .../vulkanmod/config/gui/VOptionScreen.java | 2 +- .../net/vulkanmod/config/option/Options.java | 472 +++++++++--------- .../net/vulkanmod/config/video/VideoMode.java | 15 + .../config/video/VideoModeManager.java | 123 +++-- .../vulkanmod/config/video/VideoModeSet.java | 96 +--- .../vulkanmod/config/video/WindowMode.java | 48 +- .../vulkanmod/mixin/window/WindowMixin.java | 23 +- 9 files changed, 459 insertions(+), 467 deletions(-) create mode 100644 src/main/java/net/vulkanmod/config/video/VideoMode.java diff --git a/src/main/java/net/vulkanmod/Initializer.java b/src/main/java/net/vulkanmod/Initializer.java index 198d7153a6..6a116e5a32 100644 --- a/src/main/java/net/vulkanmod/Initializer.java +++ b/src/main/java/net/vulkanmod/Initializer.java @@ -42,14 +42,7 @@ public void onInitializeClient() { } private static Config loadConfig(Path path) { - Config config = Config.load(path); - - if(config == null) { - config = new Config(); - config.write(); - } - - return config; + return Config.load(path); } public static String getVersion() { diff --git a/src/main/java/net/vulkanmod/config/Config.java b/src/main/java/net/vulkanmod/config/Config.java index e2bebdb7f5..8212660008 100644 --- a/src/main/java/net/vulkanmod/config/Config.java +++ b/src/main/java/net/vulkanmod/config/Config.java @@ -1,75 +1,143 @@ package net.vulkanmod.config; -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; +import com.google.gson.*; +import com.google.gson.annotations.JsonAdapter; +import net.vulkanmod.config.video.VideoMode; import net.vulkanmod.config.video.VideoModeManager; -import net.vulkanmod.config.video.VideoModeSet; -import java.io.FileReader; import java.io.IOException; import java.lang.reflect.Modifier; import java.nio.file.Files; import java.nio.file.Path; -import java.util.Collections; +import java.util.Objects; +@JsonAdapter(Config.GsonAdapter.class) public class Config { - public VideoModeSet.VideoMode videoMode = VideoModeManager.getFirstAvailable().getVideoMode(); + + public VideoMode videoMode; public int windowMode = 0; public int advCulling = 2; public boolean indirectDraw = true; - public boolean uniqueOpaqueLayer = true; public boolean entityCulling = true; - public int device = -1; public int ambientOcclusion = 1; public int frameQueueSize = 2; public int builderThreads = 0; - public boolean backFaceCulling = true; public boolean textureAnimations = true; - public void write() { + public int device = -1; - if(!Files.exists(CONFIG_PATH.getParent())) { - try { - Files.createDirectories(CONFIG_PATH); - } catch (IOException e) { - e.printStackTrace(); - } - } + private static Path CONFIG_PATH; + private static final Gson GSON = new GsonBuilder() + .setPrettyPrinting() + .registerTypeAdapter(Config.class, new GsonAdapter()) + .create(); + public void save() { try { - Files.write(CONFIG_PATH, Collections.singleton(GSON.toJson(this))); + Files.createDirectories(CONFIG_PATH.getParent()); + Files.writeString(CONFIG_PATH, GSON.toJson(this)); } catch (IOException e) { e.printStackTrace(); } } - private static Path CONFIG_PATH; - - private static final Gson GSON = new GsonBuilder() - .setPrettyPrinting() - .excludeFieldsWithModifiers(Modifier.PRIVATE) - .create(); - public static Config load(Path path) { - Config config; - Config.CONFIG_PATH = path; + CONFIG_PATH = path; if (Files.exists(path)) { - try (FileReader fileReader = new FileReader(path.toFile())) { - config = GSON.fromJson(fileReader, Config.class); + try { + String content = Files.readString(path); + Config config = GSON.fromJson(content, Config.class); + + if (config.videoMode == null || + VideoModeManager.findSetFor(config.videoMode) == null) { + config.videoMode = VideoModeManager.currentOsMode(); + } + + return config; + } catch (IOException | JsonSyntaxException e) { + System.err.println("Failed to load config, using defaults: " + e.getMessage()); } - catch (IOException exception) { - throw new RuntimeException(exception.getMessage()); + } + + Config config = new Config(); + config.videoMode = VideoModeManager.currentOsMode(); + return config; + } + + public static class GsonAdapter implements JsonSerializer, JsonDeserializer { + + @Override + public JsonElement serialize(Config src, java.lang.reflect.Type typeOfSrc, JsonSerializationContext context) { + JsonObject obj = new JsonObject(); + + if (src.videoMode != null) { + JsonObject vm = new JsonObject(); + vm.addProperty("width", src.videoMode.width()); + vm.addProperty("height", src.videoMode.height()); + vm.addProperty("bitDepth", src.videoMode.bitDepth()); + vm.addProperty("refreshRate", src.videoMode.refreshRate()); + obj.add("videoMode", vm); } + + obj.addProperty("windowMode", src.windowMode); + obj.addProperty("advCulling", src.advCulling); + obj.addProperty("indirectDraw", src.indirectDraw); + obj.addProperty("uniqueOpaqueLayer", src.uniqueOpaqueLayer); + obj.addProperty("entityCulling", src.entityCulling); + obj.addProperty("ambientOcclusion", src.ambientOcclusion); + obj.addProperty("frameQueueSize", src.frameQueueSize); + obj.addProperty("builderThreads", src.builderThreads); + obj.addProperty("backFaceCulling", src.backFaceCulling); + obj.addProperty("textureAnimations", src.textureAnimations); + obj.addProperty("device", src.device); + + return obj; } - else { - config = null; + + @Override + public Config deserialize(JsonElement json, java.lang.reflect.Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + Config config = new Config(); + JsonObject obj = json.getAsJsonObject(); + + if (obj.has("videoMode")) { + JsonObject vm = obj.getAsJsonObject("videoMode"); + int w = getInt(vm, "width", 1920); + int h = getInt(vm, "height", 1080); + int bd = getInt(vm, "bitDepth", 8); + int rr = getInt(vm, "refreshRate", 60); + config.videoMode = new VideoMode(w, h, bd, rr); + } else { + config.videoMode = VideoModeManager.currentOsMode(); + } + + config.windowMode = getInt(obj, "windowMode", 0); + config.advCulling = getInt(obj, "advCulling", 2); + config.indirectDraw = getBoolean(obj, "indirectDraw", true); + config.uniqueOpaqueLayer = getBoolean(obj, "uniqueOpaqueLayer", true); + config.entityCulling = getBoolean(obj, "entityCulling", true); + config.ambientOcclusion = getInt(obj, "ambientOcclusion", 1); + config.frameQueueSize = getInt(obj, "frameQueueSize", 2); + config.builderThreads = getInt(obj, "builderThreads", 0); + config.backFaceCulling = getBoolean(obj, "backFaceCulling", true); + config.textureAnimations = getBoolean(obj, "textureAnimations", true); + config.device = getInt(obj, "device", -1); + + return config; } - return config; + private int getInt(JsonObject obj, String key, int def) { + JsonElement el = obj.get(key); + return el != null && el.isJsonPrimitive() ? el.getAsInt() : def; + } + + private boolean getBoolean(JsonObject obj, String key, boolean def) { + JsonElement el = obj.get(key); + return el != null && el.isJsonPrimitive() ? el.getAsBoolean() : def; + } } -} +} \ No newline at end of file diff --git a/src/main/java/net/vulkanmod/config/gui/VOptionScreen.java b/src/main/java/net/vulkanmod/config/gui/VOptionScreen.java index 1968b9c678..ea307103c3 100644 --- a/src/main/java/net/vulkanmod/config/gui/VOptionScreen.java +++ b/src/main/java/net/vulkanmod/config/gui/VOptionScreen.java @@ -505,7 +505,7 @@ private void applyOptions() { this.captureOriginalState(); - Initializer.CONFIG.write(); + Initializer.CONFIG.save(); } @Override diff --git a/src/main/java/net/vulkanmod/config/option/Options.java b/src/main/java/net/vulkanmod/config/option/Options.java index 0ec1951f58..7749f775eb 100644 --- a/src/main/java/net/vulkanmod/config/option/Options.java +++ b/src/main/java/net/vulkanmod/config/option/Options.java @@ -2,15 +2,12 @@ import com.mojang.blaze3d.platform.Window; import net.minecraft.client.*; -import net.minecraft.network.chat.CommonComponents; import net.minecraft.network.chat.Component; import net.minecraft.server.level.ParticleStatus; import net.vulkanmod.Initializer; import net.vulkanmod.config.Config; -import net.vulkanmod.config.gui.OptionBlock; -import net.vulkanmod.config.video.VideoModeManager; -import net.vulkanmod.config.video.VideoModeSet; -import net.vulkanmod.config.video.WindowMode; +import net.vulkanmod.config.gui.*; +import net.vulkanmod.config.video.*; import net.vulkanmod.render.chunk.WorldRenderer; import net.vulkanmod.render.chunk.build.light.LightMode; import net.vulkanmod.render.vertex.TerrainRenderType; @@ -20,152 +17,155 @@ import java.util.stream.IntStream; public abstract class Options { + public static boolean fullscreenDirty = false; - static Config config = Initializer.CONFIG; - static Minecraft minecraft = Minecraft.getInstance(); - static Window window = minecraft.getWindow(); - static net.minecraft.client.Options minecraftOptions = minecraft.options; - public static OptionBlock[] getVideoOpts() { - var videoMode = config.videoMode; - var videoModeSet = VideoModeManager.getFromVideoMode(videoMode); + private static final Config config = Initializer.CONFIG; + private static final Minecraft minecraft = Minecraft.getInstance(); + private static final Window window = minecraft.getWindow(); + private static final net.minecraft.client.Options mcOptions = minecraft.options; - if (videoModeSet == null) { - videoModeSet = VideoModeSet.getDummy(); - videoMode = videoModeSet.getVideoMode(-1); - } + public static OptionBlock[] getVideoOpts() { + VideoMode currentMode = config.videoMode; + VideoModeSet currentSet = VideoModeManager.findSetFor(currentMode); + VideoModeSet[] resolutions = VideoModeManager.availableSets().toArray(VideoModeSet[]::new); - VideoModeManager.selectedVideoMode = videoMode; - var refreshRates = videoModeSet.getRefreshRates(); + CyclingOption resolutionOption = (CyclingOption) new CyclingOption<>( + Component.translatable("options.fullscreen.resolution"), + resolutions, + set -> { + int targetRate = currentSet.supportsRate(currentMode.refreshRate()) + ? currentMode.refreshRate() + : set.refreshRates().last(); - CyclingOption RefreshRate = (CyclingOption) new CyclingOption<>( - Component.translatable("vulkanmod.options.refreshRate"), - refreshRates.toArray(new Integer[0]), - (value) -> { - VideoModeManager.selectedVideoMode.refreshRate = value; - VideoModeManager.applySelectedVideoMode(); + VideoMode newMode = set.modeAtRate(targetRate); + config.videoMode = newMode; + VideoModeManager.selectMode(newMode); - if (minecraftOptions.fullscreen().get()) + if (mcOptions.fullscreen().get()) { fullscreenDirty = true; + } }, - () -> VideoModeManager.selectedVideoMode.refreshRate) - .setTranslator(refreshRate -> Component.nullToEmpty(refreshRate.toString())); + () -> currentSet + ).setTranslator(set -> Component.literal(set.toString())); - Option resolutionOption = new CyclingOption<>( - Component.translatable("options.fullscreen.resolution"), - VideoModeManager.getVideoResolutions(), - (value) -> { - VideoModeManager.selectedVideoMode = value.getVideoMode(RefreshRate.getNewValue()); - VideoModeManager.applySelectedVideoMode(); + CyclingOption refreshRateOption = (CyclingOption) new CyclingOption<>( + Component.translatable("vulkanmod.options.refreshRate"), + currentSet.refreshRates().toArray(Integer[]::new), + rate -> { + VideoMode newMode = currentMode.withRefreshRate(rate); + config.videoMode = newMode; + VideoModeManager.selectMode(newMode); - if (minecraftOptions.fullscreen().get()) + if (mcOptions.fullscreen().get()) { fullscreenDirty = true; + } }, - () -> { - var selectedVideoMode = VideoModeManager.selectedVideoMode; - var selectedVideoModeSet = VideoModeManager.getFromVideoMode(selectedVideoMode); - - return selectedVideoModeSet != null ? selectedVideoModeSet : VideoModeSet.getDummy(); - }) - .setTranslator(resolution -> Component.nullToEmpty(resolution.toString())); + currentMode::refreshRate + ).setTranslator(rate -> Component.literal(rate + " Hz")); resolutionOption.setOnChange(() -> { - var newVideoMode = resolutionOption.getNewValue(); - var newRefreshRates = newVideoMode.getRefreshRates().toArray(new Integer[0]); - - RefreshRate.setValues(newRefreshRates); - RefreshRate.setNewValue(newRefreshRates[newRefreshRates.length - 1]); + VideoModeSet newSet = resolutionOption.getNewValue(); + Integer[] rates = newSet.refreshRates().toArray(new Integer[0]); + refreshRateOption.setValues(rates); + refreshRateOption.setNewValue(rates[rates.length - 1]); }); + CyclingOption windowModeOption = (CyclingOption) new CyclingOption( + Component.translatable("vulkanmod.options.windowMode"), + WindowMode.VALUES, + mode -> { + config.windowMode = switch (mode) { + case WindowMode.Windowed() -> 0; + case WindowMode.WindowedFullscreen() -> 1; + case WindowMode.ExclusiveFullscreen() -> 2; + }; + + boolean exclusiveFullscreen = mode instanceof WindowMode.ExclusiveFullscreen; + mcOptions.fullscreen().set(exclusiveFullscreen); + fullscreenDirty = true; + }, + () -> switch (config.windowMode) { + case 1 -> new WindowMode.WindowedFullscreen(); + case 2 -> new WindowMode.ExclusiveFullscreen(); + default -> new WindowMode.Windowed(); + } + ).setTranslator(WindowMode::nameOf); + return new OptionBlock[]{ new OptionBlock("", new Option[]{ resolutionOption, - RefreshRate, - new CyclingOption<>(Component.translatable("vulkanmod.options.windowMode"), - WindowMode.values(), + refreshRateOption, + windowModeOption, + new RangeOption(Component.translatable("options.framerateLimit"), + 10, 260, 10, + value -> Component.nullToEmpty(value == 260 + ? Component.translatable("options.framerateLimit.max").getString() + : String.valueOf(value)), value -> { - boolean exclusiveFullscreen = value == WindowMode.EXCLUSIVE_FULLSCREEN; - minecraftOptions.fullscreen() - .set(exclusiveFullscreen); - - config.windowMode = value.mode; - fullscreenDirty = true; + mcOptions.framerateLimit().set(value); + minecraft.getFramerateLimitTracker().setFramerateLimit(value); }, - () -> WindowMode.fromValue(config.windowMode)) - .setTranslator(value -> Component.translatable(WindowMode.getComponentName(value))), - new RangeOption(Component.translatable("options.framerateLimit"), - 10, 260, 10, - value -> Component.nullToEmpty(value == 260 ? - Component.translatable( - "options.framerateLimit.max") - .getString() : - String.valueOf(value)), - value -> { - minecraftOptions.framerateLimit().set(value); - minecraft.getFramerateLimitTracker().setFramerateLimit(value); - }, - () -> minecraftOptions.framerateLimit().get()), + () -> mcOptions.framerateLimit().get()), new SwitchOption(Component.translatable("options.vsync"), - value -> { - minecraftOptions.enableVsync().set(value); - window.updateVsync(value); - }, - () -> minecraftOptions.enableVsync().get()), + value -> { + mcOptions.enableVsync().set(value); + window.updateVsync(value); + }, + () -> mcOptions.enableVsync().get()), new CyclingOption<>(Component.translatable("options.inactivityFpsLimit"), - InactivityFpsLimit.values(), - value -> minecraftOptions.inactivityFpsLimit().set(value), - () -> minecraftOptions.inactivityFpsLimit().get()) - .setTranslator(inactivityFpsLimit -> Component.translatable(inactivityFpsLimit.getKey())) + InactivityFpsLimit.values(), + value -> mcOptions.inactivityFpsLimit().set(value), + () -> mcOptions.inactivityFpsLimit().get()) + .setTranslator(v -> Component.translatable(v.getKey())) }), new OptionBlock("", new Option[]{ new RangeOption(Component.translatable("options.guiScale"), - 0, window.calculateScale(0, minecraft.isEnforceUnicode()), 1, - value -> Component.translatable((value == 0) - ? "options.guiScale.auto" - : String.valueOf(value)), - value -> { - minecraftOptions.guiScale().set(value); - minecraft.resizeDisplay(); - }, - () -> (minecraftOptions.guiScale().get())), + 0, window.calculateScale(0, minecraft.isEnforceUnicode()), 1, + value -> Component.translatable(value == 0 ? "options.guiScale.auto" : String.valueOf(value)), + value -> { + mcOptions.guiScale().set(value); + minecraft.resizeDisplay(); + }, + () -> mcOptions.guiScale().get()), new RangeOption(Component.translatable("options.gamma"), - 0, 100, 1, - value -> Component.translatable(switch (value) { - case 0 -> "options.gamma.min"; - case 50 -> "options.gamma.default"; - case 100 -> "options.gamma.max"; - default -> String.valueOf(value); - }), - value -> minecraftOptions.gamma().set(value * 0.01), - () -> (int) (minecraftOptions.gamma().get() * 100.0)), + 0, 100, 1, + value -> Component.translatable(switch (value) { + case 0 -> "options.gamma.min"; + case 50 -> "options.gamma.default"; + case 100 -> "options.gamma.max"; + default -> String.valueOf(value); + }), + value -> mcOptions.gamma().set(value * 0.01), + () -> (int) (mcOptions.gamma().get() * 100.0)) }), new OptionBlock("", new Option[]{ new SwitchOption(Component.translatable("options.viewBobbing"), - (value) -> minecraftOptions.bobView().set(value), - () -> minecraftOptions.bobView().get()), + value -> mcOptions.bobView().set(value), + () -> mcOptions.bobView().get()), new RangeOption(Component.translatable("options.fovEffectScale"), - 0, 100, 1, - (value) -> minecraftOptions.fovEffectScale().set(value / 100d), - () -> (int) (minecraftOptions.fovEffectScale().get() * 100)) + 0, 100, 1, + value -> mcOptions.fovEffectScale().set(value / 100.0), + () -> (int) (mcOptions.fovEffectScale().get() * 100)) .setTooltip(value -> Component.translatable("options.fovEffectScale.tooltip")), new RangeOption(Component.translatable("options.glintSpeed"), 0, 100, 1, - (value) -> minecraftOptions.glintSpeed().set(value / 100d), - () -> (int) (minecraftOptions.glintSpeed().get() * 100)) + value -> mcOptions.glintSpeed().set(value / 100.0), + () -> (int) (mcOptions.glintSpeed().get() * 100)) .setTooltip(value -> Component.translatable("options.glintSpeed.tooltip")), new RangeOption(Component.translatable("options.glintStrength"), 0, 100, 1, - (value) -> minecraftOptions.glintStrength().set(value / 100d), - () -> (int) (minecraftOptions.glintStrength().get() * 100)) + value -> mcOptions.glintStrength().set(value / 100.0), + () -> (int) (mcOptions.glintStrength().get() * 100)) .setTooltip(value -> Component.translatable("options.glintStrength.tooltip")), new CyclingOption<>(Component.translatable("options.attackIndicator"), - AttackIndicatorStatus.values(), - value -> minecraftOptions.attackIndicator().set(value), - () -> minecraftOptions.attackIndicator().get()) - .setTranslator(value -> Component.translatable(value.getKey())), + AttackIndicatorStatus.values(), + value -> mcOptions.attackIndicator().set(value), + () -> mcOptions.attackIndicator().get()) + .setTranslator(v -> Component.translatable(v.getKey())), new SwitchOption(Component.translatable("options.autosaveIndicator"), - value -> minecraftOptions.showAutosaveIndicator().set(value), - () -> minecraftOptions.showAutosaveIndicator().get()), + value -> mcOptions.showAutosaveIndicator().set(value), + () -> mcOptions.showAutosaveIndicator().get()) }) }; } @@ -174,191 +174,167 @@ public static OptionBlock[] getGraphicsOpts() { return new OptionBlock[]{ new OptionBlock("", new Option[]{ new RangeOption(Component.translatable("options.renderDistance"), - 2, 32, 1, - (value) -> minecraftOptions.renderDistance().set(value), - () -> minecraftOptions.renderDistance().get()), + 2, 32, 1, + value -> mcOptions.renderDistance().set(value), + () -> mcOptions.renderDistance().get()), new RangeOption(Component.translatable("options.simulationDistance"), - 5, 32, 1, - (value) -> minecraftOptions.simulationDistance().set(value), - () -> minecraftOptions.simulationDistance().get()), + 5, 32, 1, + value -> mcOptions.simulationDistance().set(value), + () -> mcOptions.simulationDistance().get()), new CyclingOption<>(Component.translatable("options.prioritizeChunkUpdates"), - PrioritizeChunkUpdates.values(), - value -> minecraftOptions.prioritizeChunkUpdates().set(value), - () -> minecraftOptions.prioritizeChunkUpdates().get()) - .setTranslator(value -> Component.translatable(value.getKey())), + PrioritizeChunkUpdates.values(), + value -> mcOptions.prioritizeChunkUpdates().set(value), + () -> mcOptions.prioritizeChunkUpdates().get()) + .setTranslator(v -> Component.translatable(v.getKey())) }), new OptionBlock("", new Option[]{ new CyclingOption<>(Component.translatable("options.graphics"), - new GraphicsStatus[]{GraphicsStatus.FAST, GraphicsStatus.FANCY}, - value -> minecraftOptions.graphicsMode().set(value), - () -> minecraftOptions.graphicsMode().get()) - .setTranslator(graphicsMode -> Component.translatable(graphicsMode.getKey())), + new GraphicsStatus[]{GraphicsStatus.FAST, GraphicsStatus.FANCY}, + value -> mcOptions.graphicsMode().set(value), + () -> mcOptions.graphicsMode().get()) + .setTranslator(g -> Component.translatable(g.getKey())), new CyclingOption<>(Component.translatable("options.particles"), - new ParticleStatus[]{ParticleStatus.MINIMAL, ParticleStatus.DECREASED, ParticleStatus.ALL}, - value -> minecraftOptions.particles().set(value), - () -> minecraftOptions.particles().get()) - .setTranslator(particlesMode -> Component.translatable(particlesMode.getKey())), + new ParticleStatus[]{ParticleStatus.MINIMAL, ParticleStatus.DECREASED, ParticleStatus.ALL}, + value -> mcOptions.particles().set(value), + () -> mcOptions.particles().get()) + .setTranslator(p -> Component.translatable(p.getKey())), new CyclingOption<>(Component.translatable("options.renderClouds"), - CloudStatus.values(), - value -> minecraftOptions.cloudStatus().set(value), - () -> minecraftOptions.cloudStatus().get()) - .setTranslator(value -> Component.translatable(value.getKey())), + CloudStatus.values(), + value -> mcOptions.cloudStatus().set(value), + () -> mcOptions.cloudStatus().get()) + .setTranslator(c -> Component.translatable(c.getKey())), new RangeOption(Component.translatable("options.renderCloudsDistance"), - 2, 128, 1, - (value) -> minecraftOptions.cloudRange().set(value), - () -> minecraftOptions.cloudRange().get()), + 2, 128, 1, + value -> mcOptions.cloudRange().set(value), + () -> mcOptions.cloudRange().get()), new CyclingOption<>(Component.translatable("options.ao"), - new Integer[]{LightMode.FLAT, LightMode.SMOOTH, LightMode.SUB_BLOCK}, - (value) -> { - if (value > LightMode.FLAT) - minecraftOptions.ambientOcclusion().set(true); - else - minecraftOptions.ambientOcclusion().set(false); - - config.ambientOcclusion = value; - - minecraft.levelRenderer.allChanged(); - }, - () -> config.ambientOcclusion) + new Integer[]{LightMode.FLAT, LightMode.SMOOTH, LightMode.SUB_BLOCK}, + value -> { + mcOptions.ambientOcclusion().set(value > LightMode.FLAT); + config.ambientOcclusion = value; + minecraft.levelRenderer.allChanged(); + }, + () -> config.ambientOcclusion) .setTranslator(value -> Component.translatable(switch (value) { case LightMode.FLAT -> "options.off"; case LightMode.SMOOTH -> "options.on"; case LightMode.SUB_BLOCK -> "vulkanmod.options.ao.subBlock"; default -> "vulkanmod.options.unknown"; })) - .setTooltip(value -> switch (value) { - case LightMode.FLAT -> Component.empty(); - case LightMode.SMOOTH -> Component.empty(); - case LightMode.SUB_BLOCK -> Component.translatable("vulkanmod.options.ao.subBlock.tooltip"); - default -> Component.empty(); - } - ), + .setTooltip(value -> value == LightMode.SUB_BLOCK + ? Component.translatable("vulkanmod.options.ao.subBlock.tooltip") + : Component.empty()), new RangeOption(Component.translatable("options.biomeBlendRadius"), - 0, 7, 1, - value -> { - int v = value * 2 + 1; - return Component.nullToEmpty("%d x %d".formatted(v, v)); - }, - (value) -> { - minecraftOptions.biomeBlendRadius().set(value); - minecraft.levelRenderer.allChanged(); - }, - () -> minecraftOptions.biomeBlendRadius().get()), + 0, 7, 1, + value -> Component.nullToEmpty("%d x %d".formatted(value * 2 + 1, value * 2 + 1)), + value -> { + mcOptions.biomeBlendRadius().set(value); + minecraft.levelRenderer.allChanged(); + }, + () -> mcOptions.biomeBlendRadius().get()) }), new OptionBlock("", new Option[]{ new SwitchOption(Component.translatable("options.entityShadows"), - value -> minecraftOptions.entityShadows().set(value), - () -> minecraftOptions.entityShadows().get()), + value -> mcOptions.entityShadows().set(value), + () -> mcOptions.entityShadows().get()), new RangeOption(Component.translatable("options.entityDistanceScaling"), - 50, 500, 25, - value -> minecraftOptions.entityDistanceScaling().set(value * 0.01), - () -> minecraftOptions.entityDistanceScaling().get().intValue() * 100), + 50, 500, 25, + value -> mcOptions.entityDistanceScaling().set(value * 0.01), + () -> (int)(mcOptions.entityDistanceScaling().get() * 100)), new CyclingOption<>(Component.translatable("options.mipmapLevels"), - new Integer[]{0, 1, 2, 3, 4}, - value -> { - minecraftOptions.mipmapLevels().set(value); - minecraft.updateMaxMipLevel(value); - minecraft.delayTextureReload(); - }, - () -> minecraftOptions.mipmapLevels().get()) - .setTranslator(value -> Component.nullToEmpty(value.toString())) - })/*, - - */ + new Integer[]{0,1,2,3,4}, + value -> { + mcOptions.mipmapLevels().set(value); + minecraft.updateMaxMipLevel(value); + minecraft.delayTextureReload(); + }, + () -> mcOptions.mipmapLevels().get()) + .setTranslator(v -> Component.literal(String.valueOf(v))) + }) }; } public static OptionBlock[] getOptimizationOpts() { return new OptionBlock[]{ - new OptionBlock("", new Option[]{ + new OptionBlock("", new Option[]{ new CyclingOption<>(Component.translatable("vulkanmod.options.advCulling"), - new Integer[]{1, 2, 3, 10}, - value -> config.advCulling = value, - () -> config.advCulling) - .setTranslator(value -> Component.translatable(switch (value) { + new Integer[]{1, 2, 3, 10}, + value -> config.advCulling = value, + () -> config.advCulling) + .setTranslator(v -> Component.translatable(switch (v) { case 1 -> "vulkanmod.options.advCulling.aggressive"; case 2 -> "vulkanmod.options.advCulling.normal"; case 3 -> "vulkanmod.options.advCulling.conservative"; case 10 -> "options.off"; default -> "vulkanmod.options.unknown"; })) - .setTooltip(value -> Component.translatable(switch (value) { - case 1, 2, 3 -> "vulkanmod.options.advCulling.tooltip"; - default -> ""; - })), + .setTooltip(v -> v <= 3 ? Component.translatable("vulkanmod.options.advCulling.tooltip") : Component.empty()), new SwitchOption(Component.translatable("vulkanmod.options.entityCulling"), - value -> config.entityCulling = value, - () -> config.entityCulling) - .setTooltip(value -> Component.translatable("vulkanmod.options.entityCulling.tooltip")), + v -> config.entityCulling = v, + () -> config.entityCulling) + .setTooltip(v -> Component.translatable("vulkanmod.options.entityCulling.tooltip")), new SwitchOption(Component.translatable("vulkanmod.options.uniqueOpaqueLayer"), - value -> { - config.uniqueOpaqueLayer = value; - TerrainRenderType.updateMapping(); - minecraft.levelRenderer.allChanged(); - }, - () -> config.uniqueOpaqueLayer) - .setTooltip(value -> Component.translatable("vulkanmod.options.uniqueOpaqueLayer.tooltip")), + v -> { + config.uniqueOpaqueLayer = v; + TerrainRenderType.updateMapping(); + minecraft.levelRenderer.allChanged(); + }, + () -> config.uniqueOpaqueLayer) + .setTooltip(v -> Component.translatable("vulkanmod.options.uniqueOpaqueLayer.tooltip")), new SwitchOption(Component.translatable("vulkanmod.options.backfaceCulling"), - value -> { - config.backFaceCulling = value; - Minecraft.getInstance().levelRenderer.allChanged(); - }, - () -> config.backFaceCulling) - .setTooltip(value -> Component.translatable("vulkanmod.options.backfaceCulling.tooltip")), + v -> { + config.backFaceCulling = v; + minecraft.levelRenderer.allChanged(); + }, + () -> config.backFaceCulling) + .setTooltip(v -> Component.translatable("vulkanmod.options.backfaceCulling.tooltip")), new SwitchOption(Component.translatable("vulkanmod.options.indirectDraw"), - value -> config.indirectDraw = value, - () -> config.indirectDraw) - .setTooltip(value -> Component.translatable("vulkanmod.options.indirectDraw.tooltip")), + v -> config.indirectDraw = v, + () -> config.indirectDraw) + .setTooltip(v -> Component.translatable("vulkanmod.options.indirectDraw.tooltip")) }) }; - } public static OptionBlock[] getOtherOpts() { return new OptionBlock[]{ - new OptionBlock("", new Option[]{ + new OptionBlock("", new Option[]{ new RangeOption(Component.translatable("vulkanmod.options.builderThreads"), - 0, (Runtime.getRuntime().availableProcessors() - 1), 1, - value -> { - config.builderThreads = value; - WorldRenderer.getInstance().getTaskDispatcher().createThreads(value); - }, - () -> config.builderThreads) - .setTranslator(value -> { - if (value == 0) - return Component.translatable("vulkanmod.options.builderThreads.auto"); - else - return Component.nullToEmpty(String.valueOf(value)); - }), + 0, Runtime.getRuntime().availableProcessors() - 1, 1, + value -> { + config.builderThreads = value; + WorldRenderer.getInstance().getTaskDispatcher().createThreads(value); + }, + () -> config.builderThreads) + .setTranslator(v -> v == 0 + ? Component.translatable("vulkanmod.options.builderThreads.auto") + : Component.literal(String.valueOf(v))), new RangeOption(Component.translatable("vulkanmod.options.frameQueue"), - 2, 5, 1, - value -> { - config.frameQueueSize = value; - Renderer.scheduleSwapChainUpdate(); - }, () -> config.frameQueueSize) - .setTooltip(value -> Component.translatable("vulkanmod.options.frameQueue.tooltip")), + 2, 5, 1, + value -> { + config.frameQueueSize = value; + Renderer.scheduleSwapChainUpdate(); + }, + () -> config.frameQueueSize) + .setTooltip(v -> Component.translatable("vulkanmod.options.frameQueue.tooltip")), new SwitchOption(Component.translatable("vulkanmod.options.textureAnimations"), - value -> { - config.textureAnimations = value; - }, - () -> config.textureAnimations), + v -> config.textureAnimations = v, + () -> config.textureAnimations) }), - new OptionBlock("", new Option[]{ + new OptionBlock("", new Option[]{ new CyclingOption<>(Component.translatable("vulkanmod.options.deviceSelector"), - IntStream.range(-1, DeviceManager.suitableDevices.size()).boxed() - .toArray(Integer[]::new), - value -> config.device = value, - () -> config.device) - .setTranslator(value -> Component.translatable((value == -1) - ? "vulkanmod.options.deviceSelector.auto" - : DeviceManager.suitableDevices.get( - value).deviceName) - ) - .setTooltip(value -> Component.nullToEmpty("%s: %s".formatted( - Component.translatable("vulkanmod.options.deviceSelector.tooltip").getString(), - DeviceManager.device.deviceName))) + IntStream.range(-1, DeviceManager.suitableDevices.size()) + .boxed() + .toArray(Integer[]::new), + value -> config.device = value, + () -> config.device) + .setTranslator(v -> Component.translatable( + v == -1 ? "vulkanmod.options.deviceSelector.auto" + : DeviceManager.suitableDevices.get(v).deviceName)) + .setTooltip(v -> Component.literal( + Component.translatable("vulkanmod.options.deviceSelector.tooltip").getString() + ": " + + DeviceManager.device.deviceName)) }) }; - } -} +} \ No newline at end of file diff --git a/src/main/java/net/vulkanmod/config/video/VideoMode.java b/src/main/java/net/vulkanmod/config/video/VideoMode.java new file mode 100644 index 0000000000..f31f38398c --- /dev/null +++ b/src/main/java/net/vulkanmod/config/video/VideoMode.java @@ -0,0 +1,15 @@ +package net.vulkanmod.config.video; + +import org.jetbrains.annotations.NotNull; + +public record VideoMode(int width, int height, int bitDepth, int refreshRate) { + + @Override + public @NotNull String toString() { + return width + "×" + height + (refreshRate > 0 ? " @ " + refreshRate + "Hz" : ""); + } + + public VideoMode withRefreshRate(int newRate) { + return new VideoMode(width, height, bitDepth, newRate); + } +} \ No newline at end of file diff --git a/src/main/java/net/vulkanmod/config/video/VideoModeManager.java b/src/main/java/net/vulkanmod/config/video/VideoModeManager.java index 985455a632..a96ccdf116 100644 --- a/src/main/java/net/vulkanmod/config/video/VideoModeManager.java +++ b/src/main/java/net/vulkanmod/config/video/VideoModeManager.java @@ -1,96 +1,91 @@ package net.vulkanmod.config.video; -import net.vulkanmod.Initializer; import org.lwjgl.glfw.GLFW; import org.lwjgl.glfw.GLFWVidMode; -import java.util.ArrayList; -import java.util.List; +import java.util.*; -import static org.lwjgl.glfw.GLFW.*; +public final class VideoModeManager { -public abstract class VideoModeManager { - private static VideoModeSet.VideoMode osVideoMode; - private static VideoModeSet[] videoModeSets; + private static List availableSets = List.of(); + private static VideoMode currentOsMode = new VideoMode(800, 600, 8, 60); + private static VideoMode selectedMode = currentOsMode; - public static VideoModeSet.VideoMode selectedVideoMode; + private VideoModeManager() {} public static void init() { - long monitor = glfwGetPrimaryMonitor(); - osVideoMode = getCurrentVideoMode(monitor); - videoModeSets = populateVideoResolutions(GLFW.glfwGetPrimaryMonitor()); + long monitor = GLFW.glfwGetPrimaryMonitor(); + currentOsMode = getCurrentVideoMode(monitor); + availableSets = List.copyOf(loadVideoModeSets(monitor)); + selectedMode = findClosestMatch(currentOsMode).bestMode(); } - public static void applySelectedVideoMode() { - Initializer.CONFIG.videoMode = selectedVideoMode; - } - - public static VideoModeSet[] getVideoResolutions() { - return videoModeSets; - } - - public static VideoModeSet getFirstAvailable() { - if(videoModeSets != null) - return videoModeSets[videoModeSets.length - 1]; - else - return VideoModeSet.getDummy(); - } + public static VideoMode selectedMode() { return selectedMode; } + public static void selectMode(VideoMode mode) { selectedMode = mode; } - public static VideoModeSet.VideoMode getOsVideoMode() { - return osVideoMode; - } + public static List availableSets() { return availableSets; } + public static VideoMode currentOsMode() { return currentOsMode; } - public static VideoModeSet.VideoMode getCurrentVideoMode(long monitor){ + private static VideoMode getCurrentVideoMode(long monitor) { GLFWVidMode vidMode = GLFW.glfwGetVideoMode(monitor); - - if (vidMode == null) - throw new NullPointerException("Unable to get current video mode"); - - return new VideoModeSet.VideoMode(vidMode.width(), vidMode.height(), vidMode.redBits(), vidMode.refreshRate()); + if (vidMode == null) return new VideoMode(1920, 1080, 8, 60); + return new VideoMode(vidMode.width(), vidMode.height(), vidMode.redBits(), vidMode.refreshRate()); } - public static VideoModeSet[] populateVideoResolutions(long monitor) { + private static List loadVideoModeSets(long monitor) { GLFWVidMode.Buffer buffer = GLFW.glfwGetVideoModes(monitor); + if (buffer == null) return List.of(); - List videoModeSets = new ArrayList<>(); - - int currWidth = 0, currHeight = 0, currBitDepth = 0; - VideoModeSet videoModeSet = null; + Map> map = new LinkedHashMap<>(); for (int i = 0; i < buffer.limit(); i++) { buffer.position(i); - int bitDepth = buffer.redBits(); - if (buffer.redBits() < 8 || buffer.greenBits() != bitDepth || buffer.blueBits() != bitDepth) - continue; - - int width = buffer.width(); - int height = buffer.height(); - int refreshRate = buffer.refreshRate(); + int r = buffer.redBits(); + if (r < 8 || buffer.greenBits() != r || buffer.blueBits() != r) continue; - if (currWidth != width || currHeight != height || currBitDepth != bitDepth) { - currWidth = width; - currHeight = height; - currBitDepth = bitDepth; - - videoModeSet = new VideoModeSet(currWidth, currHeight, currBitDepth); - videoModeSets.add(videoModeSet); - } + String key = buffer.width() + "x" + buffer.height() + "@" + r; + map.computeIfAbsent(key, k -> new TreeSet<>()).add(buffer.refreshRate()); + } - videoModeSet.addRefreshRate(refreshRate); + List sets = new ArrayList<>(); + for (var entry : map.entrySet()) { + String[] parts = entry.getKey().split("@"); + String[] res = parts[0].split("x"); + int bitDepth = Integer.parseInt(parts[1]); + sets.add(new VideoModeSet( + Integer.parseInt(res[0]), + Integer.parseInt(res[1]), + bitDepth, + entry.getValue() + )); } - VideoModeSet[] arr = new VideoModeSet[videoModeSets.size()]; - videoModeSets.toArray(arr); + sets.sort(Comparator + .comparingInt(VideoModeSet::width) + .thenComparingInt(VideoModeSet::height) + .thenComparingInt(VideoModeSet::bitDepth) + .reversed()); - return arr; + return sets; } - public static VideoModeSet getFromVideoMode(VideoModeSet.VideoMode videoMode) { - for (var set : videoModeSets) { - if (set.width == videoMode.width && set.height == videoMode.height) - return set; - } + public static VideoModeSet findSetFor(VideoMode mode) { + return availableSets.stream() + .filter(s -> s.width() == mode.width() && s.height() == mode.height()) + .findFirst() + .orElseGet(() -> new VideoModeSet(mode.width(), mode.height(), mode.bitDepth(), Set.of(mode.refreshRate()))); + } + + private static VideoModeSet findClosestMatch(VideoMode mode) { + return availableSets.stream() + .min(Comparator.comparingInt((VideoModeSet s) -> + Math.abs(s.width() - mode.width()) * 10000 + + Math.abs(s.height() - mode.height()) * 100 + + Math.abs(s.bitDepth() - mode.bitDepth()))) + .orElseGet(() -> new VideoModeSet(mode.width(), mode.height(), 8, Set.of(60))); + } - return null; + public static VideoModeSet getDummy() { + return new VideoModeSet(-1, -1, -1, Set.of(-1)); } } diff --git a/src/main/java/net/vulkanmod/config/video/VideoModeSet.java b/src/main/java/net/vulkanmod/config/video/VideoModeSet.java index e4fb34fac3..0962b2aab2 100644 --- a/src/main/java/net/vulkanmod/config/video/VideoModeSet.java +++ b/src/main/java/net/vulkanmod/config/video/VideoModeSet.java @@ -1,95 +1,31 @@ package net.vulkanmod.config.video; -import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import org.jetbrains.annotations.NotNull; -import java.util.List; +import java.util.*; -public class VideoModeSet { - public final int width; - public final int height; - public final int bitDepth; - List refreshRates = new ObjectArrayList<>(); +public record VideoModeSet(int width, int height, int bitDepth, NavigableSet refreshRates) { - public static VideoModeSet getDummy() { - var set = new VideoModeSet(-1, -1, -1); - set.addRefreshRate(-1); - return set; + public VideoModeSet(int width, int height, int bitDepth, Collection refreshRates) { + this(width, height, bitDepth, new TreeSet<>(refreshRates)); } - public VideoModeSet(int width, int height, int bitDepth) { - this.width = width; - this.height = height; - this.bitDepth = bitDepth; + public VideoMode bestMode() { + return new VideoMode(width, height, bitDepth, refreshRates.last()); } - public int getRefreshRate() { - return this.refreshRates.get(0); + public VideoMode modeAtRate(int rate) { + Integer closest = refreshRates.floor(rate); + if (closest == null) closest = refreshRates.first(); + return new VideoMode(width, height, bitDepth, closest); } - public boolean hasRefreshRate(int r) { - return this.refreshRates.contains(r); - } - - public List getRefreshRates() { - return this.refreshRates; - } - - void addRefreshRate(int rr) { - this.refreshRates.add(rr); - } - - public String toString() { - return this.width + " x " + this.height; + public boolean supportsRate(int rate) { + return refreshRates.contains(rate); } @Override - public boolean equals(Object o) { - if (this == o) - return true; - if (o == null || getClass() != o.getClass()) - return false; - - VideoModeSet that = (VideoModeSet) o; - return width == that.width && height == that.height && bitDepth == that.bitDepth && refreshRates.equals(that.refreshRates); - } - - public VideoMode getVideoMode(int refresh) { - int idx = refreshRates.indexOf(refresh); - - if (idx == -1) { - idx = 0; - } - - return new VideoMode(this.width, this.height, this.bitDepth, this.refreshRates.get(idx)); + public @NotNull String toString() { + return width + "×" + height; } - - public VideoMode getVideoMode() { - int refreshRate = this.refreshRates.get(this.refreshRates.size() - 1); - return new VideoMode(this.width, this.height, this.bitDepth, refreshRate); - } - - public static final class VideoMode { - public int width; - public int height; - public int bitDepth; - public int refreshRate; - - public VideoMode(int width, int height, int bitDepth, int refreshRate) { - this.width = width; - this.height = height; - this.bitDepth = bitDepth; - this.refreshRate = refreshRate; - } - - @Override - public String toString() { - return "VideoMode[" + - "width=" + width + ", " + - "height=" + height + ", " + - "bitDepth=" + bitDepth + ", " + - "refreshRate=" + refreshRate + ']'; - } - - } - -} +} \ No newline at end of file diff --git a/src/main/java/net/vulkanmod/config/video/WindowMode.java b/src/main/java/net/vulkanmod/config/video/WindowMode.java index 621f06dea2..044ffb9acf 100644 --- a/src/main/java/net/vulkanmod/config/video/WindowMode.java +++ b/src/main/java/net/vulkanmod/config/video/WindowMode.java @@ -1,31 +1,39 @@ package net.vulkanmod.config.video; -public enum WindowMode { - WINDOWED(0), - WINDOWED_FULLSCREEN(1), - EXCLUSIVE_FULLSCREEN(2); +import net.minecraft.network.chat.Component; - public final int mode; +public sealed interface WindowMode permits WindowMode.Windowed, WindowMode.WindowedFullscreen, WindowMode.ExclusiveFullscreen { - WindowMode(int mode) { - this.mode = mode; + String translationKey(); + + boolean isFullscreen(); + + record Windowed() implements WindowMode { + public String translationKey() { return "vulkanmod.options.windowMode.windowed"; } + public boolean isFullscreen() { return false; } + } + + record WindowedFullscreen() implements WindowMode { + public String translationKey() { return "vulkanmod.options.windowMode.windowedFullscreen"; } + public boolean isFullscreen() { return true; } } - public static WindowMode fromValue(int value) { - return switch (value) { - case 0 -> WINDOWED; - case 1 -> WINDOWED_FULLSCREEN; - case 2 -> EXCLUSIVE_FULLSCREEN; + record ExclusiveFullscreen() implements WindowMode { + public String translationKey() { return "options.fullscreen"; } + public boolean isFullscreen() { return true; } + } + + WindowMode[] VALUES = { new Windowed(), new WindowedFullscreen(), new ExclusiveFullscreen() }; + + static WindowMode fromIndex(int index) { + return VALUES[index % VALUES.length]; + } - default -> throw new IllegalStateException("Unexpected value: " + value); - }; + static WindowMode fromMinecraftFullscreen(boolean mcFullscreen) { + return mcFullscreen ? new ExclusiveFullscreen() : new Windowed(); } - public static String getComponentName(WindowMode windowMode) { - return switch (windowMode) { - case WINDOWED -> "vulkanmod.options.windowMode.windowed"; - case WINDOWED_FULLSCREEN -> "vulkanmod.options.windowMode.windowedFullscreen"; - case EXCLUSIVE_FULLSCREEN -> "options.fullscreen"; - }; + static Component nameOf(WindowMode mode) { + return Component.translatable(mode.translationKey()); } } diff --git a/src/main/java/net/vulkanmod/mixin/window/WindowMixin.java b/src/main/java/net/vulkanmod/mixin/window/WindowMixin.java index 4827f90431..02fabcad4d 100644 --- a/src/main/java/net/vulkanmod/mixin/window/WindowMixin.java +++ b/src/main/java/net/vulkanmod/mixin/window/WindowMixin.java @@ -6,6 +6,7 @@ import net.vulkanmod.Initializer; import net.vulkanmod.config.Config; import net.vulkanmod.config.Platform; +import net.vulkanmod.config.video.VideoMode; import net.vulkanmod.config.video.VideoModeManager; import net.vulkanmod.config.option.Options; import net.vulkanmod.config.video.VideoModeSet; @@ -114,13 +115,13 @@ private void setMode() { long monitor = GLFW.glfwGetPrimaryMonitor(); if (this.fullscreen) { { - VideoModeSet.VideoMode videoMode = config.videoMode; + VideoMode videoMode = config.videoMode; boolean supported; - VideoModeSet set = VideoModeManager.getFromVideoMode(videoMode); + VideoModeSet set = VideoModeManager.findSetFor(videoMode); if (set != null) { - supported = set.hasRefreshRate(videoMode.refreshRate); + supported = set.supportsRate(videoMode.refreshRate()); } else { supported = false; @@ -128,7 +129,7 @@ private void setMode() { if(!supported) { LOGGER.error("Resolution not supported, using first available as fallback"); - videoMode = VideoModeManager.getFirstAvailable().getVideoMode(); + videoMode = VideoModeManager.currentOsMode(); } if (!this.wasOnFullscreen) { @@ -140,15 +141,15 @@ private void setMode() { this.x = 0; this.y = 0; - this.width = videoMode.width; - this.height = videoMode.height; - GLFW.glfwSetWindowMonitor(this.handle, monitor, this.x, this.y, this.width, this.height, videoMode.refreshRate); + this.width = videoMode.width(); + this.height = videoMode.height(); + GLFW.glfwSetWindowMonitor(this.handle, monitor, this.x, this.y, this.width, this.height, videoMode.refreshRate()); this.wasOnFullscreen = true; } } - else if (config.windowMode == WindowMode.WINDOWED_FULLSCREEN.mode) { - VideoModeSet.VideoMode videoMode = VideoModeManager.getOsVideoMode(); + else if (config.windowMode == 0) { // 0 is windowed + VideoMode videoMode = VideoModeManager.currentOsMode(); if (!this.wasOnFullscreen) { this.windowedX = this.x; @@ -157,8 +158,8 @@ else if (config.windowMode == WindowMode.WINDOWED_FULLSCREEN.mode) { this.windowedHeight = this.height; } - int width = videoMode.width; - int height = videoMode.height; + int width = videoMode.width(); + int height = videoMode.height(); GLFW.glfwSetWindowAttrib(this.handle, GLFW_DECORATED, GLFW_FALSE); GLFW.glfwSetWindowMonitor(this.handle, 0L, 0, 0, width, height, -1); From f9f3845c0354e74e970d86d56fc5ccc960e1c25f Mon Sep 17 00:00:00 2001 From: NotNekodev <97458908+NotNekodev@users.noreply.github.com> Date: Tue, 2 Dec 2025 21:01:41 +0100 Subject: [PATCH 20/27] make error logging in Config.java more robust --- src/main/java/net/vulkanmod/config/Config.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/main/java/net/vulkanmod/config/Config.java b/src/main/java/net/vulkanmod/config/Config.java index 8212660008..c3358814f3 100644 --- a/src/main/java/net/vulkanmod/config/Config.java +++ b/src/main/java/net/vulkanmod/config/Config.java @@ -2,14 +2,13 @@ import com.google.gson.*; import com.google.gson.annotations.JsonAdapter; +import net.vulkanmod.Initializer; import net.vulkanmod.config.video.VideoMode; import net.vulkanmod.config.video.VideoModeManager; import java.io.IOException; -import java.lang.reflect.Modifier; import java.nio.file.Files; import java.nio.file.Path; -import java.util.Objects; @JsonAdapter(Config.GsonAdapter.class) public class Config { @@ -41,7 +40,7 @@ public void save() { Files.createDirectories(CONFIG_PATH.getParent()); Files.writeString(CONFIG_PATH, GSON.toJson(this)); } catch (IOException e) { - e.printStackTrace(); + Initializer.LOGGER.error("Error saving config file!", e); } } From 85ddb21193314d17ea76dee231228ffd617c899d Mon Sep 17 00:00:00 2001 From: NotNekodev <97458908+NotNekodev@users.noreply.github.com> Date: Tue, 2 Dec 2025 21:10:19 +0100 Subject: [PATCH 21/27] fix some small warnings --- .../java/net/vulkanmod/config/Config.java | 14 ++--- .../java/net/vulkanmod/config/Platform.java | 8 +-- .../vulkanmod/config/gui/VOptionScreen.java | 2 + .../config/gui/render/GuiRenderer.java | 61 +------------------ .../config/option/CyclingOption.java | 1 + .../net/vulkanmod/config/option/Option.java | 5 +- .../config/video/VideoModeManager.java | 2 + .../vulkanmod/config/video/WindowMode.java | 3 + 8 files changed, 26 insertions(+), 70 deletions(-) diff --git a/src/main/java/net/vulkanmod/config/Config.java b/src/main/java/net/vulkanmod/config/Config.java index c3358814f3..4e65dca0d5 100644 --- a/src/main/java/net/vulkanmod/config/Config.java +++ b/src/main/java/net/vulkanmod/config/Config.java @@ -116,14 +116,14 @@ public Config deserialize(JsonElement json, java.lang.reflect.Type typeOfT, Json config.windowMode = getInt(obj, "windowMode", 0); config.advCulling = getInt(obj, "advCulling", 2); - config.indirectDraw = getBoolean(obj, "indirectDraw", true); - config.uniqueOpaqueLayer = getBoolean(obj, "uniqueOpaqueLayer", true); - config.entityCulling = getBoolean(obj, "entityCulling", true); + config.indirectDraw = getBoolean(obj, "indirectDraw"); + config.uniqueOpaqueLayer = getBoolean(obj, "uniqueOpaqueLayer"); + config.entityCulling = getBoolean(obj, "entityCulling"); config.ambientOcclusion = getInt(obj, "ambientOcclusion", 1); config.frameQueueSize = getInt(obj, "frameQueueSize", 2); config.builderThreads = getInt(obj, "builderThreads", 0); - config.backFaceCulling = getBoolean(obj, "backFaceCulling", true); - config.textureAnimations = getBoolean(obj, "textureAnimations", true); + config.backFaceCulling = getBoolean(obj, "backFaceCulling"); + config.textureAnimations = getBoolean(obj, "textureAnimations"); config.device = getInt(obj, "device", -1); return config; @@ -134,9 +134,9 @@ private int getInt(JsonObject obj, String key, int def) { return el != null && el.isJsonPrimitive() ? el.getAsInt() : def; } - private boolean getBoolean(JsonObject obj, String key, boolean def) { + private boolean getBoolean(JsonObject obj, String key) { JsonElement el = obj.get(key); - return el != null && el.isJsonPrimitive() ? el.getAsBoolean() : def; + return el == null || !el.isJsonPrimitive() || el.getAsBoolean(); } } } \ No newline at end of file diff --git a/src/main/java/net/vulkanmod/config/Platform.java b/src/main/java/net/vulkanmod/config/Platform.java index 38914708da..8bc63c0eec 100644 --- a/src/main/java/net/vulkanmod/config/Platform.java +++ b/src/main/java/net/vulkanmod/config/Platform.java @@ -16,7 +16,7 @@ public static void init() { Configuration.STACK_SIZE.set(256); GLFW.glfwInitHint(GLFW_PLATFORM, activePlat); - LOGGER.info("Selecting Platform: {}", getStringFromPlat(activePlat)); + LOGGER.info("Selecting Platform: {}", getStringFromPlat()); LOGGER.info("GLFW: {}", GLFW.glfwGetVersionString()); GLFW.glfwInit(); } @@ -43,14 +43,14 @@ private static int getSupportedPlat() { return GLFW_ANY_PLATFORM; //Unknown platform } - private static String getStringFromPlat(int plat) { - return switch (plat) { + private static String getStringFromPlat() { + return switch (Platform.activePlat) { case GLFW_PLATFORM_WIN32 -> "WIN32"; case GLFW_PLATFORM_WAYLAND -> "WAYLAND"; case GLFW_PLATFORM_X11 -> "X11"; case GLFW_PLATFORM_COCOA -> "MACOS"; case GLFW_ANY_PLATFORM -> "ANDROID"; - default -> throw new IllegalStateException("Unexpected value: " + plat); + default -> throw new IllegalStateException("Unexpected value: " + Platform.activePlat); }; } diff --git a/src/main/java/net/vulkanmod/config/gui/VOptionScreen.java b/src/main/java/net/vulkanmod/config/gui/VOptionScreen.java index ea307103c3..569b51d4f4 100644 --- a/src/main/java/net/vulkanmod/config/gui/VOptionScreen.java +++ b/src/main/java/net/vulkanmod/config/gui/VOptionScreen.java @@ -240,6 +240,7 @@ private void buildPage() { this.addButtons(); } + @SuppressWarnings("DuplicatedCode") private void addButtons() { int rightMargin = 10; int padding = 10; @@ -297,6 +298,7 @@ private void addButtons() { this.addWidget(this.searchField); } + @SuppressWarnings("DuplicatedCode") private void addButtonsWithSearchBar() { int rightMargin = 10; int padding = 10; diff --git a/src/main/java/net/vulkanmod/config/gui/render/GuiRenderer.java b/src/main/java/net/vulkanmod/config/gui/render/GuiRenderer.java index 3ab7171b64..3a502e3e13 100644 --- a/src/main/java/net/vulkanmod/config/gui/render/GuiRenderer.java +++ b/src/main/java/net/vulkanmod/config/gui/render/GuiRenderer.java @@ -12,9 +12,7 @@ import net.minecraft.util.Mth; import org.joml.Matrix3x2f; -import java.util.HashMap; import java.util.List; -import java.util.Map; public abstract class GuiRenderer { @@ -23,60 +21,6 @@ public abstract class GuiRenderer { public static PoseStack pose; public static BufferBuilder bufferBuilder; - // Scrolling text state management - private static final Map scrollingTextStates = new HashMap<>(); - private static final float SCROLL_SPEED = 30.0f; // pixels per second - private static final float SCROLL_PAUSE_DURATION = 1.0f; // seconds to pause at ends - - private static class ScrollingTextState { - float scrollOffset = 0.0f; - long lastUpdateTime = System.currentTimeMillis(); - boolean scrollingForward = true; - float pauseTimer = 0.0f; - - void update(float textWidth, float maxWidth) { - long currentTime = System.currentTimeMillis(); - float deltaTime = (currentTime - lastUpdateTime) / 1000.0f; - lastUpdateTime = currentTime; - - if (textWidth <= maxWidth) { - scrollOffset = 0.0f; - return; - } - - // Handle pause at ends - if (pauseTimer > 0) { - pauseTimer -= deltaTime; - return; - } - - float maxScroll = textWidth - maxWidth; - - if (scrollingForward) { - scrollOffset += SCROLL_SPEED * deltaTime; - if (scrollOffset >= maxScroll) { - scrollOffset = maxScroll; - scrollingForward = false; - pauseTimer = SCROLL_PAUSE_DURATION; - } - } else { - scrollOffset -= SCROLL_SPEED * deltaTime; - if (scrollOffset <= 0) { - scrollOffset = 0; - scrollingForward = true; - pauseTimer = SCROLL_PAUSE_DURATION; - } - } - } - - void reset() { - scrollOffset = 0.0f; - scrollingForward = true; - pauseTimer = 0.0f; - lastUpdateTime = System.currentTimeMillis(); - } - } - public static void enableScissor(int i, int j, int k, int l) { guiGraphics.enableScissor(i, j, k, l); } @@ -93,15 +37,16 @@ public static void fill(int x0, int y0, int x1, int y1, int color) { fill(x0, y0, x1, y1, 0, color); } - public static void fill(int x0, int y0, int x1, int y1, int z, int color) { + public static void fill(int x0, int y0, int x1, int y1, @SuppressWarnings("unused") int z, int color) { guiGraphics.fill(x0, y0, x1, y1, color); } + @SuppressWarnings("unused") public static void fillGradient(int x0, int y0, int x1, int y1, int color1, int color2) { fillGradient(x0, y0, x1, y1, 0, color1, color2); } - public static void fillGradient(int x0, int y0, int x1, int y1, int z, int color1, int color2) { + public static void fillGradient(int x0, int y0, int x1, int y1, @SuppressWarnings("unused") int z, int color1, int color2) { guiGraphics.fillGradient(x0, y0, x1, y1, color1, color2); } diff --git a/src/main/java/net/vulkanmod/config/option/CyclingOption.java b/src/main/java/net/vulkanmod/config/option/CyclingOption.java index d6080f7fdd..b070543f5d 100644 --- a/src/main/java/net/vulkanmod/config/option/CyclingOption.java +++ b/src/main/java/net/vulkanmod/config/option/CyclingOption.java @@ -24,6 +24,7 @@ public OptionWidget createOptionWidget(int x, int y, int width, int height) { return new CyclingOptionWidget(this, x, y, width, height, this.name); } + @SuppressWarnings("unused") public void updateOption(E[] values, Consumer setter, Supplier getter) { this.onApply = setter; this.valueSupplier = getter; diff --git a/src/main/java/net/vulkanmod/config/option/Option.java b/src/main/java/net/vulkanmod/config/option/Option.java index 42b211b9db..4afbc3a0b2 100644 --- a/src/main/java/net/vulkanmod/config/option/Option.java +++ b/src/main/java/net/vulkanmod/config/option/Option.java @@ -3,13 +3,13 @@ import net.minecraft.network.chat.Component; import net.vulkanmod.config.gui.widget.OptionWidget; -import java.util.Objects; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; public abstract class Option { protected final Component name; + @SuppressWarnings("unused") protected Component tooltip; protected Consumer onApply; @@ -25,6 +25,7 @@ public abstract class Option { protected boolean active; protected Runnable onChange; + @SuppressWarnings("unused") public Option(Component name, Consumer setter, Supplier getter, Function translator, Function tooltip) { this.name = name; @@ -57,11 +58,13 @@ public Option(Component name, Consumer setter, Supplier getter) { this.newValue = this.value = this.valueSupplier.get(); } + @SuppressWarnings("unused") public Option setOnApply(Consumer onApply) { this.onApply = onApply; return this; } + @SuppressWarnings("unused") public Option setValueSupplier(Supplier supplier) { this.valueSupplier = supplier; return this; diff --git a/src/main/java/net/vulkanmod/config/video/VideoModeManager.java b/src/main/java/net/vulkanmod/config/video/VideoModeManager.java index a96ccdf116..39c0615fe9 100644 --- a/src/main/java/net/vulkanmod/config/video/VideoModeManager.java +++ b/src/main/java/net/vulkanmod/config/video/VideoModeManager.java @@ -20,6 +20,7 @@ public static void init() { selectedMode = findClosestMatch(currentOsMode).bestMode(); } + @SuppressWarnings("unused") public static VideoMode selectedMode() { return selectedMode; } public static void selectMode(VideoMode mode) { selectedMode = mode; } @@ -85,6 +86,7 @@ private static VideoModeSet findClosestMatch(VideoMode mode) { .orElseGet(() -> new VideoModeSet(mode.width(), mode.height(), 8, Set.of(60))); } + @SuppressWarnings("unused") public static VideoModeSet getDummy() { return new VideoModeSet(-1, -1, -1, Set.of(-1)); } diff --git a/src/main/java/net/vulkanmod/config/video/WindowMode.java b/src/main/java/net/vulkanmod/config/video/WindowMode.java index 044ffb9acf..975f177245 100644 --- a/src/main/java/net/vulkanmod/config/video/WindowMode.java +++ b/src/main/java/net/vulkanmod/config/video/WindowMode.java @@ -6,6 +6,7 @@ public sealed interface WindowMode permits WindowMode.Windowed, WindowMode.Windo String translationKey(); + @SuppressWarnings("unused") boolean isFullscreen(); record Windowed() implements WindowMode { @@ -25,10 +26,12 @@ record ExclusiveFullscreen() implements WindowMode { WindowMode[] VALUES = { new Windowed(), new WindowedFullscreen(), new ExclusiveFullscreen() }; + @SuppressWarnings("unused") static WindowMode fromIndex(int index) { return VALUES[index % VALUES.length]; } + @SuppressWarnings("unused") static WindowMode fromMinecraftFullscreen(boolean mcFullscreen) { return mcFullscreen ? new ExclusiveFullscreen() : new Windowed(); } From 119a0b485a72803bfa5cbace654687140ffc955b Mon Sep 17 00:00:00 2001 From: Nekodev <97458908+NotNekodev@users.noreply.github.com> Date: Tue, 3 Mar 2026 20:29:15 +0100 Subject: [PATCH 22/27] update to newest upstream commits --- .../net/vulkanmod/config/UpdateChecker.java | 2 +- .../net/vulkanmod/config/gui/VOptionList.java | 4 +- .../vulkanmod/config/gui/VOptionScreen.java | 158 +++++++----------- .../config/gui/widget/VTextInputWidget.java | 2 +- .../config/option/CyclingOption.java | 4 +- .../net/vulkanmod/config/option/Option.java | 2 +- .../vulkanmod/config/option/RangeOption.java | 4 +- .../vulkanmod/config/option/SwitchOption.java | 7 +- 8 files changed, 75 insertions(+), 108 deletions(-) diff --git a/src/main/java/net/vulkanmod/config/UpdateChecker.java b/src/main/java/net/vulkanmod/config/UpdateChecker.java index cb054a84a2..09547f1a7f 100644 --- a/src/main/java/net/vulkanmod/config/UpdateChecker.java +++ b/src/main/java/net/vulkanmod/config/UpdateChecker.java @@ -53,6 +53,6 @@ public static void checkForUpdates() { } public static boolean isUpdateAvailable() { - return updateAvailable; + return false; } } diff --git a/src/main/java/net/vulkanmod/config/gui/VOptionList.java b/src/main/java/net/vulkanmod/config/gui/VOptionList.java index 69f6650466..9d03f29660 100644 --- a/src/main/java/net/vulkanmod/config/gui/VOptionList.java +++ b/src/main/java/net/vulkanmod/config/gui/VOptionList.java @@ -59,7 +59,9 @@ public void addAll(OptionBlock[] blocks) { var options = block.options(); for (Option option : options) { int margin = this.itemMargin; - this.addEntry(new Entry(option.createOptionWidget(x0, 0, width, height), margin, null)); + OptionWidget widget = option.createWidget(); + widget.setDimensions(x0, 0, width, height); + this.addEntry(new Entry(widget, margin, null)); } this.addEntry(new Entry(null, 12, null)); diff --git a/src/main/java/net/vulkanmod/config/gui/VOptionScreen.java b/src/main/java/net/vulkanmod/config/gui/VOptionScreen.java index 8ff3b38e7b..8cd8c052db 100644 --- a/src/main/java/net/vulkanmod/config/gui/VOptionScreen.java +++ b/src/main/java/net/vulkanmod/config/gui/VOptionScreen.java @@ -91,6 +91,25 @@ private void addPages() { this.optionPages.add(page); } + private VTextInputWidget createSearchField() { + int rightMargin = 10; + int padding = 10; + int kofiWidth = Minecraft.getInstance().font.width(Component.translatable("vulkanmod.options.buttons.kofi")) + padding; + int topBarRight = this.width - kofiWidth - rightMargin; + + if (UpdateChecker.isUpdateAvailable()) { + int updateWidth = minecraft.font.width(Component.translatable("vulkanmod.options.buttons.update_available")) + padding; + topBarRight -= updateWidth + VGuiConstants.WIDGET_MARGIN; + } + + return new VTextInputWidget( + 94, 4, + topBarRight - 94 - 4, VGuiConstants.WIDGET_HEIGHT, + Component.translatable("vulkanmod.options.searchFieldPlaceholder"), + widget -> performSearch(widget.getInput()) + ); + } + @Override protected void init() { this.addPages(); @@ -103,12 +122,14 @@ protected void init() { int leftMargin = 94; int rightMargin = 3; int listWidth = this.width - rightMargin - leftMargin; - int leftMargin = MARGIN + 90; - int listWidth = Math.min(this.width - leftMargin - MARGIN, 420); + //int leftMargin = MARGIN + 90; + //int listWidth = Math.min(this.width - leftMargin - MARGIN, 420); int listHeight = this.height - top - bottom; this.buildLists(leftMargin, top, listWidth, listHeight, itemHeight); + this.searchField = createSearchField(); + int x = leftMargin + listWidth + 10; int width = this.width - x - 10; @@ -118,8 +139,6 @@ protected void init() { this.tooltipWidth = width; - addButtonsWithSearchBar(); - buildPage(); this.applyButton.active = false; @@ -143,7 +162,6 @@ private void undo() { private void buildLists(int left, int top, int listWidth, int listHeight, int itemHeight) { for (OptionPage page : this.optionPages) { page.createList(left, top, listWidth, listHeight, itemHeight); - page.updateOptionStates(); } } @@ -216,6 +234,11 @@ else if (option instanceof CyclingOption cycling) { private void buildPage() { this.buttons.clear(); this.pageButtons.clear(); + + String savedInput = this.searchField != null ? this.searchField.getInput() : ""; + boolean savedFocused = this.searchField != null && this.searchField.focused; + boolean savedSelected = this.searchField != null && this.searchField.selected; + this.clearWidgets(); int x = 10; @@ -239,68 +262,17 @@ private void buildPage() { if (searchResultsPage != null) { VOptionList searchList = searchResultsPage.getOptionList(); this.addWidget(searchList); + searchResultsPage.updateOptionStates(); } } - this.addButtons(); - } - - @SuppressWarnings("DuplicatedCode") - private void addButtons() { - int rightMargin = 10; - int padding = 10; - int buttonWidth = Minecraft.getInstance().font.width(CommonComponents.GUI_DONE) + 2 * padding; - int x0 = (this.width - buttonWidth - rightMargin); - int y0 = this.height - VGuiConstants.WIDGET_HEIGHT - 7; - - VButtonWidget doneButton = new VButtonWidget( - x0, y0, - buttonWidth, VGuiConstants.WIDGET_HEIGHT, - CommonComponents.GUI_DONE, - button -> Minecraft.getInstance().setScreen(this.parent) - ); + this.addButtonsWithSearchBar(); - buttonWidth = Minecraft.getInstance().font.width(Component.translatable("vulkanmod.options.buttons.apply")) + 2 * padding; - x0 -= (buttonWidth + VGuiConstants.WIDGET_MARGIN); - this.applyButton = new VButtonWidget( - x0, y0, - buttonWidth, VGuiConstants.WIDGET_HEIGHT, - Component.translatable("vulkanmod.options.buttons.apply"), - button -> this.applyOptions() - ); - - buttonWidth = Minecraft.getInstance().font.width(Component.translatable("vulkanmod.options.buttons.undo")) + 2 * padding; - x0 -= (buttonWidth + VGuiConstants.WIDGET_MARGIN); - this.undoButton = new VButtonWidget( - x0, y0, - buttonWidth, VGuiConstants.WIDGET_HEIGHT, - Component.translatable("vulkanmod.options.buttons.undo"), - button -> undo() - - ); - - buttonWidth = Minecraft.getInstance().font.width(Component.translatable("vulkanmod.options.buttons.kofi")) + padding; - x0 = (this.width - buttonWidth - rightMargin); - VButtonWidget supportButton = new VButtonWidget( - x0, 4, - buttonWidth, VGuiConstants.WIDGET_HEIGHT, - Component.translatable("vulkanmod.options.buttons.kofi"), - button -> Util.getPlatform().openUri("https://ko-fi.com/xcollateral") - ); - - - this.buttons.add(this.applyButton); - this.buttons.add(doneButton); - this.buttons.add(supportButton); - this.buttons.add(this.undoButton); - - - this.addWidget(this.applyButton); - this.addWidget(doneButton); - this.addWidget(supportButton); - this.addWidget(this.undoButton); - - this.addWidget(this.searchField); + this.searchField.setInput(savedInput); + if (savedFocused) { + this.searchField.setFocused(true); + this.searchField.setSelected(savedSelected); + } } @SuppressWarnings("DuplicatedCode") @@ -311,62 +283,49 @@ private void addButtonsWithSearchBar() { int x0 = (this.width - buttonWidth - rightMargin); int y0 = this.height - VGuiConstants.WIDGET_HEIGHT - 7; - VButtonWidget doneButton = new VButtonWidget( - x0, y0, - buttonWidth, VGuiConstants.WIDGET_HEIGHT, - CommonComponents.GUI_DONE, - button -> Minecraft.getInstance().setScreen(this.parent) - ); + VButtonWidget doneButton = new VButtonWidget(x0, y0, buttonWidth, VGuiConstants.WIDGET_HEIGHT, + CommonComponents.GUI_DONE, button -> Minecraft.getInstance().setScreen(this.parent)); buttonWidth = Minecraft.getInstance().font.width(Component.translatable("vulkanmod.options.buttons.apply")) + 2 * padding; x0 -= (buttonWidth + VGuiConstants.WIDGET_MARGIN); - this.applyButton = new VButtonWidget( - x0, y0, - buttonWidth, VGuiConstants.WIDGET_HEIGHT, - Component.translatable("vulkanmod.options.buttons.apply"), - button -> this.applyOptions() - ); + this.applyButton = new VButtonWidget(x0, y0, buttonWidth, VGuiConstants.WIDGET_HEIGHT, + Component.translatable("vulkanmod.options.buttons.apply"), button -> this.applyOptions()); buttonWidth = Minecraft.getInstance().font.width(Component.translatable("vulkanmod.options.buttons.undo")) + 2 * padding; x0 -= (buttonWidth + VGuiConstants.WIDGET_MARGIN); - this.undoButton = new VButtonWidget( - x0, y0, - buttonWidth, VGuiConstants.WIDGET_HEIGHT, - Component.translatable("vulkanmod.options.buttons.undo"), - button -> undo() - - ); - - this.searchField = new VTextInputWidget( - 94, 4, - x0 - 19, VGuiConstants.WIDGET_HEIGHT, - Component.translatable("vulkanmod.options.searchFieldPlaceholder"), - widget -> performSearch(widget.getInput()) - ); + this.undoButton = new VButtonWidget(x0, y0, buttonWidth, VGuiConstants.WIDGET_HEIGHT, + Component.translatable("vulkanmod.options.buttons.undo"), button -> undo()); + int kofiWidth = Minecraft.getInstance().font.width(Component.translatable("vulkanmod.options.buttons.kofi")) + padding; - buttonWidth = Minecraft.getInstance().font.width(Component.translatable("vulkanmod.options.buttons.kofi")) + padding; - x0 = (this.width - buttonWidth - rightMargin); - VButtonWidget supportButton = new VButtonWidget( - x0, 4, - buttonWidth, VGuiConstants.WIDGET_HEIGHT, + int kofiX = this.width - kofiWidth - rightMargin; + VButtonWidget supportButton = new VButtonWidget(kofiX, 4, kofiWidth, VGuiConstants.WIDGET_HEIGHT, Component.translatable("vulkanmod.options.buttons.kofi"), - button -> Util.getPlatform().openUri("https://ko-fi.com/xcollateral") - ); - + button -> Util.getPlatform().openUri("https://ko-fi.com/xcollateral")); this.buttons.add(this.applyButton); this.buttons.add(doneButton); this.buttons.add(supportButton); this.buttons.add(this.undoButton); - this.addWidget(this.applyButton); this.addWidget(doneButton); this.addWidget(supportButton); this.addWidget(this.undoButton); - this.addWidget(this.searchField); + + if (UpdateChecker.isUpdateAvailable()) { + assert minecraft != null; + int updateWidth = minecraft.font.width(Component.translatable("vulkanmod.options.buttons.update_available")) + padding; + var updateButton = new VButtonWidget( + kofiX - updateWidth - VGuiConstants.WIDGET_MARGIN, 4, + updateWidth, VGuiConstants.WIDGET_HEIGHT, + Component.translatable("vulkanmod.options.buttons.update_available").withStyle(ChatFormatting.UNDERLINE), + button -> Util.getPlatform().openUri("https://modrinth.com/mod/vulkanmod") + ); + this.buttons.add(updateButton); + this.addWidget(updateButton); + } } @Override @@ -483,6 +442,7 @@ private List getWidgetTooltip(VAbstractWidget widget) { } private void updateState() { + if (this.applyButton == null | this.undoButton == null) return; boolean modified = false; for (var page : this.optionPages) { modified |= page.optionChanged(); diff --git a/src/main/java/net/vulkanmod/config/gui/widget/VTextInputWidget.java b/src/main/java/net/vulkanmod/config/gui/widget/VTextInputWidget.java index 7d6b85aa99..c9d9690c0a 100644 --- a/src/main/java/net/vulkanmod/config/gui/widget/VTextInputWidget.java +++ b/src/main/java/net/vulkanmod/config/gui/widget/VTextInputWidget.java @@ -16,7 +16,7 @@ import java.util.function.Consumer; public class VTextInputWidget extends VAbstractWidget { - boolean selected = false; + public boolean selected = false; Consumer onSearch; // when the search is "activated", like pressing enter private String text; private final Component placeholder; diff --git a/src/main/java/net/vulkanmod/config/option/CyclingOption.java b/src/main/java/net/vulkanmod/config/option/CyclingOption.java index 2f1824768c..a96bac3abd 100644 --- a/src/main/java/net/vulkanmod/config/option/CyclingOption.java +++ b/src/main/java/net/vulkanmod/config/option/CyclingOption.java @@ -21,7 +21,9 @@ public CyclingOption(Component name, E[] values, Consumer setter, Supplier @Override public OptionWidget createWidget() { - return new CyclingOptionWidget(this, this.name); + var widget = new CyclingOptionWidget(this, this.name); + this.widget = widget; + return widget; } @SuppressWarnings("unused") diff --git a/src/main/java/net/vulkanmod/config/option/Option.java b/src/main/java/net/vulkanmod/config/option/Option.java index eca51621f8..8897c88dae 100644 --- a/src/main/java/net/vulkanmod/config/option/Option.java +++ b/src/main/java/net/vulkanmod/config/option/Option.java @@ -93,7 +93,7 @@ public Option setActive(boolean active) { return this; } - abstract OptionWidget createWidget(); + public abstract OptionWidget createWidget(); public OptionWidget getWidget() { if (this.widget == null) { diff --git a/src/main/java/net/vulkanmod/config/option/RangeOption.java b/src/main/java/net/vulkanmod/config/option/RangeOption.java index 89d5f03557..846fe66de9 100644 --- a/src/main/java/net/vulkanmod/config/option/RangeOption.java +++ b/src/main/java/net/vulkanmod/config/option/RangeOption.java @@ -26,7 +26,9 @@ public RangeOption(Component name, int min, int max, int step, Consumer } public OptionWidget createWidget() { - return new RangeOptionWidget(this, this.name); + var widget = new RangeOptionWidget(this, this.name); + this.widget = widget; + return widget; } public Component getName() { diff --git a/src/main/java/net/vulkanmod/config/option/SwitchOption.java b/src/main/java/net/vulkanmod/config/option/SwitchOption.java index 455810d83b..439bdac6fd 100644 --- a/src/main/java/net/vulkanmod/config/option/SwitchOption.java +++ b/src/main/java/net/vulkanmod/config/option/SwitchOption.java @@ -13,8 +13,9 @@ public SwitchOption(Component name, Consumer setter, Supplier } @Override - public OptionWidget createWidget() { - return new SwitchOptionWidget(this, this.name); + public OptionWidget createWidget() { + var widget = new SwitchOptionWidget(this, this.name); + this.widget = widget; + return widget; } - } From fd7ab7cb4a7affe6c01be5c47066e5c11ceb3bec Mon Sep 17 00:00:00 2001 From: Nekodev <97458908+NotNekodev@users.noreply.github.com> Date: Tue, 3 Mar 2026 20:35:22 +0100 Subject: [PATCH 23/27] disable the hardcoded false in isUpdateAvailable, that was used for testing --- src/main/java/net/vulkanmod/config/UpdateChecker.java | 2 +- src/main/java/net/vulkanmod/config/gui/VOptionScreen.java | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/net/vulkanmod/config/UpdateChecker.java b/src/main/java/net/vulkanmod/config/UpdateChecker.java index 09547f1a7f..cb054a84a2 100644 --- a/src/main/java/net/vulkanmod/config/UpdateChecker.java +++ b/src/main/java/net/vulkanmod/config/UpdateChecker.java @@ -53,6 +53,6 @@ public static void checkForUpdates() { } public static boolean isUpdateAvailable() { - return false; + return updateAvailable; } } diff --git a/src/main/java/net/vulkanmod/config/gui/VOptionScreen.java b/src/main/java/net/vulkanmod/config/gui/VOptionScreen.java index 8cd8c052db..0f5ef2ccc9 100644 --- a/src/main/java/net/vulkanmod/config/gui/VOptionScreen.java +++ b/src/main/java/net/vulkanmod/config/gui/VOptionScreen.java @@ -154,6 +154,7 @@ private void captureOriginalState() { private void undo() { for (OptionPage page : this.optionPages) { page.resetToOriginalState(); + page.updateOptionStates(); } buildPage(); From 843b6e2ca5cf59c56d40bcb677c2f92d8903581b Mon Sep 17 00:00:00 2001 From: Nekodev <97458908+NotNekodev@users.noreply.github.com> Date: Wed, 4 Mar 2026 19:16:19 +0100 Subject: [PATCH 24/27] OptionsRegistry system --- src/main/java/net/vulkanmod/Initializer.java | 4 ++ .../vulkanmod/config/gui/VOptionScreen.java | 48 ++++++++++++++--- .../vulkanmod/config/option/OptionPage.java | 9 ++++ .../config/option/OptionRegistry.java | 54 +++++++++++++++++++ 4 files changed, 109 insertions(+), 6 deletions(-) create mode 100644 src/main/java/net/vulkanmod/config/option/OptionRegistry.java diff --git a/src/main/java/net/vulkanmod/Initializer.java b/src/main/java/net/vulkanmod/Initializer.java index 93584de735..43d9f43317 100644 --- a/src/main/java/net/vulkanmod/Initializer.java +++ b/src/main/java/net/vulkanmod/Initializer.java @@ -3,9 +3,13 @@ import net.fabricmc.api.ClientModInitializer; import net.fabricmc.fabric.api.renderer.v1.Renderer; import net.fabricmc.loader.api.FabricLoader; +import net.minecraft.network.chat.Component; import net.vulkanmod.config.Config; import net.vulkanmod.config.Platform; import net.vulkanmod.config.UpdateChecker; +import net.vulkanmod.config.option.Option; +import net.vulkanmod.config.option.OptionRegistry; +import net.vulkanmod.config.option.Options; import net.vulkanmod.config.video.VideoModeManager; import net.vulkanmod.render.chunk.build.frapi.VulkanModRenderer; import org.apache.logging.log4j.LogManager; diff --git a/src/main/java/net/vulkanmod/config/gui/VOptionScreen.java b/src/main/java/net/vulkanmod/config/gui/VOptionScreen.java index 0f5ef2ccc9..cf42d5b59e 100644 --- a/src/main/java/net/vulkanmod/config/gui/VOptionScreen.java +++ b/src/main/java/net/vulkanmod/config/gui/VOptionScreen.java @@ -23,10 +23,7 @@ import net.vulkanmod.config.gui.widget.VAbstractWidget; import net.vulkanmod.config.gui.widget.VButtonWidget; import net.vulkanmod.config.gui.widget.VTextInputWidget; -import net.vulkanmod.config.option.CyclingOption; -import net.vulkanmod.config.option.OptionPage; -import net.vulkanmod.config.option.Options; -import net.vulkanmod.config.option.Option; +import net.vulkanmod.config.option.*; import net.vulkanmod.vulkan.VRenderSystem; import net.vulkanmod.vulkan.util.ColorUtil; import org.lwjgl.glfw.GLFW; @@ -38,8 +35,9 @@ public class VOptionScreen extends Screen { final ResourceLocation ICON = ResourceLocation.fromNamespaceAndPath("vulkanmod", "vlogo_transparent.png"); private final Screen parent; + private static boolean initialized = false; - private final List optionPages; + private List optionPages; private OptionPage searchResultsPage; private int currentListIdx = 0; @@ -112,7 +110,45 @@ private VTextInputWidget createSearchField() { @Override protected void init() { - this.addPages(); + if (!initialized) { + OptionRegistry registry = OptionRegistry.get(); + + registry.registerPage( + "video", + Component.translatable("vulkanmod.options.pages.video"), + Options.getVideoOpts(), + 0 + ); + + registry.registerPage( + "graphics", + Component.translatable("vulkanmod.options.pages.graphics"), + Options.getGraphicsOpts(), + 1 + ); + + registry.registerPage( + "optimizations", + Component.translatable("vulkanmod.options.pages.optimizations"), + Options.getOptimizationOpts(), + 2 + ); + + registry.registerPage( + "other", + Component.translatable("vulkanmod.options.pages.other"), + Options.getOtherOpts(), + 3 + ); + + initialized = true; + } + + this.optionPages = OptionRegistry.get().getPages(); + + if (this.optionPages.isEmpty()) { + throw new IllegalStateException("Default Options weren't added!"); + } this.captureOriginalState(); int top = 29; diff --git a/src/main/java/net/vulkanmod/config/option/OptionPage.java b/src/main/java/net/vulkanmod/config/option/OptionPage.java index 92e2f8ccea..a87699e133 100644 --- a/src/main/java/net/vulkanmod/config/option/OptionPage.java +++ b/src/main/java/net/vulkanmod/config/option/OptionPage.java @@ -7,6 +7,7 @@ public class OptionPage { public final String name; public OptionBlock[] optionBlocks; private VOptionList optionList; + private int order; public OptionPage(String name, OptionBlock[] optionBlocks) { this.name = name; @@ -66,4 +67,12 @@ public void resetToOriginalState() { } } } + + public void setOrder(int order) { + this.order = order; + } + + public int getOrder() { + return order; + } } \ No newline at end of file diff --git a/src/main/java/net/vulkanmod/config/option/OptionRegistry.java b/src/main/java/net/vulkanmod/config/option/OptionRegistry.java new file mode 100644 index 0000000000..cf3d6425df --- /dev/null +++ b/src/main/java/net/vulkanmod/config/option/OptionRegistry.java @@ -0,0 +1,54 @@ +package net.vulkanmod.config.option; + +import net.minecraft.network.chat.Component; +import net.vulkanmod.config.gui.OptionBlock; + +import java.util.*; + +public final class OptionRegistry { + + private static final OptionRegistry INSTANCE = new OptionRegistry(); + + private final Map pagesById = new HashMap<>(); + private final List pages = new ArrayList<>(); + + private OptionRegistry() {} + + public static OptionRegistry get() { + return INSTANCE; + } + + public synchronized void registerPage( + String id, + Component title, + OptionBlock[] blocks, + int order + ) { + if (pagesById.containsKey(id)) { + throw new IllegalStateException("Option page already registered: " + id); + } + + OptionPage page = new OptionPage(title.getString(), blocks); + page.setOrder(order); + + pagesById.put(id, page); + pages.add(page); + + pages.sort(Comparator.comparingInt(OptionPage::getOrder)); + } + + public List getPages() { + return Collections.unmodifiableList(pages); + } + + public synchronized void unregister(String id) { + OptionPage page = pagesById.remove(id); + if (page != null) { + pages.remove(page); + } + } + + public boolean isRegistered(String id) { + return pagesById.containsKey(id); + } +} \ No newline at end of file From ba0422f9ccb4a0e6b33bac6998aec5a361b3c63f Mon Sep 17 00:00:00 2001 From: Nekodev <97458908+NotNekodev@users.noreply.github.com> Date: Wed, 4 Mar 2026 19:33:13 +0100 Subject: [PATCH 25/27] Page builder --- .../net/vulkanmod/config/option/Page.java | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 src/main/java/net/vulkanmod/config/option/Page.java diff --git a/src/main/java/net/vulkanmod/config/option/Page.java b/src/main/java/net/vulkanmod/config/option/Page.java new file mode 100644 index 0000000000..45c9023c9a --- /dev/null +++ b/src/main/java/net/vulkanmod/config/option/Page.java @@ -0,0 +1,58 @@ +package net.vulkanmod.config.option; + +import net.minecraft.network.chat.Component; +import net.vulkanmod.config.gui.OptionBlock; + +import java.util.ArrayList; +import java.util.List; + +public class Page { + private final String name; + private final List blocks = new ArrayList<>(); + + private Page(String name) { + this.name = name; + } + + public static Page of(String name) { + return new Page(name); + } + + public Block block(String title) { + Block block = new Block(title, this); + blocks.add(block); + return block; + } + + public Page register() { + OptionBlock[] oblocks = blocks.stream() + .map(Block::build) + .toArray(OptionBlock[]::new); + OptionRegistry.get().registerPage("name", Component.literal(name), oblocks, 5); + return this; + } + + public static class Block { + private final String title; + private final List> options = new ArrayList<>(); + private final Page parent; + + private Block(String title, Page parent) { + this.title = title; + this.parent = parent; + } + + public Block add(Option option) { + options.add(option); + return this; + } + + public Page done() { + return parent; + } + + private OptionBlock build() { + return new OptionBlock(title, options.toArray(new Option[0])); + } + } +} \ No newline at end of file From a3d5460668823d7441f5daeed7ae61a9e2f92194 Mon Sep 17 00:00:00 2001 From: xCollateral <103696619+xCollateral@users.noreply.github.com> Date: Tue, 21 Apr 2026 18:48:37 +0200 Subject: [PATCH 26/27] Refactor and small adjustments to layout --- .../vulkanmod/config/gui/VOptionScreen.java | 210 ++++++++---------- .../config/gui/util/VGuiConstants.java | 2 +- .../net/vulkanmod/config/option/Option.java | 17 +- .../vulkanmod/config/option/OptionPage.java | 10 +- .../config/option/OptionRegistry.java | 5 + 5 files changed, 103 insertions(+), 141 deletions(-) diff --git a/src/main/java/net/vulkanmod/config/gui/VOptionScreen.java b/src/main/java/net/vulkanmod/config/gui/VOptionScreen.java index a74f29579f..161afd4ccb 100644 --- a/src/main/java/net/vulkanmod/config/gui/VOptionScreen.java +++ b/src/main/java/net/vulkanmod/config/gui/VOptionScreen.java @@ -32,17 +32,20 @@ import java.util.List; public class VOptionScreen extends Screen { + public final static int MARGIN = 20; final ResourceLocation ICON = ResourceLocation.fromNamespaceAndPath("vulkanmod", "vlogo_transparent.png"); private final Screen parent; - private static boolean initialized = false; + OptionRegistry registry = OptionRegistry.get(); private List optionPages; private OptionPage searchResultsPage; private int currentListIdx = 0; private boolean isSearchActive = false; + private int tooltipX; + private int tooltipY; private int tooltipWidth; private VButtonWidget applyButton; @@ -61,105 +64,20 @@ public VOptionScreen(Component title, Screen parent) { this.optionPages = new ArrayList<>(); } - private void addPages() { - this.optionPages.clear(); - - OptionPage page = new OptionPage( - Component.translatable("vulkanmod.options.pages.video").getString(), - Options.getVideoOpts() - ); - this.optionPages.add(page); - - page = new OptionPage( - Component.translatable("vulkanmod.options.pages.graphics").getString(), - Options.getGraphicsOpts() - ); - this.optionPages.add(page); - - page = new OptionPage( - Component.translatable("vulkanmod.options.pages.optimizations").getString(), - Options.getOptimizationOpts() - ); - this.optionPages.add(page); - - page = new OptionPage( - Component.translatable("vulkanmod.options.pages.other").getString(), - Options.getOtherOpts() - ); - this.optionPages.add(page); - } - - private VTextInputWidget createSearchField() { - int rightMargin = 10; - int padding = 10; - int kofiWidth = Minecraft.getInstance().font.width(Component.translatable("vulkanmod.options.buttons.kofi")) + padding; - int topBarRight = this.width - kofiWidth - rightMargin; - - if (UpdateChecker.isUpdateAvailable()) { - int updateWidth = minecraft.font.width(Component.translatable("vulkanmod.options.buttons.update_available")) + padding; - topBarRight -= updateWidth + VGuiConstants.WIDGET_MARGIN; - } - - return new VTextInputWidget( - 94, 4, - topBarRight - 94 - 4, VGuiConstants.WIDGET_HEIGHT, - Component.translatable("vulkanmod.options.searchFieldPlaceholder"), - widget -> performSearch(widget.getInput()) - ); - } - @Override protected void init() { - if (!initialized) { - OptionRegistry registry = OptionRegistry.get(); - - registry.registerPage( - "video", - Component.translatable("vulkanmod.options.pages.video"), - Options.getVideoOpts(), - 0 - ); - - registry.registerPage( - "graphics", - Component.translatable("vulkanmod.options.pages.graphics"), - Options.getGraphicsOpts(), - 1 - ); - - registry.registerPage( - "optimizations", - Component.translatable("vulkanmod.options.pages.optimizations"), - Options.getOptimizationOpts(), - 2 - ); - - registry.registerPage( - "other", - Component.translatable("vulkanmod.options.pages.other"), - Options.getOtherOpts(), - 3 - ); - - initialized = true; - } - - this.optionPages = OptionRegistry.get().getPages(); + this.addOptionPages(); if (this.optionPages.isEmpty()) { throw new IllegalStateException("Default Options weren't added!"); } - this.captureOriginalState(); - int top = 29; + int top = 32; int bottom = 60; int itemHeight = 20; - int leftMargin = 94; - int rightMargin = 3; - int listWidth = this.width - rightMargin - leftMargin; - //int leftMargin = MARGIN + 90; - //int listWidth = Math.min(this.width - leftMargin - MARGIN, 420); + int leftMargin = MARGIN + 90; + int listWidth = Math.min(this.width - leftMargin - MARGIN, 420); int listHeight = this.height - top - bottom; this.buildLists(leftMargin, top, listWidth, listHeight, itemHeight); @@ -168,32 +86,78 @@ protected void init() { int x = leftMargin + listWidth + 10; int width = this.width - x - 10; + int y = 50; if (width < 200) { + x = 100; width = listWidth; + y = this.height - bottom + 10; } + this.tooltipX = x + 10; + this.tooltipY = y; this.tooltipWidth = width; - buildPage(); + this.buildPage(); this.applyButton.active = false; this.undoButton.visible = false; } - private void captureOriginalState() { - for (OptionPage page : this.optionPages) { - page.captureOriginalState(); - } + private void addOptionPages() { + registry.clear(); + + registry.registerPage( + "video", + Component.translatable("vulkanmod.options.pages.video"), + Options.getVideoOpts(), + 0 + ); + + registry.registerPage( + "graphics", + Component.translatable("vulkanmod.options.pages.graphics"), + Options.getGraphicsOpts(), + 1 + ); + + registry.registerPage( + "optimizations", + Component.translatable("vulkanmod.options.pages.optimizations"), + Options.getOptimizationOpts(), + 2 + ); + + registry.registerPage( + "other", + Component.translatable("vulkanmod.options.pages.other"), + Options.getOtherOpts(), + 3 + ); + + this.optionPages = registry.getPages(); } - private void undo() { - for (OptionPage page : this.optionPages) { - page.resetToOriginalState(); - page.updateOptionStates(); + private VTextInputWidget createSearchField() { + int rightMargin = 10; + int padding = 10; + int kofiWidth = Minecraft.getInstance().font.width(Component.translatable("vulkanmod.options.buttons.kofi")) + padding; + int topBarRight = this.width - kofiWidth - rightMargin; + + if (UpdateChecker.isUpdateAvailable()) { + int updateWidth = minecraft.font.width(Component.translatable("vulkanmod.options.buttons.update_available")) + padding; + topBarRight -= updateWidth + VGuiConstants.WIDGET_MARGIN; } - buildPage(); + + int width = Math.min(topBarRight - 90 - MARGIN - 4, 413); + + return new VTextInputWidget( + 90 + MARGIN, 4, + width, VGuiConstants.WIDGET_HEIGHT, + Component.translatable("vulkanmod.options.searchFieldPlaceholder"), + widget -> performSearch(widget.getInput()) + ); } private void buildLists(int left, int top, int listWidth, int listHeight, int itemHeight) { @@ -256,12 +220,15 @@ else if (option instanceof CyclingOption cycling) { searchResults.toArray(new OptionBlock[0]) ); - int top = 29; + int top = 32; + int bottom = 60; int itemHeight = 20; - int leftMargin = 94; int rightMargin = 3; - int listWidth = this.width - rightMargin - leftMargin; - int listHeight = this.height - top - 60; + int leftMargin = MARGIN + 90; + int listWidth = Math.min(this.width - leftMargin - MARGIN, 420); + int listHeight = this.height - top - bottom; +// int listWidth = this.width - rightMargin - leftMargin; +// int listHeight = this.height - top - 60; searchResultsPage.createList(leftMargin, top, listWidth, listHeight, itemHeight); @@ -279,7 +246,7 @@ private void buildPage() { this.clearWidgets(); - int x = 10; + int x = MARGIN; int y = 36; for (int i = 0; i < this.optionPages.size(); ++i) { var page = this.optionPages.get(i); @@ -403,12 +370,12 @@ public void render(GuiGraphics guiGraphics, int mouseX, int mouseY, float delta) VRenderSystem.enableBlend(); int iconBackgroundColor = ColorUtil.ARGB.multiplyAlpha(VGuiConstants.COLOR_BLACK, 0.45f); - int iconBackgroundWidth = 90; + int iconBackgroundWidth = 80; int iconBackgroundHeight = (minecraft.font.lineHeight * 4); - guiGraphics.fill(10, 4, iconBackgroundWidth, iconBackgroundHeight, iconBackgroundColor); + guiGraphics.fill(MARGIN, 4, iconBackgroundWidth + MARGIN, iconBackgroundHeight, iconBackgroundColor); int size = minecraft.font.lineHeight * 4; - int iconX = 10 + (iconBackgroundWidth - 10 - size) / 2; + int iconX = MARGIN + (iconBackgroundWidth) / 2 - size / 2; int iconY = 4 + (iconBackgroundHeight - 4 - size) / 2; guiGraphics.blit(RenderPipelines.GUI_TEXTURED, ICON, iconX, iconY, 0f, 0f, size, size, size, size); @@ -444,25 +411,23 @@ public void render(GuiGraphics guiGraphics, int mouseX, int mouseY, float delta) if (hoveredWidget != null) { List tooltip = getWidgetTooltip(hoveredWidget); + if (tooltip != null) { - int padding = 3; - int tooltipWidth = GuiRenderer.getMaxTextWidth(this.font, tooltip); - int tooltipX = hoveredWidget.getX() + hoveredWidget.getWidth() - tooltipWidth - padding; - int tooltipY = hoveredWidget.getY() + hoveredWidget.getHeight() + 3 + 1; - this.renderTooltip(tooltip, tooltipX, tooltipY); + this.renderTooltip(tooltip, this.tooltipX, this.tooltipY); } } } private void renderTooltip(List list, int x, int y) { - if (list.isEmpty()) return; int padding = 3; int width = GuiRenderer.getMaxTextWidth(this.font, list); int height = list.size() * 10; - GuiRenderer.fill(x - padding, y - padding, x + width + padding, y + height + padding, - ColorUtil.ARGB.pack(0.05f, 0.05f, 0.05f, 0.6f)); + float intensity = 0.05f; + int color = ColorUtil.ARGB.pack(intensity, intensity, intensity, 0.6f); + GuiRenderer.fill(x - padding, y - padding, x + width + padding, y + height + padding, color); - GuiRenderer.renderBorder(x - padding, y - padding, x + width + padding, y + height + padding, 1, VGuiConstants.COLOR_RED); + color = VGuiConstants.COLOR_RED; + GuiRenderer.renderBorder(x - padding, y - padding, x + width + padding, y + height + padding, 1, color); int yOffset = 0; for (var text : list) { @@ -508,6 +473,15 @@ private void setOptionList(int i) { this.pageButtons.get(i).setSelected(true); } + private void undo() { + for (OptionPage page : this.optionPages) { + page.resetToOriginalState(); + page.updateOptionStates(); + } + + buildPage(); + } + private void applyOptions() { List pages = List.copyOf(this.optionPages); for (var page : pages) { @@ -515,8 +489,6 @@ private void applyOptions() { page.updateOptionStates(); } - this.captureOriginalState(); - Initializer.CONFIG.write(); } diff --git a/src/main/java/net/vulkanmod/config/gui/util/VGuiConstants.java b/src/main/java/net/vulkanmod/config/gui/util/VGuiConstants.java index 419cde2f06..2786875a96 100644 --- a/src/main/java/net/vulkanmod/config/gui/util/VGuiConstants.java +++ b/src/main/java/net/vulkanmod/config/gui/util/VGuiConstants.java @@ -6,7 +6,7 @@ public class VGuiConstants { public static final int COLOR_WHITE = ColorUtil.ARGB.pack(1f, 1f, 1f, 1f); public static final int COLOR_BLACK = ColorUtil.ARGB.pack(0f, 0f, 0f, 1f); public static final int COLOR_GRAY = ColorUtil.ARGB.pack(0.6f, 0.6f, 0.6f, 1f); - public static final int COLOR_RED = ColorUtil.ARGB.pack(0.59f, 0.18f, 0.17f, 1f); + public static final int COLOR_RED = ColorUtil.ARGB.pack(0.4f, 0.05f, 0.05f, 0.8f); public static final int WIDGET_HEIGHT = 20; public static final int WIDGET_MARGIN = 5; diff --git a/src/main/java/net/vulkanmod/config/option/Option.java b/src/main/java/net/vulkanmod/config/option/Option.java index 17695d22c7..f5761f798e 100644 --- a/src/main/java/net/vulkanmod/config/option/Option.java +++ b/src/main/java/net/vulkanmod/config/option/Option.java @@ -17,7 +17,6 @@ public abstract class Option { protected T value; protected T newValue; - protected T originalValue; protected Function translator; protected Function tooltipTranslator; @@ -144,17 +143,11 @@ public void apply() { this.value = this.newValue; } - public void captureOriginalState() { - this.originalValue = this.value; - } - - public void resetToOriginalState() { - if (this.originalValue != null) { - this.newValue = this.originalValue; + public void resetValue() { + this.newValue = this.value; - if (onChange != null) - onChange.run(); - } + if (onChange != null) + onChange.run(); } public T getNewValue() { @@ -169,7 +162,7 @@ public Component getTooltip() { if (this.tooltipTranslator != null) { return this.tooltipTranslator.apply(this.newValue); } else { - return Component.empty(); + return null; } } } \ No newline at end of file diff --git a/src/main/java/net/vulkanmod/config/option/OptionPage.java b/src/main/java/net/vulkanmod/config/option/OptionPage.java index a87699e133..22c957d025 100644 --- a/src/main/java/net/vulkanmod/config/option/OptionPage.java +++ b/src/main/java/net/vulkanmod/config/option/OptionPage.java @@ -52,18 +52,10 @@ public void updateOptionStates() { } } - public void captureOriginalState() { - for (var block : this.optionBlocks) { - for (var option : block.options()) { - option.captureOriginalState(); - } - } - } - public void resetToOriginalState() { for (var block : this.optionBlocks) { for (var option : block.options()) { - option.resetToOriginalState(); + option.resetValue(); } } } diff --git a/src/main/java/net/vulkanmod/config/option/OptionRegistry.java b/src/main/java/net/vulkanmod/config/option/OptionRegistry.java index 18a2b1f328..37f79cca38 100644 --- a/src/main/java/net/vulkanmod/config/option/OptionRegistry.java +++ b/src/main/java/net/vulkanmod/config/option/OptionRegistry.java @@ -48,6 +48,11 @@ public void unregister(String id) { } } + public void clear() { + pagesById.clear(); + pages.clear(); + } + public boolean isRegistered(String id) { return pagesById.containsKey(id); } From 2c3d9f875cd212e0d8efb66a536efd1cdf1462eb Mon Sep 17 00:00:00 2001 From: xCollateral <103696619+xCollateral@users.noreply.github.com> Date: Wed, 22 Apr 2026 16:25:36 +0200 Subject: [PATCH 27/27] Add new features to option screen - Mod entries - Perfomance impact tooltip - Update layout --- .../config/gui/ModSettingsEntry.java | 46 +++++ .../config/gui/ModSettingsRegistry.java | 34 ++++ .../net/vulkanmod/config/gui/VOptionList.java | 2 +- .../vulkanmod/config/gui/VOptionScreen.java | 164 +++++++++--------- .../config/gui/util/VGuiConstants.java | 1 + .../config/gui/widget/ModIconWidget.java | 39 +++++ .../config/gui/widget/OptionWidget.java | 5 + .../config/gui/widget/RangeOptionWidget.java | 21 ++- .../config/gui/widget/VAbstractWidget.java | 6 +- .../config/gui/widget/VButtonWidget.java | 11 +- .../config/gui/widget/VTextInputWidget.java | 2 + .../config/option/CyclingOption.java | 6 +- .../net/vulkanmod/config/option/Option.java | 17 +- .../config/option/OptionRegistry.java | 59 ------- .../net/vulkanmod/config/option/Options.java | 60 ++++++- .../net/vulkanmod/config/option/Page.java | 8 - .../config/option/PerformanceImpact.java | 20 +++ .../vulkanmod/config/option/RangeOption.java | 24 ++- .../vulkanmod/config/option/SwitchOption.java | 2 +- .../assets/vulkanmod/lang/en_us.json | 5 + .../assets/vulkanmod/vlogo_transparent.png | Bin 85962 -> 125285 bytes 21 files changed, 340 insertions(+), 192 deletions(-) create mode 100644 src/main/java/net/vulkanmod/config/gui/ModSettingsEntry.java create mode 100644 src/main/java/net/vulkanmod/config/gui/ModSettingsRegistry.java create mode 100644 src/main/java/net/vulkanmod/config/gui/widget/ModIconWidget.java delete mode 100644 src/main/java/net/vulkanmod/config/option/OptionRegistry.java create mode 100644 src/main/java/net/vulkanmod/config/option/PerformanceImpact.java diff --git a/src/main/java/net/vulkanmod/config/gui/ModSettingsEntry.java b/src/main/java/net/vulkanmod/config/gui/ModSettingsEntry.java new file mode 100644 index 0000000000..bfe3e31e60 --- /dev/null +++ b/src/main/java/net/vulkanmod/config/gui/ModSettingsEntry.java @@ -0,0 +1,46 @@ +package net.vulkanmod.config.gui; + +import net.minecraft.network.chat.FormattedText; +import net.minecraft.resources.ResourceLocation; +import net.vulkanmod.config.option.OptionPage; + +import java.util.List; +import java.util.function.Supplier; + +public class ModSettingsEntry { + public final FormattedText modName; + public final Supplier iconSupplier; + private final Supplier> optionPageSupplier; + private final Runnable onApply; + + private ResourceLocation icon; + List pages; + + public ModSettingsEntry(FormattedText modName, Supplier iconSupplier, Supplier> optionPageSupplier, Runnable onApply) { + this.modName = modName; + this.iconSupplier = iconSupplier; + this.optionPageSupplier = optionPageSupplier; + this.onApply = onApply; + } + + public List initPages() { + this.pages = this.optionPageSupplier.get(); + return this.pages; + } + + public List getPages() { + return pages; + } + + public ResourceLocation getIcon() { + if (this.icon == null) { + this.icon = this.iconSupplier.get(); + } + + return icon; + } + + public void runOnApply() { + onApply.run(); + } +} diff --git a/src/main/java/net/vulkanmod/config/gui/ModSettingsRegistry.java b/src/main/java/net/vulkanmod/config/gui/ModSettingsRegistry.java new file mode 100644 index 0000000000..dd49e32920 --- /dev/null +++ b/src/main/java/net/vulkanmod/config/gui/ModSettingsRegistry.java @@ -0,0 +1,34 @@ +package net.vulkanmod.config.gui; + +import it.unimi.dsi.fastutil.objects.ObjectArraySet; +import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; +import net.minecraft.ChatFormatting; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.vulkanmod.Initializer; +import net.vulkanmod.config.option.Options; + +import java.util.Set; + +public class ModSettingsRegistry { + + public static final ModSettingsRegistry INSTANCE = new ModSettingsRegistry(); + + private final Set modEntries = new ObjectArraySet<>(); + + ModSettingsRegistry() { + ModSettingsEntry vulkanModSettings = new ModSettingsEntry(Component.literal("VulkanMod").withStyle(ChatFormatting.DARK_RED), + () -> ResourceLocation.fromNamespaceAndPath("vulkanmod", "vlogo_transparent.png"), + Options::getOptionPages, + () -> Initializer.CONFIG.write()); + this.addModEntry(vulkanModSettings); + } + + public void addModEntry(ModSettingsEntry entry) { + this.modEntries.add(entry); + } + + public Set getModEntries() { + return modEntries; + } +} diff --git a/src/main/java/net/vulkanmod/config/gui/VOptionList.java b/src/main/java/net/vulkanmod/config/gui/VOptionList.java index 9d03f29660..9b441c9e7d 100644 --- a/src/main/java/net/vulkanmod/config/gui/VOptionList.java +++ b/src/main/java/net/vulkanmod/config/gui/VOptionList.java @@ -59,7 +59,7 @@ public void addAll(OptionBlock[] blocks) { var options = block.options(); for (Option option : options) { int margin = this.itemMargin; - OptionWidget widget = option.createWidget(); + OptionWidget widget = option.getWidget(); widget.setDimensions(x0, 0, width, height); this.addEntry(new Entry(widget, margin, null)); } diff --git a/src/main/java/net/vulkanmod/config/gui/VOptionScreen.java b/src/main/java/net/vulkanmod/config/gui/VOptionScreen.java index 161afd4ccb..b2dc168b1d 100644 --- a/src/main/java/net/vulkanmod/config/gui/VOptionScreen.java +++ b/src/main/java/net/vulkanmod/config/gui/VOptionScreen.java @@ -10,19 +10,15 @@ import net.minecraft.client.gui.screens.options.VideoSettingsScreen; import net.minecraft.client.input.KeyEvent; import net.minecraft.client.input.MouseButtonEvent; -import net.minecraft.client.renderer.RenderPipelines; import net.minecraft.network.chat.CommonComponents; import net.minecraft.network.chat.Component; -import net.minecraft.resources.ResourceLocation; import net.minecraft.util.FormattedCharSequence; import net.vulkanmod.Initializer; import net.vulkanmod.config.UpdateChecker; import net.vulkanmod.config.gui.render.GuiRenderer; import net.vulkanmod.config.gui.util.SearchHelper; import net.vulkanmod.config.gui.util.VGuiConstants; -import net.vulkanmod.config.gui.widget.VAbstractWidget; -import net.vulkanmod.config.gui.widget.VButtonWidget; -import net.vulkanmod.config.gui.widget.VTextInputWidget; +import net.vulkanmod.config.gui.widget.*; import net.vulkanmod.config.option.*; import net.vulkanmod.vulkan.VRenderSystem; import net.vulkanmod.vulkan.util.ColorUtil; @@ -32,13 +28,13 @@ import java.util.List; public class VOptionScreen extends Screen { - public final static int MARGIN = 20; - final ResourceLocation ICON = ResourceLocation.fromNamespaceAndPath("vulkanmod", "vlogo_transparent.png"); + public final static int MARGIN = 10; private final Screen parent; - OptionRegistry registry = OptionRegistry.get(); - private List optionPages; + private final List modSettingsEntries; + + private final List optionPages; private OptionPage searchResultsPage; private int currentListIdx = 0; @@ -53,6 +49,7 @@ public class VOptionScreen extends Screen { private VTextInputWidget searchField; + private final List iconWidgets = Lists.newArrayList(); private final List pageButtons = Lists.newArrayList(); private final List buttons = Lists.newArrayList(); @@ -62,11 +59,12 @@ public VOptionScreen(Component title, Screen parent) { this.parent = parent; this.optionPages = new ArrayList<>(); + this.modSettingsEntries = new ArrayList<>(ModSettingsRegistry.INSTANCE.getModEntries()); } @Override protected void init() { - this.addOptionPages(); + this.initOptionsPages(); if (this.optionPages.isEmpty()) { throw new IllegalStateException("Default Options weren't added!"); @@ -76,7 +74,7 @@ protected void init() { int bottom = 60; int itemHeight = 20; - int leftMargin = MARGIN + 90; + int leftMargin = MARGIN + VGuiConstants.PAGE_BUTTON_WIDTH + 6; int listWidth = Math.min(this.width - leftMargin - MARGIN, 420); int listHeight = this.height - top - bottom; @@ -84,19 +82,19 @@ protected void init() { this.searchField = createSearchField(); - int x = leftMargin + listWidth + 10; - int width = this.width - x - 10; - int y = 50; + int x = leftMargin + listWidth + 6; + int tooltipWidth = Math.min(this.width - x - 10, 420); + int y = top + itemHeight + 6; - if (width < 200) { - x = 100; - width = listWidth; + if (tooltipWidth < 200) { + x = leftMargin + 3; + tooltipWidth = listWidth; y = this.height - bottom + 10; } - this.tooltipX = x + 10; + this.tooltipX = x; this.tooltipY = y; - this.tooltipWidth = width; + this.tooltipWidth = tooltipWidth; this.buildPage(); @@ -104,38 +102,14 @@ protected void init() { this.undoButton.visible = false; } - private void addOptionPages() { - registry.clear(); - - registry.registerPage( - "video", - Component.translatable("vulkanmod.options.pages.video"), - Options.getVideoOpts(), - 0 - ); + private void initOptionsPages() { + this.optionPages.clear(); - registry.registerPage( - "graphics", - Component.translatable("vulkanmod.options.pages.graphics"), - Options.getGraphicsOpts(), - 1 - ); + for (var modPageSet : this.modSettingsEntries) { + modPageSet.initPages(); - registry.registerPage( - "optimizations", - Component.translatable("vulkanmod.options.pages.optimizations"), - Options.getOptimizationOpts(), - 2 - ); - - registry.registerPage( - "other", - Component.translatable("vulkanmod.options.pages.other"), - Options.getOtherOpts(), - 3 - ); - - this.optionPages = registry.getPages(); + this.optionPages.addAll(modPageSet.getPages()); + } } private VTextInputWidget createSearchField() { @@ -150,10 +124,11 @@ private VTextInputWidget createSearchField() { } - int width = Math.min(topBarRight - 90 - MARGIN - 4, 413); + int leftMargin = VGuiConstants.PAGE_BUTTON_WIDTH + MARGIN + 6; + int width = Math.min(topBarRight - leftMargin - 4, 413); return new VTextInputWidget( - 90 + MARGIN, 4, + leftMargin, 4, width, VGuiConstants.WIDGET_HEIGHT, Component.translatable("vulkanmod.options.searchFieldPlaceholder"), widget -> performSearch(widget.getInput()) @@ -223,12 +198,9 @@ else if (option instanceof CyclingOption cycling) { int top = 32; int bottom = 60; int itemHeight = 20; - int rightMargin = 3; - int leftMargin = MARGIN + 90; + int leftMargin = MARGIN + VGuiConstants.PAGE_BUTTON_WIDTH; int listWidth = Math.min(this.width - leftMargin - MARGIN, 420); int listHeight = this.height - top - bottom; -// int listWidth = this.width - rightMargin - leftMargin; -// int listHeight = this.height - top - 60; searchResultsPage.createList(leftMargin, top, listWidth, listHeight, itemHeight); @@ -239,6 +211,7 @@ else if (option instanceof CyclingOption cycling) { private void buildPage() { this.buttons.clear(); this.pageButtons.clear(); + this.iconWidgets.clear(); String savedInput = this.searchField != null ? this.searchField.getInput() : ""; boolean savedFocused = this.searchField != null && this.searchField.focused; @@ -247,16 +220,27 @@ private void buildPage() { this.clearWidgets(); int x = MARGIN; - int y = 36; - for (int i = 0; i < this.optionPages.size(); ++i) { - var page = this.optionPages.get(i); - final int finalIdx = i; - VButtonWidget widget = new VButtonWidget(x, y, 80, VGuiConstants.WIDGET_HEIGHT, Component.nullToEmpty(page.name), button -> this.setOptionList(finalIdx)); - this.buttons.add(widget); - this.pageButtons.add(widget); - this.addWidget(widget); - - y += VGuiConstants.WIDGET_HEIGHT; + int y = 4; + + int width = VGuiConstants.PAGE_BUTTON_WIDTH; + int j = 0; + for (var modEntry : this.modSettingsEntries) { + ModIconWidget iconWidget = new ModIconWidget(modEntry.modName, modEntry.getIcon(), x, y, width, 28); + this.iconWidgets.add(iconWidget); + this.addWidget(iconWidget); + y += 28; + + var pages = modEntry.getPages(); + for (OptionPage page : pages) { + final int finalIdx = j; + VButtonWidget widget = new VButtonWidget(x, y, width, VGuiConstants.WIDGET_HEIGHT, Component.nullToEmpty(page.name), button -> this.setOptionList(finalIdx)); + this.buttons.add(widget); + this.pageButtons.add(widget); + this.addWidget(widget); + + y += VGuiConstants.WIDGET_HEIGHT; + j++; + } } if (!isSearchActive) { @@ -369,16 +353,6 @@ public void render(GuiGraphics guiGraphics, int mouseX, int mouseY, float delta) GuiRenderer.guiGraphics = guiGraphics; VRenderSystem.enableBlend(); - int iconBackgroundColor = ColorUtil.ARGB.multiplyAlpha(VGuiConstants.COLOR_BLACK, 0.45f); - int iconBackgroundWidth = 80; - int iconBackgroundHeight = (minecraft.font.lineHeight * 4); - guiGraphics.fill(MARGIN, 4, iconBackgroundWidth + MARGIN, iconBackgroundHeight, iconBackgroundColor); - - int size = minecraft.font.lineHeight * 4; - int iconX = MARGIN + (iconBackgroundWidth) / 2 - size / 2; - int iconY = 4 + (iconBackgroundHeight - 4 - size) / 2; - guiGraphics.blit(RenderPipelines.GUI_TEXTURED, ICON, iconX, iconY, 0f, 0f, size, size, size, size); - VOptionList currentList; if (isSearchActive && searchResultsPage != null) { currentList = searchResultsPage.getOptionList(); @@ -389,6 +363,10 @@ public void render(GuiGraphics guiGraphics, int mouseX, int mouseY, float delta) currentList.updateState(mouseX, mouseY); currentList.renderWidget(mouseX, mouseY); + for (var widget : iconWidgets) { + widget.render(mouseX, mouseY); + } + for (VButtonWidget button : buttons) { button.updateState(mouseX, mouseY); button.render(mouseX, mouseY); @@ -410,18 +388,22 @@ public void render(GuiGraphics guiGraphics, int mouseX, int mouseY, float delta) } if (hoveredWidget != null) { - List tooltip = getWidgetTooltip(hoveredWidget); - - if (tooltip != null) { - this.renderTooltip(tooltip, this.tooltipX, this.tooltipY); - } + this.renderTooltip(hoveredWidget, this.tooltipX, this.tooltipY); } } - private void renderTooltip(List list, int x, int y) { + private void renderTooltip(VAbstractWidget widget, int x, int y) { + var list = this.getWidgetTooltip(widget); + + if (list.isEmpty()) { + return; + } + + int lines = list.size(); + int padding = 3; int width = GuiRenderer.getMaxTextWidth(this.font, list); - int height = list.size() * 10; + int height = lines * 10; float intensity = 0.05f; int color = ColorUtil.ARGB.pack(intensity, intensity, intensity, 0.6f); GuiRenderer.fill(x - padding, y - padding, x + width + padding, y + height + padding, color); @@ -438,10 +420,18 @@ private void renderTooltip(List list, int x, int y) { private List getWidgetTooltip(VAbstractWidget widget) { var tooltip = widget.getTooltip(); - if (tooltip == null) - return null; + var impact = widget.getImpact(); + + List textList = new ArrayList<>(); + if (tooltip != null) { + textList.addAll(this.font.split(tooltip, this.tooltipWidth)); + } - return this.font.split(tooltip, this.tooltipWidth); + if (impact != null) { + textList.addAll(this.font.split(Component.translatable("Performance Impact: %s", impact.component()), this.tooltipWidth)); + } + + return textList; } private void updateState() { @@ -489,7 +479,9 @@ private void applyOptions() { page.updateOptionStates(); } - Initializer.CONFIG.write(); + for (var modEntry : this.modSettingsEntries) { + modEntry.runOnApply(); + } } @Override diff --git a/src/main/java/net/vulkanmod/config/gui/util/VGuiConstants.java b/src/main/java/net/vulkanmod/config/gui/util/VGuiConstants.java index 2786875a96..4e3187f9e1 100644 --- a/src/main/java/net/vulkanmod/config/gui/util/VGuiConstants.java +++ b/src/main/java/net/vulkanmod/config/gui/util/VGuiConstants.java @@ -8,6 +8,7 @@ public class VGuiConstants { public static final int COLOR_GRAY = ColorUtil.ARGB.pack(0.6f, 0.6f, 0.6f, 1f); public static final int COLOR_RED = ColorUtil.ARGB.pack(0.4f, 0.05f, 0.05f, 0.8f); + public static final int PAGE_BUTTON_WIDTH = 100; public static final int WIDGET_HEIGHT = 20; public static final int WIDGET_MARGIN = 5; diff --git a/src/main/java/net/vulkanmod/config/gui/widget/ModIconWidget.java b/src/main/java/net/vulkanmod/config/gui/widget/ModIconWidget.java new file mode 100644 index 0000000000..a182c5d79f --- /dev/null +++ b/src/main/java/net/vulkanmod/config/gui/widget/ModIconWidget.java @@ -0,0 +1,39 @@ +package net.vulkanmod.config.gui.widget; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.RenderPipelines; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.FormattedText; +import net.minecraft.resources.ResourceLocation; +import net.vulkanmod.config.gui.render.GuiRenderer; +import net.vulkanmod.config.gui.util.VGuiConstants; +import net.vulkanmod.vulkan.util.ColorUtil; + +public class ModIconWidget extends VAbstractWidget { + final FormattedText name; + final ResourceLocation icon; + + public ModIconWidget(FormattedText name, ResourceLocation icon, int x0, int y0, int width, int height) { + this.name = name; + this.icon = icon; + this.x = x0; + this.y = y0; + this.width = width; + this.height = height; + } + + public void render(double mX, double mY) { + int iconBackgroundColor = ColorUtil.ARGB.multiplyAlpha(VGuiConstants.COLOR_BLACK, 0.6f); + int iconBackgroundWidth = this.width; + int iconBackgroundHeight = this.height; + GuiRenderer.fill(this.x, this.y, this.x + iconBackgroundWidth, this.y + iconBackgroundHeight, iconBackgroundColor); + + + int size = this.height; + int iconX = this.x; + int iconY = this.y + (iconBackgroundHeight - size) / 2; + GuiRenderer.guiGraphics.blit(RenderPipelines.GUI_TEXTURED, icon, iconX, iconY, 0f, 0f, size, size, size, size); + + GuiRenderer.drawString(Minecraft.getInstance().font, (Component) this.name, this.x + size, iconY + this.height / 2 - 4, 0xffffffff); + } +} diff --git a/src/main/java/net/vulkanmod/config/gui/widget/OptionWidget.java b/src/main/java/net/vulkanmod/config/gui/widget/OptionWidget.java index fe4a08f53b..93db44cf05 100644 --- a/src/main/java/net/vulkanmod/config/gui/widget/OptionWidget.java +++ b/src/main/java/net/vulkanmod/config/gui/widget/OptionWidget.java @@ -8,6 +8,7 @@ import net.minecraft.network.chat.Component; import net.vulkanmod.config.gui.render.GuiRenderer; import net.vulkanmod.config.option.Option; +import net.vulkanmod.config.option.PerformanceImpact; import net.vulkanmod.vulkan.util.ColorUtil; import org.jetbrains.annotations.NotNull; @@ -153,6 +154,10 @@ public Component getTooltip() { return this.option.getTooltip(); } + public PerformanceImpact getImpact() { + return this.option.getImpact(); + } + @Override public @NotNull NarrationPriority narrationPriority() { if (this.focused) { diff --git a/src/main/java/net/vulkanmod/config/gui/widget/RangeOptionWidget.java b/src/main/java/net/vulkanmod/config/gui/widget/RangeOptionWidget.java index aafdc75685..a1168fb0a2 100644 --- a/src/main/java/net/vulkanmod/config/gui/widget/RangeOptionWidget.java +++ b/src/main/java/net/vulkanmod/config/gui/widget/RangeOptionWidget.java @@ -12,8 +12,6 @@ import org.lwjgl.glfw.GLFW; public class RangeOptionWidget extends OptionWidget { - protected double value; - private boolean focused; public RangeOptionWidget(RangeOption option, Component name) { @@ -23,7 +21,8 @@ public RangeOptionWidget(RangeOption option, Component name) { @Override protected void renderControls(double mouseX, double mouseY) { - int valueX = this.controlX + (int) (this.value * (this.controlWidth)); + float scaledValue = this.option.getScaledNewValue(); + int valueX = this.controlX + (int) (scaledValue * (this.controlWidth)); if (this.controlHovered && this.active) { int halfWidth = 2; @@ -67,7 +66,8 @@ public boolean keyPressed(KeyEvent event) { if (isLeft || isRight) { float direction = isLeft ? -1.0f : 1.0f; - this.setValue(this.value + (double) (direction / (float) (this.width - 8))); + double currentValue = this.option.getScaledValue(); + this.setValue(currentValue + (double) (direction / (float) (this.width - 8))); } return false; @@ -88,10 +88,10 @@ private void setValueFromMouse(double mouseX) { } private void setValue(double value) { - double d = this.value; - this.value = Mth.clamp(value, 0.0, 1.0); - if (d != this.value) { - this.applyValue(); + double currentValue = this.option.getScaledValue(); + value = Mth.clamp(value, 0.0, 1.0); + if (currentValue != value) { + this.applyNewValue((float) value); } this.updateDisplayedValue(); } @@ -101,9 +101,8 @@ protected void onDrag(double mouseX, double mouseY, double deltaX, double deltaY this.setValueFromMouse(mouseX); } - private void applyValue() { - option.setValue((float) this.value); - this.value = option.getScaledValue(); + private void applyNewValue(float value) { + option.setNewValueFromScaledFloat(value); } @Override diff --git a/src/main/java/net/vulkanmod/config/gui/widget/VAbstractWidget.java b/src/main/java/net/vulkanmod/config/gui/widget/VAbstractWidget.java index f1cc5f50cc..c283eb0d86 100644 --- a/src/main/java/net/vulkanmod/config/gui/widget/VAbstractWidget.java +++ b/src/main/java/net/vulkanmod/config/gui/widget/VAbstractWidget.java @@ -9,6 +9,7 @@ import net.vulkanmod.config.gui.GuiElement; import net.vulkanmod.config.gui.util.VGuiConstants; import net.vulkanmod.config.gui.render.GuiRenderer; +import net.vulkanmod.config.option.PerformanceImpact; import net.vulkanmod.vulkan.util.ColorUtil; public abstract class VAbstractWidget extends GuiElement { @@ -28,7 +29,6 @@ public void setDimensions(int x, int y, int width, int height) { public void render(double mX, double mY) { this.updateState(mX, mY); this.renderWidget(mX, mY); - this.renderHovering(0, 0); } public void renderWidget(double mX, double mY) { @@ -132,4 +132,8 @@ public void playDownSound(SoundManager soundManager) { public Component getTooltip() { return null; } + + public PerformanceImpact getImpact() { + return null; + } } diff --git a/src/main/java/net/vulkanmod/config/gui/widget/VButtonWidget.java b/src/main/java/net/vulkanmod/config/gui/widget/VButtonWidget.java index 50745bcd8a..a9d7dffcd6 100644 --- a/src/main/java/net/vulkanmod/config/gui/widget/VButtonWidget.java +++ b/src/main/java/net/vulkanmod/config/gui/widget/VButtonWidget.java @@ -45,12 +45,13 @@ public void renderWidget(double mouseX, double mouseY) { GuiRenderer.fill(this.x, this.y, this.x + this.width, this.y + this.height, selectionFillColor); } + this.renderHovering(0, 0); + // this is down here because of layering - GuiRenderer.drawCenteredString( - Minecraft.getInstance().font, - this.message, - this.x + this.width / 2, (this.y + this.height / 2) - 4, - textColor | (Mth.ceil(this.alpha * 255.0f) << 24)); + GuiRenderer.drawString(Minecraft.getInstance().font, + this.message, + this.x + 8, (this.y + this.height / 2) - 4, + textColor | (Mth.ceil(this.alpha * 255.0f) << 24)); } public void onClick(double mX, double mY) { diff --git a/src/main/java/net/vulkanmod/config/gui/widget/VTextInputWidget.java b/src/main/java/net/vulkanmod/config/gui/widget/VTextInputWidget.java index c9d9690c0a..ab0e07c8d2 100644 --- a/src/main/java/net/vulkanmod/config/gui/widget/VTextInputWidget.java +++ b/src/main/java/net/vulkanmod/config/gui/widget/VTextInputWidget.java @@ -49,6 +49,8 @@ public void renderWidget(double mouseX, double mouseY) { GuiRenderer.fill(this.x, this.y, this.x + this.width, this.y + this.height, backgroundColor); + this.renderHovering(0, 0); + if (isFocused && cursorPos != selectionEnd) { int start = Math.min(cursorPos, selectionEnd); int end = Math.max(cursorPos, selectionEnd); diff --git a/src/main/java/net/vulkanmod/config/option/CyclingOption.java b/src/main/java/net/vulkanmod/config/option/CyclingOption.java index a96bac3abd..214b90d89f 100644 --- a/src/main/java/net/vulkanmod/config/option/CyclingOption.java +++ b/src/main/java/net/vulkanmod/config/option/CyclingOption.java @@ -20,7 +20,7 @@ public CyclingOption(Component name, E[] values, Consumer setter, Supplier } @Override - public OptionWidget createWidget() { + protected OptionWidget createWidget() { var widget = new CyclingOptionWidget(this, this.name); this.widget = widget; return widget; @@ -35,7 +35,9 @@ public void updateOption(E[] values, Consumer setter, Supplier getter) { this.index = ArrayUtils.indexOf(this.values, this.getNewValue()); } - public int index() { return this.index; } + public int index() { + return this.index; + } public void setValues(E[] values) { this.values = values; diff --git a/src/main/java/net/vulkanmod/config/option/Option.java b/src/main/java/net/vulkanmod/config/option/Option.java index f5761f798e..1873520534 100644 --- a/src/main/java/net/vulkanmod/config/option/Option.java +++ b/src/main/java/net/vulkanmod/config/option/Option.java @@ -11,6 +11,7 @@ public abstract class Option { protected final Component name; @SuppressWarnings("unused") protected Component tooltip; + protected PerformanceImpact impact; protected Consumer onApply; protected Supplier valueSupplier; @@ -86,13 +87,22 @@ public Option setTooltip(Function tooltipTranslator) { return this; } + public PerformanceImpact getImpact() { + return impact; + } + + public Option setImpact(PerformanceImpact impact) { + this.impact = impact; + return this; + } + public Option setActive(boolean active) { this.active = active; this.widget.active = active; return this; } - public abstract OptionWidget createWidget(); + protected abstract OptionWidget createWidget(); public OptionWidget getWidget() { if (this.widget == null) { @@ -144,10 +154,7 @@ public void apply() { } public void resetValue() { - this.newValue = this.value; - - if (onChange != null) - onChange.run(); + this.setNewValue(this.value); } public T getNewValue() { diff --git a/src/main/java/net/vulkanmod/config/option/OptionRegistry.java b/src/main/java/net/vulkanmod/config/option/OptionRegistry.java deleted file mode 100644 index 37f79cca38..0000000000 --- a/src/main/java/net/vulkanmod/config/option/OptionRegistry.java +++ /dev/null @@ -1,59 +0,0 @@ -package net.vulkanmod.config.option; - -import net.minecraft.network.chat.Component; -import net.vulkanmod.config.gui.OptionBlock; - -import java.util.*; - -public final class OptionRegistry { - - private static final OptionRegistry INSTANCE = new OptionRegistry(); - - private final Map pagesById = new HashMap<>(); - private final List pages = new ArrayList<>(); - - private OptionRegistry() {} - - public static OptionRegistry get() { - return INSTANCE; - } - - public void registerPage( - String id, - Component title, - OptionBlock[] blocks, - int order - ) { - if (pagesById.containsKey(id)) { - throw new IllegalStateException("Option page already registered: " + id); - } - - OptionPage page = new OptionPage(title.getString(), blocks); - page.setOrder(order); - - pagesById.put(id, page); - pages.add(page); - - pages.sort(Comparator.comparingInt(OptionPage::getOrder)); - } - - public List getPages() { - return Collections.unmodifiableList(pages); - } - - public void unregister(String id) { - OptionPage page = pagesById.remove(id); - if (page != null) { - pages.remove(page); - } - } - - public void clear() { - pagesById.clear(); - pages.clear(); - } - - public boolean isRegistered(String id) { - return pagesById.containsKey(id); - } -} \ No newline at end of file diff --git a/src/main/java/net/vulkanmod/config/option/Options.java b/src/main/java/net/vulkanmod/config/option/Options.java index bd89ce66d0..6da44f5706 100644 --- a/src/main/java/net/vulkanmod/config/option/Options.java +++ b/src/main/java/net/vulkanmod/config/option/Options.java @@ -14,6 +14,8 @@ import net.vulkanmod.vulkan.Renderer; import net.vulkanmod.vulkan.device.DeviceManager; +import java.util.ArrayList; +import java.util.List; import java.util.stream.IntStream; public abstract class Options { @@ -25,6 +27,36 @@ public abstract class Options { private static final Window window = minecraft.getWindow(); private static final net.minecraft.client.Options mcOptions = minecraft.options; + public static List getOptionPages() { + List optionPages = new ArrayList<>(); + + OptionPage page = new OptionPage( + Component.translatable("vulkanmod.options.pages.video").getString(), + Options.getVideoOpts() + ); + optionPages.add(page); + + page = new OptionPage( + Component.translatable("vulkanmod.options.pages.graphics").getString(), + Options.getGraphicsOpts() + ); + optionPages.add(page); + + page = new OptionPage( + Component.translatable("vulkanmod.options.pages.optimizations").getString(), + Options.getOptimizationOpts() + ); + optionPages.add(page); + + page = new OptionPage( + Component.translatable("vulkanmod.options.pages.other").getString(), + Options.getOtherOpts() + ); + optionPages.add(page); + + return optionPages; + } + public static OptionBlock[] getVideoOpts() { VideoModeManager.selectBestMonitor(window); var resolutions = VideoModeManager.getVideoResolutions(); @@ -184,7 +216,9 @@ public static OptionBlock[] getGraphicsOpts() { new RangeOption(Component.translatable("options.renderDistance"), 2, 32, 1, value -> mcOptions.renderDistance().set(value), - () -> mcOptions.renderDistance().get()), + () -> mcOptions.renderDistance().get()) + .setTooltip(v -> Component.literal("Chunk render distance")) + .setImpact(PerformanceImpact.HIGH), new RangeOption(Component.translatable("options.simulationDistance"), 5, 32, 1, value -> mcOptions.simulationDistance().set(value), @@ -205,6 +239,7 @@ public static OptionBlock[] getGraphicsOpts() { new ParticleStatus[]{ParticleStatus.MINIMAL, ParticleStatus.DECREASED, ParticleStatus.ALL}, value -> mcOptions.particles().set(value), () -> mcOptions.particles().get()) + .setImpact(PerformanceImpact.MEDIUM) .setTranslator(p -> Component.translatable(p.getKey())), new CyclingOption<>(Component.translatable("options.renderClouds"), CloudStatus.values(), @@ -231,7 +266,8 @@ public static OptionBlock[] getGraphicsOpts() { })) .setTooltip(value -> value == LightMode.SUB_BLOCK ? Component.translatable("vulkanmod.options.ao.subBlock.tooltip") - : Component.empty()), + : Component.empty()) + .setImpact(PerformanceImpact.LOW), new RangeOption(Component.translatable("options.biomeBlendRadius"), 0, 7, 1, value -> Component.nullToEmpty("%d x %d".formatted(value * 2 + 1, value * 2 + 1)), @@ -244,11 +280,13 @@ public static OptionBlock[] getGraphicsOpts() { new OptionBlock("", new Option[]{ new SwitchOption(Component.translatable("options.entityShadows"), value -> mcOptions.entityShadows().set(value), - () -> mcOptions.entityShadows().get()), + () -> mcOptions.entityShadows().get()) + .setImpact(PerformanceImpact.LOW), new RangeOption(Component.translatable("options.entityDistanceScaling"), 50, 500, 25, value -> mcOptions.entityDistanceScaling().set(value * 0.01), - () -> (int)(mcOptions.entityDistanceScaling().get() * 100)), + () -> (int)(mcOptions.entityDistanceScaling().get() * 100)) + .setImpact(PerformanceImpact.HIGH), new CyclingOption<>(Component.translatable("options.mipmapLevels"), new Integer[]{0,1,2,3,4}, value -> { @@ -258,6 +296,7 @@ public static OptionBlock[] getGraphicsOpts() { }, () -> mcOptions.mipmapLevels().get()) .setTranslator(v -> Component.literal(String.valueOf(v))) + .setImpact(PerformanceImpact.LOW) }) }; } @@ -276,11 +315,13 @@ public static OptionBlock[] getOptimizationOpts() { case 10 -> "options.off"; default -> "vulkanmod.options.unknown"; })) - .setTooltip(v -> v <= 3 ? Component.translatable("vulkanmod.options.advCulling.tooltip") : Component.empty()), + .setTooltip(v -> v <= 3 ? Component.translatable("vulkanmod.options.advCulling.tooltip") : Component.empty()) + .setImpact(PerformanceImpact.HIGH), new SwitchOption(Component.translatable("vulkanmod.options.entityCulling"), v -> config.entityCulling = v, () -> config.entityCulling) - .setTooltip(v -> Component.translatable("vulkanmod.options.entityCulling.tooltip")), + .setTooltip(v -> Component.translatable("vulkanmod.options.entityCulling.tooltip")) + .setImpact(PerformanceImpact.HIGH), new SwitchOption(Component.translatable("vulkanmod.options.uniqueOpaqueLayer"), v -> { config.uniqueOpaqueLayer = v; @@ -288,18 +329,21 @@ public static OptionBlock[] getOptimizationOpts() { minecraft.levelRenderer.allChanged(); }, () -> config.uniqueOpaqueLayer) - .setTooltip(v -> Component.translatable("vulkanmod.options.uniqueOpaqueLayer.tooltip")), + .setTooltip(v -> Component.translatable("vulkanmod.options.uniqueOpaqueLayer.tooltip")) + .setImpact(PerformanceImpact.HIGH), new SwitchOption(Component.translatable("vulkanmod.options.backfaceCulling"), v -> { config.backFaceCulling = v; minecraft.levelRenderer.allChanged(); }, () -> config.backFaceCulling) - .setTooltip(v -> Component.translatable("vulkanmod.options.backfaceCulling.tooltip")), + .setTooltip(v -> Component.translatable("vulkanmod.options.backfaceCulling.tooltip")) + .setImpact(PerformanceImpact.HIGH), new SwitchOption(Component.translatable("vulkanmod.options.indirectDraw"), v -> config.indirectDraw = v, () -> config.indirectDraw) .setTooltip(v -> Component.translatable("vulkanmod.options.indirectDraw.tooltip")) + .setImpact(PerformanceImpact.HIGH) }) }; } diff --git a/src/main/java/net/vulkanmod/config/option/Page.java b/src/main/java/net/vulkanmod/config/option/Page.java index 45c9023c9a..188b88462e 100644 --- a/src/main/java/net/vulkanmod/config/option/Page.java +++ b/src/main/java/net/vulkanmod/config/option/Page.java @@ -24,14 +24,6 @@ public Block block(String title) { return block; } - public Page register() { - OptionBlock[] oblocks = blocks.stream() - .map(Block::build) - .toArray(OptionBlock[]::new); - OptionRegistry.get().registerPage("name", Component.literal(name), oblocks, 5); - return this; - } - public static class Block { private final String title; private final List> options = new ArrayList<>(); diff --git a/src/main/java/net/vulkanmod/config/option/PerformanceImpact.java b/src/main/java/net/vulkanmod/config/option/PerformanceImpact.java new file mode 100644 index 0000000000..72d7cc4e9b --- /dev/null +++ b/src/main/java/net/vulkanmod/config/option/PerformanceImpact.java @@ -0,0 +1,20 @@ +package net.vulkanmod.config.option; + +import net.minecraft.ChatFormatting; +import net.minecraft.network.chat.Component; + +public enum PerformanceImpact { + LOW(Component.translatable("vulkanmod.options.performanceImpact.low").withStyle(ChatFormatting.DARK_GREEN)), + MEDIUM(Component.translatable("vulkanmod.options.performanceImpact.medium").withStyle(ChatFormatting.YELLOW)), + HIGH(Component.translatable("vulkanmod.options.performanceImpact.high").withStyle(ChatFormatting.RED)); + + private final Component component; + + PerformanceImpact(Component component) { + this.component = component; + } + + public Component component() { + return this.component; + } +} diff --git a/src/main/java/net/vulkanmod/config/option/RangeOption.java b/src/main/java/net/vulkanmod/config/option/RangeOption.java index 846fe66de9..7835d8d2f1 100644 --- a/src/main/java/net/vulkanmod/config/option/RangeOption.java +++ b/src/main/java/net/vulkanmod/config/option/RangeOption.java @@ -13,19 +13,21 @@ public class RangeOption extends Option { int min; int max; int step; + float scaledNewValue; public RangeOption(Component name, int min, int max, int step, Function translator, Consumer setter, Supplier getter) { super(name, setter, getter, translator); this.min = min; this.max = max; this.step = step; + this.scaledNewValue = computeScaledValue(this.newValue); } public RangeOption(Component name, int min, int max, int step, Consumer setter, Supplier getter) { this(name, min, max, step, (i) -> Component.literal(String.valueOf(i)), setter, getter); } - public OptionWidget createWidget() { + protected OptionWidget createWidget() { var widget = new RangeOptionWidget(this, this.name); this.widget = widget; return widget; @@ -36,16 +38,28 @@ public Component getName() { } public float getScaledValue() { - float value = this.getNewValue(); - - return (value - this.min) / (this.max - this.min); + return this.scaledNewValue; } - public void setValue(float f) { + public void setNewValueFromScaledFloat(float f) { double n = Mth.lerp(f, min, max); n = this.step * Math.round(n / this.step); this.setNewValue((int) n); } + + public void setNewValue(Integer newValue) { + super.setNewValue(newValue); + + this.scaledNewValue = computeScaledValue(this.newValue); + } + + public float getScaledNewValue() { + return scaledNewValue; + } + + private float computeScaledValue(float value) { + return (value - this.min) / (this.max - this.min); + } } diff --git a/src/main/java/net/vulkanmod/config/option/SwitchOption.java b/src/main/java/net/vulkanmod/config/option/SwitchOption.java index 439bdac6fd..f3ea2c1b32 100644 --- a/src/main/java/net/vulkanmod/config/option/SwitchOption.java +++ b/src/main/java/net/vulkanmod/config/option/SwitchOption.java @@ -13,7 +13,7 @@ public SwitchOption(Component name, Consumer setter, Supplier } @Override - public OptionWidget createWidget() { + protected OptionWidget createWidget() { var widget = new SwitchOptionWidget(this, this.name); this.widget = widget; return widget; diff --git a/src/main/resources/assets/vulkanmod/lang/en_us.json b/src/main/resources/assets/vulkanmod/lang/en_us.json index 41d07bb697..fe308cbbde 100644 --- a/src/main/resources/assets/vulkanmod/lang/en_us.json +++ b/src/main/resources/assets/vulkanmod/lang/en_us.json @@ -6,6 +6,11 @@ "vulkanmod.options.pages.optimizations": "Optimizations", "vulkanmod.options.pages.other": "Other", + "vulkanmod.options.performanceImpact": "Performance impact: %s", + "vulkanmod.options.performanceImpact.low": "Low", + "vulkanmod.options.performanceImpact.medium": "Medium", + "vulkanmod.options.performanceImpact.high": "High", + "vulkanmod.options.buttons.apply": "Apply", "vulkanmod.options.buttons.undo": "Undo", "vulkanmod.options.buttons.kofi": "Support me", diff --git a/src/main/resources/assets/vulkanmod/vlogo_transparent.png b/src/main/resources/assets/vulkanmod/vlogo_transparent.png index daef9f5f95d8ce720287b6993f069b3880bbd6ca..d0a662e406d5f39e3425113970b72b6d670bd297 100644 GIT binary patch literal 125285 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%0APep!;it1fkNsbHFeY(!4+Tr{? zc96z6tAyQZqtBa}GUIuH-e2h{nDm`z4D1dwU2eHS5#MLyhIOMmPOmr99}H+BUXq;6 zZapGKE{%0Pzt!wI~wqcXvAKgSq(3Y;7p{_bIwr%j(EH zt2`s|{b=0vUwV#t^v|0>K^wyCV+qk^QzF3&NjI`em`v?91hdT;upqu~0;3uLrYBaK zin&Uvbk%d(OeuZl2}%Bz>oi~<4b*0wc%fWpqt~I2_G0D!w+Dj4aA4O zxo=dl1V0+r%Id$U)5>hB2_xB(RU^Gjaq0Flv9$Zt>pTjrzAt>Er;kXx;Vm`0`vwf- zfcT&W`0@{qpN=a_4EC$^;zdwUtrxvUrIyJut#FwI<>aaiWLtLI|AP?oQk%ag$OC#<>(VFO#wm@^wL?So5 z<-pgF^(4e{GJXf|HM)1~cw^5Ia$8L9lFUrNRuO$52A`^F)I9SH{ZYisWY&9Jah)Na=nk#*;zTvGa?t6rlPS`T@9SBhs1avn!x-}eQTAQ#JQRPy zQ}>j}`i1`YUMMbIuGO{W+@!rFl^K6gXF0N+wPr`Hy;Bh=29+A_9_}yree*$&e?Sdj z#ezymy|yUYR;~>%w%jGsbm&brF(*1c*uZ@D0;7g^q8GrlXxBY_~U>x$7u0qQOc6 z1Uv-2bHaq{n@_yqob6c~X{tN=YLeW z0t+qNtMvLDn`3ST-STIZx=oMei0~%*j}2|n1Dhw<8RR; zOvp+4cs;HWtYd2&r=2hrb3wD~LH{qyk%}=bRpppSWSL;2ZHs0b+j86#j=S$eiw*{Pm&IG8unow=h3~k zo2N=<2kp#+yY$aLccMz0CVmT6KYQr+LhyK#;l-z9BP!8*O^V{+JzJM_ruZ#GLT8kJ4bB}s$FNOKIu6@c$!EomqFv12D z!Io~r^bY z5{KLk&f@5}j`vQgnA*#lf>U`UZZNH-I6ki|-qqC-bVd{yAS~^?M{PLSn^#u9N~@bd zWl%QYIl+JY_c`yStL=&cc-;FR#4O1jH8neHs~^JGdzBHbb&o;3$SVbn3TA`{HlICg zKizxDKD)}hcoug_mGuvLM3$B-%3Nm@WN9K72iYn9s8hotg0cqsic4S%S}q{c&CF&? z3iA6tYpJB@udF$Rob~hs$g`rtZxn?{@1uN_sVU4|21WMdb|$?YPm&FF3u0}xdOC-2 ziqreAXFmuj@zU%{6M&&}MPHZglxN&Hy}KEKx=p?=+uV?t;Y~sze^*yNQ{QP~XK*Ro zgzFd-I*ZrLpl){kocCrf=zd|J$!7-!@>)>9G$-u#^zZV&32FMeH|Y6t=w|cj2mV$R zW;{uUw>{IoHZ*)r?G#zj-@ZX34ya>St*l#pqHjNEi}DT{{{3oKd&aU8DYSnhi6gMt z6mlx3MmuofEI}imach)`HSm8hr7)a0NzXO6E51uh%ZJT-{sOeVV-LqJ|7-rq%CvgW zC?ldx$9*&w=oW3g9={rF>m_XdTmY7yQ!|eeWtb9zptKq1m1=okmSc#ng3W~B5LdktS zj8`ZboIMjp!jvJ+?zd`e4^s)v0cGLiZnXA0V<%gIirOpj;|2)fZH(h~{GC4D=vA-&9+Ufs^UItZI`D->%iU>Bqq~N1*F!Df4V%C3*;2R# z^j#NJb6xfsn!Ofzm04|3Ghfm-+}qm?E2*nLp7C;Ms4K9E&SbaloOX|JbY(?=RX3~5 zG(;3B#==BEt1CLju~DnXeRYo99udau(0D|;@!BU zmJv|twbr1b#_8)VH!kov@`}#&%8cIRM(=o7b+~eJt#Mk{Pmi&o&K_y?(kw_SwIZ^F9ufH%Y4FZ@p!R2Us|xz?S`+nev|qBLUe1M(LTqm*(~ryQ+bz{qm)$m z@b|wz|C7M~B=A28{2!D+^cm6rgLnUb<<|d`0)M6cCx!p}r4TJ7CdQLYahS-h(<%bR Q0SM%#q3Jd3-;V$Me-OJ*lK=n! literal 85962 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

mlHOf6!EWLOe7)ea%jF4{Kgm=sRenn{sw0WNa&F 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®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 UgI?yxY4G2wm1}iUwe14_AAr)*ssI20