From 5d72634c3dff4faa17618266e0a77c4db48c6039 Mon Sep 17 00:00:00 2001 From: douira Date: Sun, 15 Sep 2024 18:03:39 +0200 Subject: [PATCH] Add option for Fullscreen Resolution (#2642) The resolution controls would not fit in the allocated space, so the rendering of slider controls was changed to enable rendering the slider bar and the value text on separate lines. Co-authored-by: MeeniMc <68366846+MeeniMc@users.noreply.github.com> --- .../client/gui/SodiumGameOptionPages.java | 30 +++++++++-- .../sodium/client/gui/SodiumOptionsGUI.java | 4 ++ .../sodium/client/gui/options/OptionFlag.java | 1 + .../gui/options/control/ControlElement.java | 46 +++++++++++++++-- .../control/ControlValueFormatter.java | 14 +++++ .../gui/options/control/SliderControl.java | 51 +++++++++---------- 6 files changed, 111 insertions(+), 35 deletions(-) diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/gui/SodiumGameOptionPages.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/gui/SodiumGameOptionPages.java index b512dddc4e..9ff3265ce2 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/gui/SodiumGameOptionPages.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/gui/SodiumGameOptionPages.java @@ -2,15 +2,14 @@ import com.google.common.collect.ImmutableList; import com.mojang.blaze3d.pipeline.RenderTarget; +import com.mojang.blaze3d.platform.Monitor; +import com.mojang.blaze3d.platform.VideoMode; import com.mojang.blaze3d.platform.Window; import net.caffeinemc.mods.sodium.client.gl.arena.staging.MappedStagingBuffer; import net.caffeinemc.mods.sodium.client.gl.device.RenderDevice; import net.caffeinemc.mods.sodium.client.gui.options.*; import net.caffeinemc.mods.sodium.client.gui.options.binding.compat.VanillaBooleanOptionBinding; -import net.caffeinemc.mods.sodium.client.gui.options.control.ControlValueFormatter; -import net.caffeinemc.mods.sodium.client.gui.options.control.CyclingControl; -import net.caffeinemc.mods.sodium.client.gui.options.control.SliderControl; -import net.caffeinemc.mods.sodium.client.gui.options.control.TickBoxControl; +import net.caffeinemc.mods.sodium.client.gui.options.control.*; import net.caffeinemc.mods.sodium.client.gui.options.storage.MinecraftOptionsStorage; import net.caffeinemc.mods.sodium.client.gui.options.storage.SodiumOptionsStorage; import net.caffeinemc.mods.sodium.client.compatibility.workarounds.Workarounds; @@ -26,13 +25,16 @@ import java.util.ArrayList; import java.util.List; +import java.util.Optional; // TODO: Rename in Sodium 0.6 public class SodiumGameOptionPages { private static final SodiumOptionsStorage sodiumOpts = new SodiumOptionsStorage(); private static final MinecraftOptionsStorage vanillaOpts = new MinecraftOptionsStorage(); + private static final Window window = Minecraft.getInstance().getWindow(); public static OptionPage general() { + Monitor monitor = window.findBestMonitor(); List groups = new ArrayList<>(); groups.add(OptionGroup.createBuilder() @@ -90,6 +92,26 @@ public static OptionPage general() { } }, (opts) -> opts.fullscreen().get()) .build()) + .add(OptionImpl.createBuilder(int.class, vanillaOpts) + .setName(Component.translatable("options.fullscreen.resolution")) + .setTooltip(Component.translatable("options.fullscreen.resolution")) + .setControl(option -> new SliderControl(option, 0, null != monitor? monitor.getModeCount(): 0, 1, ControlValueFormatter.resolution())) + .setBinding((options, value) -> { + if (null != monitor) { + window.setPreferredFullscreenVideoMode(0 == value? Optional.empty(): Optional.of(monitor.getMode(value - 1))); + } + }, options -> { + if (null == monitor) { + return 0; + } + else { + Optional optional = window.getPreferredFullscreenVideoMode(); + return optional.map((videoMode) -> monitor.getVideoModeIndex(videoMode) + 1).orElse(0); + } + }) + .setImpact(OptionImpact.HIGH) + .setFlags(OptionFlag.REQUIRES_VIDEOMODE_RELOAD) + .build()) .add(OptionImpl.createBuilder(boolean.class, vanillaOpts) .setName(Component.translatable("options.vsync")) .setTooltip(Component.translatable("sodium.options.v_sync.tooltip")) diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/gui/SodiumOptionsGUI.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/gui/SodiumOptionsGUI.java index 876bdb44bd..d5ad5d5768 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/gui/SodiumOptionsGUI.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/gui/SodiumOptionsGUI.java @@ -355,6 +355,10 @@ private void applyChanges() { client.delayTextureReload(); } + if (flags.contains(OptionFlag.REQUIRES_VIDEOMODE_RELOAD)) { + client.getWindow().changeFullscreenVideoMode(); + } + if (flags.contains(OptionFlag.REQUIRES_GAME_RESTART)) { Console.instance().logMessage(MessageLevel.WARN, "sodium.console.game_restart", true, 10.0); diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/gui/options/OptionFlag.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/gui/options/OptionFlag.java index 8ea3e5b0bf..17568af5a5 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/gui/options/OptionFlag.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/gui/options/OptionFlag.java @@ -4,5 +4,6 @@ public enum OptionFlag { REQUIRES_RENDERER_RELOAD, REQUIRES_RENDERER_UPDATE, REQUIRES_ASSET_RELOAD, + REQUIRES_VIDEOMODE_RELOAD, REQUIRES_GAME_RESTART } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/gui/options/control/ControlElement.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/gui/options/control/ControlElement.java index 3df9998470..7f39e02eb7 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/gui/options/control/ControlElement.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/gui/options/control/ControlElement.java @@ -8,6 +8,7 @@ import net.minecraft.client.gui.GuiGraphics; import net.minecraft.client.gui.navigation.FocusNavigationEvent; import net.minecraft.client.gui.navigation.ScreenRectangle; +import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; public class ControlElement extends AbstractWidget { @@ -20,18 +21,28 @@ public ControlElement(Option option, Dim2i dim) { this.dim = dim; } + public int getContentWidth() { + return this.option.getControl().getMaxWidth(); + } + @Override public void render(GuiGraphics graphics, int mouseX, int mouseY, float delta) { String name = this.option.getName().getString(); - String label; - if ((this.hovered || this.isFocused()) && this.font.width(name) > (this.dim.width() - this.option.getControl().getMaxWidth())) { - name = name.substring(0, Math.min(name.length(), 10)) + "..."; + // add the star suffix before truncation to prevent it from overlapping with the label text + if (this.option.isAvailable() && this.option.hasChanged()) { + name = name + " *"; } + // on focus or hover truncate the label to never overlap with the control's content + if (this.hovered || this.isFocused()) { + name = truncateLabelToFit(name); + } + + String label; if (this.option.isAvailable()) { if (this.option.hasChanged()) { - label = ChatFormatting.ITALIC + name + " *"; + label = ChatFormatting.ITALIC + name; } else { label = ChatFormatting.WHITE + name; } @@ -49,6 +60,33 @@ public void render(GuiGraphics graphics, int mouseX, int mouseY, float delta) { } } + private @NotNull String truncateLabelToFit(String name) { + var suffix = "..."; + var suffixWidth = this.font.width(suffix); + var nameFontWidth = this.font.width(name); + var targetWidth = this.dim.width() - this.getContentWidth() - 20; + if (nameFontWidth > targetWidth) { + targetWidth -= suffixWidth; + int maxLabelChars = name.length() - 3; + int minLabelChars = 1; + + // binary search on how many chars fit + while (maxLabelChars - minLabelChars > 1) { + var mid = (maxLabelChars + minLabelChars) / 2; + var midName = name.substring(0, mid); + var midWidth = this.font.width(midName); + if (midWidth > targetWidth) { + maxLabelChars = mid; + } else { + minLabelChars = mid; + } + } + + name = name.substring(0, minLabelChars).trim() + suffix; + } + return name; + } + public Option getOption() { return this.option; } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/gui/options/control/ControlValueFormatter.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/gui/options/control/ControlValueFormatter.java index e72a682b1b..ee9a02d296 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/gui/options/control/ControlValueFormatter.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/gui/options/control/ControlValueFormatter.java @@ -1,5 +1,7 @@ package net.caffeinemc.mods.sodium.client.gui.options.control; +import com.mojang.blaze3d.platform.Monitor; +import net.minecraft.client.Minecraft; import net.minecraft.network.chat.Component; public interface ControlValueFormatter { @@ -7,6 +9,18 @@ static ControlValueFormatter guiScale() { return (v) -> (v == 0) ? Component.translatable("options.guiScale.auto") : Component.literal(v + "x"); } + static ControlValueFormatter resolution() { + Monitor monitor = Minecraft.getInstance().getWindow().findBestMonitor(); + return (v) -> { + if (null == monitor) { + return Component.translatable("options.fullscreen.unavailable"); + } else if (0 == v) { + return Component.translatable("options.fullscreen.current"); + } else { + return Component.literal(monitor.getMode(v - 1).toString().replace(" (24bit)","")); + } + }; + } static ControlValueFormatter fpsLimit() { return (v) -> (v == 260) ? Component.translatable("options.framerateLimit.max") : Component.translatable("options.framerate", v); } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/gui/options/control/SliderControl.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/gui/options/control/SliderControl.java index 402191ee52..11097c6cc6 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/gui/options/control/SliderControl.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/gui/options/control/SliderControl.java @@ -48,6 +48,7 @@ private static class Button extends ControlElement { private static final int THUMB_WIDTH = 2, TRACK_HEIGHT = 1; private final Rect2i sliderBounds; + private int contentWidth; private final ControlValueFormatter formatter; private final int min; @@ -75,16 +76,6 @@ public Button(Option option, Dim2i dim, int min, int max, int interval, @Override public void render(GuiGraphics graphics, int mouseX, int mouseY, float delta) { - super.render(graphics, mouseX, mouseY, delta); - - if (this.option.isAvailable() && (this.hovered || this.isFocused())) { - this.renderSlider(graphics); - } else { - this.renderStandaloneValue(graphics); - } - } - - private void renderStandaloneValue(GuiGraphics graphics) { int sliderX = this.sliderBounds.getX(); int sliderY = this.sliderBounds.getY(); int sliderWidth = this.sliderBounds.getWidth(); @@ -93,30 +84,36 @@ private void renderStandaloneValue(GuiGraphics graphics) { Component label = this.formatter.format(this.option.getValue()); int labelWidth = this.font.width(label); - this.drawString(graphics, label, sliderX + sliderWidth - labelWidth, sliderY + (sliderHeight / 2) - 4, 0xFFFFFFFF); - } - - private void renderSlider(GuiGraphics graphics) { - int sliderX = this.sliderBounds.getX(); - int sliderY = this.sliderBounds.getY(); - int sliderWidth = this.sliderBounds.getWidth(); - int sliderHeight = this.sliderBounds.getHeight(); + boolean drawSlider = this.option.isAvailable() && (this.hovered || this.isFocused()); + if (drawSlider) { + this.contentWidth = sliderWidth + labelWidth; + } else { + this.contentWidth = labelWidth; + } - this.thumbPosition = this.getThumbPositionForValue(this.option.getValue()); + // render the label first and then the slider to prevent the highlight rect from darkening the slider + super.render(graphics, mouseX, mouseY, delta); - double thumbOffset = Mth.clamp((double) (this.getIntValue() - this.min) / this.range * sliderWidth, 0, sliderWidth); + if (drawSlider) { + this.thumbPosition = this.getThumbPositionForValue(this.option.getValue()); - int thumbX = (int) (sliderX + thumbOffset - THUMB_WIDTH); - int trackY = (int) (sliderY + (sliderHeight / 2f) - ((double) TRACK_HEIGHT / 2)); + double thumbOffset = Mth.clamp((double) (this.getIntValue() - this.min) / this.range * sliderWidth, 0, sliderWidth); - this.drawRect(graphics, thumbX, sliderY, thumbX + (THUMB_WIDTH * 2), sliderY + sliderHeight, 0xFFFFFFFF); - this.drawRect(graphics, sliderX, trackY, sliderX + sliderWidth, trackY + TRACK_HEIGHT, 0xFFFFFFFF); + int thumbX = (int) (sliderX + thumbOffset - THUMB_WIDTH); + int trackY = (int) (sliderY + (sliderHeight / 2f) - ((double) TRACK_HEIGHT / 2)); - Component label = this.formatter.format(this.getIntValue()); + this.drawRect(graphics, thumbX, sliderY, thumbX + (THUMB_WIDTH * 2), sliderY + sliderHeight, 0xFFFFFFFF); + this.drawRect(graphics, sliderX, trackY, sliderX + sliderWidth, trackY + TRACK_HEIGHT, 0xFFFFFFFF); - int labelWidth = this.font.width(label); + this.drawString(graphics, label, sliderX - labelWidth - 6, sliderY + (sliderHeight / 2) - 4, 0xFFFFFFFF); + } else { + this.drawString(graphics, label, sliderX + sliderWidth - labelWidth, sliderY + (sliderHeight / 2) - 4, 0xFFFFFFFF); + } + } - this.drawString(graphics, label, sliderX - labelWidth - 6, sliderY + (sliderHeight / 2) - 4, 0xFFFFFFFF); + @Override + public int getContentWidth() { + return this.contentWidth; } public int getIntValue() {