From d163b9128d760e53e34fd6c08dbf782fa3d50c51 Mon Sep 17 00:00:00 2001 From: isXander Date: Sun, 27 Nov 2022 18:17:36 +0000 Subject: split sourcesets --- src/client/java/dev/isxander/yacl/api/Binding.java | 64 +++ .../java/dev/isxander/yacl/api/ButtonOption.java | 123 ++++++ .../java/dev/isxander/yacl/api/ConfigCategory.java | 157 +++++++ .../java/dev/isxander/yacl/api/Controller.java | 28 ++ .../java/dev/isxander/yacl/api/NameableEnum.java | 10 + src/client/java/dev/isxander/yacl/api/Option.java | 336 +++++++++++++++ .../java/dev/isxander/yacl/api/OptionFlag.java | 27 ++ .../java/dev/isxander/yacl/api/OptionGroup.java | 141 +++++++ .../dev/isxander/yacl/api/PlaceholderCategory.java | 94 +++++ .../dev/isxander/yacl/api/YetAnotherConfigLib.java | 136 ++++++ .../dev/isxander/yacl/api/utils/Dimension.java | 33 ++ .../isxander/yacl/api/utils/MutableDimension.java | 11 + .../dev/isxander/yacl/api/utils/OptionUtils.java | 37 ++ .../java/dev/isxander/yacl/gui/AbstractWidget.java | 108 +++++ .../dev/isxander/yacl/gui/CategoryListWidget.java | 96 +++++ .../java/dev/isxander/yacl/gui/CategoryWidget.java | 31 ++ .../isxander/yacl/gui/LowProfileButtonWidget.java | 29 ++ .../dev/isxander/yacl/gui/OptionListWidget.java | 470 +++++++++++++++++++++ .../isxander/yacl/gui/RequireRestartScreen.java | 16 + .../dev/isxander/yacl/gui/SearchFieldWidget.java | 62 +++ .../isxander/yacl/gui/TextScaledButtonWidget.java | 44 ++ .../dev/isxander/yacl/gui/TooltipButtonWidget.java | 30 ++ .../java/dev/isxander/yacl/gui/YACLScreen.java | 269 ++++++++++++ .../yacl/gui/controllers/ActionController.java | 120 ++++++ .../yacl/gui/controllers/BooleanController.java | 156 +++++++ .../yacl/gui/controllers/ColorController.java | 203 +++++++++ .../yacl/gui/controllers/ControllerWidget.java | 165 ++++++++ .../yacl/gui/controllers/LabelController.java | 144 +++++++ .../yacl/gui/controllers/TickBoxController.java | 120 ++++++ .../cycling/CyclingControllerElement.java | 60 +++ .../controllers/cycling/CyclingListController.java | 79 ++++ .../gui/controllers/cycling/EnumController.java | 60 +++ .../controllers/cycling/ICyclingController.java | 38 ++ .../yacl/gui/controllers/package-info.java | 12 + .../controllers/slider/DoubleSliderController.java | 114 +++++ .../controllers/slider/FloatSliderController.java | 114 +++++ .../gui/controllers/slider/ISliderController.java | 54 +++ .../slider/IntegerSliderController.java | 111 +++++ .../controllers/slider/LongSliderController.java | 111 +++++ .../slider/SliderControllerElement.java | 160 +++++++ .../yacl/gui/controllers/slider/package-info.java | 10 + .../gui/controllers/string/IStringController.java | 32 ++ .../gui/controllers/string/StringController.java | 45 ++ .../string/StringControllerElement.java | 283 +++++++++++++ .../dev/isxander/yacl/impl/ButtonOptionImpl.java | 142 +++++++ .../dev/isxander/yacl/impl/ConfigCategoryImpl.java | 10 + .../dev/isxander/yacl/impl/GenericBindingImpl.java | 35 ++ .../dev/isxander/yacl/impl/OptionGroupImpl.java | 10 + .../java/dev/isxander/yacl/impl/OptionImpl.java | 144 +++++++ .../yacl/impl/PlaceholderCategoryImpl.java | 19 + .../yacl/impl/YetAnotherConfigLibImpl.java | 19 + .../yacl/impl/utils/DimensionIntegerImpl.java | 115 +++++ .../isxander/yacl/impl/utils/YACLConstants.java | 8 + .../yacl/mixin/client/SimpleOptionAccessor.java | 11 + 54 files changed, 5056 insertions(+) create mode 100644 src/client/java/dev/isxander/yacl/api/Binding.java create mode 100644 src/client/java/dev/isxander/yacl/api/ButtonOption.java create mode 100644 src/client/java/dev/isxander/yacl/api/ConfigCategory.java create mode 100644 src/client/java/dev/isxander/yacl/api/Controller.java create mode 100644 src/client/java/dev/isxander/yacl/api/NameableEnum.java create mode 100644 src/client/java/dev/isxander/yacl/api/Option.java create mode 100644 src/client/java/dev/isxander/yacl/api/OptionFlag.java create mode 100644 src/client/java/dev/isxander/yacl/api/OptionGroup.java create mode 100644 src/client/java/dev/isxander/yacl/api/PlaceholderCategory.java create mode 100644 src/client/java/dev/isxander/yacl/api/YetAnotherConfigLib.java create mode 100644 src/client/java/dev/isxander/yacl/api/utils/Dimension.java create mode 100644 src/client/java/dev/isxander/yacl/api/utils/MutableDimension.java create mode 100644 src/client/java/dev/isxander/yacl/api/utils/OptionUtils.java create mode 100644 src/client/java/dev/isxander/yacl/gui/AbstractWidget.java create mode 100644 src/client/java/dev/isxander/yacl/gui/CategoryListWidget.java create mode 100644 src/client/java/dev/isxander/yacl/gui/CategoryWidget.java create mode 100644 src/client/java/dev/isxander/yacl/gui/LowProfileButtonWidget.java create mode 100644 src/client/java/dev/isxander/yacl/gui/OptionListWidget.java create mode 100644 src/client/java/dev/isxander/yacl/gui/RequireRestartScreen.java create mode 100644 src/client/java/dev/isxander/yacl/gui/SearchFieldWidget.java create mode 100644 src/client/java/dev/isxander/yacl/gui/TextScaledButtonWidget.java create mode 100644 src/client/java/dev/isxander/yacl/gui/TooltipButtonWidget.java create mode 100644 src/client/java/dev/isxander/yacl/gui/YACLScreen.java create mode 100644 src/client/java/dev/isxander/yacl/gui/controllers/ActionController.java create mode 100644 src/client/java/dev/isxander/yacl/gui/controllers/BooleanController.java create mode 100644 src/client/java/dev/isxander/yacl/gui/controllers/ColorController.java create mode 100644 src/client/java/dev/isxander/yacl/gui/controllers/ControllerWidget.java create mode 100644 src/client/java/dev/isxander/yacl/gui/controllers/LabelController.java create mode 100644 src/client/java/dev/isxander/yacl/gui/controllers/TickBoxController.java create mode 100644 src/client/java/dev/isxander/yacl/gui/controllers/cycling/CyclingControllerElement.java create mode 100644 src/client/java/dev/isxander/yacl/gui/controllers/cycling/CyclingListController.java create mode 100644 src/client/java/dev/isxander/yacl/gui/controllers/cycling/EnumController.java create mode 100644 src/client/java/dev/isxander/yacl/gui/controllers/cycling/ICyclingController.java create mode 100644 src/client/java/dev/isxander/yacl/gui/controllers/package-info.java create mode 100644 src/client/java/dev/isxander/yacl/gui/controllers/slider/DoubleSliderController.java create mode 100644 src/client/java/dev/isxander/yacl/gui/controllers/slider/FloatSliderController.java create mode 100644 src/client/java/dev/isxander/yacl/gui/controllers/slider/ISliderController.java create mode 100644 src/client/java/dev/isxander/yacl/gui/controllers/slider/IntegerSliderController.java create mode 100644 src/client/java/dev/isxander/yacl/gui/controllers/slider/LongSliderController.java create mode 100644 src/client/java/dev/isxander/yacl/gui/controllers/slider/SliderControllerElement.java create mode 100644 src/client/java/dev/isxander/yacl/gui/controllers/slider/package-info.java create mode 100644 src/client/java/dev/isxander/yacl/gui/controllers/string/IStringController.java create mode 100644 src/client/java/dev/isxander/yacl/gui/controllers/string/StringController.java create mode 100644 src/client/java/dev/isxander/yacl/gui/controllers/string/StringControllerElement.java create mode 100644 src/client/java/dev/isxander/yacl/impl/ButtonOptionImpl.java create mode 100644 src/client/java/dev/isxander/yacl/impl/ConfigCategoryImpl.java create mode 100644 src/client/java/dev/isxander/yacl/impl/GenericBindingImpl.java create mode 100644 src/client/java/dev/isxander/yacl/impl/OptionGroupImpl.java create mode 100644 src/client/java/dev/isxander/yacl/impl/OptionImpl.java create mode 100644 src/client/java/dev/isxander/yacl/impl/PlaceholderCategoryImpl.java create mode 100644 src/client/java/dev/isxander/yacl/impl/YetAnotherConfigLibImpl.java create mode 100644 src/client/java/dev/isxander/yacl/impl/utils/DimensionIntegerImpl.java create mode 100644 src/client/java/dev/isxander/yacl/impl/utils/YACLConstants.java create mode 100644 src/client/java/dev/isxander/yacl/mixin/client/SimpleOptionAccessor.java (limited to 'src/client/java/dev/isxander') diff --git a/src/client/java/dev/isxander/yacl/api/Binding.java b/src/client/java/dev/isxander/yacl/api/Binding.java new file mode 100644 index 0000000..91158d3 --- /dev/null +++ b/src/client/java/dev/isxander/yacl/api/Binding.java @@ -0,0 +1,64 @@ +package dev.isxander.yacl.api; + +import dev.isxander.yacl.impl.GenericBindingImpl; +import dev.isxander.yacl.mixin.client.SimpleOptionAccessor; +import net.minecraft.client.option.SimpleOption; +import org.apache.commons.lang3.Validate; + +import java.util.function.Consumer; +import java.util.function.Supplier; + +/** + * Controls modifying the bound option. + * Provides the default value, a setter and a getter. + */ +public interface Binding { + void setValue(T value); + + T getValue(); + + T defaultValue(); + + /** + * Creates a generic binding. + * + * @param def default value of the option, used to reset + * @param getter should return the current value of the option + * @param setter should set the option to the supplied value + */ + static Binding generic(T def, Supplier getter, Consumer setter) { + Validate.notNull(def, "`def` must not be null"); + Validate.notNull(getter, "`getter` must not be null"); + Validate.notNull(setter, "`setter` must not be null"); + + return new GenericBindingImpl<>(def, getter, setter); + } + + /** + * Creates a {@link Binding} for Minecraft's {@link SimpleOption} + */ + static Binding minecraft(SimpleOption minecraftOption) { + Validate.notNull(minecraftOption, "`minecraftOption` must not be null"); + + return new GenericBindingImpl<>( + ((SimpleOptionAccessor) (Object) minecraftOption).getDefaultValue(), + minecraftOption::getValue, + minecraftOption::setValue + ); + } + + /** + * Creates an immutable binding that has no default and cannot be modified. + * + * @param value the value for the binding + */ + static Binding immutable(T value) { + Validate.notNull(value, "`value` must not be null"); + + return new GenericBindingImpl<>( + value, + () -> value, + changed -> {} + ); + } +} diff --git a/src/client/java/dev/isxander/yacl/api/ButtonOption.java b/src/client/java/dev/isxander/yacl/api/ButtonOption.java new file mode 100644 index 0000000..1124a9a --- /dev/null +++ b/src/client/java/dev/isxander/yacl/api/ButtonOption.java @@ -0,0 +1,123 @@ +package dev.isxander.yacl.api; + +import dev.isxander.yacl.gui.YACLScreen; +import dev.isxander.yacl.impl.ButtonOptionImpl; +import net.minecraft.text.MutableText; +import net.minecraft.text.Text; +import org.apache.commons.lang3.Validate; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Function; + +public interface ButtonOption extends Option> { + /** + * Action to be executed upon button press + */ + BiConsumer action(); + + static Builder createBuilder() { + return new Builder(); + } + + class Builder { + private Text name; + private final List tooltipLines = new ArrayList<>(); + private boolean available = true; + private Function>> controlGetter; + private BiConsumer action; + + private Builder() { + + } + + /** + * Sets the name to be used by the option. + * + * @see Option#name() + */ + public Builder name(@NotNull Text name) { + Validate.notNull(name, "`name` cannot be null"); + + this.name = name; + return this; + } + + /** + * Sets the tooltip to be used by the option. + * Can be invoked twice to append more lines. + * No need to wrap the text yourself, the gui does this itself. + * + * @param tooltips text lines - merged with a new-line on {@link Option.Builder#build()}. + */ + public Builder tooltip(@NotNull Text... tooltips) { + Validate.notNull(tooltips, "`tooltips` cannot be empty"); + + tooltipLines.addAll(List.of(tooltips)); + return this; + } + + public Builder action(@NotNull BiConsumer action) { + Validate.notNull(action, "`action` cannot be null"); + + this.action = action; + return this; + } + + /** + * Action to be executed upon button press + * + * @see ButtonOption#action() + */ + @Deprecated + public Builder action(@NotNull Consumer action) { + Validate.notNull(action, "`action` cannot be null"); + + this.action = (screen, button) -> action.accept(screen); + return this; + } + + /** + * Sets if the option can be configured + * + * @see Option#available() + */ + public Builder available(boolean available) { + this.available = available; + return this; + } + + /** + * Sets the controller for the option. + * This is how you interact and change the options. + * + * @see dev.isxander.yacl.gui.controllers + */ + public Builder controller(@NotNull Function>> control) { + Validate.notNull(control, "`control` cannot be null"); + + this.controlGetter = control; + return this; + } + + public ButtonOption build() { + Validate.notNull(name, "`name` must not be null when building `Option`"); + Validate.notNull(controlGetter, "`control` must not be null when building `Option`"); + Validate.notNull(action, "`action` must not be null when building `Option`"); + + MutableText concatenatedTooltip = Text.empty(); + boolean first = true; + for (Text line : tooltipLines) { + if (!first) concatenatedTooltip.append("\n"); + first = false; + + concatenatedTooltip.append(line); + } + + return new ButtonOptionImpl(name, concatenatedTooltip, action, available, controlGetter); + } + } +} diff --git a/src/client/java/dev/isxander/yacl/api/ConfigCategory.java b/src/client/java/dev/isxander/yacl/api/ConfigCategory.java new file mode 100644 index 0000000..e9755dd --- /dev/null +++ b/src/client/java/dev/isxander/yacl/api/ConfigCategory.java @@ -0,0 +1,157 @@ +package dev.isxander.yacl.api; + +import com.google.common.collect.ImmutableList; +import dev.isxander.yacl.impl.ConfigCategoryImpl; +import dev.isxander.yacl.impl.OptionGroupImpl; +import net.minecraft.text.MutableText; +import net.minecraft.text.Text; +import org.apache.commons.lang3.Validate; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +/** + * Separates {@link Option}s or {@link OptionGroup}s into multiple distinct sections. + * Served to a user as a button in the left column, + * upon pressing, the options list is filled with options contained within this category. + */ +public interface ConfigCategory { + /** + * Name of category, displayed as a button on the left column. + */ + @NotNull Text name(); + + /** + * Gets every {@link OptionGroup} in this category. + */ + @NotNull ImmutableList groups(); + + /** + * Tooltip (or description) of the category. + * Rendered on hover. + */ + @NotNull Text tooltip(); + + /** + * Creates a builder to construct a {@link ConfigCategory} + */ + static Builder createBuilder() { + return new Builder(); + } + + class Builder { + private Text name; + + private final List> rootOptions = new ArrayList<>(); + private final List groups = new ArrayList<>(); + + private final List tooltipLines = new ArrayList<>(); + + private Builder() { + + } + + /** + * Sets name of the category + * + * @see ConfigCategory#name() + */ + public Builder name(@NotNull Text name) { + Validate.notNull(name, "`name` cannot be null"); + + this.name = name; + return this; + } + + /** + * Adds an option to the root group of the category. + * To add to another group, use {@link Builder#group(OptionGroup)}. + * To construct an option, use {@link Option#createBuilder(Class)} + * + * @see ConfigCategory#groups() + * @see OptionGroup#isRoot() + */ + public Builder option(@NotNull Option option) { + Validate.notNull(option, "`option` must not be null"); + + this.rootOptions.add(option); + return this; + } + + /** + * Adds multiple options to the root group of the category. + * To add to another group, use {@link Builder#groups(Collection)}. + * To construct an option, use {@link Option#createBuilder(Class)} + * + * @see ConfigCategory#groups() + * @see OptionGroup#isRoot() + */ + public Builder options(@NotNull Collection> options) { + Validate.notNull(options, "`options` must not be null"); + + this.rootOptions.addAll(options); + return this; + } + + /** + * Adds an option group. + * To add an option to the root group, use {@link Builder#option(Option)} + * To construct a group, use {@link OptionGroup#createBuilder()} + */ + public Builder group(@NotNull OptionGroup group) { + Validate.notNull(group, "`group` must not be null"); + + this.groups.add(group); + return this; + } + + /** + * Adds multiple option groups. + * To add multiple options to the root group, use {@link Builder#options(Collection)} + * To construct a group, use {@link OptionGroup#createBuilder()} + */ + public Builder groups(@NotNull Collection groups) { + Validate.notEmpty(groups, "`groups` must not be empty"); + + this.groups.addAll(groups); + return this; + } + + /** + * Sets the tooltip to be used by the category. + * Can be invoked twice to append more lines. + * No need to wrap the text yourself, the gui does this itself. + * + * @param tooltips text lines - merged with a new-line on {@link Builder#build()}. + */ + public Builder tooltip(@NotNull Text... tooltips) { + Validate.notEmpty(tooltips, "`tooltips` cannot be empty"); + + tooltipLines.addAll(List.of(tooltips)); + return this; + } + + public ConfigCategory build() { + Validate.notNull(name, "`name` must not be null to build `ConfigCategory`"); + + List combinedGroups = new ArrayList<>(); + combinedGroups.add(new OptionGroupImpl(Text.empty(), Text.empty(), ImmutableList.copyOf(rootOptions), false, true)); + combinedGroups.addAll(groups); + + Validate.notEmpty(combinedGroups, "at least one option must be added to build `ConfigCategory`"); + + MutableText concatenatedTooltip = Text.empty(); + boolean first = true; + for (Text line : tooltipLines) { + if (!first) concatenatedTooltip.append("\n"); + first = false; + + concatenatedTooltip.append(line); + } + + return new ConfigCategoryImpl(name, ImmutableList.copyOf(combinedGroups), concatenatedTooltip); + } + } +} diff --git a/src/client/java/dev/isxander/yacl/api/Controller.java b/src/client/java/dev/isxander/yacl/api/Controller.java new file mode 100644 index 0000000..7bf7e7f --- /dev/null +++ b/src/client/java/dev/isxander/yacl/api/Controller.java @@ -0,0 +1,28 @@ +package dev.isxander.yacl.api; + +import dev.isxander.yacl.api.utils.Dimension; +import dev.isxander.yacl.gui.AbstractWidget; +import dev.isxander.yacl.gui.YACLScreen; +import net.minecraft.text.Text; + +/** + * Provides a widget to control the option. + */ +public interface Controller { + /** + * Gets the dedicated {@link Option} for this controller + */ + Option option(); + + /** + * Gets the formatted value based on {@link Option#pendingValue()} + */ + Text formatValue(); + + /** + * Provides a widget to display + * + * @param screen parent screen + */ + AbstractWidget provideWidget(YACLScreen screen, Dimension widgetDimension); +} diff --git a/src/client/java/dev/isxander/yacl/api/NameableEnum.java b/src/client/java/dev/isxander/yacl/api/NameableEnum.java new file mode 100644 index 0000000..793b230 --- /dev/null +++ b/src/client/java/dev/isxander/yacl/api/NameableEnum.java @@ -0,0 +1,10 @@ +package dev.isxander.yacl.api; + +import net.minecraft.text.Text; + +/** + * Used for the default value formatter of {@link dev.isxander.yacl.gui.controllers.cycling.EnumController} + */ +public interface NameableEnum { + Text getDisplayName(); +} diff --git a/src/client/java/dev/isxander/yacl/api/Option.java b/src/client/java/dev/isxander/yacl/api/Option.java new file mode 100644 index 0000000..772c816 --- /dev/null +++ b/src/client/java/dev/isxander/yacl/api/Option.java @@ -0,0 +1,336 @@ +package dev.isxander.yacl.api; + +import com.google.common.collect.ImmutableSet; +import dev.isxander.yacl.impl.OptionImpl; +import net.minecraft.text.MutableText; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; +import org.apache.commons.lang3.Validate; +import org.jetbrains.annotations.NotNull; + +import java.util.*; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Stream; + +public interface Option { + /** + * Name of the option + */ + @NotNull Text name(); + + /** + * Tooltip (or description) of the option. + * Rendered on hover. + */ + @NotNull Text tooltip(); + + /** + * Widget provider for a type of option. + * + * @see dev.isxander.yacl.gui.controllers + */ + @NotNull Controller controller(); + + /** + * Binding for the option. + * Controls setting, getting and default value. + * + * @see Binding + */ + @NotNull Binding binding(); + + /** + * If the option can be configured + */ + boolean available(); + + /** + * Sets if the option can be configured after being built + * + * @see Option#available() + */ + void setAvailable(boolean available); + + /** + * Class of the option type. + * Used by some controllers. + */ + @NotNull Class typeClass(); + + /** + * Tasks that needs to be executed upon applying changes. + */ + @NotNull ImmutableSet flags(); + + /** + * Checks if the pending value is not equal to the current set value + */ + boolean changed(); + + /** + * If true, modifying this option recommends a restart. + */ + @Deprecated + boolean requiresRestart(); + + /** + * Value in the GUI, ready to set the actual bound value or be undone. + */ + @NotNull T pendingValue(); + + /** + * Sets the pending value + */ + void requestSet(T value); + + /** + * Applies the pending value to the bound value. + * Cannot be undone. + * + * @return if there were changes to apply {@link Option#changed()} + */ + boolean applyValue(); + + /** + * Sets the pending value to the bound value. + */ + void forgetPendingValue(); + + /** + * Sets the pending value to the default bound value. + */ + void requestSetDefault(); + + /** + * Checks if the current pending value is equal to its default value + */ + boolean isPendingValueDefault(); + + /** + * Adds a listener for when the pending value changes + */ + void addListener(BiConsumer, T> changedListener); + + /** + * Creates a builder to construct an {@link Option} + * + * @param type of the option's value + * @param typeClass used to capture the type + */ + static Builder createBuilder(Class typeClass) { + return new Builder<>(typeClass); + } + + class Builder { + private Text name = Text.literal("Name not specified!").formatted(Formatting.RED); + + private final List> tooltipGetters = new ArrayList<>(); + + private Function, Controller> controlGetter; + + private Binding binding; + + private boolean available = true; + + private boolean instant = false; + + private final Set flags = new HashSet<>(); + + private final Class typeClass; + + private final List, T>> listeners = new ArrayList<>(); + + private Builder(Class typeClass) { + this.typeClass = typeClass; + } + + /** + * Sets the name to be used by the option. + * + * @see Option#name() + */ + public Builder name(@NotNull Text name) { + Validate.notNull(name, "`name` cannot be null"); + + this.name = name; + return this; + } + + /** + * Sets the tooltip to be used by the option. + * No need to wrap the text yourself, the gui does this itself. + * + * @param tooltipGetter function to get tooltip depending on value {@link Builder#build()}. + */ + @SafeVarargs + public final Builder tooltip(@NotNull Function... tooltipGetter) { + Validate.notNull(tooltipGetter, "`tooltipGetter` cannot be null"); + + this.tooltipGetters.addAll(List.of(tooltipGetter)); + return this; + } + + /** + * Sets the tooltip to be used by the option. + * Can be invoked twice to append more lines. + * No need to wrap the text yourself, the gui does this itself. + * + * @param tooltips text lines - merged with a new-line on {@link Builder#build()}. + */ + public Builder tooltip(@NotNull Text... tooltips) { + Validate.notNull(tooltips, "`tooltips` cannot be empty"); + + this.tooltipGetters.addAll(Stream.of(tooltips).map(text -> (Function) t -> text).toList()); + return this; + } + + /** + * Sets the controller for the option. + * This is how you interact and change the options. + * + * @see dev.isxander.yacl.gui.controllers + */ + public Builder controller(@NotNull Function, Controller> control) { + Validate.notNull(control, "`control` cannot be null"); + + this.controlGetter = control; + return this; + } + + /** + * Sets the binding for the option. + * Used for default, getter and setter. + * + * @see Binding + */ + public Builder binding(@NotNull Binding binding) { + Validate.notNull(binding, "`binding` cannot be null"); + + this.binding = binding; + return this; + } + + /** + * Sets the binding for the option. + * Shorthand of {@link Binding#generic(Object, Supplier, Consumer)} + * + * @param def default value of the option, used to reset + * @param getter should return the current value of the option + * @param setter should set the option to the supplied value + * @see Binding + */ + public Builder binding(@NotNull T def, @NotNull Supplier<@NotNull T> getter, @NotNull Consumer<@NotNull T> setter) { + Validate.notNull(def, "`def` must not be null"); + Validate.notNull(getter, "`getter` must not be null"); + Validate.notNull(setter, "`setter` must not be null"); + + this.binding = Binding.generic(def, getter, setter); + return this; + } + + /** + * Sets if the option can be configured + * + * @see Option#available() + */ + public Builder available(boolean available) { + this.available = available; + return this; + } + + /** + * Adds a flag to the option. + * Upon applying changes, all flags are executed. + * {@link Option#flags()} + */ + public Builder flag(@NotNull OptionFlag... flag) { + Validate.notNull(flag, "`flag` must not be null"); + + this.flags.addAll(Arrays.asList(flag)); + return this; + } + + /** + * Adds a flag to the option. + * Upon applying changes, all flags are executed. + * {@link Option#flags()} + */ + public Builder flags(@NotNull Collection flags) { + Validate.notNull(flags, "`flags` must not be null"); + + this.flags.addAll(flags); + return this; + } + + /** + * Instantly invokes the binder's setter when modified in the GUI. + * Prevents the user from undoing the change + *

+ * Does not support {@link Option#flags()}! + */ + public Builder instant(boolean instant) { + this.instant = instant; + return this; + } + + /** + * Adds a listener to the option. Invoked upon changing the pending value. + * + * @see Option#addListener(BiConsumer) + */ + public Builder listener(@NotNull BiConsumer, T> listener) { + this.listeners.add(listener); + return this; + } + + /** + * Adds multiple listeners to the option. Invoked upon changing the pending value. + * + * @see Option#addListener(BiConsumer) + */ + public Builder listeners(@NotNull Collection, T>> listeners) { + this.listeners.addAll(listeners); + return this; + } + + /** + * Dictates whether the option should require a restart. + * {@link Option#requiresRestart()} + */ + @Deprecated + public Builder requiresRestart(boolean requiresRestart) { + if (requiresRestart) flag(OptionFlag.GAME_RESTART); + else flags.remove(OptionFlag.GAME_RESTART); + + return this; + } + + public Option build() { + Validate.notNull(controlGetter, "`control` must not be null when building `Option`"); + Validate.notNull(binding, "`binding` must not be null when building `Option`"); + Validate.isTrue(!instant || flags.isEmpty(), "instant application does not support option flags"); + + Function concatenatedTooltipGetter = value -> { + MutableText concatenatedTooltip = Text.empty(); + boolean first = true; + for (Function line : tooltipGetters) { + if (!first) concatenatedTooltip.append("\n"); + first = false; + + concatenatedTooltip.append(line.apply(value)); + } + + return concatenatedTooltip; + }; + + if (instant) { + listeners.add((opt, pendingValue) -> opt.applyValue()); + } + + return new OptionImpl<>(name, concatenatedTooltipGetter, controlGetter, binding, available, ImmutableSet.copyOf(flags), typeClass, listeners); + } + } +} diff --git a/src/client/java/dev/isxander/yacl/api/OptionFlag.java b/src/client/java/dev/isxander/yacl/api/OptionFlag.java new file mode 100644 index 0000000..203a674 --- /dev/null +++ b/src/client/java/dev/isxander/yacl/api/OptionFlag.java @@ -0,0 +1,27 @@ +package dev.isxander.yacl.api; + +import dev.isxander.yacl.gui.RequireRestartScreen; +import net.minecraft.client.MinecraftClient; + +import java.util.function.Consumer; + +/** + * Code that is executed upon certain options being applied. + * Each flag is executed only once per save, no matter the amount of options with the flag. + */ +@FunctionalInterface +public interface OptionFlag extends Consumer { + /** + * Warns the user that a game restart is required for the changes to take effect + */ + OptionFlag GAME_RESTART = client -> client.setScreen(new RequireRestartScreen(client.currentScreen)); + + /** + * Reloads chunks upon applying (F3+A) + */ + OptionFlag RELOAD_CHUNKS = client -> client.worldRenderer.reload(); + + OptionFlag WORLD_RENDER_UPDATE = client -> client.worldRenderer.scheduleTerrainUpdate(); + + OptionFlag ASSET_RELOAD = MinecraftClient::reloadResourcesConcurrently; +} diff --git a/src/client/java/dev/isxander/yacl/api/OptionGroup.java b/src/client/java/dev/isxander/yacl/api/OptionGroup.java new file mode 100644 index 0000000..3364bdf --- /dev/null +++ b/src/client/java/dev/isxander/yacl/api/OptionGroup.java @@ -0,0 +1,141 @@ +package dev.isxander.yacl.api; + +import com.google.common.collect.ImmutableList; +import dev.isxander.yacl.impl.OptionGroupImpl; +import net.minecraft.text.MutableText; +import net.minecraft.text.Text; +import org.apache.commons.lang3.Validate; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +/** + * Serves as a separator between multiple chunks of options + * that may be too similar or too few to be placed in a separate {@link ConfigCategory}. + * Or maybe you just want your config to feel less dense. + */ +public interface OptionGroup { + /** + * Name of the option group, displayed as a separator in the option lists. + * Can be empty. + */ + Text name(); + + /** + * Tooltip displayed on hover. + */ + Text tooltip(); + + /** + * List of all options in the group + */ + @NotNull ImmutableList> options(); + + /** + * Dictates if the group should be collapsed by default. + */ + boolean collapsed(); + + /** + * Always false when using the {@link Builder} + * used to not render the separator if true + */ + boolean isRoot(); + + /** + * Creates a builder to construct a {@link OptionGroup} + */ + static Builder createBuilder() { + return new Builder(); + } + + class Builder { + private Text name = Text.empty(); + private final List tooltipLines = new ArrayList<>(); + private final List> options = new ArrayList<>(); + private boolean collapsed = false; + + private Builder() { + + } + + /** + * Sets name of the group, can be {@link Text#empty()} to just separate options, like sodium. + * + * @see OptionGroup#name() + */ + public Builder name(@NotNull Text name) { + Validate.notNull(name, "`name` must not be null"); + + this.name = name; + return this; + } + + /** + * Sets the tooltip to be used by the option group. + * Can be invoked twice to append more lines. + * No need to wrap the text yourself, the gui does this itself. + * + * @param tooltips text lines - merged with a new-line on {@link Builder#build()}. + */ + public Builder tooltip(@NotNull Text... tooltips) { + Validate.notEmpty(tooltips, "`tooltips` cannot be empty"); + + tooltipLines.addAll(List.of(tooltips)); + return this; + } + + /** + * Adds an option to group. + * To construct an option, use {@link Option#createBuilder(Class)} + * + * @see OptionGroup#options() + */ + public Builder option(@NotNull Option option) { + Validate.notNull(option, "`option` must not be null"); + + this.options.add(option); + return this; + } + + /** + * Adds multiple options to group. + * To construct an option, use {@link Option#createBuilder(Class)} + * + * @see OptionGroup#options() + */ + public Builder options(@NotNull Collection> options) { + Validate.notEmpty(options, "`options` must not be empty"); + + this.options.addAll(options); + return this; + } + + /** + * Dictates if the group should be collapsed by default + * + * @see OptionGroup#collapsed() + */ + public Builder collapsed(boolean collapsible) { + this.collapsed = collapsible; + return this; + } + + public OptionGroup build() { + Validate.notEmpty(options, "`options` must not be empty to build `OptionGroup`"); + + MutableText concatenatedTooltip = Text.empty(); + boolean first = true; + for (Text line : tooltipLines) { + if (!first) concatenatedTooltip.append("\n"); + first = false; + + concatenatedTooltip.append(line); + } + + return new OptionGroupImpl(name, concatenatedTooltip, ImmutableList.copyOf(options), collapsed, false); + } + } +} diff --git a/src/client/java/dev/isxander/yacl/api/PlaceholderCategory.java b/src/client/java/dev/isxander/yacl/api/PlaceholderCategory.java new file mode 100644 index 0000000..de7441c --- /dev/null +++ b/src/client/java/dev/isxander/yacl/api/PlaceholderCategory.java @@ -0,0 +1,94 @@ +package dev.isxander.yacl.api; + +import dev.isxander.yacl.gui.YACLScreen; +import dev.isxander.yacl.impl.PlaceholderCategoryImpl; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.text.MutableText; +import net.minecraft.text.Text; +import org.apache.commons.lang3.Validate; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.BiFunction; + +/** + * A placeholder category that actually just opens another screen, + * instead of displaying options + */ +public interface PlaceholderCategory extends ConfigCategory { + /** + * Function to create a screen to open upon changing to this category + */ + BiFunction screen(); + + static Builder createBuilder() { + return new Builder(); + } + + class Builder { + private Text name; + + private final List tooltipLines = new ArrayList<>(); + + private BiFunction screenFunction; + + private Builder() { + + } + + /** + * Sets name of the category + * + * @see ConfigCategory#name() + */ + public Builder name(@NotNull Text name) { + Validate.notNull(name, "`name` cannot be null"); + + this.name = name; + return this; + } + + /** + * Sets the tooltip to be used by the category. + * Can be invoked twice to append more lines. + * No need to wrap the text yourself, the gui does this itself. + * + * @param tooltips text lines - merged with a new-line on {@link Builder#build()}. + */ + public Builder tooltip(@NotNull Text... tooltips) { + Validate.notEmpty(tooltips, "`tooltips` cannot be empty"); + + tooltipLines.addAll(List.of(tooltips)); + return this; + } + + /** + * Screen to open upon selecting this category + * + * @see PlaceholderCategory#screen() + */ + public Builder screen(@NotNull BiFunction screenFunction) { + Validate.notNull(screenFunction, "`screenFunction` cannot be null"); + + this.screenFunction = screenFunction; + return this; + } + + public PlaceholderCategory build() { + Validate.notNull(name, "`name` must not be null to build `ConfigCategory`"); + + MutableText concatenatedTooltip = Text.empty(); + boolean first = true; + for (Text line : tooltipLines) { + if (!first) concatenatedTooltip.append("\n"); + first = false; + + concatenatedTooltip.append(line); + } + + return new PlaceholderCategoryImpl(name, screenFunction, concatenatedTooltip); + } + } +} diff --git a/src/client/java/dev/isxander/yacl/api/YetAnotherConfigLib.java b/src/client/java/dev/isxander/yacl/api/YetAnotherConfigLib.java new file mode 100644 index 0000000..a69ae4e --- /dev/null +++ b/src/client/java/dev/isxander/yacl/api/YetAnotherConfigLib.java @@ -0,0 +1,136 @@ +package dev.isxander.yacl.api; + +import com.google.common.collect.ImmutableList; +import dev.isxander.yacl.gui.YACLScreen; +import dev.isxander.yacl.impl.YetAnotherConfigLibImpl; +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.text.Text; +import org.apache.commons.lang3.Validate; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.function.Consumer; + +/** + * Main class of the mod. + * Contains all data and used to provide a {@link Screen} + */ +public interface YetAnotherConfigLib { + /** + * Title of the GUI. Only used for Minecraft narration. + */ + Text title(); + + /** + * Gets all config categories. + */ + ImmutableList categories(); + + /** + * Ran when changes are saved. Can be used to save config to a file etc. + */ + Runnable saveFunction(); + + /** + * Ran every time the YACL screen initialises. Can be paired with FAPI to add custom widgets. + */ + Consumer initConsumer(); + + /** + * Generates a Screen to display based on this instance. + * + * @param parent parent screen to open once closed + */ + Screen generateScreen(@Nullable Screen parent); + + /** + * Creates a builder to construct YACL + */ + static Builder createBuilder() { + return new Builder(); + } + + class Builder { + private Text title; + private final List categories = new ArrayList<>(); + private Runnable saveFunction = () -> {}; + private Consumer initConsumer = screen -> {}; + + private Builder() { + + } + + /** + * Sets title of GUI for Minecraft narration + * + * @see YetAnotherConfigLib#title() + */ + public Builder title(@NotNull Text title) { + Validate.notNull(title, "`title` cannot be null"); + + this.title = title; + return this; + } + + /** + * Adds a new category. + * To create a category you need to use {@link ConfigCategory#createBuilder()} + * + * @see YetAnotherConfigLib#categories() + */ + public Builder category(@NotNull ConfigCategory category) { + Validate.notNull(category, "`category` cannot be null"); + + this.categories.add(category); + return this; + } + + /** + * Adds multiple categories at once. + * To create a category you need to use {@link ConfigCategory#createBuilder()} + * + * @see YetAnotherConfigLib#categories() + */ + public Builder categories(@NotNull Collection categories) { + Validate.notNull(categories, "`categories` cannot be null"); + + this.categories.addAll(categories); + return this; + } + + /** + * Used to define a save function for when user clicks the Save Changes button + * + * @see YetAnotherConfigLib#saveFunction() + */ + public Builder save(@NotNull Runnable saveFunction) { + Validate.notNull(saveFunction, "`saveFunction` cannot be null"); + + this.saveFunction = saveFunction; + return this; + } + + /** + * Defines a consumer that is accepted every time the YACL screen initialises + * + * @see YetAnotherConfigLib#initConsumer() + */ + public Builder screenInit(@NotNull Consumer initConsumer) { + Validate.notNull(initConsumer, "`initConsumer` cannot be null"); + + this.initConsumer = initConsumer; + return this; + } + + public YetAnotherConfigLib build() { + Validate.notNull(title, "`title must not be null to build `YetAnotherConfigLib`"); + Validate.notEmpty(categories, "`categories` must not be empty to build `YetAnotherConfigLib`"); + Validate.isTrue(!categories.stream().allMatch(category -> category instanceof PlaceholderCategory), "At least one regular category is required to build `YetAnotherConfigLib`"); + + return new YetAnotherConfigLibImpl(title, ImmutableList.copyOf(categories), saveFunction, initConsumer); + } + } +} diff --git a/src/client/java/dev/isxander/yacl/api/utils/Dimension.java b/src/client/java/dev/isxander/yacl/api/utils/Dimension.java new file mode 100644 index 0000000..0de0a58 --- /dev/null +++ b/src/client/java/dev/isxander/yacl/api/utils/Dimension.java @@ -0,0 +1,33 @@ +package dev.isxander.yacl.api.utils; + +import dev.isxander.yacl.impl.utils.DimensionIntegerImpl; + +public interface Dimension { + T x(); + T y(); + + T width(); + T height(); + + T xLimit(); + T yLimit(); + + T centerX(); + T centerY(); + + boolean isPointInside(T x, T y); + + MutableDimension clone(); + + Dimension withX(T x); + Dimension withY(T y); + Dimension withWidth(T width); + Dimension withHeight(T height); + + Dimension moved(T x, T y); + Dimension expanded(T width, T height); + + static MutableDimension ofInt(int x, int y, int width, int height) { + return new DimensionIntegerImpl(x, y, width, height); + } +} diff --git a/src/client/java/dev/isxander/yacl/api/utils/MutableDimension.java b/src/client/java/dev/isxander/yacl/api/utils/MutableDimension.java new file mode 100644 index 0000000..eff0186 --- /dev/null +++ b/src/client/java/dev/isxander/yacl/api/utils/MutableDimension.java @@ -0,0 +1,11 @@ +package dev.isxander.yacl.api.utils; + +public interface MutableDimension extends Dimension { + MutableDimension setX(T x); + MutableDimension setY(T y); + MutableDimension setWidth(T width); + MutableDimension setHeight(T height); + + MutableDimension move(T x, T y); + MutableDimension expand(T width, T height); +} diff --git a/src/client/java/dev/isxander/yacl/api/utils/OptionUtils.java b/src/client/java/dev/isxander/yacl/api/utils/OptionUtils.java new file mode 100644 index 0000000..ab46b5b --- /dev/null +++ b/src/client/java/dev/isxander/yacl/api/utils/OptionUtils.java @@ -0,0 +1,37 @@ +package dev.isxander.yacl.api.utils; + +import dev.isxander.yacl.api.ConfigCategory; +import dev.isxander.yacl.api.Option; +import dev.isxander.yacl.api.OptionGroup; +import dev.isxander.yacl.api.YetAnotherConfigLib; + +import java.util.function.Consumer; +import java.util.function.Function; + +public class OptionUtils { + /** + * Consumes all options, ignoring groups and categories. + * When consumer returns true, this function stops iterating. + */ + public static void consumeOptions(YetAnotherConfigLib yacl, Function, Boolean> consumer) { + for (ConfigCategory category : yacl.categories()) { + for (OptionGroup group : category.groups()) { + for (Option option : group.options()) { + if (consumer.apply(option)) return; + } + } + } + } + + /** + * Consumes all options, ignoring groups and categories. + * + * @see OptionUtils#consumeOptions(YetAnotherConfigLib, Function) + */ + public static void forEachOptions(YetAnotherConfigLib yacl, Consumer> consumer) { + consumeOptions(yacl, (opt) -> { + consumer.accept(opt); + return false; + }); + } +} diff --git a/src/client/java/dev/isxander/yacl/gui/AbstractWidget.java b/src/client/java/dev/isxander/yacl/gui/AbstractWidget.java new file mode 100644 index 0000000..ffb4dec --- /dev/null +++ b/src/client/java/dev/isxander/yacl/gui/AbstractWidget.java @@ -0,0 +1,108 @@ +package dev.isxander.yacl.gui; + +import com.mojang.blaze3d.systems.RenderSystem; +import dev.isxander.yacl.api.utils.Dimension; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.font.TextRenderer; +import net.minecraft.client.gui.Drawable; +import net.minecraft.client.gui.DrawableHelper; +import net.minecraft.client.gui.Element; +import net.minecraft.client.gui.Selectable; +import net.minecraft.client.gui.screen.narration.NarrationMessageBuilder; +import net.minecraft.client.gui.widget.ClickableWidget; +import net.minecraft.client.render.GameRenderer; +import net.minecraft.client.sound.PositionedSoundInstance; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.sound.SoundEvents; + +import java.awt.Color; + +public abstract class AbstractWidget implements Element, Drawable, Selectable { + protected final MinecraftClient client = MinecraftClient.getInstance(); + protected final TextRenderer textRenderer = client.textRenderer; + protected final int inactiveColor = 0xFFA0A0A0; + + private Dimension dim; + + public AbstractWidget(Dimension dim) { + this.dim = dim; + } + + public void postRender(MatrixStack matrices, int mouseX, int mouseY, float delta) { + + } + + public boolean canReset() { + return false; + } + + @Override + public boolean isMouseOver(double mouseX, double mouseY) { + if (dim == null) return false; + return this.dim.isPointInside((int) mouseX, (int) mouseY); + } + + public void setDimension(Dimension dim) { + this.dim = dim; + } + + public Dimension getDimension() { + return dim; + } + + @Override + public SelectionType getType() { + return SelectionType.NONE; + } + + public void unfocus() { + + } + + public boolean matchesSearch(String query) { + return true; + } + + @Override + public void appendNarrations(NarrationMessageBuilder builder) { + + } + + protected void drawButtonRect(MatrixStack matrices, int x1, int y1, int x2, int y2, boolean hovered, boolean enabled) { + if (x1 > x2) { + int xx1 = x1; + x1 = x2; + x2 = xx1; + } + if (y1 > y2) { + int yy1 = y1; + y1 = y2; + y2 = yy1; + } + int width = x2 - x1; + int height = y2 - y1; + + RenderSystem.setShader(GameRenderer::getPositionTexProgram); + RenderSystem.setShaderTexture(0, ClickableWidget.WIDGETS_TEXTURE); + RenderSystem.setShaderColor(1.0F, 1.0F, 1.0F, 1.0F); + int i = !enabled ? 0 : hovered ? 2 : 1; + RenderSystem.enableBlend(); + RenderSystem.defaultBlendFunc(); + RenderSystem.enableDepthTest(); + DrawableHelper.drawTexture(matrices, x1, y1, 0, 0, 46 + i * 20, width / 2, height, 256, 256); + DrawableHelper.drawTexture(matrices, x1 + width / 2, y1, 0, 200 - width / 2f, 46 + i * 20, width / 2, height, 256, 256); + } + + protected int multiplyColor(int hex, float amount) { + Color color = new Color(hex, true); + + return new Color(Math.max((int)(color.getRed() *amount), 0), + Math.max((int)(color.getGreen()*amount), 0), + Math.max((int)(color.getBlue() *amount), 0), + color.getAlpha()).getRGB(); + } + + public void playDownSound() { + MinecraftClient.getInstance().getSoundManager().play(PositionedSoundInstance.master(SoundEvents.UI_BUTTON_CLICK, 1.0F)); + } +} diff --git a/src/client/java/dev/isxander/yacl/gui/CategoryListWidget.java b/src/client/java/dev/isxander/yacl/gui/CategoryListWidget.java new file mode 100644 index 0000000..46a9fdf --- /dev/null +++ b/src/client/java/dev/isxander/yacl/gui/CategoryListWidget.java @@ -0,0 +1,96 @@ +package dev.isxander.yacl.gui; + +import com.google.common.collect.ImmutableList; +import com.mojang.blaze3d.systems.RenderSystem; +import dev.isxander.yacl.api.ConfigCategory; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.Element; +import net.minecraft.client.gui.Selectable; +import net.minecraft.client.gui.widget.ElementListWidget; +import net.minecraft.client.util.math.MatrixStack; + +import java.util.List; + +public class CategoryListWidget extends ElementListWidget { + private final YACLScreen yaclScreen; + + public CategoryListWidget(MinecraftClient client, YACLScreen yaclScreen, int screenWidth, int screenHeight) { + super(client, screenWidth / 3, yaclScreen.searchFieldWidget.getY() - 5, 0, yaclScreen.searchFieldWidget.getY() - 5, 21); + this.yaclScreen = yaclScreen; + setRenderBackground(false); + setRenderHorizontalShadows(false); + + for (ConfigCategory category : yaclScreen.config.categories()) { + addEntry(new CategoryEntry(category)); + } + } + + @Override + protected void renderList(MatrixStack matrices, int mouseX, int mouseY, float delta) { + double d = this.client.getWindow().getScaleFactor(); + RenderSystem.enableScissor(0, (int)((yaclScreen.height - bottom) * d), (int)(width * d), (int)(height * d)); + super.renderList(matrices, mouseX, mouseY, delta); + RenderSystem.disableScissor(); + } + + public void postRender(MatrixStack matrices, int mouseX, int mouseY, float delta) { + for (CategoryEntry entry : children()) { + entry.postRender(matrices, mouseX, mouseY, delta); + } + } + + @Override + public int getRowWidth() { + return Math.min(width - width / 10, 396); + } + + @Override + public int getRowLeft() { + return super.getRowLeft() - 2; + } + + @Override + protected int getScrollbarPositionX() { + return width - 2; + } + + public class CategoryEntry extends Entry { + private final CategoryWidget categoryButton; + public final int categoryIndex; + + public CategoryEntry(ConfigCategory category) { + this.categoryIndex = yaclScreen.config.categories().indexOf(category); + categoryButton = new CategoryWidget( + yaclScreen, + category, + categoryIndex, + getRowLeft(), 0, + getRowWidth(), 20 + ); + } + + @Override + public void render(MatrixStack matrices, int index, int y, int x, int entryWidth, int entryHeight, int mouseX, int mouseY, boolean hovered, float tickDelta) { + if (mouseY > bottom) { + mouseY = -20; + } + + categoryButton.setY(y); + categoryButton.render(matrices, mouseX, mouseY, tickDelta); + } + + private void postRender(MatrixStack matrices, int mouseX, int mouseY, float tickDelta) { + categoryButton.renderHoveredTooltip(matrices); + } + + @Override + public List children() { + return ImmutableList.of(categoryButton); + } + + @Override + public List selectableChildren() { + return ImmutableList.of(categoryButton); + } + } +} diff --git a/src/client/java/dev/isxander/yacl/gui/CategoryWidget.java b/src/client/java/dev/isxander/yacl/gui/CategoryWidget.java new file mode 100644 index 0000000..3c5d8d2 --- /dev/null +++ b/src/client/java/dev/isxander/yacl/gui/CategoryWidget.java @@ -0,0 +1,31 @@ +package dev.isxander.yacl.gui; + +import dev.isxander.yacl.api.ConfigCategory; +import net.minecraft.client.sound.SoundManager; + +public class CategoryWidget extends TooltipButtonWidget { + private final int categoryIndex; + + public CategoryWidget(YACLScreen screen, ConfigCategory category, int categoryIndex, int x, int y, int width, int height) { + super(screen, x, y, width, height, category.name(), category.tooltip(), btn -> { + screen.searchFieldWidget.setText(""); + screen.changeCategory(categoryIndex); + }); + this.categoryIndex = categoryIndex; + } + + private boolean isCurrentCategory() { + return ((YACLScreen) screen).getCurrentCategoryIdx() == categoryIndex; + } + + @Override + protected int getYImage(boolean hovered) { + return super.getYImage(hovered || isCurrentCategory()); + } + + @Override + public void playDownSound(SoundManager soundManager) { + if (!isCurrentCategory()) + super.playDownSound(soundManager); + } +} diff --git a/src/client/java/dev/isxander/yacl/gui/LowProfileButtonWidget.java b/src/client/java/dev/isxander/yacl/gui/LowProfileButtonWidget.java new file mode 100644 index 0000000..9fa01a7 --- /dev/null +++ b/src/client/java/dev/isxander/yacl/gui/LowProfileButtonWidget.java @@ -0,0 +1,29 @@ +package dev.isxander.yacl.gui; + +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.tooltip.Tooltip; +import net.minecraft.client.gui.widget.ButtonWidget; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.text.Text; +import net.minecraft.util.math.MathHelper; + +public class LowProfileButtonWidget extends ButtonWidget { + public LowProfileButtonWidget(int x, int y, int width, int height, Text message, PressAction onPress) { + super(x, y, width, height, message, onPress, DEFAULT_NARRATION_SUPPLIER); + } + + public LowProfileButtonWidget(int x, int y, int width, int height, Text message, PressAction onPress, Tooltip tooltip) { + this(x, y, width, height, message, onPress); + setTooltip(tooltip); + } + + @Override + public void renderButton(MatrixStack matrices, int mouseX, int mouseY, float delta) { + if (!isHovered()) { + int j = this.active ? 0xFFFFFF : 0xA0A0A0; + drawCenteredText(matrices, MinecraftClient.getInstance().textRenderer, this.getMessage(), this.getX() + this.width / 2, this.getY() + (this.height - 8) / 2, j | MathHelper.ceil(this.alpha * 255.0F) << 24); + } else { + super.renderButton(matrices, mouseX, mouseY, delta); + } + } +} diff --git a/src/client/java/dev/isxander/yacl/gui/OptionListWidget.java b/src/client/java/dev/isxander/yacl/gui/OptionListWidget.java new file mode 100644 index 0000000..eed3aff --- /dev/null +++ b/src/client/java/dev/isxander/yacl/gui/OptionListWidget.java @@ -0,0 +1,470 @@ +package dev.isxander.yacl.gui; + +import com.google.common.collect.ImmutableList; +import dev.isxander.yacl.api.ConfigCategory; +import dev.isxander.yacl.api.Option; +import dev.isxander.yacl.api.OptionGroup; +import dev.isxander.yacl.api.utils.Dimension; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.font.MultilineText; +import net.minecraft.client.font.TextRenderer; +import net.minecraft.client.gui.Element; +import net.minecraft.client.gui.Selectable; +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.client.gui.screen.narration.NarrationMessageBuilder; +import net.minecraft.client.gui.screen.narration.NarrationPart; +import net.minecraft.client.gui.widget.ElementListWidget; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.text.Text; +import net.minecraft.util.math.MathHelper; +import org.jetbrains.annotations.Nullable; + +import java.util.*; +import java.util.function.Supplier; + +public class OptionListWidget extends ElementListWidget { + private final YACLScreen yaclScreen; + private boolean singleCategory = false; + + private ImmutableList viewableChildren; + + private double smoothScrollAmount = getScrollAmount(); + private boolean returnSmoothAmount = false; + + public OptionListWidget(YACLScreen screen, MinecraftClient client, int width, int height) { + super(client, width / 3 * 2, height, 0, height, 22); + this.yaclScreen = screen; + left = width - this.width; + right = width; + + refreshOptions(); + } + + public void refreshOptions() { + clearEntries(); + + List categories = new ArrayList<>(); + if (yaclScreen.getCurrentCategoryIdx() == -1) { + categories.addAll(yaclScreen.config.categories()); + } else { + categories.add(yaclScreen.config.categories().get(yaclScreen.getCurrentCategoryIdx())); + } + singleCategory = categories.size() == 1; + + for (ConfigCategory category : categories) { + for (OptionGroup group : category.groups()) { + Supplier viewableSupplier; + GroupSeparatorEntry groupSeparatorEntry = null; + if (!group.isRoot()) { + groupSeparatorEntry = new GroupSeparatorEntry(group, yaclScreen); + viewableSupplier = groupSeparatorEntry::isExpanded; + addEntry(groupSeparatorEntry); + } else { + viewableSupplier = () -> true; + } + + List optionEntries = new ArrayList<>(); + for (Option option : group.options()) { + OptionEntry entry = new OptionEntry(option, category, group, option.controller().provideWidget(yaclScreen, Dimension.ofInt(getRowLeft(), 0, getRowWidth(), 20)), viewableSupplier); + addEntry(entry); + optionEntries.add(entry); + } + + if (groupSeparatorEntry != null) { + groupSeparatorEntry.setOptionEntries(optionEntries); + } + } + } + + recacheViewableChildren(); + setScrollAmount(0); + } + + public void expandAllGroups() { + for (Entry entry : super.children()) { + if (entry instanceof GroupSeparatorEntry groupSeparatorEntry) { + groupSeparatorEntry.setExpanded(true); + } + } + } + + /* + below code is licensed from cloth-config under LGPL3 + modified to inherit vanilla's EntryListWidget and use yarn mappings + */ + + @Nullable + @Override + protected Entry getEntryAtPosition(double x, double y) { + int listMiddleX = this.left + this.width / 2; + int minX = listMiddleX - this.getRowWidth() / 2; + int maxX = listMiddleX + this.getRowWidth() / 2; + int currentY = MathHelper.floor(y - (double) this.top) - this.headerHeight + (int) this.getScrollAmount() - 4; + int itemY = 0; + int itemIndex = -1; + for (int i = 0; i < children().size(); i++) { + Entry item = children().get(i); + itemY += item.getItemHeight(); + if (itemY > currentY) { + itemIndex = i; + break; + } + } + return x < (double) this.getScrollbarPositionX() && x >= minX && y <= maxX && itemIndex >= 0 && currentY >= 0 && itemIndex < this.getEntryCount() ? this.children().get(itemIndex) : null; + } + + @Override + protected int getMaxPosition() { + return