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() {