From 89ea69cc894c95d0b6ce8a9fe8555a57a1893698 Mon Sep 17 00:00:00 2001 From: seji1 <152516748+seji1@users.noreply.github.com> Date: Sun, 1 Sep 2024 22:30:01 +0600 Subject: [PATCH] Settings screen rewrite --- src/main/java/net/vulkanmod/Initializer.java | 2 - .../java/net/vulkanmod/config/Config.java | 58 +-- .../vulkanmod/config/gui/ConfigScreen.java | 269 ++++++++++++++ .../config/gui/ConfigScreenPages.java | 351 ++++++++++++++++++ .../net/vulkanmod/config/gui/GuiElement.java | 167 --------- .../net/vulkanmod/config/gui/GuiRenderer.java | 123 +++--- .../net/vulkanmod/config/gui/OptionBlock.java | 7 - .../net/vulkanmod/config/gui/VOptionList.java | 335 ----------------- .../vulkanmod/config/gui/VOptionScreen.java | 326 ---------------- .../gui/container/AbstractScrollableList.java | 109 ++++++ .../container/AbstractWidgetContainer.java | 128 +++++++ .../config/gui/container/OptionList.java | 136 +++++++ .../config/gui/container/PageList.java | 86 +++++ .../vulkanmod/config/gui/option/Option.java | 158 ++++++++ .../config/gui/option/OptionGroup.java | 76 ++++ .../config/gui/option/OptionPage.java | 89 +++++ .../config/gui/option/control/Controller.java | 11 + .../gui/option/control/CyclingController.java | 171 +++++++++ .../gui/option/control/SliderController.java | 158 ++++++++ .../gui/option/control/SwitchController.java | 122 ++++++ .../vulkanmod/config/gui/util/Binding.java | 22 ++ .../vulkanmod/config/gui/util/Dimension.java | 62 ++++ .../config/gui/util/GuiConstants.java | 18 + .../config/gui/util/SearchFieldTextModel.java | 217 +++++++++++ .../config/gui/widget/AbstractWidget.java | 170 +++++++++ .../config/gui/widget/ButtonWidget.java | 158 ++++++++ .../config/gui/widget/ControllerWidget.java | 93 +++++ .../gui/widget/CyclingOptionWidget.java | 172 --------- .../config/gui/widget/OptionWidget.java | 210 ----------- .../config/gui/widget/RangeOptionWidget.java | 128 ------- .../config/gui/widget/SearchFieldWidget.java | 213 +++++++++++ .../config/gui/widget/SwitchOptionWidget.java | 89 ----- .../config/gui/widget/VAbstractWidget.java | 114 ------ .../config/gui/widget/VButtonWidget.java | 70 ---- .../config/option/CyclingOption.java | 78 ---- .../net/vulkanmod/config/option/Option.java | 110 ------ .../vulkanmod/config/option/OptionPage.java | 44 --- .../net/vulkanmod/config/option/Options.java | 304 --------------- .../vulkanmod/config/option/RangeOption.java | 49 --- .../vulkanmod/config/option/SwitchOption.java | 20 - .../config/video/VideoModeManager.java | 108 ++---- .../vulkanmod/config/video/VideoModeSet.java | 95 ----- .../mixin/screen/OptionsScreenM.java | 4 +- .../vulkanmod/mixin/window/WindowMixin.java | 156 ++++---- .../render/profiling/ProfilerOverlay.java | 2 +- src/main/resources/assets/vulkanmod/Vlogo.png | Bin 31960 -> 0 bytes .../assets/vulkanmod/lang/en_us.json | 50 +-- src/main/resources/assets/vulkanmod/vlogo.png | Bin 0 -> 3867 bytes .../assets/vulkanmod/vlogo_transparent.png | Bin 85962 -> 3842 bytes src/main/resources/fabric.mod.json | 2 +- 50 files changed, 3047 insertions(+), 2593 deletions(-) create mode 100644 src/main/java/net/vulkanmod/config/gui/ConfigScreen.java create mode 100644 src/main/java/net/vulkanmod/config/gui/ConfigScreenPages.java delete mode 100644 src/main/java/net/vulkanmod/config/gui/GuiElement.java delete mode 100644 src/main/java/net/vulkanmod/config/gui/OptionBlock.java delete mode 100644 src/main/java/net/vulkanmod/config/gui/VOptionList.java delete mode 100644 src/main/java/net/vulkanmod/config/gui/VOptionScreen.java create mode 100644 src/main/java/net/vulkanmod/config/gui/container/AbstractScrollableList.java create mode 100644 src/main/java/net/vulkanmod/config/gui/container/AbstractWidgetContainer.java create mode 100644 src/main/java/net/vulkanmod/config/gui/container/OptionList.java create mode 100644 src/main/java/net/vulkanmod/config/gui/container/PageList.java create mode 100644 src/main/java/net/vulkanmod/config/gui/option/Option.java create mode 100644 src/main/java/net/vulkanmod/config/gui/option/OptionGroup.java create mode 100644 src/main/java/net/vulkanmod/config/gui/option/OptionPage.java create mode 100644 src/main/java/net/vulkanmod/config/gui/option/control/Controller.java create mode 100644 src/main/java/net/vulkanmod/config/gui/option/control/CyclingController.java create mode 100644 src/main/java/net/vulkanmod/config/gui/option/control/SliderController.java create mode 100644 src/main/java/net/vulkanmod/config/gui/option/control/SwitchController.java create mode 100644 src/main/java/net/vulkanmod/config/gui/util/Binding.java create mode 100644 src/main/java/net/vulkanmod/config/gui/util/Dimension.java create mode 100644 src/main/java/net/vulkanmod/config/gui/util/GuiConstants.java create mode 100644 src/main/java/net/vulkanmod/config/gui/util/SearchFieldTextModel.java create mode 100644 src/main/java/net/vulkanmod/config/gui/widget/AbstractWidget.java create mode 100644 src/main/java/net/vulkanmod/config/gui/widget/ButtonWidget.java create mode 100644 src/main/java/net/vulkanmod/config/gui/widget/ControllerWidget.java delete mode 100644 src/main/java/net/vulkanmod/config/gui/widget/CyclingOptionWidget.java delete mode 100644 src/main/java/net/vulkanmod/config/gui/widget/OptionWidget.java delete mode 100644 src/main/java/net/vulkanmod/config/gui/widget/RangeOptionWidget.java create mode 100644 src/main/java/net/vulkanmod/config/gui/widget/SearchFieldWidget.java delete mode 100644 src/main/java/net/vulkanmod/config/gui/widget/SwitchOptionWidget.java delete mode 100644 src/main/java/net/vulkanmod/config/gui/widget/VAbstractWidget.java delete mode 100644 src/main/java/net/vulkanmod/config/gui/widget/VButtonWidget.java delete mode 100644 src/main/java/net/vulkanmod/config/option/CyclingOption.java delete mode 100644 src/main/java/net/vulkanmod/config/option/Option.java delete mode 100644 src/main/java/net/vulkanmod/config/option/OptionPage.java delete mode 100644 src/main/java/net/vulkanmod/config/option/Options.java delete mode 100644 src/main/java/net/vulkanmod/config/option/RangeOption.java delete mode 100644 src/main/java/net/vulkanmod/config/option/SwitchOption.java delete mode 100644 src/main/java/net/vulkanmod/config/video/VideoModeSet.java delete mode 100644 src/main/resources/assets/vulkanmod/Vlogo.png create mode 100644 src/main/resources/assets/vulkanmod/vlogo.png diff --git a/src/main/java/net/vulkanmod/Initializer.java b/src/main/java/net/vulkanmod/Initializer.java index ea9360265..1651bf58c 100644 --- a/src/main/java/net/vulkanmod/Initializer.java +++ b/src/main/java/net/vulkanmod/Initializer.java @@ -5,7 +5,6 @@ import net.fabricmc.loader.api.FabricLoader; import net.vulkanmod.config.Config; import net.vulkanmod.config.Platform; -import net.vulkanmod.config.video.VideoModeManager; import net.vulkanmod.render.chunk.build.frapi.VulkanModRenderer; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -30,7 +29,6 @@ public void onInitializeClient() { LOGGER.info("== VulkanMod =="); Platform.init(); - VideoModeManager.init(); var configPath = FabricLoader.getInstance() .getConfigDir() diff --git a/src/main/java/net/vulkanmod/config/Config.java b/src/main/java/net/vulkanmod/config/Config.java index 4110c0815..3a6030126 100644 --- a/src/main/java/net/vulkanmod/config/Config.java +++ b/src/main/java/net/vulkanmod/config/Config.java @@ -2,73 +2,43 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; -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; public class Config { - + private static final Gson GSON = new GsonBuilder() + .setPrettyPrinting() + .create(); + private static Path CONFIG_PATH; public int frameQueueSize = 2; - public VideoModeSet.VideoMode videoMode = VideoModeManager.getFirstAvailable().getVideoMode(); public boolean windowedFullscreen = false; - public int advCulling = 2; public boolean indirectDraw = true; - + public boolean backFaceCulling = true; public boolean uniqueOpaqueLayer = true; public boolean entityCulling = true; public int device = -1; - public int ambientOcclusion = 1; - public boolean backFaceCulling = true; - - public void write() { - - if(!Files.exists(CONFIG_PATH.getParent())) { - try { - Files.createDirectories(CONFIG_PATH); - } catch (IOException e) { - e.printStackTrace(); - } + public static Config load(Path path) { + CONFIG_PATH = path; + try (FileReader fileReader = new FileReader(path.toFile())) { + return GSON.fromJson(fileReader, Config.class); + } catch (IOException exception) { + return null; } + } + public void write() { try { + Files.createDirectories(CONFIG_PATH.getParent()); Files.write(CONFIG_PATH, Collections.singleton(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; - - if (Files.exists(path)) { - try (FileReader fileReader = new FileReader(path.toFile())) { - config = GSON.fromJson(fileReader, Config.class); - } - catch (IOException exception) { - throw new RuntimeException(exception.getMessage()); - } - } - else { - config = null; - } - - return config; - } } diff --git a/src/main/java/net/vulkanmod/config/gui/ConfigScreen.java b/src/main/java/net/vulkanmod/config/gui/ConfigScreen.java new file mode 100644 index 000000000..329210cb6 --- /dev/null +++ b/src/main/java/net/vulkanmod/config/gui/ConfigScreen.java @@ -0,0 +1,269 @@ +package net.vulkanmod.config.gui; + +import net.minecraft.Util; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.gui.screens.options.VideoSettingsScreen; +import net.minecraft.network.chat.Component; +import net.vulkanmod.Initializer; +import net.vulkanmod.config.gui.container.OptionList; +import net.vulkanmod.config.gui.container.PageList; +import net.vulkanmod.config.gui.option.Option; +import net.vulkanmod.config.gui.option.OptionPage; +import net.vulkanmod.config.gui.util.Dimension; +import net.vulkanmod.config.gui.util.GuiConstants; +import net.vulkanmod.config.gui.util.SearchFieldTextModel; +import net.vulkanmod.config.gui.widget.ButtonWidget; +import net.vulkanmod.config.gui.widget.SearchFieldWidget; +import org.jetbrains.annotations.NotNull; +import org.lwjgl.glfw.GLFW; + +import java.util.List; +import java.util.function.Predicate; + +public class ConfigScreen extends Screen { + private final Screen parent; + private final List optionPages; + private final SearchFieldTextModel searchFieldTextModel; + private List currentOptionPages; + private SearchFieldWidget searchField; + private PageList pageList; + private Dimension pageDim, headerDim, footerDim, pageListDim, optionListDim; + private ButtonWidget doneButton, applyButton, undoButton, kofiButton; + + public ConfigScreen(Screen parent) { + super(Component.literal("VulkanMod Settings")); + + this.parent = parent; + this.optionPages = List.of( + ConfigScreenPages.video(), + ConfigScreenPages.graphics(), + ConfigScreenPages.optimizations(), + ConfigScreenPages.other()); + this.currentOptionPages = optionPages; + this.searchFieldTextModel = new SearchFieldTextModel(this::filterSearchResults); + } + + @Override + public void render(GuiGraphics guiGraphics, int mouseX, int mouseY, float delta) { + GuiRenderer.guiGraphics = guiGraphics; + GuiRenderer.pose = guiGraphics.pose(); + + updateControls(); + + super.render(guiGraphics, mouseX, mouseY, delta); + } + + @Override + protected void init() { + // Remember if the search field was previously focused since we'll lose that information after recreating the widget + boolean wasSearchFocused = false; + if (this.searchField != null) + wasSearchFocused = this.searchField.isFocused(); + + this.createDimensions(); + this.createHeader(); + this.createPageList(); + this.createOptionList(); + this.createFooter(); + + if (wasSearchFocused) { + this.setFocused(this.searchField); + } + } + + @Override + public void rebuildWidgets() { + this.clearWidgets(); + this.init(); + } + + private void createDimensions() { + final float aspectRatio = 4f / 3f; + final int minWidth = 550; + + int newWidth = this.width; + if (newWidth > minWidth && (float) this.width / (float) this.height > aspectRatio) { + newWidth = Math.max(minWidth, (int) (this.height * aspectRatio)); + } + + this.pageDim = Dimension.ofInt( + (this.width - newWidth) / 2 + GuiConstants.WIDGET_MARGIN, GuiConstants.WIDGET_MARGIN, + newWidth - GuiConstants.WIDGET_MARGIN * 2, this.height - GuiConstants.WIDGET_MARGIN * 2); + this.headerDim = Dimension.ofInt( + pageDim.x(), pageDim.y(), + pageDim.width(), GuiConstants.WIDGET_HEIGHT); + this.footerDim = Dimension.ofInt( + pageDim.x(), pageDim.yLimit() - GuiConstants.WIDGET_HEIGHT, + pageDim.width(), GuiConstants.WIDGET_HEIGHT); + this.pageListDim = Dimension.ofInt( + pageDim.x(), pageDim.y(), + 80, footerDim.y() - pageDim.y() - GuiConstants.WIDGET_MARGIN); + this.optionListDim = Dimension.ofInt( + pageListDim.xLimit() + GuiConstants.WIDGET_MARGIN, headerDim.yLimit() + GuiConstants.WIDGET_MARGIN, + pageDim.xLimit() - pageListDim.xLimit() - GuiConstants.WIDGET_MARGIN, footerDim.y() - headerDim.yLimit() - GuiConstants.WIDGET_MARGIN * 2); + } + + private void createPageList() { + this.pageList = new PageList(pageListDim, this, currentOptionPages); + this.addRenderableWidget(pageList); + } + + private void createOptionList() { + this.addRenderableWidget(new OptionList(optionListDim, this, pageList.getCurrentPage())); + } + + private void createHeader() { + final int padding = 10; + final int minButtonWidth = 50; + + String kofiKey = "vulkanmod.options.buttons.kofi"; + int kofiButtonWidth = Math.max(this.font.width(Component.translatable(kofiKey)) + 2 * padding, minButtonWidth); + this.kofiButton = new ButtonWidget( + Dimension.ofInt( + this.headerDim.xLimit() - kofiButtonWidth, this.headerDim.y(), + kofiButtonWidth, this.headerDim.height()), + Component.translatable(kofiKey), + this::openDonationPage); + + Dimension searchFieldDim = Dimension.ofInt( + this.pageListDim.xLimit() + GuiConstants.WIDGET_MARGIN, this.headerDim.y(), + this.headerDim.width() - this.pageListDim.width() - kofiButtonWidth - GuiConstants.WIDGET_MARGIN * 2, this.headerDim.height()); + + this.searchField = new SearchFieldWidget(searchFieldDim, searchFieldTextModel); + + this.addRenderableWidget(searchField); + this.addRenderableWidget(kofiButton); + } + + private void createFooter() { + final int padding = 10; + final int minButtonWidth = 50; + + String doneKey = "gui.done"; + String applyKey = "vulkanmod.options.buttons.apply"; + String undoKey = "vulkanmod.options.buttons.undo"; + + int doneButtonWidth = Math.max(this.font.width(Component.translatable(doneKey)) + 2 * padding, minButtonWidth); + int applyButtonWidth = Math.max(this.font.width(Component.translatable(applyKey)) + 2 * padding, minButtonWidth); + int undoButtonWidth = Math.max(this.font.width(Component.translatable(undoKey)) + 2 * padding, minButtonWidth); + + + int x0 = this.footerDim.xLimit() - doneButtonWidth; + this.doneButton = new ButtonWidget( + Dimension.ofInt( + x0, this.footerDim.y(), + doneButtonWidth, this.footerDim.height()), + Component.translatable(doneKey), + this::onClose); + + x0 -= (applyButtonWidth + GuiConstants.WIDGET_MARGIN); + this.applyButton = new ButtonWidget( + Dimension.ofInt( + x0, this.footerDim.y(), + applyButtonWidth, this.footerDim.height()), + Component.translatable(applyKey), + this::applyChanges); + + x0 -= (undoButtonWidth + GuiConstants.WIDGET_MARGIN); + this.undoButton = new ButtonWidget( + Dimension.ofInt( + x0, this.footerDim.y(), + undoButtonWidth, this.footerDim.height()), + Component.translatable(undoKey), + this::undoChanges); + + this.applyButton.setActive(false); + this.undoButton.setVisible(false); + + this.addRenderableWidget(undoButton); + this.addRenderableWidget(applyButton); + this.addRenderableWidget(doneButton); + } + + private void applyChanges() { + pageList.getCurrentPage().options().forEach(Option::apply); + Initializer.CONFIG.write(); + } + + private void undoChanges() { + pageList.getCurrentPage().options().forEach(Option::undo); + } + + private void updateControls() { + boolean hasChanges = this.optionPages.stream() + .flatMap(page -> page.options().stream()) + .anyMatch(Option::isChanged); + + this.applyButton.setActive(hasChanges); + this.undoButton.setVisible(hasChanges); + + if (!this.applyButton.isActive()) this.applyButton.setFocused(false); + if (!this.undoButton.isActive()) this.undoButton.setFocused(false); + } + + private void openDonationPage() { + Util.getPlatform().openUri("https://ko-fi.com/xcollateral"); + } + + private void filterSearchResults(@NotNull String query) { + Predicate queryContains = string -> + string != null && string.toLowerCase() + .replaceAll("\\s+", "") + .contains(query.toLowerCase() + .replaceAll("\\s+", "")); + + Predicate> nameFilter = option -> + queryContains.test(option.name().getString()); + Predicate> tooltipFilter = option -> + queryContains.test(option.tooltip() != null + ? option.tooltip().getString() + : null); + Predicate> valueFilter = option -> + queryContains.test(option.displayedValue().getString()); + + currentOptionPages = optionPages.stream() + .map(page -> page.filtered(nameFilter.or(tooltipFilter).or(valueFilter))) + .filter(page -> !page.options().isEmpty()) + .toList(); + + rebuildWidgets(); + } + + @Override + public void onClose() { + Minecraft.getInstance().setScreen(parent); + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + boolean clicked = super.mouseClicked(mouseX, mouseY, button); + if (clicked && (getFocused() instanceof PageList || // This is done so focus doesn't get "stuck" after clicking pageList buttons + (getFocused() instanceof SearchFieldWidget searchFieldWidget + && !searchFieldWidget.isMouseOver(mouseX, mouseY)))) { + setFocused(null); + } + + return clicked; + } + + @Override + public boolean keyPressed(int keyCode, int scanCode, int modifiers) { + if ((modifiers & (Minecraft.ON_OSX ? GLFW.GLFW_MOD_SUPER : GLFW.GLFW_MOD_CONTROL)) != 0 && keyCode == GLFW.GLFW_KEY_L) { + this.setFocused(searchField); + + return true; + } + + if (!(getFocused() instanceof SearchFieldWidget) + && keyCode == GLFW.GLFW_KEY_P + && (modifiers & GLFW.GLFW_MOD_SHIFT) != 0) { + Minecraft.getInstance().setScreen(new VideoSettingsScreen(this, Minecraft.getInstance(), Minecraft.getInstance().options)); + + return true; + } + + return super.keyPressed(keyCode, scanCode, modifiers); + } +} diff --git a/src/main/java/net/vulkanmod/config/gui/ConfigScreenPages.java b/src/main/java/net/vulkanmod/config/gui/ConfigScreenPages.java new file mode 100644 index 000000000..87ad59393 --- /dev/null +++ b/src/main/java/net/vulkanmod/config/gui/ConfigScreenPages.java @@ -0,0 +1,351 @@ +package net.vulkanmod.config.gui; + +import com.google.common.collect.Lists; +import com.mojang.blaze3d.platform.VideoMode; +import com.mojang.blaze3d.platform.Window; +import net.minecraft.client.*; +import net.minecraft.network.chat.Component; +import net.vulkanmod.Initializer; +import net.vulkanmod.config.Config; +import net.vulkanmod.config.gui.option.Option; +import net.vulkanmod.config.gui.option.OptionGroup; +import net.vulkanmod.config.gui.option.OptionPage; +import net.vulkanmod.config.gui.option.control.CyclingController; +import net.vulkanmod.config.gui.option.control.SliderController; +import net.vulkanmod.config.gui.option.control.SwitchController; +import net.vulkanmod.config.video.VideoModeManager; +import net.vulkanmod.render.chunk.build.light.LightMode; +import net.vulkanmod.render.vertex.TerrainRenderType; +import net.vulkanmod.vulkan.Renderer; +import net.vulkanmod.vulkan.device.DeviceManager; + +import java.util.List; +import java.util.stream.IntStream; + +public class ConfigScreenPages { + private static final Minecraft minecraft = Minecraft.getInstance(); + private static final Window window = minecraft.getWindow(); + private static final Config config = Initializer.CONFIG; + private static final Options minecraftOptions = minecraft.options; + public static boolean fullscreenDirty = false; + + public static OptionPage video() { + return OptionPage.createBuilder() + .name(Component.translatable("vulkanmod.options.pages.video")) + .group(OptionGroup.createBuilder() + .option(Option.createBuilder() + .name(Component.translatable("options.fullscreen.resolution")) + .binding(() -> { + int index; + if (VideoModeManager.getSelectedVideoMode().equals(VideoModeManager.getOsVideoMode())) + index = VideoModeManager.getVideoModes().size(); + else + index = VideoModeManager.getVideoModes().indexOf(VideoModeManager.getSelectedVideoMode()); + + return index != -1 ? index : VideoModeManager.getVideoModes().size() - 1; + }, + value -> { + if (value == VideoModeManager.getVideoModes().size()) + VideoModeManager.setSelectedVideoMode(VideoModeManager.getOsVideoMode()); + else + VideoModeManager.setSelectedVideoMode(VideoModeManager.getVideoModes().get(value)); + + if (minecraftOptions.fullscreen().get()) + fullscreenDirty = true; + }) + .controller(opt -> new SliderController(opt, 0, VideoModeManager.getVideoModes().size(), 1)) + .active(VideoModeManager.getVideoModes().size() != 1) + .translator(value -> { + VideoMode v = null; + if (value != VideoModeManager.getVideoModes().size()) + v = VideoModeManager.getVideoModes().get(value); + return Component.translatable(value == VideoModeManager.getVideoModes().size() + ? "options.fullscreen.current" + : "%sx%s@%s".formatted(v.getWidth(), v.getHeight(), v.getRefreshRate())); + }) + .build()) + .option(Option.createBuilder() + .name(Component.translatable("options.fullscreen")) + .binding(() -> minecraft.options.fullscreen().get(), + value -> { + minecraft.options.fullscreen().set(value); + fullscreenDirty = true; + }) + .controller(SwitchController::new) + .build()) + .option(Option.createBuilder() + .name(Component.translatable("vulkanmod.options.windowedFullscreen")) + .binding(() -> config.windowedFullscreen, + value -> { + config.windowedFullscreen = value; + fullscreenDirty = true; + }) + .controller(SwitchController::new) + .build()) + .option(Option.createBuilder() + .name(Component.translatable("options.framerateLimit")) + .binding(() -> minecraftOptions.framerateLimit().get(), + value -> { + minecraftOptions.framerateLimit().set(value); + window.setFramerateLimit(value); + }) + .controller(opt -> new SliderController(opt, 10, 260, 10)) + .translator(value -> Component.nullToEmpty(value == 260 + ? Component.translatable("options.framerateLimit.max").getString() + : String.valueOf(value))) + .build()) + .option(Option.createBuilder() + .name(Component.translatable("options.vsync")) + .binding(() -> minecraftOptions.enableVsync().get(), + value -> { + minecraftOptions.enableVsync().set(value); + window.updateVsync(value); + }) + .controller(SwitchController::new) + .build()) + .build()) + .group(OptionGroup.createBuilder() + .option(Option.createBuilder() + .name(Component.translatable("options.guiScale")) + .binding(() -> minecraftOptions.guiScale().get(), + value -> { + minecraftOptions.guiScale().set(value); + minecraft.resizeDisplay(); + }) + .controller(opt -> new SliderController(opt, 0, window.calculateScale(0, minecraft.isEnforceUnicode()), 1)) + .translator(value -> Component.translatable((value == 0) + ? "options.guiScale.auto" + : String.valueOf(value))) + .build()) + .option(Option.createBuilder() + .name(Component.translatable("options.gamma")) + .binding(() -> (int) (minecraftOptions.gamma().get() * 100), + value -> minecraftOptions.gamma().set(value * 0.01)) + .controller(opt -> new SliderController(opt, 0, 100, 1)) + .translator(value -> Component.translatable(switch (value) { + case 0 -> "options.gamma.min"; + case 50 -> "options.gamma.default"; + case 100 -> "options.gamma.max"; + default -> String.valueOf(value); + })) + .build()) + .build()) + .group(OptionGroup.createBuilder() + .option(Option.createBuilder() + .name(Component.translatable("options.viewBobbing")) + .binding(() -> minecraftOptions.bobView().get(), + value -> minecraftOptions.bobView().set(value)) + .controller(SwitchController::new) + .build()) + .option(Option.createBuilder() + .name(Component.translatable("options.attackIndicator")) + .binding(() -> minecraftOptions.attackIndicator().get(), + value -> minecraftOptions.attackIndicator().set(value)) + .controller(opt -> new CyclingController<>(opt, List.of(AttackIndicatorStatus.values()))) + .build()) + .option(Option.createBuilder() + .name(Component.translatable("options.autosaveIndicator")) + .binding(() -> minecraftOptions.showAutosaveIndicator().get(), + value -> minecraftOptions.showAutosaveIndicator().set(value)) + .controller(SwitchController::new) + .build()) + .build()) + .build(); + } + + public static OptionPage graphics() { + return OptionPage.createBuilder() + .name(Component.translatable("vulkanmod.options.pages.graphics")) + .group(OptionGroup.createBuilder() + .option(Option.createBuilder() + .name(Component.translatable("options.renderDistance")) + .binding(() -> minecraftOptions.renderDistance().get(), + value -> minecraftOptions.renderDistance().set(value)) + .controller(opt -> new SliderController(opt, 2, 32, 1)) + .build()) + .option(Option.createBuilder() + .name(Component.translatable("options.simulationDistance")) + .binding(() -> minecraftOptions.simulationDistance().get(), + value -> minecraftOptions.simulationDistance().set(value)) + .controller(opt -> new SliderController(opt, 5, 32, 1)) + .build()) + .option(Option.createBuilder() + .name(Component.translatable("options.prioritizeChunkUpdates")) + .binding(() -> minecraftOptions.prioritizeChunkUpdates().get(), + value -> minecraftOptions.prioritizeChunkUpdates().set(value)) + .controller(opt -> new CyclingController<>(opt, List.of(PrioritizeChunkUpdates.values()))) + .build()) + .build()) + .group(OptionGroup.createBuilder() + .option(Option.createBuilder() + .name(Component.translatable("options.graphics")) + .binding(() -> minecraftOptions.graphicsMode().get(), + value -> minecraftOptions.graphicsMode().set(value)) + .controller(opt -> new CyclingController<>(opt, List.of(GraphicsStatus.FAST, GraphicsStatus.FANCY))) + .build()) + .option(Option.createBuilder() + .name(Component.translatable("options.particles")) + .binding(() -> minecraftOptions.particles().get(), + value -> minecraftOptions.particles().set(value)) + .controller(opt -> new CyclingController<>(opt, Lists.reverse(List.of(ParticleStatus.values())))) + .build()) + .option(Option.createBuilder() + .name(Component.translatable("options.renderClouds")) + .binding(() -> minecraftOptions.cloudStatus().get(), + value -> minecraftOptions.cloudStatus().set(value)) + .controller(opt -> new CyclingController<>(opt, List.of(CloudStatus.values()))) + .build()) + .option(Option.createBuilder() + .name(Component.translatable("options.ao")) + .tooltip(Component.translatable("vulkanmod.options.ao.subBlock.tooltip")) + .binding(() -> config.ambientOcclusion, + value -> { + if (value > LightMode.FLAT) + minecraftOptions.ambientOcclusion().set(true); + else + minecraftOptions.ambientOcclusion().set(false); + + config.ambientOcclusion = value; + + minecraft.levelRenderer.allChanged(); + }) + .controller(opt -> new CyclingController<>(opt, List.of(LightMode.FLAT, LightMode.SMOOTH, LightMode.SUB_BLOCK))) + .translator(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"; + })) + .build()) + .option(Option.createBuilder() + .name(Component.translatable("options.biomeBlendRadius")) + .binding(() -> minecraftOptions.biomeBlendRadius().get(), + value -> { + minecraftOptions.biomeBlendRadius().set(value); + minecraft.levelRenderer.allChanged(); + }) + .controller(opt -> new SliderController(opt, 0, 7, 1)) + .translator(value -> { + int v = value * 2 + 1; + return Component.literal("%d x %d".formatted(v, v)); + }) + .build()) + .build()) + .group(OptionGroup.createBuilder() + .option(Option.createBuilder() + .name(Component.translatable("options.entityShadows")) + .binding(() -> minecraftOptions.entityShadows().get(), + value -> minecraftOptions.entityShadows().set(value)) + .controller(SwitchController::new) + .build()) + .option(Option.createBuilder() + .name(Component.translatable("options.entityDistanceScaling")) + .binding(() -> (int) (minecraftOptions.entityDistanceScaling().get() * 100), + value -> minecraftOptions.entityDistanceScaling().set(value / 100.0)) + .controller(opt -> new SliderController(opt, 50, 500, 25)) + .translator(value -> Component.literal("%s%%".formatted(value))) + .build()) + .option(Option.createBuilder() + .name(Component.translatable("options.mipmapLevels")) + .binding(() -> minecraftOptions.mipmapLevels().get(), + value -> { + minecraftOptions.mipmapLevels().set(value); + minecraft.updateMaxMipLevel(value); + minecraft.delayTextureReload(); + }) + .controller(opt -> new SliderController(opt, 0, 4, 1)) + .translator(value -> Component.translatable(value == 0 + ? "options.off" + : String.valueOf(value))) + .build()) + .build()) + .build(); + } + + public static OptionPage optimizations() { + return OptionPage.createBuilder() + .name(Component.translatable("vulkanmod.options.pages.optimizations")) + .group(OptionGroup.createBuilder() + .option(Option.createBuilder() + .name(Component.translatable("vulkanmod.options.advCulling")) + .tooltip(Component.translatable("vulkanmod.options.advCulling.tooltip")) + .binding(() -> config.advCulling, + value -> config.advCulling = value) + .controller(opt -> new CyclingController<>(opt, List.of(1, 2, 3, 10))) + .translator(value -> Component.translatable(switch (value) { + 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"; + })) + .build()) + .option(Option.createBuilder() + .name(Component.translatable("vulkanmod.options.entityCulling")) + .tooltip(Component.translatable("vulkanmod.options.entityCulling.tooltip")) + .binding(() -> config.entityCulling, + value -> config.entityCulling = value) + .controller(SwitchController::new) + .build()) + .option(Option.createBuilder() + .name(Component.translatable("vulkanmod.options.uniqueOpaqueLayer")) + .tooltip(Component.translatable("vulkanmod.options.uniqueOpaqueLayer.tooltip")) + .binding(() -> config.uniqueOpaqueLayer, + value -> { + config.uniqueOpaqueLayer = value; + TerrainRenderType.updateMapping(); + minecraft.levelRenderer.allChanged(); + }) + .controller(SwitchController::new) + .build()) + .option(Option.createBuilder() + .name(Component.translatable("vulkanmod.options.backfaceCulling")) + .tooltip(Component.translatable("vulkanmod.options.backfaceCulling.tooltip")) + .binding(() -> config.backFaceCulling, + value -> { + config.backFaceCulling = value; + Minecraft.getInstance().levelRenderer.allChanged(); + }) + .controller(SwitchController::new) + .build()) + .option(Option.createBuilder() + .name(Component.translatable("vulkanmod.options.indirectDraw")) + .tooltip(Component.translatable("vulkanmod.options.indirectDraw.tooltip")) + .binding(() -> config.indirectDraw, + value -> config.indirectDraw = value) + .controller(SwitchController::new) + .build()) + .build()) + .build(); + } + + public static OptionPage other() { + return OptionPage.createBuilder() + .name(Component.translatable("vulkanmod.options.pages.other")) + .group(OptionGroup.createBuilder() + .option(Option.createBuilder() + .name(Component.translatable("vulkanmod.options.frameQueue")) + .tooltip(Component.translatable("vulkanmod.options.frameQueue.tooltip")) + .binding(() -> config.frameQueueSize, + value -> { + config.frameQueueSize = value; + Renderer.scheduleSwapChainUpdate(); + }) + .controller(opt -> new SliderController(opt, 2, 5, 1)) + .build()) + .option(Option.createBuilder() + .name(Component.translatable("vulkanmod.options.deviceSelector")) + .tooltip(Component.nullToEmpty("%s: %s".formatted( + Component.translatable("vulkanmod.options.deviceSelector.tooltip").getString(), + DeviceManager.device.deviceName))) + .binding(() -> config.device, + value -> config.device = value) + .controller(opt -> new CyclingController<>(opt, IntStream.range(-1, DeviceManager.suitableDevices.size()).boxed().toList())) + .translator(value -> Component.translatable((value == -1) + ? "options.guiScale.auto" + : DeviceManager.suitableDevices.get(value).deviceName)) + .build()) + .build()) + .build(); + } +} diff --git a/src/main/java/net/vulkanmod/config/gui/GuiElement.java b/src/main/java/net/vulkanmod/config/gui/GuiElement.java deleted file mode 100644 index ee4305de1..000000000 --- a/src/main/java/net/vulkanmod/config/gui/GuiElement.java +++ /dev/null @@ -1,167 +0,0 @@ -package net.vulkanmod.config.gui; - -import net.minecraft.Util; -import net.minecraft.client.gui.ComponentPath; -import net.minecraft.client.gui.components.events.GuiEventListener; -import net.minecraft.client.gui.narration.NarratableEntry; -import net.minecraft.client.gui.narration.NarrationElementOutput; -import net.minecraft.client.gui.navigation.FocusNavigationEvent; -import net.minecraft.client.gui.navigation.ScreenRectangle; -import org.jetbrains.annotations.Nullable; - -public abstract class GuiElement implements GuiEventListener, NarratableEntry { - - protected int width; - protected int height; - public int x; - public int y; - - protected boolean hovered; - protected long hoverStartTime; - protected int hoverTime; - protected long hoverStopTime; - - public void setPosition(int x, int y) { - this.x = x; - this.y = y; - } - - public void setPosition(int x, int y, int width, int height) { - this.x = x; - this.y = y; - this.width = width; - this.height = height; - } - - public void resize(int width, int height) { - this.width = width; - this.height = height; - } - - public int getX() { - return x; - } - - public int getY() { - return y; - } - - public int getWidth() { - return width; - } - - public int getHeight() { - return height; - } - - public void updateState(double mX, double mY) { - // Update hover - if (isMouseOver(mX, mY)) { - if (!this.hovered) { - this.hoverStartTime = Util.getMillis(); - } - - this.hovered = true; - this.hoverTime = (int) (Util.getMillis() - this.hoverStartTime); - } else { - if (this.hovered) { - this.hoverStopTime = Util.getMillis(); - } - this.hovered = false; - this.hoverTime = 0; - } - } - - public float getHoverMultiplier(float time) { - if (this.hovered) { - return Math.min(((this.hoverTime) / time), 1.0f); - } - else { - int delta = (int) (Util.getMillis() - this.hoverStopTime); - return Math.max(1.0f - (delta / time), 0.0f); - } - } - - @Override - public void mouseMoved(double d, double e) { - GuiEventListener.super.mouseMoved(d, e); - } - - @Override - public boolean mouseClicked(double d, double e, int i) { - return GuiEventListener.super.mouseClicked(d, e, i); - } - - @Override - public boolean mouseReleased(double d, double e, int i) { - return GuiEventListener.super.mouseReleased(d, e, i); - } - - @Override - public boolean mouseDragged(double d, double e, int i, double f, double g) { - return GuiEventListener.super.mouseDragged(d, e, i, f, g); - } - - @Override - public boolean mouseScrolled(double d, double e, double f, double g) { - return GuiEventListener.super.mouseScrolled(d, e, f, g); - } - - @Override - public boolean keyPressed(int i, int j, int k) { - return GuiEventListener.super.keyPressed(i, j, k); - } - - @Override - public boolean keyReleased(int i, int j, int k) { - return GuiEventListener.super.keyReleased(i, j, k); - } - - @Override - public boolean charTyped(char c, int i) { - return GuiEventListener.super.charTyped(c, i); - } - - @Nullable - @Override - public ComponentPath nextFocusPath(FocusNavigationEvent focusNavigationEvent) { - return GuiEventListener.super.nextFocusPath(focusNavigationEvent); - } - - @Override - public boolean isMouseOver(double mouseX, double mouseY) { - return mouseX >= this.x && mouseY >= this.y - && mouseX <= (this.x + this.width) && mouseY <= (this.y + this.height); - } - - @Nullable - @Override - public ComponentPath getCurrentFocusPath() { - return GuiEventListener.super.getCurrentFocusPath(); - } - - @Override - public ScreenRectangle getRectangle() { - return GuiEventListener.super.getRectangle(); - } - - @Override - public void setFocused(boolean bl) { - - } - - @Override - public boolean isFocused() { - return false; - } - - @Override - public NarrationPriority narrationPriority() { - return NarrationPriority.NONE; - } - - @Override - public void updateNarration(NarrationElementOutput narrationElementOutput) { - - } -} diff --git a/src/main/java/net/vulkanmod/config/gui/GuiRenderer.java b/src/main/java/net/vulkanmod/config/gui/GuiRenderer.java index 7dce13097..0b50dc514 100644 --- a/src/main/java/net/vulkanmod/config/gui/GuiRenderer.java +++ b/src/main/java/net/vulkanmod/config/gui/GuiRenderer.java @@ -1,8 +1,8 @@ package net.vulkanmod.config.gui; -import com.mojang.blaze3d.platform.Window; import com.mojang.blaze3d.systems.RenderSystem; 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; @@ -10,43 +10,18 @@ import net.minecraft.network.chat.Component; import net.minecraft.util.FastColor; import net.minecraft.util.FormattedCharSequence; +import net.minecraft.util.Mth; import org.joml.Matrix4f; -import java.util.List; - public abstract class GuiRenderer { - - public static Minecraft minecraft; - public static Font font; + public static Minecraft minecraft = Minecraft.getInstance(); + public static Font font = minecraft.font; public static GuiGraphics guiGraphics; public static PoseStack pose; public static BufferBuilder bufferBuilder; public static boolean batching = false; - public static void setPoseStack(PoseStack poseStack) { - pose = poseStack; - } - - public static void disableScissor() { - RenderSystem.disableScissor(); - } - - public static void enableScissor(int x, int y, int width, int height) { - Window window = Minecraft.getInstance().getWindow(); - int wHeight = window.getHeight(); - double scale = window.getGuiScale(); - int xScaled = (int) (x * scale); - int yScaled = (int) (wHeight - (y + height) * scale); - int widthScaled = (int) (width * scale); - int heightScaled = (int) (height * scale); - RenderSystem.enableScissor(xScaled, yScaled, Math.max(0, widthScaled), Math.max(0, heightScaled)); - } - - public static void fillBox(float x0, float y0, float width, float height, int color) { - fill(x0, y0, x0 + width, y0 + height, 0, color); - } - public static void fill(float x0, float y0, float x1, float y1, int color) { fill(x0, y0, x1, y1, 0, color); } @@ -103,48 +78,88 @@ public static void fillGradient(float x0, float y0, float x1, float y1, float z, BufferUploader.drawWithShader(bufferBuilder.buildOrThrow()); } - public static void renderBoxBorder(float x0, float y0, float width, float height, float borderWidth, int color) { - renderBorder(x0, y0, x0 + width, y0 + height, borderWidth, color); + public static void renderBorder(float x0, float y0, float x1, float y1, float width, int color) { + renderBorder(x0, y0, x1, y1, 0, width, color); } - public static void renderBorder(float x0, float y0, float x1, float y1, float width, int color) { - GuiRenderer.fill(x0, y0, x1, y0 + width, color); - GuiRenderer.fill(x0, y1 - width, x1, y1, color); + public static void renderBorder(float x0, float y0, float x1, float y1, float z, float width, int color) { + GuiRenderer.fill(x0, y0, x1, y0 + width, z, color); + GuiRenderer.fill(x0, y1 - width, x1, y1, z, color); - GuiRenderer.fill(x0, y0 + width, x0 + width, y1 - width, color); - GuiRenderer.fill(x1 - width, y0 + width, x1, y1 - width, color); + GuiRenderer.fill(x0, y0 + width, x0 + width, y1 - width, z, color); + GuiRenderer.fill(x1 - width, y0 + width, x1, y1 - width, z, color); } - public static void drawString(Font font, Component component, int x, int y, int color) { - drawString(font, component.getVisualOrderText(), x, y, color); + public static void drawString(Component component, int x, int y, int color) { + drawString(component, x, y, color, true); } - public static void drawString(Font font, FormattedCharSequence formattedCharSequence, int x, int y, int color) { - guiGraphics.drawString(font, formattedCharSequence, x, y, color); + public static void drawString(FormattedCharSequence formattedCharSequence, int x, int y, int color) { + drawString(formattedCharSequence, x, y, color, true); } - public static void drawString(Font font, Component component, int x, int y, int color, boolean shadow) { - drawString(font, component.getVisualOrderText(), x, y, color, shadow); + public static void drawString(Component component, int x, int y, int color, boolean shadow) { + drawString(component, x, y, 0, color, shadow); } - public static void drawString(Font font, FormattedCharSequence formattedCharSequence, int x, int y, int color, boolean shadow) { + public static void drawString(FormattedCharSequence formattedCharSequence, int x, int y, int color, boolean shadow) { + drawString(formattedCharSequence, x, y, 0, color, shadow); + } + + public static void drawString(Component component, int x, int y, int z, int color) { + drawString(component, x, y, z, color, true); + } + + public static void drawString(FormattedCharSequence formattedCharSequence, int x, int y, int z, int color) { + drawString(formattedCharSequence, x, y, z, color, true); + } + + public static void drawString(Component component, int x, int y, int z, int color, boolean shadow) { + drawString(component.getVisualOrderText(), x, y, z, color, shadow); + } + + public static void drawString(FormattedCharSequence formattedCharSequence, int x, int y, int z, int color, boolean shadow) { + if (z == 0) { + guiGraphics.drawString(font, formattedCharSequence, x, y, color, shadow); + return; + } + + guiGraphics.pose().pushPose(); + guiGraphics.pose().translate(0, 0, z); + guiGraphics.drawString(font, formattedCharSequence, x, y, color, shadow); + + guiGraphics.pose().popPose(); + } + + public static void drawCenteredString(Component component, int x, int y, int color) { + drawCenteredString(component.getVisualOrderText(), x, y, color); } - public static void drawCenteredString(Font font, Component component, int x, int y, int color) { - FormattedCharSequence formattedCharSequence = component.getVisualOrderText(); - guiGraphics.drawString(font, formattedCharSequence, x - font.width(formattedCharSequence) / 2, y, color); + public static void drawCenteredString(FormattedCharSequence formattedCharSequence, int x, int y, int color) { + drawString(formattedCharSequence, x - font.width(formattedCharSequence) / 2, y, color); } - public static int getMaxTextWidth(Font font, List list) { - int maxWidth = 0; - for (var text : list) { - int width = font.width(text); - if (width > maxWidth) { - maxWidth = width; - } + public static void drawScrollingString(Component component, int x, int y, int maxWidth, int color) { + drawScrollingString(component.getVisualOrderText(), x, y, maxWidth, color); + } + + public static void drawScrollingString(FormattedCharSequence formattedCharSequence, int x, int y, int maxWidth, int color) { + int textWidth = font.width(formattedCharSequence); + if (textWidth <= maxWidth) { + drawCenteredString(formattedCharSequence, 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); + + guiGraphics.enableScissor(x0 - 1, 0, x1, minecraft.getWindow().getScreenHeight()); + drawString(formattedCharSequence, (int) (x0 - horizontalScroll), y, color); + guiGraphics.disableScissor(); } - return maxWidth; } public static void beginBatch(VertexFormat.Mode mode, VertexFormat format) { diff --git a/src/main/java/net/vulkanmod/config/gui/OptionBlock.java b/src/main/java/net/vulkanmod/config/gui/OptionBlock.java deleted file mode 100644 index 6a6b31079..000000000 --- a/src/main/java/net/vulkanmod/config/gui/OptionBlock.java +++ /dev/null @@ -1,7 +0,0 @@ -package net.vulkanmod.config.gui; - -import net.vulkanmod.config.option.Option; - -public record OptionBlock(String title, Option[] options) { - -} diff --git a/src/main/java/net/vulkanmod/config/gui/VOptionList.java b/src/main/java/net/vulkanmod/config/gui/VOptionList.java deleted file mode 100644 index f114dee64..000000000 --- a/src/main/java/net/vulkanmod/config/gui/VOptionList.java +++ /dev/null @@ -1,335 +0,0 @@ -package net.vulkanmod.config.gui; - -import com.mojang.blaze3d.systems.RenderSystem; -import it.unimi.dsi.fastutil.objects.ObjectArrayList; -import net.minecraft.client.gui.components.events.GuiEventListener; -import net.minecraft.client.renderer.GameRenderer; -import net.minecraft.util.Mth; -import net.vulkanmod.config.gui.widget.OptionWidget; -import net.vulkanmod.config.gui.widget.VAbstractWidget; -import net.vulkanmod.config.option.Option; -import net.vulkanmod.vulkan.util.ColorUtil; -import org.jetbrains.annotations.Nullable; - -import java.util.List; - -public class VOptionList extends GuiElement { - private final List children = new ObjectArrayList<>(); - boolean scrolling = false; - float scrollAmount = 0.0f; - int itemWidth; - int totalItemHeight; - int itemHeight; - int itemMargin; - int listLength = 0; - Entry focused; - - public VOptionList(int x, int y, int width, int height, int itemHeight) { - this.setPosition(x, y, width, height); - - this.width = width; - this.height = height; - - this.itemWidth = (int) (0.95f * this.width); - this.itemHeight = itemHeight; - this.itemMargin = 3; - this.totalItemHeight = this.itemHeight + this.itemMargin; - } - - public void addButton(OptionWidget widget) { - this.addEntry(new Entry(widget, this.itemMargin)); - } - - public void addAll(OptionBlock[] blocks) { - for (OptionBlock block : blocks) { - int x0 = this.x; - int width = this.itemWidth; - int height = this.itemHeight; - - 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(null, 12)); - } - } - - 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(); - } - - public void clearEntries() { - this.listLength = 0; - this.children.clear(); - } - - protected void updateScrollingState(double mouseX, int button) { - this.scrolling = button == 0 && mouseX >= (double) this.getScrollbarPosition() && mouseX < (double) (this.getScrollbarPosition() + 6); - } - - protected float getScrollAmount() { - return scrollAmount; - } - - public void setScrollAmount(double d) { - this.scrollAmount = (float) Mth.clamp(d, 0.0, this.getMaxScroll()); - } - - private int getItemCount() { - return this.children.size(); - } - - GuiEventListener getFocused() { - return focused; - } - - void setFocused(Entry focussed) { - this.focused = focussed; - } - - public boolean mouseClicked(double mouseX, double mouseY, int button) { - this.updateScrollingState(mouseX, button); - if (this.isMouseOver(mouseX, mouseY)) { - Entry entry = this.getEntryAtPos(mouseX, mouseY); - if (entry != null && entry.mouseClicked(mouseX, mouseY, button)) { - setFocused(entry); - entry.setFocused(true); - return true; - } - - return button == 0; - } - - return false; - } - - public boolean mouseReleased(double mouseX, double mouseY, int button) { - if (this.isValidClickButton(button)) { - Entry entry = this.getEntryAtPos(mouseX, mouseY); - if (entry != null) { - if (entry.mouseReleased(mouseX, mouseY, button)) { - entry.setFocused(false); - setFocused(null); - return true; - } - } - } - return false; - } - - public boolean mouseDragged(double mouseX, double mouseY, int button, double deltaX, double deltaY) { - if (button != 0) { - return false; - } - - if (this.getFocused() != null) { - return this.getFocused().mouseDragged(mouseX, mouseY, button, deltaX, deltaY); - } - - if (!this.scrolling) { - return false; - } - - double maxScroll = this.getMaxScroll(); - if (mouseY < this.y) { - this.setScrollAmount(0.0); - } else if (mouseY > this.getBottom()) { - this.setScrollAmount(maxScroll); - } else if (maxScroll > 0.0) { - double barHeight = (double) this.height * this.height / this.getTotalLength(); - double scrollFactor = Math.max(1.0, maxScroll / (this.height - barHeight)); - this.setScrollAmount(this.getScrollAmount() + deltaY * scrollFactor); - } - - return true; - } - - public boolean mouseScrolled(double mouseX, double mouseY, double xScroll, double yScroll) { - this.setScrollAmount(this.getScrollAmount() - yScroll * (double) this.totalItemHeight / 2.0); - return true; - } - - public int getMaxScroll() { - return Math.max(0, this.getTotalLength() - (this.height)); - } - - protected int getTotalLength() { - return this.listLength; - } - - public int getBottom() { - return this.y + this.height; - } - - @Nullable - protected VOptionList.Entry getEntryAtPos(double x, double y) { - int x0 = this.x; - - if (x > this.getScrollbarPosition() || x < (double) x0) - return null; - - for (var entry : this.children) { - VAbstractWidget widget = entry.widget; - if (widget != null && y >= widget.y && y <= widget.y + widget.height) { - return entry; - } - } - return null; - } - - @Override - public void updateState(double mX, double mY) { - if (this.focused != null) - return; - - super.updateState(mX, mY); - } - - public void renderWidget(int mouseX, int mouseY) { - RenderSystem.setShaderColor(1.0f, 1.0f, 1.0f, 1.0f); - GuiRenderer.enableScissor(x, y, width, height); - - this.renderList(mouseX, mouseY); - GuiRenderer.disableScissor(); - - // Scroll bar - int maxScroll = this.getMaxScroll(); - if (maxScroll > 0) { - RenderSystem.enableBlend(); - RenderSystem.setShader(GameRenderer::getPositionColorShader); - - int height = this.getHeight(); - int totalLength = this.getTotalLength(); - int barHeight = (int) ((float) (height * height) / totalLength); - barHeight = Mth.clamp(barHeight, 32, height - 8); - - int scrollAmount = (int) this.getScrollAmount(); - int barY = scrollAmount * (height - barHeight) / maxScroll + this.getY(); - barY = Math.max(barY, this.getY()); - - int scrollbarPosition = this.getScrollbarPosition(); - int thickness = 3; - - int backgroundColor = ColorUtil.ARGB.pack(0.8f, 0.8f, 0.8f, 0.2f); - GuiRenderer.fill(scrollbarPosition, this.getY(), scrollbarPosition + thickness, this.getY() + height, backgroundColor); - - int barColor = ColorUtil.ARGB.pack(0.3f, 0.0f, 0.0f, 0.6f); - GuiRenderer.fill(scrollbarPosition, barY, scrollbarPosition + thickness, barY + barHeight, barColor); - } - } - - protected int getScrollbarPosition() { - return this.x + this.itemWidth + 5; - } - - public VAbstractWidget getHoveredWidget(double mouseX, double mouseY) { - if (this.focused != null) - return focused.widget; - - if (!this.isMouseOver(mouseX, mouseY)) - return null; - - for (VOptionList.Entry entry : this.children) { - var widget = entry.widget; - - if (widget == null || !widget.isMouseOver(mouseX, mouseY)) - continue; - return widget; - } - return null; - } - - protected void renderList(int mouseX, int mouseY) { - int itemCount = this.getItemCount(); - - 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); - } - - rowTop += entry.getTotalHeight(); - } - } - - private Entry getEntry(int j) { - return this.children.get(j); - } - - protected boolean isValidClickButton(int i) { - return i == 0; - } - - protected static class Entry implements GuiEventListener { - final VAbstractWidget widget; - final int margin; - - private Entry(OptionWidget widget, int margin) { - this.widget = widget; - this.margin = margin; - } - - public void render(int y, int mouseX, int mouseY, boolean updateState) { - if (widget == null) - return; - - widget.y = y; - - if (updateState) - widget.updateState(mouseX, mouseY); - - widget.render(mouseX, mouseY); - } - - public int getTotalHeight() { - if (widget != null) - return widget.height + margin; - else - return margin; - } - - public boolean mouseClicked(double mouseX, double mouseY, int button) { - return widget.mouseClicked(mouseX, mouseY, button); - } - - public boolean mouseReleased(double mouseX, double mouseY, int button) { - return widget.mouseReleased(mouseX, mouseY, button); - } - - public boolean mouseDragged(double mouseX, double mouseY, int button, double deltaX, double deltaY) { - return widget.mouseDragged(mouseX, mouseY, button, deltaX, deltaY); - } - - @Override - public boolean isFocused() { - return false; - } - - @Override - public void setFocused(boolean bl) { - widget.setFocused(bl); - } - } -} diff --git a/src/main/java/net/vulkanmod/config/gui/VOptionScreen.java b/src/main/java/net/vulkanmod/config/gui/VOptionScreen.java deleted file mode 100644 index 1a6d255b3..000000000 --- a/src/main/java/net/vulkanmod/config/gui/VOptionScreen.java +++ /dev/null @@ -1,326 +0,0 @@ -package net.vulkanmod.config.gui; - -import com.google.common.collect.Lists; -import com.mojang.blaze3d.systems.RenderSystem; -import net.minecraft.Util; -import net.minecraft.client.gui.GuiGraphics; -import net.minecraft.client.gui.components.events.GuiEventListener; -import net.minecraft.client.gui.screens.Screen; -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.gui.widget.VAbstractWidget; -import net.vulkanmod.config.gui.widget.VButtonWidget; -import net.vulkanmod.config.option.OptionPage; -import net.vulkanmod.config.option.Options; -import net.vulkanmod.vulkan.util.ColorUtil; - -import java.util.ArrayList; -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; - - private final List optionPages; - - private int currentListIdx = 0; - - private int tooltipX; - private int tooltipY; - private int tooltipWidth; - - private VButtonWidget supportButton; - - private VButtonWidget doneButton; - private VButtonWidget applyButton; - - private final List pageButtons = Lists.newArrayList(); - private final List buttons = Lists.newArrayList(); - - public VOptionScreen(Component title, Screen parent) { - super(title); - this.parent = 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); - } - - @Override - protected void init() { - this.addPages(); - - 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; - - if (width < 200) { - x = 100; - width = listWidth; - y = this.height - bottom + 10; - } - - this.tooltipX = x; - this.tooltipY = y; - this.tooltipWidth = width; - - buildPage(); - - this.applyButton.active = false; - } - - 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); - } - } - - private void addPageButtons(int x0, int y0, int width, int height, boolean verticalLayout) { - int x = x0; - int y = y0; - 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)); - this.buttons.add(widget); - this.pageButtons.add(widget); - this.addWidget(widget); - - if (verticalLayout) - y += height + 1; - else - x += width + 1; - } - - this.pageButtons.get(this.currentListIdx).setSelected(true); - } - - private void buildPage() { - this.buttons.clear(); - 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(); - this.addWidget(currentList); - - this.addButtons(); - } - - 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; - - this.doneButton = new VButtonWidget( - x0, y0, - buttonWidth, buttonHeight, - CommonComponents.GUI_DONE, - button -> this.minecraft.setScreen(this.parent) - ); - - buttonWidth = minecraft.font.width(Component.translatable("vulkanmod.options.buttons.apply")) + 2 * padding; - x0 -= (buttonWidth + buttonMargin); - this.applyButton = new VButtonWidget( - x0, y0, - buttonWidth, buttonHeight, - Component.translatable("vulkanmod.options.buttons.apply"), - button -> this.applyOptions() - ); - - buttonWidth = minecraft.font.width(Component.translatable("vulkanmod.options.buttons.kofi")) + 10; - x0 = (this.width - buttonWidth - rightMargin); - this.supportButton = new VButtonWidget( - x0, 6, - buttonWidth, buttonHeight, - 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.addWidget(this.applyButton); - this.addWidget(this.doneButton); - this.addWidget(this.supportButton); - } - - public boolean mouseClicked(double mouseX, double mouseY, int button) { - for (GuiEventListener element : this.children()) { - if (element.mouseClicked(mouseX, mouseY, button)) { - this.setFocused(element); - if (button == 0) { - this.setDragging(true); - } - - this.updateState(); - return true; - } - } - - return false; - } - - @Override - public boolean mouseReleased(double mouseX, double mouseY, int button) { - this.setDragging(false); - this.updateState(); - return this.getChildAt(mouseX, mouseY) - .filter(guiEventListener -> guiEventListener.mouseReleased(mouseX, mouseY, button)) - .isPresent(); - } - - @Override - public void onClose() { - this.minecraft.setScreen(this.parent); - } - - @Override - public void renderBackground(GuiGraphics guiGraphics, int i, int j, float f) { - if (this.minecraft.level == null) { - this.renderPanorama(guiGraphics, f); - } - - this.renderBlurredBackground(f); - this.renderMenuBackground(guiGraphics); - - } - - @Override - public void render(GuiGraphics guiGraphics, int mouseX, int mouseY, float delta) { - this.renderBackground(guiGraphics, 0, 0, delta); - - GuiRenderer.guiGraphics = guiGraphics; - GuiRenderer.setPoseStack(guiGraphics.pose()); - - RenderSystem.enableBlend(); - - int size = minecraft.font.lineHeight * 4; - - guiGraphics.blit(ICON, 30, 4, 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); - - List list = getHoveredButtonTooltip(currentList, mouseX, mouseY); - if (list != null) { - this.renderTooltip(list, this.tooltipX, this.tooltipY); - } - } - - public void renderButtons(int mouseX, int mouseY) { - for (VButtonWidget button : buttons) { - button.render(mouseX, mouseY); - } - } - - 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); - -// intensity = 0.4f; -// color = ColorUtil.ARGB.pack(intensity, intensity, intensity, 0.9f); - color = RED; - GuiRenderer.renderBorder(x - padding, y - padding, x + width + padding, y + height + padding, 1, color); - - int yOffset = 0; - for (var text : list) { - GuiRenderer.drawString(this.font, text, x, y + yOffset, 0xffffffff); - yOffset += 10; - } - } - - 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; - - return this.font.split(tooltip, this.tooltipWidth); - } - return null; - } - - private void updateState() { - boolean modified = false; - for (var page : this.optionPages) { - modified |= page.optionChanged(); - } - - this.applyButton.active = modified; - } - - private void setOptionList(int i) { - this.currentListIdx = i; - - this.buildPage(); - - this.pageButtons.get(i).setSelected(true); - } - - private void applyOptions() { - List pages = List.copyOf(this.optionPages); - for (var page : pages) { - page.applyOptionChanges(); - } - - Initializer.CONFIG.write(); - } -} diff --git a/src/main/java/net/vulkanmod/config/gui/container/AbstractScrollableList.java b/src/main/java/net/vulkanmod/config/gui/container/AbstractScrollableList.java new file mode 100644 index 000000000..4ff73926f --- /dev/null +++ b/src/main/java/net/vulkanmod/config/gui/container/AbstractScrollableList.java @@ -0,0 +1,109 @@ +package net.vulkanmod.config.gui.container; + +import com.mojang.blaze3d.systems.RenderSystem; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.navigation.ScreenRectangle; +import net.minecraft.client.renderer.GameRenderer; +import net.minecraft.util.Mth; +import net.vulkanmod.config.gui.ConfigScreen; +import net.vulkanmod.config.gui.GuiRenderer; +import net.vulkanmod.config.gui.util.Dimension; +import net.vulkanmod.config.gui.util.GuiConstants; +import net.vulkanmod.vulkan.util.ColorUtil; +import org.jetbrains.annotations.NotNull; + +public abstract class AbstractScrollableList extends AbstractWidgetContainer { + public final Dimension viewPortDim; + private int scrollAmount = 0; + + public AbstractScrollableList(Dimension dim, ConfigScreen screen) { + super(dim, screen); + this.viewPortDim = dim; + } + + @Override + public void render(GuiGraphics guiGraphics, int mouseX, int mouseY, float delta) { + GuiRenderer.guiGraphics.enableScissor( + viewPortDim.x(), viewPortDim.y(), + viewPortDim.xLimit(), viewPortDim.yLimit()); + + if (getMaxScrollAmount() > 0) { + dim = dim.withWidth(viewPortDim.width() - 5); + renderScrollBar(); + } + + guiGraphics.pose().pushPose(); + guiGraphics.pose().translate(0, -getScrollAmount(), 0); + + super.render(guiGraphics, mouseX, mouseY + getScrollAmount(), delta); + + guiGraphics.pose().popPose(); + + GuiRenderer.guiGraphics.disableScissor(); + } + + private void renderScrollBar() { + RenderSystem.enableBlend(); + RenderSystem.setShader(GameRenderer::getPositionColorShader); + + int backgroundColor = ColorUtil.ARGB.multiplyAlpha(GuiConstants.COLOR_BLACK, 0.45f); + int barColor = ColorUtil.ARGB.multiplyAlpha(GuiConstants.COLOR_RED, 0.6f); + + float barHeight = Mth.clamp((float) (viewPortDim.height() * viewPortDim.height()) / getTotalHeight(), 32, viewPortDim.height() - 8); + float barY = Math.max(this.getScrollAmount() * (viewPortDim.height() - barHeight) / getMaxScrollAmount() + viewPortDim.y(), viewPortDim.y()); + + Dimension scrollBarBoxDim = Dimension.ofFloat( + dim.xLimit() + 2, viewPortDim.y(), + 3, viewPortDim.yLimit()); + Dimension scrollBarDim = Dimension.ofFloat( + scrollBarBoxDim.x(), barY, + scrollBarBoxDim.width(), barHeight); + + GuiRenderer.fill( + scrollBarBoxDim.x(), scrollBarBoxDim.y(), + scrollBarBoxDim.xLimit(), scrollBarBoxDim.yLimit(), + backgroundColor); + GuiRenderer.fill( + scrollBarDim.x(), scrollBarDim.y(), + scrollBarDim.xLimit(), scrollBarDim.yLimit(), + barColor); + } + + public int getScrollAmount() { + return scrollAmount; + } + + public void setScrollAmount(int scrollAmount) { + this.scrollAmount = scrollAmount; + } + + public int getMaxScrollAmount() { + return Math.max(getTotalHeight() - viewPortDim.height(), 0); + } + + public abstract int getTotalHeight(); + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + if (!isMouseOver(mouseX, mouseY)) + return false; + + return super.mouseClicked(mouseX, mouseY + getScrollAmount(), button); + } + + @Override + public boolean mouseScrolled(double mouseX, double mouseY, double xScroll, double yScroll) { + this.setScrollAmount(Mth.clamp((int) (getScrollAmount() - (yScroll * 6)), 0, getMaxScrollAmount())); + return true; + } + + @Override + public boolean isMouseOver(double mouseX, double mouseY) { + return viewPortDim.isPointInside(mouseX, mouseY); + } + + @Override + public @NotNull ScreenRectangle getRectangle() { + return viewPortDim.rectangle(); + } +} diff --git a/src/main/java/net/vulkanmod/config/gui/container/AbstractWidgetContainer.java b/src/main/java/net/vulkanmod/config/gui/container/AbstractWidgetContainer.java new file mode 100644 index 000000000..60fa401d8 --- /dev/null +++ b/src/main/java/net/vulkanmod/config/gui/container/AbstractWidgetContainer.java @@ -0,0 +1,128 @@ +package net.vulkanmod.config.gui.container; + +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.Renderable; +import net.minecraft.client.gui.components.events.ContainerEventHandler; +import net.minecraft.client.gui.components.events.GuiEventListener; +import net.minecraft.client.gui.narration.NarratableEntry; +import net.minecraft.client.gui.narration.NarratedElementType; +import net.minecraft.client.gui.narration.NarrationElementOutput; +import net.minecraft.client.gui.navigation.ScreenRectangle; +import net.minecraft.network.chat.Component; +import net.vulkanmod.config.gui.ConfigScreen; +import net.vulkanmod.config.gui.util.Dimension; +import net.vulkanmod.config.gui.widget.AbstractWidget; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; + +public abstract class AbstractWidgetContainer implements ContainerEventHandler, Renderable, NarratableEntry { + private final ConfigScreen screen; + public Dimension dim; + private List children; + private GuiEventListener focused; + private boolean dragging; + + public AbstractWidgetContainer(Dimension dim, ConfigScreen screen) { + this.dim = dim; + this.screen = screen; + } + + @Override + public void render(GuiGraphics guiGraphics, int mouseX, int mouseY, float delta) { + for (AbstractWidget widget : children()) { + widget.render(guiGraphics, mouseX, mouseY, delta); + } + } + + @Override + public @NotNull List children() { + if (children == null) { + children = new ArrayList<>(); + addChildren(); + } + + return this.children; + } + + protected abstract void addChildren(); + + @Override + public boolean isDragging() { + return this.dragging; + } + + @Override + public void setDragging(boolean dragging) { + this.dragging = dragging; + } + + public AbstractWidget getHovered() { + return this.children.stream() + .filter(AbstractWidget::isHovered) + .findFirst() + .orElse(null); + } + + @Nullable + @Override + public GuiEventListener getFocused() { + return this.focused; + } + + @Override + public void setFocused(@Nullable GuiEventListener focused) { + this.focused = focused; + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + this.children().forEach(widget -> widget.setFocused(false)); + this.setFocused(null); + + for (GuiEventListener guiEventListener : this.children()) { + if (guiEventListener.mouseClicked(mouseX, mouseY, button)) { + this.setFocused(guiEventListener); + if (button == 0) { + this.setDragging(true); + } + + return true; + } + } + + return false; + } + + @Override + public NarratableEntry.@NotNull NarrationPriority narrationPriority() { + if (this.isFocused()) { + return NarratableEntry.NarrationPriority.FOCUSED; + } + + return NarratableEntry.NarrationPriority.NONE; + } + + @Override + public void updateNarration(NarrationElementOutput builder) { + if (this.isFocused()) { + builder.add(NarratedElementType.USAGE, Component.translatable("narration.button.usage.focused")); + } + } + + @Override + public boolean isMouseOver(double mouseX, double mouseY) { + return this.dim.isPointInside(mouseX, mouseY); + } + + @Override + public @NotNull ScreenRectangle getRectangle() { + return dim.rectangle(); + } + + public ConfigScreen getScreen() { + return screen; + } +} diff --git a/src/main/java/net/vulkanmod/config/gui/container/OptionList.java b/src/main/java/net/vulkanmod/config/gui/container/OptionList.java new file mode 100644 index 000000000..37d29d7c9 --- /dev/null +++ b/src/main/java/net/vulkanmod/config/gui/container/OptionList.java @@ -0,0 +1,136 @@ +package net.vulkanmod.config.gui.container; + +import com.mojang.blaze3d.systems.RenderSystem; +import net.minecraft.Util; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.util.FormattedCharSequence; +import net.vulkanmod.config.gui.ConfigScreen; +import net.vulkanmod.config.gui.GuiRenderer; +import net.vulkanmod.config.gui.option.Option; +import net.vulkanmod.config.gui.option.OptionGroup; +import net.vulkanmod.config.gui.option.OptionPage; +import net.vulkanmod.config.gui.option.control.Controller; +import net.vulkanmod.config.gui.util.Dimension; +import net.vulkanmod.config.gui.util.GuiConstants; +import net.vulkanmod.config.gui.widget.ControllerWidget; +import net.vulkanmod.vulkan.util.ColorUtil; + +import java.util.List; + +public class OptionList extends AbstractScrollableList { + private static final int TOOLTIP_WIDTH = 170; + + private final OptionPage page; + + private long lastTooltipTime = 0; + + public OptionList(Dimension dim, ConfigScreen screen, OptionPage page) { + super(dim, screen); + + this.page = page; + } + + @Override + public void render(GuiGraphics guiGraphics, int mouseX, int mouseY, float delta) { + super.render(guiGraphics, mouseX, mouseY, delta); + + ControllerWidget widget = (ControllerWidget) + (getFocused() != null + ? getFocused() + : getHovered()); + if (widget != null && widget.getOption().tooltip() != null) { + if (lastTooltipTime == 0) + lastTooltipTime = Util.getMillis(); + + renderWidgetTooltip(widget); + } else { + lastTooltipTime = 0; + } + } + + private void renderWidgetTooltip(ControllerWidget widget) { + if ((lastTooltipTime + 500) > Util.getMillis() + || widget == null + || widget.getOption().tooltip() == null + || (!widget.isHovered() && !widget.isFocused())) + return; + + GuiRenderer.guiGraphics.enableScissor( + dim.x(), dim.y(), + dim.xLimit(), dim.yLimit()); + + int padding = 3; + + List tooltip = GuiRenderer.font.split( + widget.getOption().tooltip(), + TOOLTIP_WIDTH - padding * 2); + + Dimension tooltipDim = Dimension.ofInt( + widget.dim.xLimit() - TOOLTIP_WIDTH, widget.dim.yLimit() + 3, + TOOLTIP_WIDTH, (tooltip.size() * 12) + padding); + + int backgroundColor1 = ColorUtil.ARGB.multiplyAlpha(GuiConstants.COLOR_BLACK, 0.8f); + int backgroundColor2 = ColorUtil.ARGB.multiplyAlpha(GuiConstants.COLOR_DARK_RED, 0.8f); + int borderColor = ColorUtil.ARGB.multiplyAlpha(GuiConstants.COLOR_RED, 0.8f); + int textColor = GuiConstants.COLOR_WHITE; + + // Ensure the tooltip doesn't go off any edges + if ((tooltipDim.yLimit() - getScrollAmount()) > dim.yLimit()) + tooltipDim = tooltipDim.withY(widget.dim.y() - tooltipDim.height() - 3); + if ((tooltipDim.y() - getScrollAmount()) < 0) + tooltipDim = tooltipDim.withY(widget.dim.yLimit()); + + RenderSystem.enableBlend(); + + GuiRenderer.guiGraphics.pose().pushPose(); + GuiRenderer.guiGraphics.pose().translate(0, -getScrollAmount(), 0); + + GuiRenderer.fillGradient( + tooltipDim.x() + 1, tooltipDim.y() + 1, + tooltipDim.xLimit() - 1, tooltipDim.yLimit() - 1, + 90, backgroundColor1, backgroundColor2); + GuiRenderer.renderBorder( + tooltipDim.x(), tooltipDim.y(), + tooltipDim.xLimit(), tooltipDim.yLimit(), + 90, 1, borderColor); + + for (int i = 0; i < tooltip.size(); i++) { + GuiRenderer.drawString( + tooltip.get(i), + tooltipDim.x() + padding, tooltipDim.y() + padding + (i * 12), + 90, textColor); + } + + GuiRenderer.guiGraphics.pose().popPose(); + GuiRenderer.guiGraphics.disableScissor(); + } + + @Override + public void addChildren() { + if (page == null) return; + + int x = dim.x(); + int topY = dim.y(); + for (OptionGroup group : page.groups()) { + if (group.options().isEmpty()) continue; + for (Option option : group.options()) { + Dimension widgetDim = Dimension.ofInt(x, topY, dim.width(), GuiConstants.WIDGET_HEIGHT); + Controller controller = option.controller(); + ControllerWidget controllerWidget = controller.createWidget(widgetDim); + + children().add(controllerWidget); + + topY += GuiConstants.WIDGET_HEIGHT; + } + + topY += GuiConstants.WIDGET_MARGIN; + } + } + + @Override + public int getTotalHeight() { + int groupsMarginHeight = (page.groups().size() - 1) * GuiConstants.WIDGET_MARGIN; + int widgetsHeight = page.options().size() * GuiConstants.WIDGET_HEIGHT; + return groupsMarginHeight + widgetsHeight; + } +} diff --git a/src/main/java/net/vulkanmod/config/gui/container/PageList.java b/src/main/java/net/vulkanmod/config/gui/container/PageList.java new file mode 100644 index 000000000..0c9949279 --- /dev/null +++ b/src/main/java/net/vulkanmod/config/gui/container/PageList.java @@ -0,0 +1,86 @@ +package net.vulkanmod.config.gui.container; + +import com.mojang.blaze3d.systems.RenderSystem; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.resources.ResourceLocation; +import net.vulkanmod.config.gui.ConfigScreen; +import net.vulkanmod.config.gui.GuiRenderer; +import net.vulkanmod.config.gui.option.OptionPage; +import net.vulkanmod.config.gui.util.Dimension; +import net.vulkanmod.config.gui.util.GuiConstants; +import net.vulkanmod.config.gui.widget.ButtonWidget; +import net.vulkanmod.vulkan.util.ColorUtil; + +import java.util.List; + +public class PageList extends AbstractWidgetContainer { + private static final ResourceLocation ICON = ResourceLocation.fromNamespaceAndPath("vulkanmod", "vlogo_transparent.png"); + private static final int ICON_SIZE = GuiConstants.WIDGET_HEIGHT - 3; + + private static OptionPage currentPage; + + private final List pages; + + public PageList(Dimension dim, ConfigScreen screen, List pages) { + super(dim, screen); + this.pages = pages; + } + + @Override + public void render(GuiGraphics guiGraphics, int mouseX, int mouseY, float delta) { + super.render(guiGraphics, mouseX, mouseY, delta); + + RenderSystem.enableBlend(); + + int backgroundColor = ColorUtil.ARGB.multiplyAlpha(GuiConstants.COLOR_BLACK, 0.45f); + GuiRenderer.fill( + dim.x(), dim.y(), + dim.xLimit(), dim.y() + GuiConstants.WIDGET_HEIGHT + GuiConstants.WIDGET_MARGIN, + backgroundColor); + + GuiRenderer.guiGraphics.blit( + ICON, + dim.centerX() - ICON_SIZE / 2, dim.y() + (((GuiConstants.WIDGET_HEIGHT + GuiConstants.WIDGET_MARGIN) - ICON_SIZE) / 2), + 0, 0, + ICON_SIZE, ICON_SIZE, ICON_SIZE, ICON_SIZE); + } + + @Override + protected void addChildren() { + if (pages == null) return; + + int topY = dim.y() + GuiConstants.WIDGET_HEIGHT + GuiConstants.WIDGET_MARGIN; + for (OptionPage page : pages) { + Dimension buttonDim = Dimension.ofInt( + dim.x(), topY, + dim.width(), GuiConstants.WIDGET_HEIGHT); + + ButtonWidget button = new ButtonWidget( + buttonDim, + page.name(), + () -> setPage(page), + true); + + button.setSelected(page == getCurrentPage()); + + children().add(button); + + topY += GuiConstants.WIDGET_HEIGHT; + } + } + + public OptionPage getCurrentPage() { + if (pages.isEmpty()) + currentPage = OptionPage.getDummy(); + if (currentPage == null || (!pages.contains(currentPage) && !pages.isEmpty())) + currentPage = pages.getFirst(); + + return currentPage; + } + + public void setPage(OptionPage currentPage) { + PageList.currentPage = currentPage; + + getScreen().rebuildWidgets(); + } +} diff --git a/src/main/java/net/vulkanmod/config/gui/option/Option.java b/src/main/java/net/vulkanmod/config/gui/option/Option.java new file mode 100644 index 000000000..4297c20e8 --- /dev/null +++ b/src/main/java/net/vulkanmod/config/gui/option/Option.java @@ -0,0 +1,158 @@ +package net.vulkanmod.config.gui.option; + +import net.minecraft.network.chat.Component; +import net.minecraft.util.OptionEnum; +import net.vulkanmod.config.gui.option.control.Controller; +import net.vulkanmod.config.gui.util.Binding; +import org.apache.commons.lang3.Validate; +import org.jetbrains.annotations.NotNull; + +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; + +public class Option { + private final Component name; + private final Component tooltip; + private final Binding binding; + private final Controller controller; + private final Function translator; + private T value; + private T pendingValue; + private boolean active = true; + + private Option(Component name, + Component tooltip, + Binding binding, + Function, Controller> controllerGetter, + Function translator, + boolean active) { + this.name = name; + this.tooltip = tooltip; + this.binding = binding; + this.controller = controllerGetter.apply(this); + this.translator = translator; + this.active = active; + this.value = this.pendingValue = binding.get(); + } + + public static Option.@NotNull Builder createBuilder() { + return new Builder<>(); + } + + public Component name() { + return name; + } + + public Component tooltip() { + return tooltip; + } + + public Binding binding() { + return binding; + } + + public Controller controller() { + return controller; + } + + public Function translator() { + return translator; + } + + public T value() { + return value; + } + + public Component displayedValue() { + return translator.apply(pendingValue); + } + + public T pendingValue() { + return pendingValue; + } + + public void setPendingValue(T pendingValue) { + this.pendingValue = pendingValue; + } + + public boolean isActive() { + return active; + } + + public boolean isChanged() { + return !this.pendingValue.equals(this.value); + } + + public void apply() { + if (!isChanged()) + return; + + binding.set(this.pendingValue); + this.value = this.pendingValue; + } + + public void undo() { + if (!isChanged()) + return; + + binding.set(this.value); + this.pendingValue = value; + } + + public static class Builder { + private Component name; + private Component tooltip; + private Binding binding; + private Function, Controller> controllerGetter; + private Function translator = value -> value instanceof OptionEnum + ? ((OptionEnum) value).getCaption() + : Component.literal(String.valueOf(value)); + private boolean active = true; + + private Builder() { + } + + public Builder name(@NotNull Component name) { + this.name = name; + return this; + } + + public Builder tooltip(Component tooltip) { + this.tooltip = tooltip; + return this; + } + + public Builder binding(@NotNull Supplier getter, @NotNull Consumer setter) { + Validate.notNull(getter, "`getter` must not be null"); + Validate.notNull(setter, "`setter` must not be null"); + + this.binding = new Binding<>(getter, setter); + return this; + } + + public Builder controller(Function, Controller> controller) { + this.controllerGetter = controller; + return this; + } + + public Builder translator(Function translator) { + this.translator = translator; + return this; + } + + public Builder active(boolean active) { + this.active = active; + return this; + } + + public Option build() { + Validate.notNull(name, "`name` must not be null"); + Validate.notNull(binding, "`binding` must not be null"); + Validate.notNull(controllerGetter, "`controller` must not be null"); + + return new Option<>(name, tooltip, binding, controllerGetter, translator, active); + } + } + +} diff --git a/src/main/java/net/vulkanmod/config/gui/option/OptionGroup.java b/src/main/java/net/vulkanmod/config/gui/option/OptionGroup.java new file mode 100644 index 000000000..bd7b3d679 --- /dev/null +++ b/src/main/java/net/vulkanmod/config/gui/option/OptionGroup.java @@ -0,0 +1,76 @@ +package net.vulkanmod.config.gui.option; + +import com.google.common.collect.ImmutableList; +import net.minecraft.network.chat.Component; +import org.apache.commons.lang3.Validate; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.function.Predicate; + +public class OptionGroup { + private final @NotNull Component name; + private final ImmutableList> options; + + private OptionGroup(@NotNull Component name, ImmutableList> options) { + this.name = name; + this.options = options; + } + + public static OptionGroup.@NotNull Builder createBuilder() { + return new Builder(); + } + + public @NotNull Component name() { + return name; + } + + public @NotNull ImmutableList> options() { + return options; + } + + public OptionGroup filtered(Predicate> filter) { + ImmutableList> filteredOptions = options().stream() + .filter(filter) + .collect(ImmutableList.toImmutableList()); + + return new OptionGroup(name, filteredOptions); + } + + public static class Builder { + private final List> options = new ArrayList<>(); + private Component name = Component.empty(); + + private Builder() { + } + + public Builder name(@NotNull Component name) { + Validate.notNull(name, "`name` must not be null"); + + this.name = name; + return this; + } + + public Builder option(@NotNull Option option) { + Validate.notNull(option, "`option` must not be null"); + + this.options.add(option); + return this; + } + + public Builder options(@NotNull Collection> options) { + Validate.notEmpty(options, "`options` must not be empty"); + + this.options.addAll(options); + return this; + } + + public OptionGroup build() { + Validate.notEmpty(options, "`options` must not be empty to build `OptionGroup`"); + + return new OptionGroup(name, ImmutableList.copyOf(options)); + } + } +} diff --git a/src/main/java/net/vulkanmod/config/gui/option/OptionPage.java b/src/main/java/net/vulkanmod/config/gui/option/OptionPage.java new file mode 100644 index 000000000..004176757 --- /dev/null +++ b/src/main/java/net/vulkanmod/config/gui/option/OptionPage.java @@ -0,0 +1,89 @@ +package net.vulkanmod.config.gui.option; + +import com.google.common.collect.ImmutableList; +import net.minecraft.network.chat.Component; +import org.apache.commons.lang3.Validate; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.function.Predicate; + +public class OptionPage { + private final Component name; + private final ImmutableList groups; + + private OptionPage(Component name, ImmutableList groups) { + this.name = name; + this.groups = groups; + } + + public static OptionPage.@NotNull Builder createBuilder() { + return new Builder(); + } + + public static @NotNull OptionPage getDummy() { + return new OptionPage(Component.empty(), ImmutableList.of()); + } + + public @NotNull Component name() { + return name; + } + + public @NotNull ImmutableList groups() { + return groups; + } + + public @NotNull ImmutableList> options() { + return groups.stream() + .flatMap(group -> group.options().stream()) + .collect(ImmutableList.toImmutableList()); + } + + public OptionPage filtered(@NotNull Predicate> filter) { + Validate.notNull(filter, "`filter` must not be null"); + + ImmutableList filteredGroups = groups.stream() + .map(group -> group.filtered(filter)) + .collect(ImmutableList.toImmutableList()); + + return new OptionPage(name, filteredGroups); + } + + public static class Builder { + private final List groups = new ArrayList<>(); + private Component name; + + private Builder() { + } + + public Builder name(@NotNull Component name) { + Validate.notNull(name, "`name` cannot be null"); + + this.name = name; + return this; + } + + public Builder group(@NotNull OptionGroup group) { + Validate.notNull(group, "`group` must not be null"); + + this.groups.add(group); + return this; + } + + public Builder groups(@NotNull Collection groups) { + Validate.notEmpty(groups, "`groups` must not be empty"); + + this.groups.addAll(groups); + return this; + } + + public OptionPage build() { + Validate.notNull(name, "`name` must not be null to build `OptionPage`"); + Validate.notEmpty(groups, "at least one group must be added to build `OptionPage`"); + + return new OptionPage(name, ImmutableList.copyOf(groups)); + } + } +} diff --git a/src/main/java/net/vulkanmod/config/gui/option/control/Controller.java b/src/main/java/net/vulkanmod/config/gui/option/control/Controller.java new file mode 100644 index 000000000..85f85ec09 --- /dev/null +++ b/src/main/java/net/vulkanmod/config/gui/option/control/Controller.java @@ -0,0 +1,11 @@ +package net.vulkanmod.config.gui.option.control; + +import net.vulkanmod.config.gui.option.Option; +import net.vulkanmod.config.gui.util.Dimension; +import net.vulkanmod.config.gui.widget.ControllerWidget; + +public interface Controller { + Option option(); + + ControllerWidget createWidget(Dimension dim); +} diff --git a/src/main/java/net/vulkanmod/config/gui/option/control/CyclingController.java b/src/main/java/net/vulkanmod/config/gui/option/control/CyclingController.java new file mode 100644 index 000000000..ab61a8fc4 --- /dev/null +++ b/src/main/java/net/vulkanmod/config/gui/option/control/CyclingController.java @@ -0,0 +1,171 @@ +package net.vulkanmod.config.gui.option.control; + +import com.mojang.blaze3d.platform.InputConstants; +import com.mojang.blaze3d.systems.RenderSystem; +import com.mojang.blaze3d.vertex.*; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.renderer.GameRenderer; +import net.vulkanmod.config.gui.GuiRenderer; +import net.vulkanmod.config.gui.option.Option; +import net.vulkanmod.config.gui.util.Dimension; +import net.vulkanmod.config.gui.util.GuiConstants; +import net.vulkanmod.config.gui.widget.ControllerWidget; +import net.vulkanmod.vulkan.util.ColorUtil; +import org.joml.Matrix4f; + +import java.util.List; + +public record CyclingController(Option option, List values) implements Controller { + @Override + public ControllerWidget createWidget(Dimension dim) { + return new CyclingControllerWidget(dim); + } + + private class CyclingControllerWidget extends ControllerWidget { + private final Dimension barsDim; + private final Dimension leftArrowDim; + private final Dimension rightArrowDim; + + public CyclingControllerWidget(Dimension dim) { + super(CyclingController.this.option, dim); + + this.barsDim = Dimension.ofFloat( + controllerDim.x() + 30, dim.yLimit() - 5, + controllerDim.width() - 60, 1.5f); + + this.leftArrowDim = Dimension.ofFloat( + controllerDim.x() + 8, dim.centerY() - 4, + 7, 9); + this.rightArrowDim = Dimension.ofFloat( + controllerDim.xLimit() - 18, dim.centerY() - 4, + 7, 9); + } + + @Override + public void render(GuiGraphics guiGraphics, int mouseX, int mouseY, float delta) { + super.render(guiGraphics, mouseX, mouseY, delta); + + renderBars(); + renderArrow(leftArrowDim, mouseX, mouseY); + renderArrow(rightArrowDim, mouseX, mouseY); + + GuiRenderer.drawScrollingString( + getDisplayedValue(), + controllerDim.centerX(), (int) (dim.centerY() - 4.5f), + (int) (rightArrowDim.x() - leftArrowDim.xLimit() - 12), + optionStateColor); + } + + private void renderBars() { + int pendingValueId = values.indexOf(option.pendingValue()); + + int padding = 4; + int totalPadding = padding * values.size(); + float availableWidth = barsDim.width() - totalPadding; + float barWidth = availableWidth / (float) values.size(); + + if (barWidth < 1) + return; + + RenderSystem.enableBlend(); + + for (int i = 0; i < values.size(); i++) { + float x = barsDim.x() + i * (barWidth + padding); + int barColor = (i == pendingValueId) + ? GuiConstants.COLOR_WHITE + : ColorUtil.ARGB.multiplyAlpha(GuiConstants.COLOR_WHITE, 0.4f); + + GuiRenderer.fill( + x, barsDim.y(), + x + barWidth, barsDim.yLimit(), + barColor); + } + } + + + private void renderArrow(Dimension arrowDim, double mouseX, double mouseY) { + Tesselator tesselator = Tesselator.getInstance(); + BufferBuilder bufferBuilder = tesselator.begin(VertexFormat.Mode.TRIANGLE_STRIP, DefaultVertexFormat.POSITION); + + RenderSystem.setShaderColor(1.0f, 1.0f, 1.0f, 1.0f); + + Matrix4f matrix4f = GuiRenderer.pose.last().pose(); + + RenderSystem.setShader(GameRenderer::getPositionShader); + RenderSystem.enableBlend(); + + int valueIndex = values.indexOf(option.pendingValue()); + + if (arrowDim.isPointInside(mouseX, mouseY) && this.isActive()) + RenderSystem.setShaderColor(1.0f, 1.0f, 1.0f, 1.0f); + else if (this.isActive()) + RenderSystem.setShaderColor(1.0f, 1.0f, 1.0f, 0.8f); + + if (arrowDim == leftArrowDim) { + if (valueIndex == 0 || !this.isActive()) + RenderSystem.setShaderColor(0.3f, 0.3f, 0.3f, 0.8f); + + bufferBuilder.addVertex(matrix4f, arrowDim.x(), arrowDim.centerY(), 0); + bufferBuilder.addVertex(matrix4f, arrowDim.xLimit(), arrowDim.yLimit(), 0); + bufferBuilder.addVertex(matrix4f, arrowDim.xLimit(), arrowDim.y(), 0); + } else if (arrowDim == rightArrowDim) { + if (valueIndex == values.size() - 1 || !this.isActive()) + RenderSystem.setShaderColor(0.3f, 0.3f, 0.3f, 0.8f); + + bufferBuilder.addVertex(matrix4f, arrowDim.x(), arrowDim.y(), 0); + bufferBuilder.addVertex(matrix4f, arrowDim.x(), arrowDim.yLimit(), 0); + bufferBuilder.addVertex(matrix4f, arrowDim.xLimit(), arrowDim.centerY(), 0); + } + + BufferUploader.drawWithShader(bufferBuilder.buildOrThrow()); + + RenderSystem.setShaderColor(1.0f, 1.0f, 1.0f, 1.0f); + RenderSystem.setShader(GameRenderer::getPositionTexShader); + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + if (!this.isActive()) + return false; + + if (rightArrowDim.isPointInside(mouseX, mouseY)) { + nextValue(1); + playDownSound(); + return true; + } else if (leftArrowDim.isPointInside(mouseX, mouseY)) { + nextValue(-1); + playDownSound(); + return true; + } + + return false; + } + + @Override + public boolean keyPressed(int keyCode, int scanCode, int modifiers) { + if (!this.isFocused() || !this.isActive()) + return false; + + switch (keyCode) { + case InputConstants.KEY_LEFT -> nextValue(-1); + case InputConstants.KEY_RIGHT -> nextValue(1); + case InputConstants.KEY_RETURN, InputConstants.KEY_SPACE, InputConstants.KEY_NUMPADENTER -> + nextValue(Screen.hasControlDown() || Screen.hasShiftDown() ? -1 : 1); + default -> { + return false; + } + } + + return true; + } + + private void nextValue(int direction) { + var nextIndex = values.indexOf(option.pendingValue()) + direction; + + if (nextIndex >= 0 && nextIndex < values.size()) { + option.setPendingValue(values.get(nextIndex)); + } + } + } +} diff --git a/src/main/java/net/vulkanmod/config/gui/option/control/SliderController.java b/src/main/java/net/vulkanmod/config/gui/option/control/SliderController.java new file mode 100644 index 000000000..795ee03d0 --- /dev/null +++ b/src/main/java/net/vulkanmod/config/gui/option/control/SliderController.java @@ -0,0 +1,158 @@ +package net.vulkanmod.config.gui.option.control; + +import com.mojang.blaze3d.platform.InputConstants; +import com.mojang.blaze3d.systems.RenderSystem; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.util.Mth; +import net.vulkanmod.config.gui.GuiRenderer; +import net.vulkanmod.config.gui.option.Option; +import net.vulkanmod.config.gui.util.Dimension; +import net.vulkanmod.config.gui.util.GuiConstants; +import net.vulkanmod.config.gui.widget.ControllerWidget; +import net.vulkanmod.vulkan.util.ColorUtil; +import org.apache.commons.lang3.Validate; + +public record SliderController(Option option, int min, int max, int step) implements Controller { + public SliderController { + Validate.isTrue(max >= min, "`max` cannot be smaller than `min`"); + Validate.isTrue(step > 0, "`step` must be more than 0"); + } + + @Override + public ControllerWidget createWidget(Dimension dim) { + return new SliderControllerWidget(dim); + } + + private class SliderControllerWidget extends ControllerWidget { + private final Dimension sliderDim; + private final Dimension sliderHoveredDim; + + private boolean mouseDown = false; + + public SliderControllerWidget(Dimension dim) { + super(SliderController.this.option, dim); + + this.sliderDim = Dimension.ofFloat( + controllerDim.x(), dim.yLimit() - 5, + controllerDim.width(), 1.5f + ); + + this.sliderHoveredDim = Dimension.ofFloat( + controllerDim.x(), dim.y() + (dim.height() * 0.5f) - 1, + controllerDim.width(), 2 + ); + } + + @Override + public void render(GuiGraphics guiGraphics, int mouseX, int mouseY, float delta) { + super.render(guiGraphics, mouseX, mouseY, delta); + + RenderSystem.enableBlend(); + + float ratio = Mth.clamp((float) (option.pendingValue() - min) / (max - min), 0, 1); + int valueX = (int) (sliderDim.x() + ratio * controllerDim.width()); + + if (controllerDim.isPointInside(mouseX, mouseY) || this.isFocused()) { + int thumbWidth = 2; + int thumbHeight = 4; + + GuiRenderer.fill( + sliderHoveredDim.x(), sliderHoveredDim.y(), + sliderHoveredDim.xLimit(), sliderHoveredDim.yLimit(), + ColorUtil.ARGB.multiplyAlpha(GuiConstants.COLOR_WHITE, 0.1f)); + GuiRenderer.fill( + sliderHoveredDim.x(), sliderHoveredDim.y(), + valueX - thumbWidth, sliderHoveredDim.yLimit(), + ColorUtil.ARGB.multiplyAlpha(GuiConstants.COLOR_WHITE, 0.3f)); + + GuiRenderer.renderBorder( + valueX - thumbWidth, sliderHoveredDim.y() - thumbHeight, + valueX + thumbWidth, sliderHoveredDim.yLimit() + thumbHeight, + 1, ColorUtil.ARGB.multiplyAlpha(GuiConstants.COLOR_WHITE, 0.3f)); + } else { + GuiRenderer.fill( + sliderDim.x(), sliderDim.y(), + sliderDim.xLimit(), sliderDim.yLimit(), + ColorUtil.ARGB.multiplyAlpha(GuiConstants.COLOR_WHITE, 0.3f)); + GuiRenderer.fill( + sliderDim.x(), sliderDim.y(), + valueX, sliderDim.yLimit(), + optionStateColor); + } + + GuiRenderer.drawString( + getDisplayedValue(), + controllerDim.centerX() - (GuiRenderer.font.width(getDisplayedValue()) / 2), dim.centerY() - 4, + optionStateColor); + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + if (!this.isActive() || button != 0 || !controllerDim.isPointInside(mouseX, mouseY)) + return false; + + mouseDown = true; + + setValueFromMouse(mouseX); + return true; + } + + @Override + public boolean mouseDragged(double mouseX, double mouseY, int button, double deltaX, double deltaY) { + if (!this.isActive() || button != 0 || !mouseDown) + return false; + + setValueFromMouse(mouseX); + return true; + } + + @Override + public boolean mouseScrolled(double mouseX, double mouseY, double horizontal, double vertical) { + if (!this.isActive() || (!controllerDim.isPointInside(mouseX, mouseY)) || (!Screen.hasShiftDown() && !Screen.hasControlDown())) + return false; + + incrementValue(vertical); + return true; + } + + @Override + public boolean mouseReleased(double mouseX, double mouseY, int button) { + if (this.isActive() && mouseDown) + playDownSound(); + mouseDown = false; + + return super.mouseReleased(mouseX, mouseY, button); + } + + @Override + public boolean keyPressed(int keyCode, int scanCode, int modifiers) { + if (!this.isFocused()) + return false; + + switch (keyCode) { + case InputConstants.KEY_LEFT -> incrementValue(-1); + case InputConstants.KEY_RIGHT -> incrementValue(1); + default -> { + return false; + } + } + + return true; + } + + protected void setValueFromMouse(double mouseX) { + double value = (mouseX - sliderHoveredDim.x()) / sliderHoveredDim.width() * (max - min); + option.setPendingValue((int) roundToStepSize(value)); + } + + protected double roundToStepSize(double value) { + return Mth.clamp(min + (step * Math.round(value / step)), min, max); + } + + public void incrementValue(double amount) { + option.setPendingValue((int) Mth.clamp(option.pendingValue() + step * amount, min, max)); + } + + } +} diff --git a/src/main/java/net/vulkanmod/config/gui/option/control/SwitchController.java b/src/main/java/net/vulkanmod/config/gui/option/control/SwitchController.java new file mode 100644 index 000000000..7128a09b0 --- /dev/null +++ b/src/main/java/net/vulkanmod/config/gui/option/control/SwitchController.java @@ -0,0 +1,122 @@ +package net.vulkanmod.config.gui.option.control; + +import com.mojang.blaze3d.platform.InputConstants; +import com.mojang.blaze3d.systems.RenderSystem; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.network.chat.Component; +import net.vulkanmod.config.gui.GuiRenderer; +import net.vulkanmod.config.gui.option.Option; +import net.vulkanmod.config.gui.util.Dimension; +import net.vulkanmod.config.gui.util.GuiConstants; +import net.vulkanmod.config.gui.widget.ControllerWidget; +import net.vulkanmod.vulkan.util.ColorUtil; + +public record SwitchController(Option option) implements Controller { + @Override + public ControllerWidget createWidget(Dimension dim) { + return new SwitchControllerWidget(dim); + } + + private class SwitchControllerWidget extends ControllerWidget { + private final Dimension switchDim; + private final Dimension switchInnerDim; + + public SwitchControllerWidget(Dimension dim) { + super(SwitchController.this.option, dim); + + int switchBoxWidth = 24; + int switchBoxHeight = dim.height() - 8; + this.switchDim = Dimension.ofInt( + controllerDim.centerX() - (switchBoxWidth / 2), dim.y() + 4, + switchBoxWidth, switchBoxHeight + ); + + this.switchInnerDim = Dimension.ofInt( + switchDim.x() + 2, switchDim.y() + 2, + switchDim.width() - 4, switchDim.height() - 4 + ); + } + + @Override + public void render(GuiGraphics guiGraphics, int mouseX, int mouseY, float delta) { + super.render(guiGraphics, mouseX, mouseY, delta); + + int textXPosition = controllerDim.centerX() - (switchDim.width() * 3 / 4) - 4 - getOnOffMargin(); + + int offStateColor = ColorUtil.ARGB.multiplyAlpha(GuiConstants.COLOR_WHITE, 0.4f); + int onStateBackgroundColor = GuiConstants.COLOR_DARK_GRAY; + int borderColor = GuiConstants.COLOR_GRAY; + + RenderSystem.enableBlend(); + + if (option.pendingValue()) { + GuiRenderer.fill( + switchInnerDim.x(), switchInnerDim.y(), + switchInnerDim.centerX() + 1, switchInnerDim.yLimit(), + onStateBackgroundColor); + GuiRenderer.fill( + switchInnerDim.centerX() + 2, switchInnerDim.y(), + switchInnerDim.xLimit(), switchInnerDim.yLimit(), + optionStateColor); + } else { + GuiRenderer.fill( + switchInnerDim.x(), switchInnerDim.y(), + switchInnerDim.centerX() - 2, switchInnerDim.yLimit(), + offStateColor); + } + + GuiRenderer.renderBorder( + switchDim.x(), switchDim.y(), + switchDim.xLimit(), switchDim.yLimit(), + 1, borderColor); + GuiRenderer.drawCenteredString( + getDisplayedValue(), + textXPosition, dim.centerY() - 4, + optionStateColor); + } + + private int getOnOffMargin() { + return Math.max( + GuiRenderer.font.width(Component.translatable("options.on")) / 3, + GuiRenderer.font.width(Component.translatable("options.off")) / 3 + ); + } + + public void toggleSetting() { + option.setPendingValue(!option.pendingValue()); + updateDisplayedValue(); + playDownSound(); + } + + @Override + protected void updateDisplayedValue() { + setDisplayedValue(option.pendingValue() + ? Component.translatable("options.on") + : Component.translatable("options.off")); + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + if (!switchDim.isPointInside(mouseX, mouseY) || !this.isActive() || button != 0) { + return false; + } + + toggleSetting(); + return true; + } + + @Override + public boolean keyPressed(int keyCode, int scanCode, int modifiers) { + if (!this.isFocused()) { + return false; + } + + if (keyCode == InputConstants.KEY_RETURN || keyCode == InputConstants.KEY_SPACE || keyCode == InputConstants.KEY_NUMPADENTER) { + toggleSetting(); + return true; + } + + return false; + } + } +} diff --git a/src/main/java/net/vulkanmod/config/gui/util/Binding.java b/src/main/java/net/vulkanmod/config/gui/util/Binding.java new file mode 100644 index 000000000..1f2ff8740 --- /dev/null +++ b/src/main/java/net/vulkanmod/config/gui/util/Binding.java @@ -0,0 +1,22 @@ +package net.vulkanmod.config.gui.util; + +import java.util.function.Consumer; +import java.util.function.Supplier; + +public class Binding { + private final Supplier getter; + private final Consumer setter; + + public Binding(Supplier getter, Consumer setter) { + this.getter = getter; + this.setter = setter; + } + + public T get() { + return getter.get(); + } + + public void set(T value) { + setter.accept(value); + } +} diff --git a/src/main/java/net/vulkanmod/config/gui/util/Dimension.java b/src/main/java/net/vulkanmod/config/gui/util/Dimension.java new file mode 100644 index 000000000..6228f8f6d --- /dev/null +++ b/src/main/java/net/vulkanmod/config/gui/util/Dimension.java @@ -0,0 +1,62 @@ +package net.vulkanmod.config.gui.util; + +import net.minecraft.client.gui.navigation.ScreenRectangle; + +import java.util.function.BiFunction; + +public record Dimension(T x, T y, T width, T height, BiFunction adder, + BiFunction divider) { + public Dimension { + if (adder == null || divider == null) { + throw new IllegalArgumentException("Adder and divider functions must not be null."); + } + } + + public static Dimension ofInt(int x, int y, int width, int height) { + return new Dimension<>(x, y, width, height, Integer::sum, (a, b) -> a / b); + } + + public static Dimension ofFloat(float x, float y, float width, float height) { + return new Dimension<>(x, y, width, height, Float::sum, (a, b) -> a / b); + } + + public T xLimit() { + return adder.apply(x, width); + } + + public T yLimit() { + return adder.apply(y, height); + } + + public T centerX() { + return divider.apply(adder.apply(x, xLimit()), 2); + } + + public T centerY() { + return divider.apply(adder.apply(y, yLimit()), 2); + } + + public Dimension withX(T newX) { + return new Dimension<>(newX, y, width, height, adder, divider); + } + + public Dimension withY(T newY) { + return new Dimension<>(x, newY, width, height, adder, divider); + } + + public Dimension withWidth(T newWidth) { + return new Dimension<>(x, y, newWidth, height, adder, divider); + } + + public Dimension withHeight(T newHeight) { + return new Dimension<>(x, y, width, newHeight, adder, divider); + } + + public ScreenRectangle rectangle() { + return new ScreenRectangle(x.intValue(), y.intValue(), width.intValue(), height.intValue()); + } + + public boolean isPointInside(double px, double py) { + return px >= x.doubleValue() && px < xLimit().doubleValue() && py >= y.doubleValue() && py < yLimit().doubleValue(); + } +} diff --git a/src/main/java/net/vulkanmod/config/gui/util/GuiConstants.java b/src/main/java/net/vulkanmod/config/gui/util/GuiConstants.java new file mode 100644 index 000000000..3f4eb871a --- /dev/null +++ b/src/main/java/net/vulkanmod/config/gui/util/GuiConstants.java @@ -0,0 +1,18 @@ +package net.vulkanmod.config.gui.util; + +import net.vulkanmod.vulkan.util.ColorUtil; + +public class GuiConstants { + 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_DARK_GRAY = ColorUtil.ARGB.pack(0.4f, 0.4f, 0.4f, 1f); + public static final int COLOR_RED = ColorUtil.ARGB.pack(0.59f, 0.18f, 0.17f, 1f); + public static final int COLOR_DARK_RED = ColorUtil.ARGB.pack(0.15f, 0.05f, 0.04f, 1f); + + public static final int WIDGET_HEIGHT = 20; + public static final int WIDGET_MARGIN = 5; + + private GuiConstants() { + } +} diff --git a/src/main/java/net/vulkanmod/config/gui/util/SearchFieldTextModel.java b/src/main/java/net/vulkanmod/config/gui/util/SearchFieldTextModel.java new file mode 100644 index 000000000..f2df7dd49 --- /dev/null +++ b/src/main/java/net/vulkanmod/config/gui/util/SearchFieldTextModel.java @@ -0,0 +1,217 @@ +package net.vulkanmod.config.gui.util; + +import net.minecraft.Util; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.util.Mth; +import net.minecraft.util.StringUtil; +import net.vulkanmod.config.gui.GuiRenderer; + +import java.util.function.Consumer; + +public class SearchFieldTextModel { + private final StringBuilder query = new StringBuilder(); + private final Consumer updateConsumer; + private final int innerWidth = 200; + private boolean selecting; + private int firstCharacterIndex; + private int selectionStart; + private int selectionEnd; + private int lastCursorPosition = this.getCursor(); + + public SearchFieldTextModel(Consumer updateConsumer) { + this.updateConsumer = updateConsumer; + } + + public String getQuery() { + return query.toString(); + } + + public String getSelectedText() { + int start = Math.min(selectionStart, selectionEnd); + int end = Math.max(selectionStart, selectionEnd); + return query.substring(start, end); + } + + public void write(String text) { + int start = Math.min(selectionStart, selectionEnd); + int end = Math.max(selectionStart, selectionEnd); + + int currentWidth = GuiRenderer.font.width(query.toString()); + int remainingWidth = innerWidth - currentWidth + GuiRenderer.font.width(query.substring(start, end)); + + String filteredText = StringUtil.filterText(text); + int textWidth = GuiRenderer.font.width(filteredText); + + if (textWidth > remainingWidth) { + int cutoffIndex = 0; + for (int i = 0; i < filteredText.length(); i++) { + if (GuiRenderer.font.width(filteredText.substring(0, i + 1)) > remainingWidth) { + break; + } + cutoffIndex = i + 1; + } + filteredText = filteredText.substring(0, cutoffIndex); + } + + query.replace(start, end, filteredText); + setCursor(start + filteredText.length()); + setSelectionEnd(getCursor()); + + updateConsumer.accept(getQuery()); + } + + + public void erase(int offset) { + if (Screen.hasControlDown()) { + eraseWords(offset); + } else { + eraseCharacters(offset); + } + + updateConsumer.accept(getQuery()); + } + + public void eraseWords(int wordOffset) { + if (!query.isEmpty()) { + if (selectionStart != selectionEnd) { + write(""); + } else { + eraseCharacters(getWordSkipPosition(wordOffset) - selectionStart); + } + } + } + + public void eraseCharacters(int characterOffset) { + if (!query.isEmpty()) { + if (selectionStart != selectionEnd) { + write(""); + } else { + int cursorPos = getCursorPosWithOffset(characterOffset); + int start = Math.min(cursorPos, selectionStart); + int end = Math.max(cursorPos, selectionStart); + query.delete(start, end); + setCursor(start); + } + } + } + + public int getWordSkipPosition(int wordOffset) { + return getWordSkipPosition(wordOffset, selectionStart); + } + + public int getWordSkipPosition(int wordOffset, int cursorPosition) { + int currentPosition = cursorPosition; + boolean isMovingBackward = wordOffset < 0; + int numberOfWordsToSkip = Math.abs(wordOffset); + + for (int skippedWords = 0; skippedWords < numberOfWordsToSkip; skippedWords++) { + if (!isMovingBackward) { + currentPosition = query.indexOf(" ", currentPosition); + currentPosition = (currentPosition == -1) ? query.length() : skipSpaces(currentPosition); + } else { + currentPosition = skipSpacesBackward(currentPosition); + while (currentPosition > 0 && query.charAt(currentPosition - 1) != ' ') { + --currentPosition; + } + } + } + return currentPosition; + } + + private int skipSpaces(int currentPosition) { + int queryLength = query.length(); + while (currentPosition < queryLength && query.charAt(currentPosition) == ' ') { + ++currentPosition; + } + return currentPosition; + } + + private int skipSpacesBackward(int currentPosition) { + while (currentPosition > 0 && query.charAt(currentPosition - 1) == ' ') { + --currentPosition; + } + return currentPosition; + } + + public int getCursor() { + return selectionStart; + } + + public void setCursor(int cursor) { + setSelectionStart(cursor); + if (!isSelecting()) { + setSelectionEnd(selectionStart); + } + } + + public void moveCursor(int offset) { + setCursor(getCursorPosWithOffset(offset)); + } + + private int getCursorPosWithOffset(int offset) { + return Util.offsetByCodepoints(getQuery(), this.selectionStart, offset); + } + + public void setCursorToStart() { + setCursor(0); + } + + public void setCursorToEnd() { + setCursor(query.length()); + } + + public boolean isSelecting() { + return selecting; + } + + public void setSelecting(boolean selecting) { + this.selecting = selecting; + } + + public int getLastCursorPosition() { + return lastCursorPosition; + } + + public void setLastCursorPosition(int lastCursorPosition) { + this.lastCursorPosition = lastCursorPosition; + } + + public int getFirstCharacterIndex() { + return firstCharacterIndex; + } + + public int getSelectionStart() { + return selectionStart; + } + + public void setSelectionStart(int cursor) { + selectionStart = Mth.clamp(cursor, 0, query.length()); + } + + public int getSelectionEnd() { + return selectionEnd; + } + + public void setSelectionEnd(int index) { + selectionEnd = Mth.clamp(index, 0, query.length()); + + if (firstCharacterIndex > query.length()) { + firstCharacterIndex = query.length(); + } + + String visibleString = GuiRenderer.font.plainSubstrByWidth(query.substring(firstCharacterIndex), innerWidth); + int visibleEndIndex = visibleString.length() + firstCharacterIndex; + + if (selectionEnd == firstCharacterIndex) { + firstCharacterIndex -= GuiRenderer.font.plainSubstrByWidth(getQuery(), innerWidth, true).length(); + } + + if (selectionEnd > visibleEndIndex) { + firstCharacterIndex += selectionEnd - visibleEndIndex; + } else if (selectionEnd < firstCharacterIndex) { + firstCharacterIndex = selectionEnd; + } + + firstCharacterIndex = Mth.clamp(firstCharacterIndex, 0, query.length()); + } +} \ No newline at end of file diff --git a/src/main/java/net/vulkanmod/config/gui/widget/AbstractWidget.java b/src/main/java/net/vulkanmod/config/gui/widget/AbstractWidget.java new file mode 100644 index 000000000..f88f41353 --- /dev/null +++ b/src/main/java/net/vulkanmod/config/gui/widget/AbstractWidget.java @@ -0,0 +1,170 @@ +package net.vulkanmod.config.gui.widget; + +import com.mojang.blaze3d.systems.RenderSystem; +import net.minecraft.Util; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.ComponentPath; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.Renderable; +import net.minecraft.client.gui.components.events.GuiEventListener; +import net.minecraft.client.gui.narration.NarratableEntry; +import net.minecraft.client.gui.narration.NarratedElementType; +import net.minecraft.client.gui.narration.NarrationElementOutput; +import net.minecraft.client.gui.navigation.FocusNavigationEvent; +import net.minecraft.client.gui.navigation.ScreenRectangle; +import net.minecraft.client.renderer.GameRenderer; +import net.minecraft.client.resources.sounds.SimpleSoundInstance; +import net.minecraft.network.chat.Component; +import net.minecraft.sounds.SoundEvents; +import net.vulkanmod.config.gui.GuiRenderer; +import net.vulkanmod.config.gui.util.Dimension; +import net.vulkanmod.config.gui.util.GuiConstants; +import net.vulkanmod.vulkan.util.ColorUtil; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public abstract class AbstractWidget implements GuiEventListener, Renderable, NarratableEntry { + public Dimension dim; + private boolean focused; + private boolean hovered; + private long hoverStartTime; + private int hoverTime; + private long hoverStopTime; + + public AbstractWidget(Dimension dim) { + this.dim = dim; + } + + @Override + public void render(GuiGraphics guiGraphics, int mouseX, int mouseY, float delta) { + RenderSystem.setShader(GameRenderer::getPositionTexShader); + RenderSystem.setShaderColor(1.0f, 1.0f, 1.0f, 1.0f); + RenderSystem.defaultBlendFunc(); + RenderSystem.enableDepthTest(); + RenderSystem.enableBlend(); + + this.renderFocusing(); + this.updateHoverState(mouseX, mouseY); + this.renderHovering(); + } + + private void renderFocusing() { + if (!this.isFocused()) { + return; + } + + int borderColor = ColorUtil.ARGB.multiplyAlpha(GuiConstants.COLOR_RED, 0.8f); + int backgroundColor = ColorUtil.ARGB.multiplyAlpha(GuiConstants.COLOR_RED, 0.3f); + + GuiRenderer.fill( + dim.x() + 1, dim.y() + 1, + dim.xLimit() - 1, dim.yLimit() - 1, + backgroundColor); + GuiRenderer.renderBorder( + dim.x(), dim.y(), + dim.xLimit(), dim.yLimit(), + 45, + 1, borderColor); + } + + protected void renderHovering() { + if (this.isFocused() || !this.isActive()) + return; + + float hoverMultiplier = this.getHoverMultiplier(200); + + int borderColor = ColorUtil.ARGB.multiplyAlpha(GuiConstants.COLOR_RED, hoverMultiplier); + int backgroundColor = ColorUtil.ARGB.multiplyAlpha(GuiConstants.COLOR_RED, 0.3f * hoverMultiplier); + + if (hoverMultiplier > 0.0f) { + GuiRenderer.fill( + dim.x() + 1, dim.y() + 1, + dim.xLimit() - 1, dim.yLimit() - 1, + backgroundColor); + GuiRenderer.renderBorder( + dim.x(), dim.y(), + dim.xLimit(), dim.yLimit(), + 1, borderColor); + } + } + + public float getHoverMultiplier(float time) { + if (this.hovered) { + return Math.min(((this.hoverTime) / time), 1.0f); + } else { + int delta = (int) (Util.getMillis() - this.hoverStopTime); + return Math.max(1.0f - (delta / time), 0.0f); + } + } + + public void updateHoverState(double mouseX, double mouseY) { + if (dim.isPointInside(mouseX, mouseY)) { + if (!this.hovered) { + this.hoverStartTime = Util.getMillis(); + } + + this.hovered = true; + this.hoverTime = (int) (Util.getMillis() - this.hoverStartTime); + } else { + if (this.hovered) { + this.hoverStopTime = Util.getMillis(); + } + this.hovered = false; + this.hoverTime = 0; + } + } + + public boolean isHovered() { + return hovered; + } + + public void playDownSound() { + Minecraft.getInstance().getSoundManager().play(SimpleSoundInstance.forUI(SoundEvents.UI_BUTTON_CLICK, 1.0F)); + } + + @Override + public boolean isFocused() { + return this.focused; + } + + @Override + public void setFocused(boolean focused) { + this.focused = focused; + } + + @Override + public boolean isMouseOver(double mouseX, double mouseY) { + return this.dim.isPointInside(mouseX, mouseY); + } + + @Nullable + public ComponentPath nextFocusPath(FocusNavigationEvent event) { + return !this.isFocused() ? ComponentPath.leaf(this) : null; + } + + @Override + public NarratableEntry.@NotNull NarrationPriority narrationPriority() { + if (this.isFocused()) { + return NarratableEntry.NarrationPriority.FOCUSED; + } + if (this.isHovered()) { + return NarratableEntry.NarrationPriority.HOVERED; + } + + return NarratableEntry.NarrationPriority.NONE; + } + + @Override + public void updateNarration(NarrationElementOutput builder) { + if (this.isFocused()) { + builder.add(NarratedElementType.USAGE, Component.translatable("narration.button.usage.focused")); + } else if (this.hovered) { + builder.add(NarratedElementType.USAGE, Component.translatable("narration.button.usage.hovered")); + } + } + + @Override + public @NotNull ScreenRectangle getRectangle() { + return dim.rectangle(); + } +} diff --git a/src/main/java/net/vulkanmod/config/gui/widget/ButtonWidget.java b/src/main/java/net/vulkanmod/config/gui/widget/ButtonWidget.java new file mode 100644 index 000000000..7791cac0e --- /dev/null +++ b/src/main/java/net/vulkanmod/config/gui/widget/ButtonWidget.java @@ -0,0 +1,158 @@ +package net.vulkanmod.config.gui.widget; + +import com.mojang.blaze3d.systems.RenderSystem; +import net.minecraft.client.gui.ComponentPath; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.navigation.CommonInputs; +import net.minecraft.client.gui.navigation.FocusNavigationEvent; +import net.minecraft.client.renderer.GameRenderer; +import net.minecraft.network.chat.Component; +import net.minecraft.util.Mth; +import net.vulkanmod.config.gui.GuiRenderer; +import net.vulkanmod.config.gui.util.Dimension; +import net.vulkanmod.config.gui.util.GuiConstants; +import net.vulkanmod.vulkan.util.ColorUtil; +import org.jetbrains.annotations.Nullable; + +public class ButtonWidget extends AbstractWidget { + private final Runnable onPress; + private final float alpha = 1.0f; + private final boolean leftAlignedText; + private Component name; + private boolean selected; + private boolean active = true; + private boolean visible = true; + + public ButtonWidget(Dimension dim, Component name, Runnable onPress, boolean leftAlignedText) { + super(dim); + this.name = name; + this.onPress = onPress; + + this.leftAlignedText = leftAlignedText; + } + + public ButtonWidget(Dimension dim, Component name, Runnable onPress) { + this(dim, name, onPress, false); + } + + + @Override + public void render(GuiGraphics guiGraphics, int mouseX, int mouseY, float delta) { + if (!this.isVisible()) return; + + super.render(guiGraphics, mouseX, mouseY, delta); + + int backgroundColor = this.isActive() + ? ColorUtil.ARGB.multiplyAlpha(GuiConstants.COLOR_BLACK, 0.45f) + : ColorUtil.ARGB.multiplyAlpha(GuiConstants.COLOR_BLACK, 0.3f); + int textColor = this.isActive() + ? GuiConstants.COLOR_WHITE + : GuiConstants.COLOR_GRAY; + int selectionOutlineColor = ColorUtil.ARGB.multiplyAlpha(GuiConstants.COLOR_RED, 0.8f); + int selectionFillColor = ColorUtil.ARGB.multiplyAlpha(GuiConstants.COLOR_RED, 0.2f); + + RenderSystem.setShader(GameRenderer::getPositionTexShader); + RenderSystem.setShaderColor(1.0f, 1.0f, 1.0f, this.alpha); + RenderSystem.enableBlend(); + + GuiRenderer.fill( + dim.x(), dim.y(), + dim.xLimit(), dim.yLimit(), + backgroundColor); + + if (this.leftAlignedText) { + GuiRenderer.drawString( + this.name, + dim.x() + 8, dim.centerY() - 4, + textColor | (Mth.ceil(this.alpha * 255.0f) << 24)); + } else { + GuiRenderer.drawCenteredString( + this.name, + dim.centerX(), dim.centerY() - 4, + textColor | (Mth.ceil(this.alpha * 255.0f) << 24)); + } + + if (this.selected) { + RenderSystem.enableBlend(); + + GuiRenderer.fill( + dim.x(), dim.y(), + dim.x() + 1.5f, dim.yLimit(), + selectionOutlineColor); + GuiRenderer.fill( + dim.x(), dim.y(), + dim.xLimit(), dim.yLimit(), + selectionFillColor); + } + } + + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + if (!this.active || !this.visible) { + return false; + } + + if (button == 0 && dim.isPointInside(mouseX, mouseY)) { + doAction(); + + return true; + } + + return false; + } + + @Override + public boolean keyPressed(int keyCode, int scanCode, int modifiers) { + if (!this.isFocused()) + return false; + + if (CommonInputs.selected(keyCode)) { + doAction(); + return true; + } + + return false; + } + + private void doAction() { + this.onPress.run(); + this.playDownSound(); + } + + public void setSelected(boolean selected) { + this.selected = selected; + } + + public Component getName() { + return name; + } + + public void setName(Component name) { + this.name = name; + } + + 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/java/net/vulkanmod/config/gui/widget/ControllerWidget.java b/src/main/java/net/vulkanmod/config/gui/widget/ControllerWidget.java new file mode 100644 index 000000000..04da35e5c --- /dev/null +++ b/src/main/java/net/vulkanmod/config/gui/widget/ControllerWidget.java @@ -0,0 +1,93 @@ +package net.vulkanmod.config.gui.widget; + +import net.minecraft.ChatFormatting; +import net.minecraft.client.gui.ComponentPath; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.navigation.FocusNavigationEvent; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.MutableComponent; +import net.vulkanmod.config.gui.GuiRenderer; +import net.vulkanmod.config.gui.option.Option; +import net.vulkanmod.config.gui.util.Dimension; +import net.vulkanmod.config.gui.util.GuiConstants; +import net.vulkanmod.vulkan.util.ColorUtil; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class ControllerWidget extends AbstractWidget { + public final Option option; + public final int optionStateColor; + public Dimension controllerDim; + private Component displayedValue; + + public ControllerWidget(@NotNull Option option, Dimension dim) { + super(dim); + + this.option = option; + + this.optionStateColor = option.isActive() + ? GuiConstants.COLOR_WHITE + : GuiConstants.COLOR_GRAY; + + int controlWidth = Math.min((int) (dim.width() * 0.5f) - 8, 120); + int controlX = dim.xLimit() - controlWidth - 8; + + this.controllerDim = Dimension.ofInt( + controlX, this.dim.y(), + controlWidth, this.dim.height() + ); + } + + @Override + public void render(GuiGraphics guiGraphics, int mouseX, int mouseY, float delta) { + super.render(guiGraphics, mouseX, mouseY, delta); + + this.updateDisplayedValue(); + + int backgroundColor = ColorUtil.ARGB.multiplyAlpha(GuiConstants.COLOR_BLACK, 0.45f); + + GuiRenderer.fill( + dim.x(), dim.y(), + dim.xLimit(), dim.yLimit(), + backgroundColor); + if (option.isChanged()) { + GuiRenderer.drawString( + MutableComponent.create(option.name().getContents()).withStyle(ChatFormatting.ITALIC), + dim.x() + 8, dim.centerY() - 4, + optionStateColor); + } else { + GuiRenderer.drawString( + option.name(), + dim.x() + 8, dim.centerY() - 4, + optionStateColor); + } + } + + public Option getOption() { + return option; + } + + public Component getDisplayedValue() { + return this.displayedValue; + } + + public void setDisplayedValue(Component displayedValue) { + this.displayedValue = displayedValue; + } + + protected void updateDisplayedValue() { + setDisplayedValue(option.displayedValue()); + } + + @Override + public boolean isActive() { + return option.isActive(); + } + + @Override + public @Nullable ComponentPath nextFocusPath(FocusNavigationEvent event) { + if (!option.isActive()) + return null; + return super.nextFocusPath(event); + } +} diff --git a/src/main/java/net/vulkanmod/config/gui/widget/CyclingOptionWidget.java b/src/main/java/net/vulkanmod/config/gui/widget/CyclingOptionWidget.java deleted file mode 100644 index 20c601476..000000000 --- a/src/main/java/net/vulkanmod/config/gui/widget/CyclingOptionWidget.java +++ /dev/null @@ -1,172 +0,0 @@ -package net.vulkanmod.config.gui.widget; - -import com.mojang.blaze3d.systems.RenderSystem; -import com.mojang.blaze3d.vertex.*; -import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.Font; -import net.minecraft.client.renderer.GameRenderer; -import net.minecraft.network.chat.Component; -import net.vulkanmod.config.gui.GuiRenderer; -import net.vulkanmod.config.option.CyclingOption; -import net.vulkanmod.vulkan.util.ColorUtil; -import org.joml.Matrix4f; - -public class CyclingOptionWidget extends OptionWidget> { - private Button leftButton; - private Button rightButton; - - private boolean focused; - - public CyclingOptionWidget(CyclingOption option, int x, int y, int width, int height, Component name) { - super(x, y, width, height, name); - this.option = option; - this.leftButton = new Button(this.controlX, 16, Button.Direction.LEFT); - this.rightButton = new Button(this.controlX + this.controlWidth - 16, 16, Button.Direction.RIGHT); - -// updateDisplayedValue(option.getValueText()); - } - - @Override - protected int getYImage(boolean hovered) { - return 0; - } - - public void renderControls(double mouseX, double mouseY) { - RenderSystem.setShaderColor(1.0f, 1.0f, 1.0f, 1.0f); - - this.renderBars(); - - this.leftButton.setStatus(option.index() > 0); - this.rightButton.setStatus(option.index() < option.getValues().length - 1); - - int color = this.active ? 0xFFFFFF : 0xA0A0A0; - 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); - - this.leftButton.renderButton(GuiRenderer.guiGraphics.pose(), mouseX, mouseY); - this.rightButton.renderButton(GuiRenderer.guiGraphics.pose(), mouseX, mouseY); - } - - public void renderBars() { - int count = option.getValues().length; - int current = option.index(); - - int margin = 30; - int padding = 4; - - int barWidth = (this.controlWidth - (2 * margin) - (padding * count)) / count; - int color = ColorUtil.ARGB.pack(1.0f, 1.0f, 1.0f, 0.4f); - int activeColor = ColorUtil.ARGB.pack(1.0f, 1.0f, 1.0f, 1.0f); - - if (barWidth <= 0) - return; - - for (int i = 0; i < count; i++) { - float x0 = this.controlX + margin + i * (barWidth + padding); - float y0 = this.y + this.height - 5.0f; - - int c = i == current ? activeColor : color; - GuiRenderer.fill(x0, y0, x0 + barWidth, y0 + 1.5f, c); - } - } - - @Override - public void onClick(double mouseX, double mouseY) { - if (leftButton.isHovered(mouseX, mouseY)) { - option.prevValue(); - } - else if (rightButton.isHovered(mouseX, mouseY)) { - option.nextValue(); - } - } - - @Override - public void onRelease(double mouseX, double mouseY) { - - } - - @Override - protected void onDrag(double mouseX, double mouseY, double deltaX, double deltaY) { - - } - - @Override - public void setFocused(boolean bl) { - this.focused = bl; - } - - @Override - public boolean isFocused() { - return this.focused; - } - - class Button { - int x; - int width; - boolean active; - Direction direction; - - Button(int x, int width, Direction direction) { - this.x = x; - this.width = width; - this.active = true; - this.direction = direction; - } - - boolean isHovered(double mouseX, double mouseY) { - return mouseX >= x && mouseX <= x + width && mouseY >= y && mouseY <= y + height; - } - - void setStatus(boolean status) { - this.active = status; - } - - void renderButton(PoseStack matrices, double mouseX, double mouseY) { - Tesselator tesselator = Tesselator.getInstance(); - BufferBuilder bufferBuilder = tesselator.begin(VertexFormat.Mode.TRIANGLE_STRIP, DefaultVertexFormat.POSITION); - - RenderSystem.setShaderColor(1.0f, 1.0f, 1.0f, 1.0f); - - float f = this.isHovered(mouseX, mouseY) && this.active ? 5.0f : 4.5f; - - Matrix4f matrix4f = matrices.last().pose(); - - RenderSystem.setShader(GameRenderer::getPositionShader); - RenderSystem.enableBlend(); - - if(this.isHovered(mouseX, mouseY) && this.active) - RenderSystem.setShaderColor(1.0f, 1.0f, 1.0f, 1.0f); - else if(this.active) - RenderSystem.setShaderColor(1.0f, 1.0f, 1.0f, 0.8f); - else - RenderSystem.setShaderColor(0.3f, 0.3f, 0.3f, 0.8f); - - float h = f; - float w = f - 1.0f; - float yC = y + height * 0.5f; - float xC = x + width * 0.5f; - if (this.direction == Direction.LEFT) { - bufferBuilder.addVertex(matrix4f, xC - w, yC, 0); - bufferBuilder.addVertex(matrix4f, xC + w, yC + h, 0); - bufferBuilder.addVertex(matrix4f, xC + w, yC - h, 0); - } else { - bufferBuilder.addVertex(matrix4f, xC + w, yC, 0); - bufferBuilder.addVertex(matrix4f, xC - w, yC - h, 0); - bufferBuilder.addVertex(matrix4f, xC - w, yC + h, 0); - } - - BufferUploader.drawWithShader(bufferBuilder.buildOrThrow()); - - RenderSystem.setShaderColor(1.0f, 1.0f, 1.0f, 1.0f); - RenderSystem.setShader(GameRenderer::getPositionTexShader); - } - - enum Direction { - LEFT, - RIGHT - } - } - -} diff --git a/src/main/java/net/vulkanmod/config/gui/widget/OptionWidget.java b/src/main/java/net/vulkanmod/config/gui/widget/OptionWidget.java deleted file mode 100644 index a9fe679ce..000000000 --- a/src/main/java/net/vulkanmod/config/gui/widget/OptionWidget.java +++ /dev/null @@ -1,210 +0,0 @@ -package net.vulkanmod.config.gui.widget; - -import com.mojang.blaze3d.systems.RenderSystem; -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.narration.NarratableEntry; -import net.minecraft.client.gui.narration.NarrationElementOutput; -import net.minecraft.client.renderer.GameRenderer; -import net.minecraft.client.resources.sounds.SimpleSoundInstance; -import net.minecraft.client.sounds.SoundManager; -import net.minecraft.network.chat.Component; -import net.minecraft.resources.ResourceLocation; -import net.minecraft.sounds.SoundEvents; -import net.minecraft.util.Mth; -import net.vulkanmod.config.gui.GuiElement; -import net.vulkanmod.config.gui.GuiRenderer; -import net.vulkanmod.config.option.CyclingOption; -import net.vulkanmod.config.option.Option; -import net.vulkanmod.render.util.MathUtil; -import net.vulkanmod.vulkan.util.ColorUtil; - -import java.util.Objects; - -public abstract class OptionWidget> extends VAbstractWidget - implements NarratableEntry { - - public int controlX; - public int controlWidth; - private final Component name; - protected Component displayedValue; - - protected boolean controlHovered; - - O option; - - public OptionWidget(int x, int y, int width, int height, Component name) { - this.x = x; - this.y = y; - this.width = width; - this.height = height; - this.name = name; - this.displayedValue = Component.literal("N/A"); - - this.controlWidth = Math.min((int) (width * 0.5f) - 8, 120); - this.controlX = this.x + this.width - this.controlWidth - 8; - } - - public void setOption(O option) { - this.option = option; - } - - public void render(double mouseX, double mouseY) { - if (!this.visible) { - return; - } - - this.updateDisplayedValue(); - - this.controlHovered = mouseX >= this.controlX && mouseY >= this.y && mouseX < this.controlX + this.controlWidth && mouseY < this.y + this.height; - this.renderWidget(mouseX, mouseY); - } - - public void updateState() { - - } - - public void renderWidget(double mouseX, double mouseY) { - Minecraft minecraftClient = Minecraft.getInstance(); - - RenderSystem.setShader(GameRenderer::getPositionTexShader); - RenderSystem.setShaderColor(1.0f, 1.0f, 1.0f, 1.0f); - int i = this.getYImage(this.isHovered()); - RenderSystem.enableBlend(); - RenderSystem.defaultBlendFunc(); - RenderSystem.enableDepthTest(); - - int xPadding = 0; - int yPadding = 0; - - int color = ColorUtil.ARGB.pack(0.0f, 0.0f, 0.0f, 0.45f); - GuiRenderer.fill(this.x - xPadding, this.y - yPadding, this.x + this.width + xPadding, this.y + this.height + yPadding, color); - - this.renderHovering(0, 0); - - color = this.active ? 0xFFFFFF : 0xA0A0A0; -// j = 0xB0f0d0a0; - - Font textRenderer = minecraftClient.font; - GuiRenderer.drawString(textRenderer, this.getName().getVisualOrderText(), this.x + 8, this.y + (this.height - 8) / 2, color); - - RenderSystem.enableBlend(); - - 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); - - public abstract void onRelease(double mouseX, double mouseY); - - protected abstract void onDrag(double mouseX, double mouseY, double deltaX, double deltaY); - - protected boolean isValidClickButton(int button) { - return button == 0; - } - - @Override - public boolean mouseDragged(double mouseX, double mouseY, int button, double deltaX, double deltaY) { - if (this.isValidClickButton(button)) { - this.onDrag(mouseX, mouseY, deltaX, deltaY); - return true; - } - return false; - } - - @Override - public boolean mouseClicked(double mouseX, double mouseY, int button) { - if (!this.active || !this.visible) { - return false; - } - - if (this.isValidClickButton(button) && this.clicked(mouseX, mouseY)) { - this.playDownSound(Minecraft.getInstance().getSoundManager()); - this.onClick(mouseX, mouseY); - return true; - } - return false; - } - - @Override - public boolean mouseReleased(double mouseX, double mouseY, int button) { - if (this.isValidClickButton(button)) { - this.onRelease(mouseX, mouseY); - return true; - } - return false; - } - - @Override - public boolean isMouseOver(double mouseX, double mouseY) { - return this.active && this.visible && mouseX >= (double)this.x && mouseY >= (double)this.y && mouseX < (double)(this.x + this.width) && mouseY < (double)(this.y + this.height); - } - - @Override - public void setFocused(boolean bl) { - this.focused = bl; - } - - @Override - public boolean isFocused() { - return this.focused; - } - - protected boolean clicked(double mouseX, double mouseY) { - return this.active && this.visible && mouseX >= (double)this.controlX && mouseY >= (double)this.y && mouseX < (double)(this.x + this.width) && mouseY < (double)(this.y + this.height); - } - - public Component getName() { - return this.name; - } - - public Component getDisplayedValue() { - return this.displayedValue; - } - - protected void updateDisplayedValue() { - this.displayedValue = this.option.getDisplayedValue(); - } - - public Component getTooltip() { - return this.option.getTooltip(); - } - - @Override - public NarrationPriority narrationPriority() { - if (this.focused) { - return NarrationPriority.FOCUSED; - } - if (this.hovered) { - return NarrationPriority.HOVERED; - } - return NarrationPriority.NONE; - } - - @Override - 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 deleted file mode 100644 index 7cef7e107..000000000 --- a/src/main/java/net/vulkanmod/config/gui/widget/RangeOptionWidget.java +++ /dev/null @@ -1,128 +0,0 @@ -package net.vulkanmod.config.gui.widget; - -import com.mojang.blaze3d.systems.RenderSystem; -import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.Font; -import net.minecraft.client.sounds.SoundManager; -import net.minecraft.network.chat.Component; -import net.minecraft.util.Mth; -import net.vulkanmod.config.gui.GuiRenderer; -import net.vulkanmod.config.option.RangeOption; -import net.vulkanmod.vulkan.util.ColorUtil; -import org.lwjgl.glfw.GLFW; - -public class RangeOptionWidget extends OptionWidget { - protected double value; - - private boolean focused; - - public RangeOptionWidget(RangeOption option, int x, int y, int width, int height, Component name) { - super(x, y, width, height, name); - this.setOption(option); - this.setValue(option.getScaledValue()); - - } - - @Override - protected int getYImage(boolean hovered) { - return 0; - } - - @Override - protected void renderControls(double mouseX, double mouseY) { - RenderSystem.setShaderColor(1.0f, 1.0f, 1.0f, 1.0f); - - int valueX = this.controlX + (int) (this.value * (this.controlWidth)); - - if (this.controlHovered) { - int halfWidth = 2; - int halfHeight = 4; - - float y0 = this.y + this.height * 0.5f - 1.0f; - float y1 = y0 + 2.0f; - GuiRenderer.fill(this.controlX, y0, this.controlX + this.controlWidth, y1, ColorUtil.ARGB.pack(1.0f, 1.0f, 1.0f, 0.1f)); - GuiRenderer.fill(this.controlX, y0, valueX - halfWidth, y1, ColorUtil.ARGB.pack(1.0f, 1.0f, 1.0f, 0.3f)); - - int color = ColorUtil.ARGB.pack(1.0f, 1.0f, 1.0f, 0.3f); - GuiRenderer.renderBorder(valueX - halfWidth, y0 - halfHeight, valueX + halfWidth, y1 + halfHeight, 1, color); -// GuiRenderer.fill(valueX - halfWidth, y0 - 3.0f, valueX + halfWidth, y1 + 3.0f, color); - - } else { - float y0 = this.y + this.height - 5.0f; - float y1 = y0 + 1.5f; - GuiRenderer.fill(this.controlX, y0, this.controlX + this.controlWidth, y1, ColorUtil.ARGB.pack(1.0f, 1.0f, 1.0f, 0.3f)); - GuiRenderer.fill(this.controlX, y0, valueX, y1, ColorUtil.ARGB.pack(1.0f, 1.0f, 1.0f, 0.8f)); - } - - int color = this.active ? 0xFFFFFF : 0xA0A0A0; - Font font = Minecraft.getInstance().font; - var text = this.getDisplayedValue(); - int width = font.width(text); - int x = this.controlX + this.controlWidth / 2 - width / 2; -// int x = (int) (this.x + 0.5f * width); - int y = this.y + (this.height - 9) / 2; - GuiRenderer.drawString(font, text.getVisualOrderText(), x, y, color); - } - - @Override - public void onClick(double mouseX, double mouseY) { - this.setValueFromMouse(mouseX); - } - - @Override - public boolean keyPressed(int keyCode, int scanCode, int modifiers) { - boolean isLeft = keyCode == GLFW.GLFW_KEY_LEFT; - boolean isRight = keyCode == GLFW.GLFW_KEY_RIGHT; - - if (isLeft || isRight) { - float direction = isLeft ? -1.0f : 1.0f; - this.setValue(this.value + (double) (direction / (float) (this.width - 8))); - } - - return false; - } - - @Override - public void setFocused(boolean bl) { - this.focused = bl; - } - - @Override - public boolean isFocused() { - return this.focused; - } - - private void setValueFromMouse(double mouseX) { - this.setValue((mouseX - (double) (this.controlX + 4)) / (double) ((this.controlWidth) - 8)); - } - - private void setValue(double value) { - double d = this.value; - this.value = Mth.clamp(value, 0.0, 1.0); - if (d != this.value) { - this.applyValue(); - } - this.updateDisplayedValue(); - } - - @Override - 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(); - } - - @Override - public void playDownSound(SoundManager soundManager) { - } - - @Override - public void onRelease(double mouseX, double mouseY) { - if (this.controlHovered) { - super.playDownSound(Minecraft.getInstance().getSoundManager()); - } - } -} diff --git a/src/main/java/net/vulkanmod/config/gui/widget/SearchFieldWidget.java b/src/main/java/net/vulkanmod/config/gui/widget/SearchFieldWidget.java new file mode 100644 index 000000000..923f17a51 --- /dev/null +++ b/src/main/java/net/vulkanmod/config/gui/widget/SearchFieldWidget.java @@ -0,0 +1,213 @@ +package net.vulkanmod.config.gui.widget; + +import com.mojang.blaze3d.systems.RenderSystem; +import net.minecraft.Util; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.Component; +import net.minecraft.util.Mth; +import net.minecraft.util.StringUtil; +import net.vulkanmod.config.gui.GuiRenderer; +import net.vulkanmod.config.gui.util.Dimension; +import net.vulkanmod.config.gui.util.GuiConstants; +import net.vulkanmod.config.gui.util.SearchFieldTextModel; +import net.vulkanmod.vulkan.util.ColorUtil; +import org.lwjgl.glfw.GLFW; + +public class SearchFieldWidget extends AbstractWidget { + private final SearchFieldTextModel model; + private long lastTimeBlink = 0; + + public SearchFieldWidget(Dimension dim, SearchFieldTextModel model) { + super(dim); + this.model = model; + } + + @Override + public void render(GuiGraphics guiGraphics, int mouseX, int mouseY, float delta) { + super.render(guiGraphics, mouseX, mouseY, delta); + + int backgroundColor = ColorUtil.ARGB.multiplyAlpha(GuiConstants.COLOR_BLACK, 0.45f); + + GuiRenderer.fill( + dim.x(), dim.y(), + dim.xLimit(), dim.yLimit(), + backgroundColor); + + GuiRenderer.guiGraphics.enableScissor( + dim.x(), dim.y(), + dim.xLimit() - 8, dim.yLimit()); + + renderQuery(); + renderCursor(); + renderSelection(); + + GuiRenderer.guiGraphics.disableScissor(); + } + + private void renderQuery() { + if (!this.isFocused() && model.getQuery().isEmpty()) { + GuiRenderer.drawString( + Component.translatable("vulkanmod.options.searchFieldEmpty"), + dim.x() + 8, dim.centerY() - 4, + GuiConstants.COLOR_GRAY); + } else { + GuiRenderer.drawString( + Component.literal(model.getQuery()), + dim.x() + 8, dim.centerY() - 4, + GuiConstants.COLOR_WHITE); + } + } + + private void renderCursor() { + if (!this.isFocused() || !isCursorVisible()) + return; + + String visibleQuery = model.getQuery().substring(model.getFirstCharacterIndex()); + + int cursorPos = dim.x() + 8 + GuiRenderer.font.width(visibleQuery.substring(0, model.getCursor())); + GuiRenderer.drawString( + Component.literal("_"), + cursorPos, dim.centerY() - GuiRenderer.font.lineHeight / 2, + GuiConstants.COLOR_WHITE); + } + + private void renderSelection() { + if (model.getQuery().isEmpty()) + return; + + RenderSystem.enableBlend(); + + int selectionStart = model.getSelectionStart() - model.getFirstCharacterIndex(); + int selectionEnd = model.getSelectionEnd() - model.getFirstCharacterIndex(); + + if (selectionEnd != selectionStart) { + int selectionColor = ColorUtil.ARGB.multiplyAlpha(GuiConstants.COLOR_WHITE, 0.45f); + + String visibleQuery = model.getQuery().substring(model.getFirstCharacterIndex()); + + int x0 = dim.x() + 8 + GuiRenderer.font.width(visibleQuery.substring(0, selectionStart)); + int x1 = dim.x() + 8 + GuiRenderer.font.width(visibleQuery.substring(0, selectionEnd)); + int y0 = dim.centerY() - GuiRenderer.font.lineHeight / 2; + int y1 = dim.centerY() + GuiRenderer.font.lineHeight / 2; + + GuiRenderer.fill( + Math.min(Math.min(x0, x1), dim.xLimit()), Math.min(y0, y1), + Math.min(Math.max(x0, x1), dim.xLimit()), Math.max(y0, y1), + selectionColor); + } + } + + private boolean isCursorVisible() { + long timeSinceLastBlink = Util.getMillis() - lastTimeBlink; + if (timeSinceLastBlink > 800 || timeSinceLastBlink < 400) { + if (timeSinceLastBlink > 400) { + lastTimeBlink = Util.getMillis(); + } + return true; + } + return false; + } + + @Override + public boolean charTyped(char chr, int modifiers) { + if (this.isFocused() && StringUtil.isAllowedChatCharacter(chr)) { + model.write(Character.toString(chr)); + return true; + } + + return false; + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + this.setFocused(isMouseOver(mouseX, mouseY)); + + if (!this.isFocused()) + return false; + + model.setSelecting(Screen.hasShiftDown()); + + int offsetX = Mth.floor(mouseX) - dim.x() - 8; + String visibleString = GuiRenderer.font.plainSubstrByWidth(model.getQuery().substring(model.getFirstCharacterIndex()), dim.width() - 16); + + int clickPosition = GuiRenderer.font.plainSubstrByWidth(visibleString, offsetX).length(); + model.setCursor(Math.min(model.getFirstCharacterIndex() + clickPosition, model.getQuery().length())); + + return true; + } + + + @Override + public boolean keyPressed(int keyCode, int scanCode, int modifiers) { + if (!this.isActive() || !this.isFocused()) { + return false; + } + + model.setSelecting(Screen.hasShiftDown()); + if (Screen.isSelectAll(keyCode)) { + model.setCursorToEnd(); + model.setSelectionEnd(0); + return true; + } else if (Screen.isCopy(keyCode)) { + Minecraft.getInstance().keyboardHandler.setClipboard(model.getSelectedText()); + return true; + } else if (Screen.isPaste(keyCode)) { + model.write(Minecraft.getInstance().keyboardHandler.getClipboard()); + + return true; + } else if (Screen.isCut(keyCode)) { + Minecraft.getInstance().keyboardHandler.setClipboard(model.getSelectedText()); + model.write(""); + + return true; + } else { + switch (keyCode) { + case GLFW.GLFW_KEY_BACKSPACE -> { + model.setSelecting(false); + model.erase(-1); + model.setSelecting(Screen.hasShiftDown()); + return true; + } + case GLFW.GLFW_KEY_DELETE -> { + model.setSelecting(false); + model.erase(1); + model.setSelecting(Screen.hasShiftDown()); + return true; + } + case GLFW.GLFW_KEY_RIGHT -> { + if (Screen.hasControlDown()) { + model.setCursor(model.getWordSkipPosition(1)); + } else { + model.moveCursor(1); + } + boolean state = model.getCursor() != model.getLastCursorPosition() && model.getCursor() != model.getQuery().length() + 1; + model.setLastCursorPosition(model.getCursor()); + return state; + } + case GLFW.GLFW_KEY_LEFT -> { + if (Screen.hasControlDown()) { + model.setCursor(model.getWordSkipPosition(-1)); + } else { + model.moveCursor(-1); + } + boolean state = model.getCursor() != model.getLastCursorPosition() && model.getCursor() != 0; + model.setLastCursorPosition(model.getCursor()); + return state; + } + case GLFW.GLFW_KEY_HOME -> { + model.setCursorToStart(); + return true; + } + case GLFW.GLFW_KEY_END -> { + model.setCursorToEnd(); + return true; + } + default -> { + return false; + } + } + } + } +} diff --git a/src/main/java/net/vulkanmod/config/gui/widget/SwitchOptionWidget.java b/src/main/java/net/vulkanmod/config/gui/widget/SwitchOptionWidget.java deleted file mode 100644 index 340902f3c..000000000 --- a/src/main/java/net/vulkanmod/config/gui/widget/SwitchOptionWidget.java +++ /dev/null @@ -1,89 +0,0 @@ -package net.vulkanmod.config.gui.widget; - -import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.Font; -import net.minecraft.network.chat.Component; -import net.vulkanmod.config.gui.GuiRenderer; -import net.vulkanmod.config.option.SwitchOption; -import net.vulkanmod.vulkan.util.ColorUtil; - -public class SwitchOptionWidget extends OptionWidget { - private boolean focused; - - public SwitchOptionWidget(SwitchOption option, int x, int y, int width, int height, Component name) { - super(x, y, width, height, name); - this.option = option; - updateDisplayedValue(); - } - - @Override - protected void renderControls(double mouseX, double mouseY) { - int center = controlX + controlWidth / 2; - float halfWidth = 12; - float x0 = center - halfWidth; - float y0 = y + 4; - float height = this.height - 8; - int color; - - float w1 = halfWidth - 4; - float h1 = height - 4; - if (this.option.getNewValue()) { - float x1 = x0 + halfWidth + 2; - - color = ColorUtil.ARGB.pack(0.4f, 0.4f, 0.4f, 1.0f); - GuiRenderer.fillBox(x0 + 2, y0 + 2, x1 - (x0 + 2) - 1, h1, color); - - color = ColorUtil.ARGB.pack(1.0f, 1.0f, 1.0f, 1.0f); - GuiRenderer.fillBox(x1, y0 + 2, w1, h1, color); - } else { - color = ColorUtil.ARGB.pack(1.0f, 1.0f, 1.0f, 0.4f); - GuiRenderer.fillBox(x0 + 2, y0 + 2, w1, h1, color); - } - - color = ColorUtil.ARGB.pack(0.6f, 0.6f, 0.6f, 1.0f); - GuiRenderer.renderBoxBorder(x0, y0, halfWidth * 2, height, 1, color); - - color = this.active ? 0xFFFFFF : 0xA0A0A0; - Font textRenderer = Minecraft.getInstance().font; - int margin = Math.max( - textRenderer.width(Component.translatable("options.on").getString()) / 3, - textRenderer.width(Component.translatable("options.off").getString()) / 3 - ); - - int x = this.controlX + this.controlWidth / 2 - (int) (halfWidth * 1.5f) - 4 - margin; - int y = this.y + (this.height - 8) / 2; - GuiRenderer.drawCenteredString(textRenderer, this.getDisplayedValue(), x, y, color); - } - - public void onClick(double mouseX, double mouseY) { - this.option.setNewValue(!this.option.getNewValue()); - updateDisplayedValue(); - } - - @Override - public void onRelease(double mouseX, double mouseY) { - - } - - @Override - protected void onDrag(double mouseX, double mouseY, double deltaX, double deltaY) { - - } - - protected void updateDisplayedValue() { - this.displayedValue = option.getNewValue() - ? Component.translatable("options.on") - : Component.translatable("options.off"); - } - - @Override - public void setFocused(boolean bl) { - this.focused = bl; - } - - @Override - public boolean isFocused() { - return this.focused; - } - -} diff --git a/src/main/java/net/vulkanmod/config/gui/widget/VAbstractWidget.java b/src/main/java/net/vulkanmod/config/gui/widget/VAbstractWidget.java deleted file mode 100644 index 58bbd0d0e..000000000 --- a/src/main/java/net/vulkanmod/config/gui/widget/VAbstractWidget.java +++ /dev/null @@ -1,114 +0,0 @@ -package net.vulkanmod.config.gui.widget; - -import net.minecraft.client.Minecraft; -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.GuiElement; -import net.vulkanmod.config.gui.GuiRenderer; -import net.vulkanmod.vulkan.util.ColorUtil; - -public abstract class VAbstractWidget extends GuiElement { - public boolean active = true; - public boolean visible = true; - public boolean focused; - - protected Component message; - - public void render(double mX, double mY) { - this.updateState(mX, mY); - this.renderWidget(mX, mY); - } - - public void renderWidget(double mX, double mY) { - } - - public void onClick(double mX, double mY) { - } - - public void onRelease(double mX, double mY) { - } - - protected void onDrag(double mX, double mY, double f, double g) { - } - - protected void renderHovering(int xPadding, int yPadding) { - float hoverMultiplier = this.getHoverMultiplier(200); - - 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); - - int x0 = this.x - xPadding; - int x1 = this.x + this.width + xPadding; - int y0 = this.y - yPadding; - int y1 = this.y + height + yPadding; - int border = 1; - - GuiRenderer.renderBorder(x0, y0, x1, y1, border, color); - } - } - - @Override - public boolean mouseClicked(double mX, double mY, int button) { - if (this.active && this.visible) { - if (this.isValidClickButton(button)) { - boolean bl = this.clicked(mX, mY); - if (bl) { - this.playDownSound(Minecraft.getInstance().getSoundManager()); - this.onClick(mX, mY); - return true; - } - } - - } - return false; - } - - protected boolean clicked(double mX, double mY) { - return this.active - && this.visible - && mX >= (double)this.getX() - && mY >= (double)this.getY() - && mX < (double)(this.getX() + this.getWidth()) - && mY < (double)(this.getY() + this.getHeight()); - } - - @Override - public boolean mouseReleased(double mX, double mY, int button) { - if (this.isValidClickButton(button)) { - this.onRelease(mX, mY); - return true; - } else { - return false; - } - } - - protected boolean isValidClickButton(int button) { - return button == 0; - } - - @Override - public boolean mouseDragged(double mX, double mY, int button, double f, double g) { - if (this.isValidClickButton(button)) { - this.onDrag(mX, mY, f, g); - return true; - } else { - return false; - } - } - - public void playDownSound(SoundManager soundManager) { - soundManager.play(SimpleSoundInstance.forUI(SoundEvents.UI_BUTTON_CLICK, 1.0F)); - } - - public Component getTooltip() { - 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 deleted file mode 100644 index 3664d9429..000000000 --- a/src/main/java/net/vulkanmod/config/gui/widget/VButtonWidget.java +++ /dev/null @@ -1,70 +0,0 @@ -package net.vulkanmod.config.gui.widget; - -import com.mojang.blaze3d.systems.RenderSystem; -import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.Font; -import net.minecraft.client.renderer.GameRenderer; -import net.minecraft.network.chat.Component; -import net.minecraft.util.Mth; -import net.vulkanmod.config.gui.GuiRenderer; -import net.vulkanmod.vulkan.util.ColorUtil; - -import java.util.function.Consumer; - -public class VButtonWidget extends VAbstractWidget { - boolean selected = false; - Consumer onPress; - - float alpha = 1.0f; - - public VButtonWidget(int x, int y, int width, int height, Component message, Consumer onPress) { - this.setPosition(x, y, width, height); - - this.message = message; - this.onPress = onPress; - } - - public void renderWidget(double mouseX, double mouseY) { - Minecraft minecraftClient = Minecraft.getInstance(); - Font textRenderer = minecraftClient.font; - RenderSystem.setShader(GameRenderer::getPositionTexShader); - RenderSystem.setShaderColor(1.0f, 1.0f, 1.0f, this.alpha); - - RenderSystem.enableBlend(); - - int xPadding = 0; - int yPadding = 0; - - 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); - - if (this.active) { - this.renderHovering(0, 0); - } - - 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); - - RenderSystem.enableBlend(); - - if(this.selected) { -// color = ColorUtil.ARGB.pack(1.0f, 1.0f, 1.0f, 1.0f); - color = ColorUtil.ARGB.pack(0.3f, 0.0f, 0.0f, 1.0f); -// GuiRenderer.fillBox(this.x, this.y + this.height - 1, this.width, 1, color); - GuiRenderer.fillBox(this.x, this.y, 1.5f, this.height, color); - -// color = ColorUtil.ARGB.pack(0.5f, 0.5f, 0.5f, 0.2f); - color = ColorUtil.ARGB.pack(0.3f, 0.0f, 0.0f, 0.2f); - GuiRenderer.fillBox(this.x, this.y, this.width, this.height, color); - } - } - - public void setSelected(boolean selected) { - this.selected = selected; - } - - public void onClick(double mX, double mY) { - this.onPress.accept(this); - } - -} diff --git a/src/main/java/net/vulkanmod/config/option/CyclingOption.java b/src/main/java/net/vulkanmod/config/option/CyclingOption.java deleted file mode 100644 index d6080f7fd..000000000 --- a/src/main/java/net/vulkanmod/config/option/CyclingOption.java +++ /dev/null @@ -1,78 +0,0 @@ -package net.vulkanmod.config.option; - -import net.minecraft.network.chat.Component; -import net.vulkanmod.config.gui.widget.CyclingOptionWidget; -import net.vulkanmod.config.gui.widget.OptionWidget; -import org.apache.commons.lang3.ArrayUtils; - -import java.util.function.Consumer; -import java.util.function.Supplier; - -public class CyclingOption extends Option { - private E[] values; - private int index; - - public CyclingOption(Component name, E[] values, Consumer setter, Supplier getter) { - super(name, setter, getter); - this.values = values; - - this.index = this.findNewValueIndex(); - } - - @Override - public OptionWidget createOptionWidget(int x, int y, int width, int height) { - return new CyclingOptionWidget(this, x, y, width, height, this.name); - } - - public void updateOption(E[] values, Consumer setter, Supplier getter) { - this.onApply = setter; - this.valueSupplier = getter; - - this.values = values; - this.index = ArrayUtils.indexOf(this.values, this.getNewValue()); - } - - public int index() { return this.index; } - - public void setValues(E[] values) { - this.values = values; - } - - public void prevValue() { - if(this.index > 0) - this.index--; - this.updateValue(); - } - - public void nextValue() { - if(this.index < values.length - 1) - this.index++; - this.updateValue(); - } - - private void updateValue() { - if(this.index >= 0 && this.index < this.values.length) { - this.newValue = values[this.index]; - - if (onChange != null) - onChange.run(); - } - } - - public void setNewValue(E e) { - super.setNewValue(e); - this.index = findNewValueIndex(); - } - - private int findNewValueIndex() { - for (int i = 0; i < this.values.length; i++) { - if (this.values[i].equals(this.newValue)) - return i; - } - return -1; - } - - public E[] getValues() { - return values; - } -} diff --git a/src/main/java/net/vulkanmod/config/option/Option.java b/src/main/java/net/vulkanmod/config/option/Option.java deleted file mode 100644 index a1425ccf8..000000000 --- a/src/main/java/net/vulkanmod/config/option/Option.java +++ /dev/null @@ -1,110 +0,0 @@ -package net.vulkanmod.config.option; - -import net.minecraft.network.chat.Component; -import net.vulkanmod.config.gui.widget.OptionWidget; - -import java.util.function.Consumer; -import java.util.function.Function; -import java.util.function.Supplier; - -public abstract class Option { - protected final Component name; - protected Component tooltip; - - protected Consumer onApply; - protected Supplier valueSupplier; - - protected T value; - protected T newValue; - - protected Function translator; - - protected boolean active; - protected Runnable onChange; - - public Option(Component name, Consumer setter, Supplier getter, Function translator) { - this.name = name; - - this.onApply = setter; - this.valueSupplier = getter; - - this.newValue = this.value = this.valueSupplier.get(); - - this.translator = translator; - } - - public Option(Component name, Consumer setter, Supplier getter) { - this.name = name; - - this.onApply = setter; - this.valueSupplier = getter; - - this.newValue = this.value = this.valueSupplier.get(); - } - - public Option setOnApply(Consumer onApply) { - this.onApply = onApply; - return this; - } - - public Option setValueSupplier(Supplier supplier) { - this.valueSupplier = supplier; - return this; - } - - public Option setTranslator(Function translator) { - this.translator = translator; - return this; - } - - public Option setActive(boolean active) { - this.active = active; - return this; - } - - public abstract OptionWidget createOptionWidget(int x, int y, int width, int height); - - public void setNewValue(T t) { - this.newValue = t; - - if (onChange != null) - onChange.run(); - } - - public Component getName() { - return this.name; - } - - public void setOnChange(Runnable runnable) { - onChange = runnable; - } - - public boolean isChanged() { - return !this.newValue.equals(this.value); - } - - public void apply() { - if(!isChanged()) - return; - - onApply.accept(this.newValue); - this.value = this.newValue; - } - - public T getNewValue() { - return this.newValue; - } - - 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; - } -} diff --git a/src/main/java/net/vulkanmod/config/option/OptionPage.java b/src/main/java/net/vulkanmod/config/option/OptionPage.java deleted file mode 100644 index 8d783fe4e..000000000 --- a/src/main/java/net/vulkanmod/config/option/OptionPage.java +++ /dev/null @@ -1,44 +0,0 @@ -package net.vulkanmod.config.option; - -import net.vulkanmod.config.gui.OptionBlock; -import net.vulkanmod.config.gui.VOptionList; - -public class OptionPage { - public final String name; - OptionBlock[] optionBlocks; - private VOptionList optionList; - - public OptionPage(String name, OptionBlock[] optionBlocks) { - this.name = name; - this.optionBlocks = optionBlocks; - } - - public void createList(int x, int y, int width, int height, int itemHeight) { - this.optionList = new VOptionList(x, y, width, height, itemHeight); - this.optionList.addAll(optionBlocks); - } - - public VOptionList getOptionList() { - return this.optionList; - } - - public boolean optionChanged() { - boolean changed = false; - for (var block : this.optionBlocks) { - for (var option : block.options()) { - if (option.isChanged()) - changed = true; - } - } - return changed; - } - - public void applyOptionChanges() { - for (var block : this.optionBlocks) { - for (var option : block.options()) { - if (option.isChanged()) - option.apply(); - } - } - } -} diff --git a/src/main/java/net/vulkanmod/config/option/Options.java b/src/main/java/net/vulkanmod/config/option/Options.java deleted file mode 100644 index 5fc3d9717..000000000 --- a/src/main/java/net/vulkanmod/config/option/Options.java +++ /dev/null @@ -1,304 +0,0 @@ -package net.vulkanmod.config.option; - -import com.mojang.blaze3d.platform.Window; -import net.minecraft.client.*; -import net.minecraft.network.chat.Component; -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.render.chunk.build.light.LightMode; -import net.vulkanmod.render.vertex.TerrainRenderType; -import net.vulkanmod.vulkan.Renderer; -import net.vulkanmod.vulkan.device.DeviceManager; - -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); - - if (videoModeSet == null) { - videoModeSet = VideoModeSet.getDummy(); - videoMode = videoModeSet.getVideoMode(-1); - } - - VideoModeManager.selectedVideoMode = videoMode; - var refreshRates = videoModeSet.getRefreshRates(); - - CyclingOption RefreshRate = (CyclingOption) new CyclingOption<>( - Component.translatable("vulkanmod.options.refreshRate"), - refreshRates.toArray(new Integer[0]), - (value) -> { - VideoModeManager.selectedVideoMode.refreshRate = value; - VideoModeManager.applySelectedVideoMode(); - - if (minecraftOptions.fullscreen().get()) - fullscreenDirty = true; - }, - () -> VideoModeManager.selectedVideoMode.refreshRate) - .setTranslator(refreshRate -> Component.nullToEmpty(refreshRate.toString())); - - Option resolutionOption = new CyclingOption<>( - Component.translatable("options.fullscreen.resolution"), - VideoModeManager.getVideoResolutions(), - (value) -> { - VideoModeManager.selectedVideoMode = value.getVideoMode(RefreshRate.getNewValue()); - VideoModeManager.applySelectedVideoMode(); - - if (minecraftOptions.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())); - - resolutionOption.setOnChange(() -> { - var newVideoMode = resolutionOption.getNewValue(); - var newRefreshRates = newVideoMode.getRefreshRates().toArray(new Integer[0]); - - RefreshRate.setValues(newRefreshRates); - RefreshRate.setNewValue(newRefreshRates[newRefreshRates.length - 1]); - }); - - return new OptionBlock[]{ - new OptionBlock("", new Option[]{ - resolutionOption, - RefreshRate, - new SwitchOption(Component.translatable("options.fullscreen"), - value -> { - minecraftOptions.fullscreen().set(value); -// window.toggleFullScreen(); - fullscreenDirty = true; - }, - () -> minecraftOptions.fullscreen().get()), - new SwitchOption(Component.translatable("vulkanmod.options.windowedFullscreen"), - value -> { - config.windowedFullscreen = value; - fullscreenDirty = true; - }, - () -> config.windowedFullscreen), - 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); - window.setFramerateLimit(value); - }, - () -> minecraftOptions.framerateLimit().get()), - new SwitchOption(Component.translatable("options.vsync"), - value -> { - minecraftOptions.enableVsync().set(value); - window.updateVsync(value); - }, - () -> minecraftOptions.enableVsync().get()), - }), - 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())), - 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)), - }), - new OptionBlock("", new Option[]{ - new SwitchOption(Component.translatable("options.viewBobbing"), - (value) -> minecraftOptions.bobView().set(value), - () -> minecraftOptions.bobView().get()), - new CyclingOption<>(Component.translatable("options.attackIndicator"), - AttackIndicatorStatus.values(), - value -> minecraftOptions.attackIndicator().set(value), - () -> minecraftOptions.attackIndicator().get()) - .setTranslator(value -> Component.translatable(value.getKey())), - new SwitchOption(Component.translatable("options.autosaveIndicator"), - value -> minecraftOptions.showAutosaveIndicator().set(value), - () -> minecraftOptions.showAutosaveIndicator().get()), - }) - }; - } - - 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()), - new RangeOption(Component.translatable("options.simulationDistance"), - 5, 32, 1, - (value) -> minecraftOptions.simulationDistance().set(value), - () -> minecraftOptions.simulationDistance().get()), - new CyclingOption<>(Component.translatable("options.prioritizeChunkUpdates"), - PrioritizeChunkUpdates.values(), - value -> minecraftOptions.prioritizeChunkUpdates().set(value), - () -> minecraftOptions.prioritizeChunkUpdates().get()) - .setTranslator(value -> Component.translatable(value.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 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 CyclingOption<>(Component.translatable("options.renderClouds"), - CloudStatus.values(), - value -> minecraftOptions.cloudStatus().set(value), - () -> minecraftOptions.cloudStatus().get()) - .setTranslator(value -> Component.translatable(value.getKey())), - 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) - .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(Component.translatable("vulkanmod.options.ao.subBlock.tooltip")), - 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()), - }), - new OptionBlock("", new Option[]{ - new SwitchOption(Component.translatable("options.entityShadows"), - value -> minecraftOptions.entityShadows().set(value), - () -> minecraftOptions.entityShadows().get()), - new RangeOption(Component.translatable("options.entityDistanceScaling"), - 50, 500, 25, - value -> minecraftOptions.entityDistanceScaling().set(value * 0.01), - () -> minecraftOptions.entityDistanceScaling().get().intValue() * 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())) - }) - }; - } - - public static OptionBlock[] getOptimizationOpts() { - return new OptionBlock[]{ - 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) { - 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(Component.translatable("vulkanmod.options.advCulling.tooltip")), - new SwitchOption(Component.translatable("vulkanmod.options.entityCulling"), - value -> config.entityCulling = value, - () -> config.entityCulling) - .setTooltip(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(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")), - new SwitchOption(Component.translatable("vulkanmod.options.indirectDraw"), - value -> config.indirectDraw = value, - () -> config.indirectDraw) - .setTooltip(Component.translatable("vulkanmod.options.indirectDraw.tooltip")), - }) - }; - - } - - public static OptionBlock[] getOtherOpts() { - return new OptionBlock[]{ - new OptionBlock("", new Option[]{ - new RangeOption(Component.translatable("vulkanmod.options.frameQueue"), - 2, 5, 1, - value -> { - config.frameQueueSize = value; - Renderer.scheduleSwapChainUpdate(); - }, () -> config.frameQueueSize) - .setTooltip(Component.translatable("vulkanmod.options.frameQueue.tooltip")), - 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(Component.nullToEmpty("%s: %s".formatted( - Component.translatable("vulkanmod.options.deviceSelector.tooltip").getString(), - DeviceManager.device.deviceName - )) - ) - }) - }; - - } -} diff --git a/src/main/java/net/vulkanmod/config/option/RangeOption.java b/src/main/java/net/vulkanmod/config/option/RangeOption.java deleted file mode 100644 index 1a3270cb3..000000000 --- a/src/main/java/net/vulkanmod/config/option/RangeOption.java +++ /dev/null @@ -1,49 +0,0 @@ -package net.vulkanmod.config.option; - -import net.minecraft.network.chat.Component; -import net.minecraft.util.Mth; -import net.vulkanmod.config.gui.widget.OptionWidget; -import net.vulkanmod.config.gui.widget.RangeOptionWidget; - -import java.util.function.Consumer; -import java.util.function.Function; -import java.util.function.Supplier; - -public class RangeOption extends Option { - int min; - int max; - int step; - - 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; - } - - 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 createOptionWidget(int x, int y, int width, int height) { - return new RangeOptionWidget(this, x, y, width, height, this.name); - } - - public Component getName() { - return Component.nullToEmpty(this.name.getString() + ": " + this.getNewValue().toString()); - } - - public float getScaledValue() { - float value = this.getNewValue(); - - return (value - this.min) / (this.max - this.min); - } - - public void setValue(float f) { - double n = Mth.lerp(f, min, max); - - n = this.step * Math.round(n / this.step); - - this.setNewValue((int) n); - } -} diff --git a/src/main/java/net/vulkanmod/config/option/SwitchOption.java b/src/main/java/net/vulkanmod/config/option/SwitchOption.java deleted file mode 100644 index aa4fd84b0..000000000 --- a/src/main/java/net/vulkanmod/config/option/SwitchOption.java +++ /dev/null @@ -1,20 +0,0 @@ -package net.vulkanmod.config.option; - -import net.minecraft.network.chat.Component; -import net.vulkanmod.config.gui.widget.OptionWidget; -import net.vulkanmod.config.gui.widget.SwitchOptionWidget; - -import java.util.function.Consumer; -import java.util.function.Supplier; - -public class SwitchOption extends Option { - public SwitchOption(Component name, Consumer setter, Supplier getter) { - super(name, setter, getter, i -> Component.nullToEmpty(String.valueOf(i))); - } - - @Override - public OptionWidget createOptionWidget(int x, int y, int width, int height) { - return new SwitchOptionWidget(this, x, y, width, height, this.name); - } - -} diff --git a/src/main/java/net/vulkanmod/config/video/VideoModeManager.java b/src/main/java/net/vulkanmod/config/video/VideoModeManager.java index 985455a63..6df62f3c5 100644 --- a/src/main/java/net/vulkanmod/config/video/VideoModeManager.java +++ b/src/main/java/net/vulkanmod/config/video/VideoModeManager.java @@ -1,96 +1,66 @@ package net.vulkanmod.config.video; -import net.vulkanmod.Initializer; +import com.mojang.blaze3d.platform.VideoMode; +import com.mojang.blaze3d.platform.Window; +import net.minecraft.client.Minecraft; +import org.jetbrains.annotations.NotNull; import org.lwjgl.glfw.GLFW; import org.lwjgl.glfw.GLFWVidMode; -import java.util.ArrayList; +import java.util.Collections; import java.util.List; - -import static org.lwjgl.glfw.GLFW.*; +import java.util.Optional; public abstract class VideoModeManager { - private static VideoModeSet.VideoMode osVideoMode; - private static VideoModeSet[] videoModeSets; - - public static VideoModeSet.VideoMode selectedVideoMode; + private static final Window window = Minecraft.getInstance().getWindow(); + private static final List videoModes = populateVideoModes(GLFW.glfwGetPrimaryMonitor()); + private static final VideoMode osVideoMode = getCurrentVideoMode(GLFW.glfwGetPrimaryMonitor()); - public static void init() { - long monitor = glfwGetPrimaryMonitor(); - osVideoMode = getCurrentVideoMode(monitor); - videoModeSets = populateVideoResolutions(GLFW.glfwGetPrimaryMonitor()); + public static VideoMode getSelectedVideoMode() { + return window.getPreferredFullscreenVideoMode().orElse(getOsVideoMode()); } - public static void applySelectedVideoMode() { - Initializer.CONFIG.videoMode = selectedVideoMode; + public static void setSelectedVideoMode(VideoMode videoMode) { + window.setPreferredFullscreenVideoMode(Optional.of(videoMode)); } - public static VideoModeSet[] getVideoResolutions() { - return videoModeSets; + public static List getVideoModes() { + return videoModes; } - public static VideoModeSet getFirstAvailable() { - if(videoModeSets != null) - return videoModeSets[videoModeSets.length - 1]; - else - return VideoModeSet.getDummy(); + public static VideoMode getFirstAvailable() { + return videoModes.getLast(); } - public static VideoModeSet.VideoMode getOsVideoMode() { + public static VideoMode getOsVideoMode() { return osVideoMode; } - public static VideoModeSet.VideoMode getCurrentVideoMode(long monitor){ + public static @NotNull 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()); + throw new NullPointerException("Unable to get current VideoMode"); + int retinaScale = Minecraft.ON_OSX ? 2 : 1; + return new VideoMode( + vidMode.width() * retinaScale, + vidMode.height() * retinaScale, + vidMode.redBits(), + vidMode.greenBits(), + vidMode.blueBits(), + vidMode.refreshRate()); } - public static VideoModeSet[] populateVideoResolutions(long monitor) { - GLFWVidMode.Buffer buffer = GLFW.glfwGetVideoModes(monitor); - - List videoModeSets = new ArrayList<>(); - - int currWidth = 0, currHeight = 0, currBitDepth = 0; - VideoModeSet videoModeSet = null; - - 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(); - - if (currWidth != width || currHeight != height || currBitDepth != bitDepth) { - currWidth = width; - currHeight = height; - currBitDepth = bitDepth; - - videoModeSet = new VideoModeSet(currWidth, currHeight, currBitDepth); - videoModeSets.add(videoModeSet); - } - - videoModeSet.addRefreshRate(refreshRate); - } - - VideoModeSet[] arr = new VideoModeSet[videoModeSets.size()]; - videoModeSets.toArray(arr); - - return arr; - } - - public static VideoModeSet getFromVideoMode(VideoModeSet.VideoMode videoMode) { - for (var set : videoModeSets) { - if (set.width == videoMode.width && set.height == videoMode.height) - return set; - } - - return null; + public static @NotNull List populateVideoModes(long monitor) { + GLFWVidMode.Buffer videoModes = GLFW.glfwGetVideoModes(monitor); + if (videoModes == null) return Collections.emptyList(); + + return videoModes.stream() + .filter(mode -> { + int bitDepth = mode.redBits(); + return bitDepth >= 8 && mode.greenBits() == bitDepth && mode.blueBits() == bitDepth; + }) + .map(VideoMode::new) + .toList(); } } diff --git a/src/main/java/net/vulkanmod/config/video/VideoModeSet.java b/src/main/java/net/vulkanmod/config/video/VideoModeSet.java deleted file mode 100644 index e4fb34fac..000000000 --- a/src/main/java/net/vulkanmod/config/video/VideoModeSet.java +++ /dev/null @@ -1,95 +0,0 @@ -package net.vulkanmod.config.video; - -import it.unimi.dsi.fastutil.objects.ObjectArrayList; - -import java.util.List; - -public class VideoModeSet { - public final int width; - public final int height; - public final int bitDepth; - List refreshRates = new ObjectArrayList<>(); - - public static VideoModeSet getDummy() { - var set = new VideoModeSet(-1, -1, -1); - set.addRefreshRate(-1); - return set; - } - - public VideoModeSet(int width, int height, int bitDepth) { - this.width = width; - this.height = height; - this.bitDepth = bitDepth; - } - - public int getRefreshRate() { - return this.refreshRates.get(0); - } - - 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; - } - - @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 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 + ']'; - } - - } - -} diff --git a/src/main/java/net/vulkanmod/mixin/screen/OptionsScreenM.java b/src/main/java/net/vulkanmod/mixin/screen/OptionsScreenM.java index e3f20791b..14ab74233 100644 --- a/src/main/java/net/vulkanmod/mixin/screen/OptionsScreenM.java +++ b/src/main/java/net/vulkanmod/mixin/screen/OptionsScreenM.java @@ -4,7 +4,7 @@ import net.minecraft.client.gui.screens.Screen; import net.minecraft.client.gui.screens.options.OptionsScreen; import net.minecraft.network.chat.Component; -import net.vulkanmod.config.gui.VOptionScreen; +import net.vulkanmod.config.gui.ConfigScreen; import org.spongepowered.asm.mixin.Final; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Shadow; @@ -25,6 +25,6 @@ protected OptionsScreenM(Component title) { @Inject(method = "method_19828", at = @At("HEAD"), cancellable = true) private void injectVideoOptionScreen(CallbackInfoReturnable cir) { - cir.setReturnValue(new VOptionScreen(Component.literal("Video Setting"), this)); + cir.setReturnValue(new ConfigScreen(this)); } } diff --git a/src/main/java/net/vulkanmod/mixin/window/WindowMixin.java b/src/main/java/net/vulkanmod/mixin/window/WindowMixin.java index 72ef9ae12..438055d97 100644 --- a/src/main/java/net/vulkanmod/mixin/window/WindowMixin.java +++ b/src/main/java/net/vulkanmod/mixin/window/WindowMixin.java @@ -5,9 +5,8 @@ import net.vulkanmod.Initializer; import net.vulkanmod.config.Config; import net.vulkanmod.config.Platform; +import net.vulkanmod.config.gui.ConfigScreenPages; import net.vulkanmod.config.video.VideoModeManager; -import net.vulkanmod.config.option.Options; -import net.vulkanmod.config.video.VideoModeSet; import net.vulkanmod.vulkan.Renderer; import net.vulkanmod.vulkan.VRenderSystem; import net.vulkanmod.vulkan.Vulkan; @@ -23,41 +22,60 @@ import org.spongepowered.asm.mixin.injection.Redirect; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import java.util.List; + import static org.lwjgl.glfw.GLFW.*; @Mixin(Window.class) public abstract class WindowMixin { - @Final @Shadow private long window; - - @Shadow private boolean vsync; - - @Shadow protected abstract void updateFullscreen(boolean bl); - - @Shadow private boolean fullscreen; - - @Shadow @Final private static Logger LOGGER; - - @Shadow private int windowedX; - @Shadow private int windowedY; - @Shadow private int windowedWidth; - @Shadow private int windowedHeight; - @Shadow private int x; - @Shadow private int y; - @Shadow private int width; - @Shadow private int height; + @Shadow + @Final + private static Logger LOGGER; + @Final + @Shadow + private long window; + @Shadow + private boolean vsync; + @Shadow + private boolean fullscreen; + @Shadow + private int windowedX; + @Shadow + private int windowedY; + @Shadow + private int windowedWidth; + @Shadow + private int windowedHeight; + @Shadow + private int x; + @Shadow + private int y; + @Shadow + private int width; + @Shadow + private int height; + @Shadow + private int framebufferWidth; + @Shadow + private int framebufferHeight; + private boolean wasOnFullscreen = false; - @Shadow private int framebufferWidth; - @Shadow private int framebufferHeight; + @Shadow + protected abstract void updateFullscreen(boolean bl); - @Shadow public abstract int getWidth(); + @Shadow + public abstract int getWidth(); - @Shadow public abstract int getHeight(); + @Shadow + public abstract int getHeight(); @Redirect(method = "", at = @At(value = "INVOKE", target = "Lorg/lwjgl/glfw/GLFW;glfwWindowHint(II)V")) - private void redirect(int hint, int value) { } + private void redirect(int hint, int value) { + } @Redirect(method = "", at = @At(value = "INVOKE", target = "Lorg/lwjgl/glfw/GLFW;glfwMakeContextCurrent(J)V")) - private void redirect2(long window) { } + private void redirect2(long window) { + } @Redirect(method = "", at = @At(value = "INVOKE", target = "Lorg/lwjgl/opengl/GL;createCapabilities()Lorg/lwjgl/opengl/GLCapabilities;")) private GLCapabilities redirect2() { @@ -102,7 +120,7 @@ public void updateVsync(boolean vsync) { @Overwrite public void toggleFullScreen() { this.fullscreen = !this.fullscreen; - Options.fullscreenDirty = true; + ConfigScreenPages.fullscreenDirty = true; } /** @@ -112,14 +130,12 @@ public void toggleFullScreen() { public void updateDisplay() { RenderSystem.flipFrame(this.window); - if (Options.fullscreenDirty) { - Options.fullscreenDirty = false; + if (ConfigScreenPages.fullscreenDirty) { + ConfigScreenPages.fullscreenDirty = false; this.updateFullscreen(this.vsync); } } - private boolean wasOnFullscreen = false; - /** * @author */ @@ -129,42 +145,41 @@ private void setMode() { long monitor = GLFW.glfwGetPrimaryMonitor(); if (this.fullscreen) { - { - VideoModeSet.VideoMode videoMode = config.videoMode; - - boolean supported; - VideoModeSet set = VideoModeManager.getFromVideoMode(videoMode); - - if (set != null) { - supported = set.hasRefreshRate(videoMode.refreshRate); - } - else { - supported = false; - } - - if(!supported) { - LOGGER.error("Resolution not supported, using first available as fallback"); - videoMode = VideoModeManager.getFirstAvailable().getVideoMode(); - } - - if (!this.wasOnFullscreen) { - this.windowedX = this.x; - this.windowedY = this.y; - this.windowedWidth = this.width; - this.windowedHeight = this.height; - } - - this.x = 0; - this.y = 0; - this.width = videoMode.width; - this.height = videoMode.height; - GLFW.glfwSetWindowMonitor(this.window, monitor, this.x, this.y, this.width, this.height, videoMode.refreshRate); - - this.wasOnFullscreen = true; + VideoMode videoMode = VideoModeManager.getSelectedVideoMode(); + + boolean supported; + if (VideoModeManager.getVideoModes() != null) { + VideoMode finalVideoMode = videoMode; + supported = VideoModeManager.getVideoModes().stream() + .mapToInt(VideoMode::getRefreshRate) + .filter(refreshRate -> refreshRate == finalVideoMode.getRefreshRate()) + .findAny() + .isPresent(); + } else { + supported = false; } - } - else if (config.windowedFullscreen) { - VideoModeSet.VideoMode videoMode = VideoModeManager.getOsVideoMode(); + + if (!supported) { + LOGGER.error("Resolution not supported, using first available as fallback"); + videoMode = VideoModeManager.getFirstAvailable(); + } + + if (!this.wasOnFullscreen) { + this.windowedX = this.x; + this.windowedY = this.y; + this.windowedWidth = this.width; + this.windowedHeight = this.height; + } + + this.x = 0; + this.y = 0; + this.width = videoMode.getWidth(); + this.height = videoMode.getHeight(); + GLFW.glfwSetWindowMonitor(this.window, monitor, this.x, this.y, this.width, this.height, videoMode.getRefreshRate()); + + this.wasOnFullscreen = true; + } else if (config.windowedFullscreen) { + VideoMode videoMode = VideoModeManager.getOsVideoMode(); if (!this.wasOnFullscreen) { this.windowedX = this.x; @@ -173,8 +188,8 @@ else if (config.windowedFullscreen) { this.windowedHeight = this.height; } - int width = videoMode.width; - int height = videoMode.height; + int width = videoMode.getWidth(); + int height = videoMode.getHeight(); GLFW.glfwSetWindowAttrib(this.window, GLFW_DECORATED, GLFW_FALSE); GLFW.glfwSetWindowMonitor(this.window, 0L, 0, 0, width, height, -1); @@ -205,7 +220,7 @@ private void onFramebufferResize(long window, int width, int height) { int prevWidth = this.getWidth(); int prevHeight = this.getHeight(); - if(width > 0 && height > 0) { + if (width > 0 && height > 0) { this.framebufferWidth = width; this.framebufferHeight = height; // if (this.framebufferWidth != prevWidth || this.framebufferHeight != prevHeight) { @@ -227,8 +242,7 @@ private void onResize(long window, int width, int height) { this.width = width; this.height = height; - if(width > 0 && height > 0) + if (width > 0 && height > 0) Renderer.scheduleSwapChainUpdate(); } - } diff --git a/src/main/java/net/vulkanmod/render/profiling/ProfilerOverlay.java b/src/main/java/net/vulkanmod/render/profiling/ProfilerOverlay.java index 8fec0f550..cca9e289f 100644 --- a/src/main/java/net/vulkanmod/render/profiling/ProfilerOverlay.java +++ b/src/main/java/net/vulkanmod/render/profiling/ProfilerOverlay.java @@ -90,7 +90,7 @@ public void render(GuiGraphics guiGraphics) { if (!Strings.isNullOrEmpty(line)) { int yPosition = xOffset + lineHeight * i; GuiRenderer.drawString( - this.font, Component.literal(line), + Component.literal(line), xOffset, yPosition, textColor, false); } diff --git a/src/main/resources/assets/vulkanmod/Vlogo.png b/src/main/resources/assets/vulkanmod/Vlogo.png deleted file mode 100644 index 1d45783f9c0c84bcd196262afb6e2bb541dcdafd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 31960 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 literal 85962 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

mlHOf6!EWLOe7)ea%jF4{Kgm=sRenn{sw0WNa&F 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®d;zh$d%)NRmQ~gXg_GfGUd~sokG+g_`Z+@QXoZI$J_Qjeg;;=yscWpt#y{KTNR=8i|p?o6Mxx9Q%|Ho^eLu*Gfn4f%jo4!S^+q}H- zuchcdAuUV3SW(;z9paXe8b|+#PTorf#5Fuyu8w$b>%_86t^w=BWf%Uo?k?n5>u}*7 ztJ-pXRxwS;dg#Tcgz;AD65g(z?H{trL86PYYe1TAz*>6r`!IHB?YI4zQlH#-_5;4f z1=@WLn#K79sgFpG6I<29ZXocMo=$t!s-C?VIFOL(O7)CUfsD|6^=mT0ao?EZT=-^% zBjd>HuGwdRjz?xx6%aB|DN)+_z~+7H(^D%9-*16xH?ynh+WZ5)J@fp8eyNyq* zd?CT5i0AwY{avL$Z0*fjlBDA$U@I@Cc@wTPPo1@(Hsy3H^S+;8|JdV|<#CoeiT(Pg z4?bi;M?ihhU@Gu{3j8y=#LYqNY{A3*A=-iwzh{u=S#&$a(;e4{+fvAQsfi$79Xt(2=kbH{+-EMGNIcgI zGW)ajh!XLfms}!sZSvDQ^ZWCHvUVK@M-{x2@_Q{WGk~LnASb|L}2HMPz^(NhMRzvX*E1n}u~e z`UW+Yc%#&XO6mJ9JbOxcN9M$gfTG_Q3Vn{mbhgf4wPgEa8JJe*=YM{3Zd1tfsyZKh zs0ieibrOhHap^okw6IoJ zbj;MCl#4_z05ZU0&ZQOwFn)q@;*+=khcCNi!`^&)u9|p`Ki{v*`lOleD(~8I%vPkx zCH61z7<8ld7?6R9wq3%G3`lMIfteeyd@Dme0T!$zr2JQC;f+hqEKSPiHyOn%atYb_ z1v2-v?Vu8|k4<9}soygjV`)hsMBgt>uZ3H%il=ZJVY}h2X??`kYUN?OC37N)?T&L~ zOGF!Ol7yZQwnR~SWbcp4^4#8DxA~Fb>pY=nMVDJwJ?84G+{eTV=%)>VZQBlfCH_?) zqd1OiJL{{wdg5tNlAe&i9mdB)22NgrL`)>L@IYIF!P?M$*BXjO5mHW9(}y=~*t|xV z0c9Co;D5Pf_3*ON<(ZIJ$bpW*m|tJ)6iOb%jIAyI+vy@sdnMgo7}JonK}l$P#ke+b z{E2n#p_s~brIp^*$vX5!2+Qd(LM4{rJO3+@j88nw ze^vFotNWK$_NVyth5glwS0+=-ByB16ngjLaeGlyVPANh0=mFK& zA-Qo$J^=?I$s*=_c5Eb(zq9oPXZ%9F=++NNC8Y zDA^6WIB5@&vND@SLc>Vm|GDmS?xDVK|G!tBS5LQd-`910_P$g$xCx`G; zkvRH3$g^`o*QK?FLA;@$H*#FA?X+K?2U^np*;cP3#@e~6p|S z3Xq$xk`*DRAX@=u9e+;p)u&U_6ZplpJ}xhfk81_5`KoNj=DnfYYLs2s%@)F&3+h^& zPgPWxkR@?+8wg$ABSzw(&vR}4R!a8#JFdC@5yxj0JDuyfR-(oy9_Kf4nRzrhX59^3 za@2^`^>!RYVlwB<<^d?|VRyV5m;bWR^l9#)AFh^tCL?&;Kx&+M>7jyefjR%cQ4CjMl`_Oe z=(Ek$;<$!8<0io3Y_Z?0^}XE}4;M3a#)b5s>}ML`mx0-h2LZpiCIv6rR05+c`>ffl zL;MY(Q;4f9^3H?T zEwTi;G6E5v4C)Arwvb`wIZ84~A9^&vqC#feji#KOX!A0#WgWX)BRP4oV=JJpTioGd6GFOOGzz;aQsk?t`8z#eI+kO=jE_ zpowEaQaXG07H=P4FB9B-XxIGpqbbAnj;uX%PG8=0`3E_Sr_u?f1!WqS!i-N5raRQ) z0RA#Ox&$*TU|xs`=bPfohm~V_B8qRZYUlE&N67IX)Pfjto0 zqn0cO93@d8Z9LaHp|)ckeG$Wf?cd#Qt*1vxg7@(B^E(1gBS4=_#0Po83GDK@jORY` zdaR?Wus`ZNxkAv&k_NvNrEeP8dZ zDC_E#5WF#Lc=KbY$(>J694q$W?7=q51*|tuHWLhMfWvcm_5E*;Z8|!#3C#4F5@BIy zm9IsF$;o$Vv-NoXw*K{wQIjU_D8|oH2*8taP5*{(aD|k0UChWkV$iGyZNA^vX{z`B z?DjwF3!E@umVtPITd`OUF9U@55OB@aH~$cpq3krb%^to!``i7dmMKAB**1oO{)-5s zd-FB-fJho8mRPZPRKd4@aOp(j?EzrEYCar=lism0XIpD$W~v^BX%U+m&dtbI?S1ep zZEGYwqCchHQBOw6hSQt%V`LIh4w(f2sO!5)gg3_uMAXfuur)GGwhdS3`dt3Ca>>T! zy;<@WXB0y&f`kiPb8{Pxb~eZREBV;y%+gI0YOM zGxJp3A>$I9BHgqGe29Twy|m~bnGFF_s|HkMICt6A_3-`is|LJSIAWO}6u^iv&T7SdRU@!I-KL&25$BC% z^jG*FP=?vABPBkL-Pj-l(H&ODn|Sfi75Ex(dn6bZSH`H~^6sd7dgjeV`O7qq8K)aa zuojy)92xkjbrQLnt48uX^p#?yK97W=zR^fXIi4w3Y0@T;2Zh`7Xl>&wi7Ge7i?Jbb z>eUZY7}F%#vyh~!KHI)%`mE3#B zy>Gtd4(5resOIx^ zylkMs2IX{Ij)D0KF7A|ZUl;Mt(rv)F>hGHpZQ}1*i8?^z{56RNErtP@Mk?1L9dO!` zoyHck)|U@o4wLFtjQKXUd524F0%*KwhRc-t$*Tss4mtlrVA>X>toPD$gzHZAJ5Ma@WNw|s?u*>@-br|UIaIV*0~LDgs=;F~?0IqU zW~z+(GC*U=$_?F*>V3wd8e6dHPX1xl{k8S!>ABuU7AwImvs=&GG|*I5zJXts@%u~t zN{zQm>Dwgsr>v)hfY}{8L`3a^8N_L}ik0unjN_jE5_D3!$vn2`zDs09i}_s#RG@DZ z@1ImL!P)i|5t@TCMK%ab?QyMS5v(;YJo2?!y*NG#z`L=o5rmw*#P4TGT9!-1YPN?i zj>vbA2%Ev~v$9=hs)Gky{@h9&l3Q|{V&oL>=tatNVyEzM1Mk*r7Rav{Tg$_o7EoT- zJRkSt1P@V}c{E#H)JQF4fjdn7rXPh)(sWDa?XT}Vq}q*pJvT3zA)~aG?|A&(#zb<~ zupVG?%H0lV!xy~FISgLg0DqfWl z43(0T=SJxd zJoGw|5Wzn{kCBrL2HZSstZmE8bNXs)vgA!PR9iySP%EvVeUXm6=5;da;sOZBbKh%hZ#n9 z_C0p3*oS)~aQ=UR6Cc!&bJXXhL(J|4Un6}f%rjyx{eHhf7t$QzX>lMzjhlirS^6tL z{GD7@iPt1?IsO3CK1(Y+48nJUEu7@b4k-Yg)~{(I0@GLI)`5GGd9h^z<0-g>%9n!^alo^7BBA*l zAEJdtfb>PM2oUW5GLo3Q$jt{JE~Gy&oe~#fHLYoQQfGEUI0g#ov5_b~2BuNP-EbSI zalgQji{n1pThGUt94bgd8x?UYicP?CCqycdl#>z{vi@pLQ2@G+>z(s^XRlg z&wDVhE8()vw&28pe`t^VCM8636y1LHv^B-}4Yd2Ue|#%^1%~B;Q}IMQ)8-|Pyc^)4 zD*8NaNpN7!nQqiKW%+RbaW_52@=6&X%}SRf$`NET9YDT@OgEzP_V{46N2aBZ@Miy5 zuQhJ&-Ya@5RuF>fPgom80IX4X0%Ly*6TV}>N;EzLxzlhoqho-YnNcSA7unf(bS^tU z5Q^Y9@n-c|-I;zgC6>JCtqHdFa*9X`p1hD%V1G3>S`aQU5ywKr*-m8PRRVIdGayB0 zZ8!!OibrP)LU1)*Nf=E8fW8#>rzsIOeZyQ>!2ep=39$seMYH0A>ujgKQcG0 zRs3pg-ImOa=;!QEu4nJj*UlM)7+eSn$mP;U3w)ovR)$%s%Lx!d#V+-#LRs^CY)F69 zm-kT0FO*c-fJS%kwB5o7obkLTby4#&W(l$vI>7NKOSUZ{7MU~4j&?giI1w0a^oDc@ z{lyhiRY5Gv0XV&!nC}L5tr5tz*pudXUXpztuzo8j=?r?+0CEP(88=&Sn~$~A5R7u= zRTa27#M0jg0j}n*N+s*B<%8DYwQ{Vqw`>??V4;l zO2A35opEqln}l5Geu0~6>c%%_d1&CtHn4@#fOre)%;a&Q zG2Q~Qk#-G}Ere71Ep+(B9V?;95=S0uJva&W6=k@HXU+kSOi^r7_J9H6NVw_>W%(8? zEv~J`GhOi153KoLotbzS02hWK&sOV}sXVMKWQ4t9asHnT)bR`hq*ZoqrM0fdx5Z|y zh2-9subs{7vvbD?*4_;P?4s+AUHDGG$OCgVq_>7 za3BdgN;K>&u+_i6QXLQg9~*)WO7p8yCcrs?F2mpTak44_?2;_* z*7f!8$_41@WYqn1DyZWH5p)HXZWx?-=EzNiJkc2qu_VN`Pla5oztQe{@TL=qF)vrS zmVP4k&r|?{zp!(Q6Je^vYVo8ZKl|OSnUWQb{fuvonHw=h9^sUs?+s2s`8RE9@E9+f z1sSQu7Wcw2_4P!u=iCJ+M934UrDtGThLV+_2*};1XNhQ1+ ztg#n&4tA}>KTwhgJfCzyMHhSE0HVH}k_Djg&cT)z9DrsUq1Y;@#x)n3WCeo z$(yZ~8P8pzMVG};Wy6Hf@x_+&EV<};n^7-U7yop zo@+c5_7U-`=&oz0`;FVHdeVwu4u}e{MFvF$bXeK9wN4scnSlL<^O^1!n7OtkfH(r< z1#slD(+^Hko{R-WziioU&w*BrTA?_&Fx#kaxMufSqA(CJU2+~!>KLX%a2{)=-3`wf z2G?2y6qy1L%q(o0Mt<1=a9Tmpwlv-mL40@0?jC_;c@8Qz%20S$35LlOcqdD@S)2IE zDNvZW>ymzg`=fn6nw*O>V+rDc9DE1^Vz3i&Tuta*L>!|D)bF*H!ecN{K*Y@|G?kFd zthIn2>t_O0Gy;?tH5c($29V2H4IzxPzUG)pd_6kb*T++#&#L$-ikBh=g7x13d|iww zk$^%-Z69&W2|x&my#fkbcWE}M-{=fOgeO0$iWR4Ih{AZg0Sd$ ztGtZ(5qxPb$m9ID6yhXCkj!seem!HR0I+kWHSBml4aP&0kP3pcYXqg@F<3Us0xLIK z>^2aN)hu7dRQ}DcKT`53n(6O_ZlSuCOP`H}M$c@BR+v9^Cn1mvc3#`VGU1FO07-y^ zb~7v65Pqzda#FZ$;Jmx`ICIu91k?%49?MD^^!*WF zUIIF{jT$p}2Vj2mIp-MqegotQ+E+-$kYFAUG12|{zK%pVO?C$RrwQ&)EE4-YJXzx7 zJfI#S8YhF*<~h9e8}Z;eP~nzNs|@s@V+YUi)#aB^gK!pidJyXG!8@bsMKy$k0EK+3 zIrPNzz*$)K_LJo8Jnwnk)T_MMCBc9atrunll9&pB{JHJxL`{>i0Xs+Mq(jU>x2!n- zvA6^7DM6)iD7@Xx4 zH2$(XH;fU&6HvIVn0j-1Am}_f?OIaPyanXn%dke#U?nrV&hH299gPKJIx(#&1Di2y zx-OiQ>0K`SdDicD56Dwm;T{Qthjdul9Z7E^WT;H<8!YncqFDn2- zSq{q_RJE-lmboXPZ!}_uE0(9TSj{g^rG}0;c1&1i>gyDeJ~j$U{z`&r4}e4AA~Tq1 z+Pf`__#QIC$*44QIGep{5iM5{Vc2e}h}O-qFsU9eFE}OdF8qAhIkEp}|MW@{4>X;TcM9715;ypwj&V-ovL?jqK5(oq(S6<&j z?2jiF#}!rEgzTor94yn@m;BT&DU(P8fg>bOx4o|w@D`&v0WeoEZ};_np0Og?aSgWj zd4>%|Pwuy$+%A$sm~>puC2(2(Q+=3;qmc2A30dBULZ*dVxnkB>-g+{fpmyeknRYav z>BlFMPkzk6^Sb_08wK&V=h6v&szvJeYl$U@G(pAWBv}VIl&H z0N6%`puEPOCY@dAZxSRWtnAG(H}X7K-;2oldcf^^z`f4IEmp+3UxB&acUnJy2l^;w zk`Yu%_`@(v0%QX+{a%0q~#X($Xa~J0~cRhT8zrdgi%20;26`bO^Y&c-A6Xjk{(=%e1d5V3feP0Ucp zm>|3fdOH)T`S-S-1ObA&&I5JbHu;u6j!7O%P|vndC*>G2czA7$#2Cf0;;=R|H@vn1 zu~;DJYq?y}a#Hgf88;Mu%H331aZ7NL3(5^Uo>Zy05Flb3A=_Y8tqIYy&-seVm5!yY z=OH>OImz`5ZK7IdkjwzK->BD06q#}P!+BZDe%nDINdF1>nw1#4Lz2B;6D8RR!Z8>P z$-$J%Erj)=WQ)|iwa@7>AorlBIL_Znoa*LC071{FnPf$Bf?EXtY1s(4QOd4H7p>9= z{oAz=B*CEjv467G7G)VIKq2AZ%?C8ng}(H9Wr#~XW9ir<1CYPRuI7lYkhzDzis3(Ik8LOt#4k5)9{1wIP`&EH~TncvoLZ! z&x9%sYM}nF1_>IEOGlBWS#LiI${CkJvh-I%U@>{p`(P3a$rr;IQ&y=XNY;&KV~TWR z#Au7!&Mf~kqxaykPQZ~5u)y~(QBed7uzF7Oy<>b9g0X%{5E}0Xlhi-S^C+K)lmnPo z!*Oso2uTgjW_qaY;YB7;GRLye<`*AQByORAKgS|_kbXXaqC(AGoD4<0$D?=o`n?qg z1sg{Hgx^xac`$~BsqZtUR3DO596JdSEvX1>hO`j+kjH{*mR)i4adLqYWD)P-@LP5! zcv`LfNksgC%NK=R43p`{bQ$(O?CVLIa9?wjpy1B+?t&xp5EZF9`hESkK6At|Eosy z&(-+UMA$q_n?ql$6FG)7iPkiH>tDF>Negf`B*Th-ZnjEu!>LDvz5~h;VxGLyC7&FS zf|`ga&@>d|DZLL1m}D>#8-H&cwuGQr>D2m5PKQlu)2W<$B2RdZg zlgD`tB$fJrymc0JlVXiQFVdV*b`S)3X#p<4F(|PQf%rM9I$Jmm6%c`*4;yt={mjol00B4)t28 zEiHOWAQ`&0z=IuGx^o+`&V95_t$!J;^OsqUZk`Zr$XsbdvDpH!sQn2nYR|hZme}|W z^m}Q+fvlHpByjhDxm7bW?KsJN`g#zxXGawYYQEq0xR8va+l_M zU6`bZ*bf5~%M8*3PM^nMfdK>=)_Oc}f4>wZ4S2~Q-fz0ZK!*zcQj;mHV(xOB5Y?>l zwwwT`9=S?14duM#{x+9cpZ4RFCqj1z$O7t{%c_Lh}z5D$os>oMVwDbe>3P1w;$#NgP~cEH#EFCM7=32a)8a z@fHhJ``Wvhv%8&M=CP;KI;twu8OtdNw#Vba*@wAs$r?81@R?wv*V=7fZ;UumBjWi%%JE;!eOEFGhu=Kb=W8t7O0|}tmH>Fsegs)ZAVT+*y50G9*n0@u z|FSLjLZ6-od3sOrpgbII@a!x&5KB)#iXv8K*fUpmfr-0kBdqj+4Y*lxrj@7_`yXZ} z^T%|7AE4%p3YP3Z7`Ho3=_dFImbHR_OlC0jHRJSi2oiN8UpX9x8i;g|-GxQh@y=oZ z$2l;%c9berlbX0B1LR6YVJh%8uL?%(|Lm@e_unxwhYIDNKr`P@P9ilI`vkmn)@*hd z4JNlt&-$&|#i+J7B+q#ROqv&|+wf#0>A4? zQKIbAfV7W0C0!y3YR+6NSKsmNL@Im&juq!$9PdsIof!gp=kgkbL#2@rikH&A5?;5t zQyQfM8!J&!FD{!nv8XI;w|CNfk~a*Y+I;SsChQo-oZjx$W7R0c1}%poZc;~8NJ4<} zPKgXbd1s)EMNW>|>(X-4hUUb&!3u>FlVQpNevD@zLFUeet0aH~u)Y`5;*_hdop`wb zwz)Ub9l<9D8>C<9t_A(QtG=$Bh5EKMnYY!?_4uwH^&$c%dl0Rrp!*#f%P0rDh&uA_ zEtrIf-(aczD!$hDD~PQ?$(b^!j?9G5q^MOg0mq=Xr=zYvCHd@_sj znhwM*g5Dh1OUi5u4utS`@NQnfqQc?aA8C*9(N*X~zJQnRi%xipIXpSc#Pq*P+!?5X zkM#$=(O;B8hB^N1O9a6!0~*~Bjdb9pgZ>|E#@qoG=w9oxoCNa$KxAXBR^AK_G@&%t zQ<_jfdH>P$2pmdp~3$NihWC5NGrIOp^=J<{n2mj>?2zdNTmD~$Of^2dpPL{+{VY$S{pdheAwHwCUsKG zaon(k+|?tG?v4pV(lAXD#pu+(C3R<$o)Ms~7W9Yb=g(x+ig9clN@2ZoG+_h5oCn`a zb&Lnpa}k?!M!6Wp<|V`fMt~$9%HMbgnY|sRfTXnZ)P;Ec0E=HYGbsT%dohLDW6mjs z;2d!H9`ESnd=?~O7Xh!8E7_1{DhD-#XWuc0Cc~>BE7p1UU`YT0C-VA1zWh?lnnvuC zH%&~T?ieO=TRqAoSm57U_clNhj$U=TY6Uh%t-LL_oH>@d`sX4E+gZi%R* z*ZpMg8!)BVya>Rrm)L8OXq05()yw0I{*!@^&f5f5Kt5cxNbap^9RNY) zXlbJS=C33H4F+ru=Xx}U`I2(|u@NNi?p>(@y__K|KrX+8-`$8Uqlx6pa2HCdl_1&vZq*g;Y_gP-aPnn(tT!U0#zB`fPFn{x#~cI zPGKgL8i#zycS2-)oVnVi*7C!`MiR1s!f|kN+q23P6lMxkf9nx$i-fE^_}p`Cs>Aw~ zuUwWS;wx+;s1R^FplOO<#sD;>AO=lJiaB#50j`|UBJDd%bc9#O3y696RIud^<&vFp zOoptmJxn%zw<0(+$B@fV_cAuj>out#xg1pWu&v9~lq8x^*^mo{H#$;?0A? zy2ua!`}~^~X|V$eXEvZb4=)rVfrc$tHj)qu1N~*a&)<|NBIK~uMI>@RY_wm2b&u4T ziaw#HN)+G}7jz~%QF;O4SfIAYYTv#2byK`#uOtM9*(m1iH*svQtJVq1Uxn~ev3|Doiv6L7}n%v zZSwC^xMNd=FeE35lzr`!6(cE$NEkQEWw?p2xv`S`QoJMt^dmN&&xQ07$VU^AH759^ zT-XQ~04oxix|RI>H74%rKi$`gNbxFo`U%ErJtTPG;XlY^E>W-|1)tFk6wZJ0@!Clm zZ#4+k1b4qk%gJttC6(RjvilB%oy;c_r&f;awh0$tsMYlspZ5RU(mgK1&UO(p_` z&uc^|@r9ISm$Hi{c^XD8N?9AgL>5vKSm;AtJLcl2gOwXSC>h7)AXWe5R-rBq4_G9D zfhyfAuIkN$hb57ix9{y`RiaG*4ei7THn+n13@MyZ{r0NcSP~9O+oL_kdU%jMIwJI4 zU>RJk8ka}PW)?Lg*R&1-F@rS0-&hCP9Timv09EIQY^^crg{)Ixm+#PPJl$LyJAs zOG52jwDBiOaV`A)1Q-+xT%uD*9zCisFK6$J4LJ^qoLFd4QYt~iLnvT40b84&k}kN% zQUIOp61c~|WAeLYK#qTx+%V>&lDONvOp47A%HO(9a`A?OG6M*2HP;UcZ_#+NuruS< zW11Amxm^*2@)vBYfe92SGQR{S%@Y{nuswk{YczI3<2#PH07ym;bIjf?aZ7>SS1l9w z4F3cCBIGt4<(u-2;V5` z85E?RYgcA4ofDf1@dTThk0==UEAvC++0h};9-Ou=IKVW;n$Bneo2PvJZqR}R&=i67 z&s3T&BEJK-!n_@qP-UP90S%887{?LtH2Sl&k;dB(^7X4%sy!VEaB5R65O64viD6sn z8v^4HSq=Fig(9m7GaJ+|P7k|B{)wdoH9FaN#SiwJG$AYjdz2AbK=K?0&=b?fjG}yK zbQyn<^T|5UV-GsMZ1Gd73BgiA++I_cNR+g4IZDE&9__MZs7hZeRP+I_vU({FhY0;T zVE|v@1cet6M#zah5!LiAC?p<`b|6cKApyCSjubh9=QFhhKS0-r+I%2P5}B` z&Y14)8DNsQ8j3YVnl}aGIt@bF z@vnp>gb27SWYycf3ZGQhC;7<;q#xB)G3U@n*);mrc>ma(SrK>CRABlewXTK za5-!s7D}Ty?DHr+DNiOq`d8|wZ(`6k^UXV1n*x%t5D2s#RG-S+I?b*5eUW>(?2qMK zjyljIWd<4A;H|k3lgYm}n<|eHoUi~aO!bPk9aLBgZKoit_IQsWZD1}(QP>t8087B_ zbG=!;(JHaBiWFp^`Ug>>{y_jp-fM<}xbf$seJrT8>HcO9*Rc^a$6*J7AVuB381~VZ zsFk&w0sw;&HYTDJ5Zi#$>HgF0zKk zF?^rX0O5LV`ENX6u1FEJ)EXAw1XS+3%8E=jgAc<{V4mK2a1Cqc95le}WU)JG{=&Yd zfr!#;%{@<*o)9XANh>>M6Kyk9W9{CP8vq*!`Wv$KoD501#ty+|WU1*JTpqJ;d29p< z=Vt_Y5T)lBd`{>>-aw4ypHW=|;Y3W>5NRx||CrKVXeFfJnB|fzK4Y z&r3jqW<=wcYuLHdI~hJ{N=1OUra5>hjn}*?%93KS()Am9>ZrVbt18cJ{kdiKgT}-7@MGjCfjjy#SCb`@A_JM>W8cETs;aXk)(t}tn?wHqi`<) z_+<9 zFZho#B^igyF$57YOxflMO!$TVneK)`<+Kor+XXutRK&_*rwhFJhW^>U7N{)Vchtx{ zsU*^XgAj*5y!G2V0V@~~nr<7R-Z7`*U70m_DYRU_hjA4aAUeHs#Di z)$xzO5xARZro!6{xH;09g^$TazuN|}yEA3$$|wW@VQ>X|6b1)dx;;nSVkD`W;53t9 zU+SDTiqpW32Jv+}I%)r6u;$*jra;<{SpVi-Brput7wD(RODS!bsrVX>fXFEvL7BYQxh z+&&8TZ;}URPKBykG^XX~#xbj6T~YDw`cBRA?p$F*j$ofCtb5Va>qr>|^!!pReE+0$ zoI9L+tpsxDqwdd?Ci`??6(~cIXc!drr||+%f38&O+Fgv^aUGgvXb>N{pQG}i^R8B) z{2&fcyXB~92j%6)Ev?=KT3W@(cb%qRWE$l)Y$ee@S1J?(WiOd#bO0j$Fcj%g6*b_0 z9|ioYy~8O4Si%v^$rGJ>L}oEKcHK+=$)fxjo#wAKXkg=kVRe5kkeSBEz4$mcZ1cZY z@oAN!MDr**(IQaDj@s%c_()4O^0Uc+8vIciL`K%p<_#3$2%8I21^j|dD^K!U8i&zz%xIWB7HC8rc8s`RM+E4Bp1n*Ap=Fx6$a{ppYGFrxo8->CM=^)DTFa#OkiL zLrIo*8QUdRyZnqk`|&+;NeR|~aqYC4_*Ak?A63Qxy*}&#B(pMRg~AAv7Vo!EvH@)) z!#?^AdRGD79qbRY455tC7S*pdOyZT*9`tA%WS7Dv@>AiRBR3Y2J76i^qx#CjML=OPT4w2$k&^m7!2=Z%jFFm_moE ze|Pr`|IXK^6~p5q8t7&9eSKH!2lDD?GW;iz9pPZ~FBDVYzG^*gL$L~tLO6?*G;Vtq z#2yQivEkQe2T=y$A@I)xbNFh_WN&%$tg+(}HQ76?*LUKzwD+6&zh9CW&_Wf}A*sOR zxp%G)aazgX8(0=qb>m(8zaE#( zv>(NofI-I#hR-PYPkg36NbvT7Bx>wqI${Sf>=u@f69eL8Y_)-V#^w&xAFK5HL-K31JfCBtw3 zW9rjplrpO+vYMTV>Mnv6cw4l@H(aahPzzs+4bj0Nz!*EaW8l-^Mn|xDbvQEFqs_*p z%(UyV8f+7>^ew=Cefz0$0&k$cuJ5+)-jyOWKj*7cuiU-zUu@+xW`<1M@UaTY@$6bt z&lD^3+OakJ8Z-Sl<}Q18Gqf`#X%^dqGgWP_1pW8 zMc(ax+HT*rf6DjHUl6%qMmNMO@twL$?xa6D*S%b~w&lVKP0fOx)s4%i-z6^KK)-{o zBXKu_kU&aMj|-Dv>ivGN^0rS1{S@+WT8S}xp?|^eufys`{dnz8RtN#~9*lMqcl)|~ zJ6t`03lXLcyQ8dF>(00C-pkBsW8SgF3U{pj?iuorn=8377`8IEVXn_BwMAsc7jebt zbeYAA3`Fm+S?3Ghjv8T`-D*yKiKUdy9m5w}7u!ssXd@S1No>;Gtf!OXn&yw&nVPz* ze|u_MAZ16a$GZGYz1!cns!$(RhVy?uIsIk9k%#*h1*(l^(ktYW70$HPeDmc7Q$1&f z!K~PZ*}LoylNn7e=O-h7v8{8A#!}mSzE0RVe%a3lTXQYDZ8+QCxll-#2G_UB`c_5e zkpV3@=03#+>js;zD}R1+t84$5b2v`5x=kLGyVlMsH(%1O zR{s1rMIG_ZiY?@Bd68d9k#Ar<;-OAg`w8owO+})Pg?y1nUhDs+& zFHQXGqCkCHeSCN^R>NbmAb!g;5tSZ*|%{ivrxh z`hYWM2=;~lVeb=pu%$JI_LV|YtQ4-UTfXs>2?cL2)XzRy?a=LW^=a#%Q_U7xxiz(P z^L)ydS&wZw@{7H{7qumP%{v#=Nc}x0HU@B_HMN+s3I{HfKiP11`jQJ@fAtj_e5-ZM z^pbO$UPIBQAaur-&{X#IKPRie0sYscR!=_X6r;H~V$#IPYki$W^XHUU+Zb-|-)P`) zbHt+FXG>G5M}#ZZ#bnP^Grr>^NH7Yzui$pkD{=CKyQ5cxpDnEWZgEewIO9QCTz+Y3 zPpyZu+8|h3!_6q}AFXatk6U?)Qz_SbIZHQ1vhGyf!^lF1`jQ_RMc)Je)1&^34VHws zTW+-OAf{>-(_YZ?O7z!h&#n>S%gPthhwu2+UuyZqJKo)DM6`Iqzk{G>XDsrd@ygO@ zGLMQ(jUvV}E!!`t!^?;J*)|6s`MkVp`$fo`lfJ+G)C zeSS}6liS_P&6<5~(d|AK&DFbl%ePN#`CPX;mHHJf=OJD4^@+Q+JjvG{%9z}P=B<_(Kq}_i??|&n^ ztivn5T6W}!cgtNTPtM$J`@5h@BP+Ac*Qc+}Cd0#Tnd)}3Vgp$nz6{yxzTBmh*c3dh zlDe?W>yl=jrQ)Osg`=ayzrGrC@9mrX7a3nvkKL23>uBG6_3lKuBLnw}3k!l59=N$K zO=Hk8F^~_Uz@aBpb{c}l5HzUlJOqs)Xbj1OAubr=f*~##;({SA7~+BF0hzo|eV2BHbxL}A2{=aYm>m1)-@{PmA UgI?yxY4G2wm1}iUwe14_AAr)*ssI20 diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index 595e357a3..846e5aa27 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -13,7 +13,7 @@ "sources": "https://github.com/xCollateral/VulkanMod" }, - "icon": "assets/vulkanmod/Vlogo.png", + "icon": "assets/vulkanmod/vlogo.png", "environment": "client", "entrypoints": {