From 13c7ba45ff201423eb8dba8a40cfb66ebb531439 Mon Sep 17 00:00:00 2001 From: Xander Date: Tue, 25 Apr 2023 16:28:41 +0100 Subject: Architectury! (#61) --- .../main/java/dev/isxander/yacl/api/Binding.java | 64 +++ .../java/dev/isxander/yacl/api/ButtonOption.java | 66 +++ .../java/dev/isxander/yacl/api/ConfigCategory.java | 94 ++++ .../java/dev/isxander/yacl/api/Controller.java | 28 + .../java/dev/isxander/yacl/api/LabelOption.java | 41 ++ .../java/dev/isxander/yacl/api/ListOption.java | 152 ++++++ .../dev/isxander/yacl/api/ListOptionEntry.java | 23 + .../java/dev/isxander/yacl/api/NameableEnum.java | 10 + .../main/java/dev/isxander/yacl/api/Option.java | 227 ++++++++ .../java/dev/isxander/yacl/api/OptionAddable.java | 19 + .../java/dev/isxander/yacl/api/OptionFlag.java | 23 + .../java/dev/isxander/yacl/api/OptionGroup.java | 94 ++++ .../dev/isxander/yacl/api/PlaceholderCategory.java | 52 ++ .../dev/isxander/yacl/api/YetAnotherConfigLib.java | 107 ++++ .../dev/isxander/yacl/api/utils/Dimension.java | 33 ++ .../isxander/yacl/api/utils/MutableDimension.java | 11 + .../dev/isxander/yacl/api/utils/OptionUtils.java | 39 ++ .../java/dev/isxander/yacl/config/ConfigEntry.java | 11 + .../dev/isxander/yacl/config/ConfigInstance.java | 48 ++ .../isxander/yacl/config/GsonConfigInstance.java | 212 ++++++++ .../java/dev/isxander/yacl/gui/AbstractWidget.java | 107 ++++ .../dev/isxander/yacl/gui/CategoryListWidget.java | 99 ++++ .../java/dev/isxander/yacl/gui/CategoryWidget.java | 38 ++ .../isxander/yacl/gui/ElementListWidgetExt.java | 177 +++++++ .../isxander/yacl/gui/LowProfileButtonWidget.java | 28 + .../dev/isxander/yacl/gui/OptionListWidget.java | 570 +++++++++++++++++++++ .../isxander/yacl/gui/RequireRestartScreen.java | 21 + .../dev/isxander/yacl/gui/SearchFieldWidget.java | 66 +++ .../isxander/yacl/gui/TextScaledButtonWidget.java | 34 ++ .../dev/isxander/yacl/gui/TooltipButtonWidget.java | 33 ++ .../java/dev/isxander/yacl/gui/YACLScreen.java | 319 ++++++++++++ .../yacl/gui/controllers/ActionController.java | 120 +++++ .../yacl/gui/controllers/BooleanController.java | 157 ++++++ .../yacl/gui/controllers/ColorController.java | 221 ++++++++ .../yacl/gui/controllers/ControllerWidget.java | 170 ++++++ .../yacl/gui/controllers/LabelController.java | 193 +++++++ .../yacl/gui/controllers/ListEntryWidget.java | 135 +++++ .../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 | 164 ++++++ .../yacl/gui/controllers/slider/package-info.java | 10 + .../gui/controllers/string/IStringController.java | 44 ++ .../gui/controllers/string/StringController.java | 37 ++ .../string/StringControllerElement.java | 408 +++++++++++++++ .../string/number/DoubleFieldController.java | 104 ++++ .../string/number/FloatFieldController.java | 104 ++++ .../string/number/IntegerFieldController.java | 109 ++++ .../string/number/LongFieldController.java | 109 ++++ .../string/number/NumberFieldController.java | 69 +++ .../controllers/string/number/package-info.java | 10 + .../java/dev/isxander/yacl/gui/utils/GuiUtils.java | 41 ++ .../dev/isxander/yacl/impl/ButtonOptionImpl.java | 218 ++++++++ .../dev/isxander/yacl/impl/ConfigCategoryImpl.java | 137 +++++ .../dev/isxander/yacl/impl/GenericBindingImpl.java | 35 ++ .../dev/isxander/yacl/impl/LabelOptionImpl.java | 154 ++++++ .../isxander/yacl/impl/ListOptionEntryImpl.java | 149 ++++++ .../dev/isxander/yacl/impl/ListOptionImpl.java | 338 ++++++++++++ .../dev/isxander/yacl/impl/OptionGroupImpl.java | 129 +++++ .../java/dev/isxander/yacl/impl/OptionImpl.java | 303 +++++++++++ .../yacl/impl/PlaceholderCategoryImpl.java | 99 ++++ .../yacl/impl/YetAnotherConfigLibImpl.java | 122 +++++ .../yacl/impl/utils/DimensionIntegerImpl.java | 115 +++++ .../isxander/yacl/impl/utils/YACLConstants.java | 8 + .../yacl/mixin/AbstractSelectionListMixin.java | 25 + .../yacl/mixin/OptionInstanceAccessor.java | 13 + 74 files changed, 7769 insertions(+) create mode 100644 common/src/main/java/dev/isxander/yacl/api/Binding.java create mode 100644 common/src/main/java/dev/isxander/yacl/api/ButtonOption.java create mode 100644 common/src/main/java/dev/isxander/yacl/api/ConfigCategory.java create mode 100644 common/src/main/java/dev/isxander/yacl/api/Controller.java create mode 100644 common/src/main/java/dev/isxander/yacl/api/LabelOption.java create mode 100644 common/src/main/java/dev/isxander/yacl/api/ListOption.java create mode 100644 common/src/main/java/dev/isxander/yacl/api/ListOptionEntry.java create mode 100644 common/src/main/java/dev/isxander/yacl/api/NameableEnum.java create mode 100644 common/src/main/java/dev/isxander/yacl/api/Option.java create mode 100644 common/src/main/java/dev/isxander/yacl/api/OptionAddable.java create mode 100644 common/src/main/java/dev/isxander/yacl/api/OptionFlag.java create mode 100644 common/src/main/java/dev/isxander/yacl/api/OptionGroup.java create mode 100644 common/src/main/java/dev/isxander/yacl/api/PlaceholderCategory.java create mode 100644 common/src/main/java/dev/isxander/yacl/api/YetAnotherConfigLib.java create mode 100644 common/src/main/java/dev/isxander/yacl/api/utils/Dimension.java create mode 100644 common/src/main/java/dev/isxander/yacl/api/utils/MutableDimension.java create mode 100644 common/src/main/java/dev/isxander/yacl/api/utils/OptionUtils.java create mode 100644 common/src/main/java/dev/isxander/yacl/config/ConfigEntry.java create mode 100644 common/src/main/java/dev/isxander/yacl/config/ConfigInstance.java create mode 100644 common/src/main/java/dev/isxander/yacl/config/GsonConfigInstance.java create mode 100644 common/src/main/java/dev/isxander/yacl/gui/AbstractWidget.java create mode 100644 common/src/main/java/dev/isxander/yacl/gui/CategoryListWidget.java create mode 100644 common/src/main/java/dev/isxander/yacl/gui/CategoryWidget.java create mode 100644 common/src/main/java/dev/isxander/yacl/gui/ElementListWidgetExt.java create mode 100644 common/src/main/java/dev/isxander/yacl/gui/LowProfileButtonWidget.java create mode 100644 common/src/main/java/dev/isxander/yacl/gui/OptionListWidget.java create mode 100644 common/src/main/java/dev/isxander/yacl/gui/RequireRestartScreen.java create mode 100644 common/src/main/java/dev/isxander/yacl/gui/SearchFieldWidget.java create mode 100644 common/src/main/java/dev/isxander/yacl/gui/TextScaledButtonWidget.java create mode 100644 common/src/main/java/dev/isxander/yacl/gui/TooltipButtonWidget.java create mode 100644 common/src/main/java/dev/isxander/yacl/gui/YACLScreen.java create mode 100644 common/src/main/java/dev/isxander/yacl/gui/controllers/ActionController.java create mode 100644 common/src/main/java/dev/isxander/yacl/gui/controllers/BooleanController.java create mode 100644 common/src/main/java/dev/isxander/yacl/gui/controllers/ColorController.java create mode 100644 common/src/main/java/dev/isxander/yacl/gui/controllers/ControllerWidget.java create mode 100644 common/src/main/java/dev/isxander/yacl/gui/controllers/LabelController.java create mode 100644 common/src/main/java/dev/isxander/yacl/gui/controllers/ListEntryWidget.java create mode 100644 common/src/main/java/dev/isxander/yacl/gui/controllers/TickBoxController.java create mode 100644 common/src/main/java/dev/isxander/yacl/gui/controllers/cycling/CyclingControllerElement.java create mode 100644 common/src/main/java/dev/isxander/yacl/gui/controllers/cycling/CyclingListController.java create mode 100644 common/src/main/java/dev/isxander/yacl/gui/controllers/cycling/EnumController.java create mode 100644 common/src/main/java/dev/isxander/yacl/gui/controllers/cycling/ICyclingController.java create mode 100644 common/src/main/java/dev/isxander/yacl/gui/controllers/package-info.java create mode 100644 common/src/main/java/dev/isxander/yacl/gui/controllers/slider/DoubleSliderController.java create mode 100644 common/src/main/java/dev/isxander/yacl/gui/controllers/slider/FloatSliderController.java create mode 100644 common/src/main/java/dev/isxander/yacl/gui/controllers/slider/ISliderController.java create mode 100644 common/src/main/java/dev/isxander/yacl/gui/controllers/slider/IntegerSliderController.java create mode 100644 common/src/main/java/dev/isxander/yacl/gui/controllers/slider/LongSliderController.java create mode 100644 common/src/main/java/dev/isxander/yacl/gui/controllers/slider/SliderControllerElement.java create mode 100644 common/src/main/java/dev/isxander/yacl/gui/controllers/slider/package-info.java create mode 100644 common/src/main/java/dev/isxander/yacl/gui/controllers/string/IStringController.java create mode 100644 common/src/main/java/dev/isxander/yacl/gui/controllers/string/StringController.java create mode 100644 common/src/main/java/dev/isxander/yacl/gui/controllers/string/StringControllerElement.java create mode 100644 common/src/main/java/dev/isxander/yacl/gui/controllers/string/number/DoubleFieldController.java create mode 100644 common/src/main/java/dev/isxander/yacl/gui/controllers/string/number/FloatFieldController.java create mode 100644 common/src/main/java/dev/isxander/yacl/gui/controllers/string/number/IntegerFieldController.java create mode 100644 common/src/main/java/dev/isxander/yacl/gui/controllers/string/number/LongFieldController.java create mode 100644 common/src/main/java/dev/isxander/yacl/gui/controllers/string/number/NumberFieldController.java create mode 100644 common/src/main/java/dev/isxander/yacl/gui/controllers/string/number/package-info.java create mode 100644 common/src/main/java/dev/isxander/yacl/gui/utils/GuiUtils.java create mode 100644 common/src/main/java/dev/isxander/yacl/impl/ButtonOptionImpl.java create mode 100644 common/src/main/java/dev/isxander/yacl/impl/ConfigCategoryImpl.java create mode 100644 common/src/main/java/dev/isxander/yacl/impl/GenericBindingImpl.java create mode 100644 common/src/main/java/dev/isxander/yacl/impl/LabelOptionImpl.java create mode 100644 common/src/main/java/dev/isxander/yacl/impl/ListOptionEntryImpl.java create mode 100644 common/src/main/java/dev/isxander/yacl/impl/ListOptionImpl.java create mode 100644 common/src/main/java/dev/isxander/yacl/impl/OptionGroupImpl.java create mode 100644 common/src/main/java/dev/isxander/yacl/impl/OptionImpl.java create mode 100644 common/src/main/java/dev/isxander/yacl/impl/PlaceholderCategoryImpl.java create mode 100644 common/src/main/java/dev/isxander/yacl/impl/YetAnotherConfigLibImpl.java create mode 100644 common/src/main/java/dev/isxander/yacl/impl/utils/DimensionIntegerImpl.java create mode 100644 common/src/main/java/dev/isxander/yacl/impl/utils/YACLConstants.java create mode 100644 common/src/main/java/dev/isxander/yacl/mixin/AbstractSelectionListMixin.java create mode 100644 common/src/main/java/dev/isxander/yacl/mixin/OptionInstanceAccessor.java (limited to 'common/src/main/java/dev/isxander') diff --git a/common/src/main/java/dev/isxander/yacl/api/Binding.java b/common/src/main/java/dev/isxander/yacl/api/Binding.java new file mode 100644 index 0000000..b4cd2d0 --- /dev/null +++ b/common/src/main/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.OptionInstanceAccessor; +import net.minecraft.client.OptionInstance; +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 OptionInstance} + */ + static Binding minecraft(OptionInstance minecraftOption) { + Validate.notNull(minecraftOption, "`minecraftOption` must not be null"); + + return new GenericBindingImpl<>( + ((OptionInstanceAccessor) (Object) minecraftOption).getInitialValue(), + minecraftOption::get, + minecraftOption::set + ); + } + + /** + * 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/common/src/main/java/dev/isxander/yacl/api/ButtonOption.java b/common/src/main/java/dev/isxander/yacl/api/ButtonOption.java new file mode 100644 index 0000000..88e1c4b --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl/api/ButtonOption.java @@ -0,0 +1,66 @@ +package dev.isxander.yacl.api; + +import dev.isxander.yacl.gui.YACLScreen; +import dev.isxander.yacl.impl.ButtonOptionImpl; +import net.minecraft.network.chat.Component; +import org.jetbrains.annotations.NotNull; + +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 dev.isxander.yacl.api.ButtonOption.Builder createBuilder() { + return new ButtonOptionImpl.BuilderImpl(); + } + + interface Builder { + /** + * Sets the name to be used by the option. + * + * @see Option#name() + */ + dev.isxander.yacl.api.ButtonOption.Builder name(@NotNull Component name); + + /** + * 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()}. + */ + dev.isxander.yacl.api.ButtonOption.Builder tooltip(@NotNull Component... tooltips); + + dev.isxander.yacl.api.ButtonOption.Builder action(@NotNull BiConsumer action); + + /** + * Action to be executed upon button press + * + * @see ButtonOption#action() + */ + @Deprecated + dev.isxander.yacl.api.ButtonOption.Builder action(@NotNull Consumer action); + + /** + * Sets if the option can be configured + * + * @see Option#available() + */ + dev.isxander.yacl.api.ButtonOption.Builder available(boolean available); + + /** + * Sets the controller for the option. + * This is how you interact and change the options. + * + * @see dev.isxander.yacl.gui.controllers + */ + dev.isxander.yacl.api.ButtonOption.Builder controller(@NotNull Function>> control); + + ButtonOption build(); + } +} diff --git a/common/src/main/java/dev/isxander/yacl/api/ConfigCategory.java b/common/src/main/java/dev/isxander/yacl/api/ConfigCategory.java new file mode 100644 index 0000000..7764479 --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl/api/ConfigCategory.java @@ -0,0 +1,94 @@ +package dev.isxander.yacl.api; + +import com.google.common.collect.ImmutableList; +import dev.isxander.yacl.impl.ConfigCategoryImpl; +import net.minecraft.network.chat.Component; +import org.jetbrains.annotations.NotNull; + +import java.util.Collection; + +/** + * 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 Component name(); + + /** + * Gets every {@link OptionGroup} in this category. + */ + @NotNull ImmutableList groups(); + + /** + * Tooltip (or description) of the category. + * Rendered on hover. + */ + @NotNull Component tooltip(); + + /** + * Creates a builder to construct a {@link ConfigCategory} + */ + static Builder createBuilder() { + return new ConfigCategoryImpl.BuilderImpl(); + } + + interface Builder extends OptionAddable { + /** + * Sets name of the category + * + * @see ConfigCategory#name() + */ + Builder name(@NotNull Component name); + + /** + * 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() + */ + @Override + Builder option(@NotNull Option option); + + /** + * 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() + */ + @Override + Builder options(@NotNull Collection> options); + + /** + * 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()} + */ + Builder group(@NotNull OptionGroup group); + + /** + * 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()} + */ + Builder groups(@NotNull Collection groups); + + /** + * 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()}. + */ + Builder tooltip(@NotNull Component... tooltips); + + ConfigCategory build(); + } +} diff --git a/common/src/main/java/dev/isxander/yacl/api/Controller.java b/common/src/main/java/dev/isxander/yacl/api/Controller.java new file mode 100644 index 0000000..0b8e2ed --- /dev/null +++ b/common/src/main/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.network.chat.Component; + +/** + * 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()} + */ + Component formatValue(); + + /** + * Provides a widget to display + * + * @param screen parent screen + */ + AbstractWidget provideWidget(YACLScreen screen, Dimension widgetDimension); +} diff --git a/common/src/main/java/dev/isxander/yacl/api/LabelOption.java b/common/src/main/java/dev/isxander/yacl/api/LabelOption.java new file mode 100644 index 0000000..05c7214 --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl/api/LabelOption.java @@ -0,0 +1,41 @@ +package dev.isxander.yacl.api; + +import dev.isxander.yacl.impl.LabelOptionImpl; +import net.minecraft.network.chat.Component; +import org.jetbrains.annotations.NotNull; + +import java.util.Collection; + +/** + * A label option is an easier way of creating a label with a {@link dev.isxander.yacl.gui.controllers.LabelController}. + * This option is immutable and cannot be disabled. Tooltips are supported through + * {@link Component} styling. + */ +public interface LabelOption extends Option { + @NotNull Component label(); + + /** + * Creates a new label option with the given label, skipping a builder for ease. + */ + static LabelOption create(@NotNull Component label) { + return new LabelOptionImpl(label); + } + + static dev.isxander.yacl.api.LabelOption.Builder createBuilder() { + return new LabelOptionImpl.BuilderImpl(); + } + + interface Builder { + /** + * Appends a line to the label + */ + dev.isxander.yacl.api.LabelOption.Builder line(@NotNull Component line); + + /** + * Appends multiple lines to the label + */ + dev.isxander.yacl.api.LabelOption.Builder lines(@NotNull Collection lines); + + LabelOption build(); + } +} diff --git a/common/src/main/java/dev/isxander/yacl/api/ListOption.java b/common/src/main/java/dev/isxander/yacl/api/ListOption.java new file mode 100644 index 0000000..afba8ee --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl/api/ListOption.java @@ -0,0 +1,152 @@ +package dev.isxander.yacl.api; + +import com.google.common.collect.ImmutableList; +import dev.isxander.yacl.impl.ListOptionImpl; +import net.minecraft.network.chat.Component; +import org.jetbrains.annotations.ApiStatus; +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; + +/** + * A list option that takes form as an option group for UX. + * You add this option through {@link ConfigCategory.Builder#group(OptionGroup)}. Do NOT add as an option. + * Users can add remove and reshuffle a list type. You can use any controller you wish, there are no dedicated + * controllers for list types. List options do not manipulate your list but get and set the list with a + * regular binding for simplicity. + * + * You may apply option flags like a normal option and collapse like a normal group, it is a merge of them both. + * Methods in this interface marked with {@link ApiStatus.Internal} should not be used, and could be subject to + * change at any time + * @param + */ +public interface ListOption extends OptionGroup, Option> { + @Override + @NotNull ImmutableList> options(); + + /** + * Class of the entry type + */ + @NotNull Class elementTypeClass(); + + @ApiStatus.Internal + ListOptionEntry insertNewEntryToTop(); + + @ApiStatus.Internal + void insertEntry(int index, ListOptionEntry entry); + + @ApiStatus.Internal + int indexOf(ListOptionEntry entry); + + @ApiStatus.Internal + void removeEntry(ListOptionEntry entry); + + @ApiStatus.Internal + void addRefreshListener(Runnable changedListener); + + static Builder createBuilder(Class typeClass) { + return new ListOptionImpl.BuilderImpl<>(typeClass); + } + + interface Builder { + /** + * Sets name of the list, for UX purposes, a name should always be given, + * but isn't enforced. + * + * @see ListOption#name() + */ + Builder name(@NotNull Component name); + + /** + * Sets the tooltip to be used by the list. It is displayed like a normal + * group when you hover over the name. Entries do not allow a tooltip. + *

+ * 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 dev.isxander.yacl.api.ListOption.Builder#build()}. + */ + Builder tooltip(@NotNull Component... tooltips); + + /** + * Sets the value that is used when creating new entries + */ + Builder initial(@NotNull T initialValue); + + /** + * Sets the controller for the option. + * This is how you interact and change the options. + * + * @see dev.isxander.yacl.gui.controllers + */ + Builder controller(@NotNull Function, Controller> control); + + /** + * Sets the binding for the option. + * Used for default, getter and setter. + * + * @see Binding + */ + Builder binding(@NotNull Binding> binding); + + /** + * 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 + */ + Builder binding(@NotNull List def, @NotNull Supplier<@NotNull List> getter, @NotNull Consumer<@NotNull List> setter); + + /** + * Sets if the option can be configured + * + * @see Option#available() + */ + Builder available(boolean available); + + /** + * Adds a flag to the option. + * Upon applying changes, all flags are executed. + * {@link Option#flags()} + */ + Builder flag(@NotNull OptionFlag... flag); + + /** + * Adds a flag to the option. + * Upon applying changes, all flags are executed. + * {@link Option#flags()} + */ + Builder flags(@NotNull Collection flags); + + /** + * Dictates if the group should be collapsed by default. + * If not set, it will not be collapsed by default. + * + * @see OptionGroup#collapsed() + */ + Builder collapsed(boolean collapsible); + + /** + * Adds a listener to the option. Invoked upon changing any of the list's entries. + * + * @see Option#addListener(BiConsumer) + */ + ListOption.Builder listener(@NotNull BiConsumer>, List> listener); + + /** + * Adds multiple listeners to the option. Invoked upon changing of any of the list's entries. + * + * @see Option#addListener(BiConsumer) + */ + ListOption.Builder listeners(@NotNull Collection>, List>> listeners); + + ListOption build(); + } +} diff --git a/common/src/main/java/dev/isxander/yacl/api/ListOptionEntry.java b/common/src/main/java/dev/isxander/yacl/api/ListOptionEntry.java new file mode 100644 index 0000000..e0a3424 --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl/api/ListOptionEntry.java @@ -0,0 +1,23 @@ +package dev.isxander.yacl.api; + +import com.google.common.collect.ImmutableSet; +import org.jetbrains.annotations.NotNull; + +public interface ListOptionEntry extends Option { + ListOption parentGroup(); + + @Override + default @NotNull Class typeClass() { + return parentGroup().elementTypeClass(); + } + + @Override + default @NotNull ImmutableSet flags() { + return parentGroup().flags(); + } + + @Override + default boolean available() { + return parentGroup().available(); + } +} diff --git a/common/src/main/java/dev/isxander/yacl/api/NameableEnum.java b/common/src/main/java/dev/isxander/yacl/api/NameableEnum.java new file mode 100644 index 0000000..4b04057 --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl/api/NameableEnum.java @@ -0,0 +1,10 @@ +package dev.isxander.yacl.api; + +import net.minecraft.network.chat.Component; + +/** + * Used for the default value formatter of {@link dev.isxander.yacl.gui.controllers.cycling.EnumController} + */ +public interface NameableEnum { + Component getDisplayName(); +} diff --git a/common/src/main/java/dev/isxander/yacl/api/Option.java b/common/src/main/java/dev/isxander/yacl/api/Option.java new file mode 100644 index 0000000..a6c0311 --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl/api/Option.java @@ -0,0 +1,227 @@ +package dev.isxander.yacl.api; + +import com.google.common.collect.ImmutableSet; +import dev.isxander.yacl.impl.OptionImpl; +import net.minecraft.network.chat.Component; +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; + +public interface Option { + /** + * Name of the option + */ + @NotNull Component name(); + + /** + * Tooltip (or description) of the option. + * Rendered on hover. + */ + @NotNull Component 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(); + + /** + * 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(); + + default boolean canResetToDefault() { + return true; + } + + /** + * 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 OptionImpl.BuilderImpl<>(typeClass); + } + + interface Builder { + /** + * Sets the name to be used by the option. + * + * @see Option#name() + */ + Builder name(@NotNull Component name); + + /** + * 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()}. + */ + Builder tooltip(@NotNull Function tooltipGetter); + + /** + * 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()}. + */ + @Deprecated + Builder tooltip(@NotNull Function... tooltipGetter); + + /** + * 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()}. + */ + Builder tooltip(@NotNull Component... tooltips); + + /** + * Sets the controller for the option. + * This is how you interact and change the options. + * + * @see dev.isxander.yacl.gui.controllers + */ + Builder controller(@NotNull Function, Controller> control); + + /** + * Sets the binding for the option. + * Used for default, getter and setter. + * + * @see Binding + */ + Builder binding(@NotNull Binding binding); + + /** + * 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 + */ + Builder binding(@NotNull T def, @NotNull Supplier<@NotNull T> getter, @NotNull Consumer<@NotNull T> setter); + + /** + * Sets if the option can be configured + * + * @see Option#available() + */ + Builder available(boolean available); + + /** + * Adds a flag to the option. + * Upon applying changes, all flags are executed. + * {@link Option#flags()} + */ + Builder flag(@NotNull OptionFlag... flag); + + /** + * Adds a flag to the option. + * Upon applying changes, all flags are executed. + * {@link Option#flags()} + */ + Builder flags(@NotNull Collection flags); + + /** + * Instantly invokes the binder's setter when modified in the GUI. + * Prevents the user from undoing the change + *

+ * Does not support {@link Option#flags()}! + */ + Builder instant(boolean instant); + + /** + * Adds a listener to the option. Invoked upon changing the pending value. + * + * @see Option#addListener(BiConsumer) + */ + Builder listener(@NotNull BiConsumer, T> listener); + + /** + * Adds multiple listeners to the option. Invoked upon changing the pending value. + * + * @see Option#addListener(BiConsumer) + */ + Builder listeners(@NotNull Collection, T>> listeners); + + Option build(); + } +} diff --git a/common/src/main/java/dev/isxander/yacl/api/OptionAddable.java b/common/src/main/java/dev/isxander/yacl/api/OptionAddable.java new file mode 100644 index 0000000..57be06c --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl/api/OptionAddable.java @@ -0,0 +1,19 @@ +package dev.isxander.yacl.api; + +import org.jetbrains.annotations.NotNull; + +import java.util.Collection; + +public interface OptionAddable { + /** + * Adds an option to an abstract builder. + * To construct an option, use {@link Option#createBuilder(Class)} + */ + OptionAddable option(@NotNull Option option); + + /** + * Adds multiple options to an abstract builder. + * To construct an option, use {@link Option#createBuilder(Class)} + */ + OptionAddable options(@NotNull Collection> options); +} diff --git a/common/src/main/java/dev/isxander/yacl/api/OptionFlag.java b/common/src/main/java/dev/isxander/yacl/api/OptionFlag.java new file mode 100644 index 0000000..51d57e4 --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl/api/OptionFlag.java @@ -0,0 +1,23 @@ +package dev.isxander.yacl.api; + +import dev.isxander.yacl.gui.RequireRestartScreen; +import net.minecraft.client.Minecraft; + +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.screen)); + + /** Reloads chunks upon applying (F3+A) */ + OptionFlag RELOAD_CHUNKS = client -> client.levelRenderer.allChanged(); + + OptionFlag WORLD_RENDER_UPDATE = client -> client.levelRenderer.needsUpdate(); + + OptionFlag ASSET_RELOAD = Minecraft::delayTextureReload; +} diff --git a/common/src/main/java/dev/isxander/yacl/api/OptionGroup.java b/common/src/main/java/dev/isxander/yacl/api/OptionGroup.java new file mode 100644 index 0000000..4fe43c7 --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl/api/OptionGroup.java @@ -0,0 +1,94 @@ +package dev.isxander.yacl.api; + +import com.google.common.collect.ImmutableList; +import dev.isxander.yacl.impl.OptionGroupImpl; +import net.minecraft.network.chat.Component; +import org.jetbrains.annotations.NotNull; + +import java.util.Collection; + +/** + * 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. + */ + Component name(); + + /** + * Tooltip displayed on hover. + */ + Component 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 OptionGroupImpl.BuilderImpl(); + } + + interface Builder extends OptionAddable { + /** + * Sets name of the group, can be {@link Component#empty()} to just separate options, like sodium. + * + * @see OptionGroup#name() + */ + Builder name(@NotNull Component name); + + /** + * Sets the tooltip to be used by the option group. + * Can be invoked twice to append more lines. + * No need to wrap the Component yourself, the gui does this itself. + * + * @param tooltips Component lines - merged with a new-line on {@link Builder#build()}. + */ + Builder tooltip(@NotNull Component... tooltips); + + /** + * Adds an option to group. + * To construct an option, use {@link Option#createBuilder(Class)} + * + * @see OptionGroup#options() + */ + @Override + Builder option(@NotNull Option option); + + /** + * Adds multiple options to group. + * To construct an option, use {@link Option#createBuilder(Class)} + * + * @see OptionGroup#options() + */ + @Override + Builder options(@NotNull Collection> options); + + /** + * Dictates if the group should be collapsed by default + * + * @see OptionGroup#collapsed() + */ + Builder collapsed(boolean collapsible); + + OptionGroup build(); + } +} diff --git a/common/src/main/java/dev/isxander/yacl/api/PlaceholderCategory.java b/common/src/main/java/dev/isxander/yacl/api/PlaceholderCategory.java new file mode 100644 index 0000000..3641fad --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl/api/PlaceholderCategory.java @@ -0,0 +1,52 @@ +package dev.isxander.yacl.api; + +import dev.isxander.yacl.gui.YACLScreen; +import dev.isxander.yacl.impl.PlaceholderCategoryImpl; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.Component; +import org.jetbrains.annotations.NotNull; + +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 PlaceholderCategoryImpl.BuilderImpl(); + } + + interface Builder { + /** + * Sets name of the category + * + * @see ConfigCategory#name() + */ + Builder name(@NotNull Component name); + + /** + * Sets the tooltip to be used by the category. + * Can be invoked twice to append more lines. + * No need to wrap the Component yourself, the gui does this itself. + * + * @param tooltips Component lines - merged with a new-line on {@link dev.isxander.yacl.api.PlaceholderCategory.Builder#build()}. + */ + Builder tooltip(@NotNull Component... tooltips); + + /** + * Screen to open upon selecting this category + * + * @see PlaceholderCategory#screen() + */ + Builder screen(@NotNull BiFunction screenFunction); + + PlaceholderCategory build(); + } +} diff --git a/common/src/main/java/dev/isxander/yacl/api/YetAnotherConfigLib.java b/common/src/main/java/dev/isxander/yacl/api/YetAnotherConfigLib.java new file mode 100644 index 0000000..c6da1d1 --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl/api/YetAnotherConfigLib.java @@ -0,0 +1,107 @@ +package dev.isxander.yacl.api; + +import com.google.common.collect.ImmutableList; +import dev.isxander.yacl.config.ConfigInstance; +import dev.isxander.yacl.gui.YACLScreen; +import dev.isxander.yacl.impl.YetAnotherConfigLibImpl; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.Component; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Collection; +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. + */ + Component 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 YetAnotherConfigLibImpl.BuilderImpl(); + } + + /** + * Creates an instance using a {@link ConfigInstance} which autofills the save() builder method. + * This also takes an easy functional interface that provides defaults and config to help build YACL bindings. + */ + static YetAnotherConfigLib create(ConfigInstance configInstance, ConfigBackedBuilder builder) { + return builder.build(configInstance.getDefaults(), configInstance.getConfig(), createBuilder().save(configInstance::save)).build(); + } + + interface Builder { + /** + * Sets title of GUI for Minecraft narration + * + * @see YetAnotherConfigLib#title() + */ + Builder title(@NotNull Component title); + + /** + * Adds a new category. + * To create a category you need to use {@link ConfigCategory#createBuilder()} + * + * @see YetAnotherConfigLib#categories() + */ + Builder category(@NotNull ConfigCategory category); + + /** + * Adds multiple categories at once. + * To create a category you need to use {@link ConfigCategory#createBuilder()} + * + * @see YetAnotherConfigLib#categories() + */ + Builder categories(@NotNull Collection categories); + + /** + * Used to define a save function for when user clicks the Save Changes button + * + * @see YetAnotherConfigLib#saveFunction() + */ + Builder save(@NotNull Runnable saveFunction); + + /** + * Defines a consumer that is accepted every time the YACL screen initialises + * + * @see YetAnotherConfigLib#initConsumer() + */ + Builder screenInit(@NotNull Consumer initConsumer); + + YetAnotherConfigLib build(); + } + + @FunctionalInterface + interface ConfigBackedBuilder { + YetAnotherConfigLib.Builder build(T defaults, T config, YetAnotherConfigLib.Builder builder); + } +} diff --git a/common/src/main/java/dev/isxander/yacl/api/utils/Dimension.java b/common/src/main/java/dev/isxander/yacl/api/utils/Dimension.java new file mode 100644 index 0000000..0de0a58 --- /dev/null +++ b/common/src/main/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/common/src/main/java/dev/isxander/yacl/api/utils/MutableDimension.java b/common/src/main/java/dev/isxander/yacl/api/utils/MutableDimension.java new file mode 100644 index 0000000..eff0186 --- /dev/null +++ b/common/src/main/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/common/src/main/java/dev/isxander/yacl/api/utils/OptionUtils.java b/common/src/main/java/dev/isxander/yacl/api/utils/OptionUtils.java new file mode 100644 index 0000000..22032bd --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl/api/utils/OptionUtils.java @@ -0,0 +1,39 @@ +package dev.isxander.yacl.api.utils; + +import dev.isxander.yacl.api.*; + +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()) { + if (group instanceof ListOption list) { + if (consumer.apply(list)) return; + } else { + 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/common/src/main/java/dev/isxander/yacl/config/ConfigEntry.java b/common/src/main/java/dev/isxander/yacl/config/ConfigEntry.java new file mode 100644 index 0000000..7f04c33 --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl/config/ConfigEntry.java @@ -0,0 +1,11 @@ +package dev.isxander.yacl.config; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface ConfigEntry { +} diff --git a/common/src/main/java/dev/isxander/yacl/config/ConfigInstance.java b/common/src/main/java/dev/isxander/yacl/config/ConfigInstance.java new file mode 100644 index 0000000..c207161 --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl/config/ConfigInstance.java @@ -0,0 +1,48 @@ +package dev.isxander.yacl.config; + +import java.lang.reflect.InvocationTargetException; + +/** + * Responsible for handing the actual config data type. + * Holds the instance along with a final default instance + * to reference default values for options and should not be changed. + * + * Abstract methods to save and load the class, implementations are responsible for + * how it saves and load. + * + * @param config data type + */ +public abstract class ConfigInstance { + private final Class configClass; + private final T defaultInstance; + private T instance; + + public ConfigInstance(Class configClass) { + this.configClass = configClass; + + try { + this.defaultInstance = this.instance = configClass.getConstructor().newInstance(); + } catch (InstantiationException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e) { + throw new IllegalStateException(String.format("Could not create default instance of config for %s. Make sure there is a default constructor!", this.configClass.getSimpleName())); + } + } + + public abstract void save(); + public abstract void load(); + + public T getConfig() { + return this.instance; + } + + protected void setConfig(T instance) { + this.instance = instance; + } + + public T getDefaults() { + return this.defaultInstance; + } + + public Class getConfigClass() { + return this.configClass; + } +} diff --git a/common/src/main/java/dev/isxander/yacl/config/GsonConfigInstance.java b/common/src/main/java/dev/isxander/yacl/config/GsonConfigInstance.java new file mode 100644 index 0000000..ad7f550 --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl/config/GsonConfigInstance.java @@ -0,0 +1,212 @@ +package dev.isxander.yacl.config; + +import com.google.gson.*; +import dev.isxander.yacl.impl.utils.YACLConstants; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.Style; + +import java.awt.*; +import java.io.IOException; +import java.lang.reflect.Type; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.function.UnaryOperator; + +/** + * Uses GSON to serialize and deserialize config data from JSON to a file. + * + * Only fields annotated with {@link ConfigEntry} are included in the JSON. + * {@link Component}, {@link Style} and {@link Color} have default type adapters, so there is no need to provide them in your GSON instance. + * GSON is automatically configured to format fields as {@code lower_camel_case}. + * + * @param config data type + */ +public class GsonConfigInstance extends ConfigInstance { + private final Gson gson; + private final Path path; + + @Deprecated + public GsonConfigInstance(Class configClass, Path path) { + this(configClass, path, new GsonBuilder()); + } + + @Deprecated + public GsonConfigInstance(Class configClass, Path path, Gson gson) { + this(configClass, path, gson.newBuilder()); + } + + @Deprecated + public GsonConfigInstance(Class configClass, Path path, UnaryOperator builder) { + this(configClass, path, builder.apply(new GsonBuilder())); + } + + @Deprecated + public GsonConfigInstance(Class configClass, Path path, GsonBuilder builder) { + super(configClass); + this.path = path; + this.gson = builder + .setExclusionStrategies(new ConfigExclusionStrategy()) + .registerTypeHierarchyAdapter(Component.class, new Component.Serializer()) + .registerTypeHierarchyAdapter(Style.class, new Style.Serializer()) + .registerTypeHierarchyAdapter(Color.class, new ColorTypeAdapter()) + .serializeNulls() + .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES) + .create(); + } + + private GsonConfigInstance(Class configClass, Path path, Gson gson, boolean fromBuilder) { + super(configClass); + this.path = path; + this.gson = gson; + } + + @Override + public void save() { + try { + YACLConstants.LOGGER.info("Saving {}...", getConfigClass().getSimpleName()); + Files.writeString(path, gson.toJson(getConfig()), StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE); + } catch (IOException e) { + e.printStackTrace(); + } + } + + @Override + public void load() { + try { + if (Files.notExists(path)) { + save(); + return; + } + + YACLConstants.LOGGER.info("Loading {}...", getConfigClass().getSimpleName()); + setConfig(gson.fromJson(Files.readString(path), getConfigClass())); + } catch (IOException e) { + e.printStackTrace(); + } + } + + public Path getPath() { + return this.path; + } + + private static class ConfigExclusionStrategy implements ExclusionStrategy { + @Override + public boolean shouldSkipField(FieldAttributes fieldAttributes) { + return fieldAttributes.getAnnotation(ConfigEntry.class) == null; + } + + @Override + public boolean shouldSkipClass(Class aClass) { + return false; + } + } + + public static class ColorTypeAdapter implements JsonSerializer, JsonDeserializer { + @Override + public Color deserialize(JsonElement jsonElement, Type type, JsonDeserializationContext jsonDeserializationContext) throws JsonParseException { + return new Color(jsonElement.getAsInt(), true); + } + + @Override + public JsonElement serialize(Color color, Type type, JsonSerializationContext jsonSerializationContext) { + return new JsonPrimitive(color.getRGB()); + } + } + + /** + * Creates a builder for a GSON config instance. + * @param configClass the config class + * @return a new builder + * @param the config type + */ + public static Builder createBuilder(Class configClass) { + return new Builder<>(configClass); + } + + public static class Builder { + private final Class configClass; + private Path path; + private UnaryOperator gsonBuilder = builder -> builder + .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES) + .serializeNulls() + .registerTypeHierarchyAdapter(Component.class, new Component.Serializer()) + .registerTypeHierarchyAdapter(Style.class, new Style.Serializer()) + .registerTypeHierarchyAdapter(Color.class, new ColorTypeAdapter()); + + private Builder(Class configClass) { + this.configClass = configClass; + } + + /** + * Sets the file path to save and load the config from. + */ + public Builder setPath(Path path) { + this.path = path; + return this; + } + + /** + * Sets the GSON instance to use. Overrides all YACL defaults such as: + *

    + *
  • lower_camel_case field naming policy
  • + *
  • null serialization
  • + *
  • {@link Component}, {@link Style} and {@link Color} type adapters
  • + *
+ * Still respects the exclusion strategy to only serialize {@link ConfigEntry} + * but these can be added to with setExclusionStrategies. + * + * @param gsonBuilder gson builder to use + */ + public Builder overrideGsonBuilder(GsonBuilder gsonBuilder) { + this.gsonBuilder = builder -> gsonBuilder; + return this; + } + + /** + * Sets the GSON instance to use. Overrides all YACL defaults such as: + *
    + *
  • lower_camel_case field naming policy
  • + *
  • null serialization
  • + *
  • {@link Component}, {@link Style} and {@link Color} type adapters
  • + *
+ * Still respects the exclusion strategy to only serialize {@link ConfigEntry} + * but these can be added to with setExclusionStrategies. + * + * @param gson gson instance to be converted to a builder + */ + public Builder overrideGsonBuilder(Gson gson) { + return this.overrideGsonBuilder(gson.newBuilder()); + } + + /** + * Appends extra configuration to a GSON builder. + * This is the intended way to add functionality to the GSON instance. + *

+ * By default, YACL sets the GSON with the following options: + *

    + *
  • lower_camel_case field naming policy
  • + *
  • null serialization
  • + *
  • {@link Component}, {@link Style} and {@link Color} type adapters
  • + *
+ * + * @param gsonBuilder the function to apply to the builder + */ + public Builder appendGsonBuilder(UnaryOperator gsonBuilder) { + this.gsonBuilder = builder -> gsonBuilder.apply(this.gsonBuilder.apply(builder)); + return this; + } + + /** + * Builds the config instance. + * @return the built config instance + */ + public GsonConfigInstance build() { + UnaryOperator gsonBuilder = builder -> this.gsonBuilder.apply(builder) + .addSerializationExclusionStrategy(new ConfigExclusionStrategy()) + .addDeserializationExclusionStrategy(new ConfigExclusionStrategy()); + + return new GsonConfigInstance<>(configClass, path, gsonBuilder.apply(new GsonBuilder()).create(), true); + } + } +} diff --git a/common/src/main/java/dev/isxander/yacl/gui/AbstractWidget.java b/common/src/main/java/dev/isxander/yacl/gui/AbstractWidget.java new file mode 100644 index 0000000..ae3c83b --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl/gui/AbstractWidget.java @@ -0,0 +1,107 @@ +package dev.isxander.yacl.gui; + +import com.mojang.blaze3d.systems.RenderSystem; +import com.mojang.blaze3d.vertex.PoseStack; +import dev.isxander.yacl.api.utils.Dimension; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.GuiComponent; +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.NarrationElementOutput; +import net.minecraft.client.renderer.GameRenderer; +import net.minecraft.client.resources.sounds.SimpleSoundInstance; +import net.minecraft.sounds.SoundEvents; + +import java.awt.*; + +public abstract class AbstractWidget implements GuiEventListener, Renderable, NarratableEntry { + protected final Minecraft client = Minecraft.getInstance(); + protected final Font textRenderer = client.font; + protected final int inactiveColor = 0xFFA0A0A0; + + private Dimension dim; + + public AbstractWidget(Dimension dim) { + this.dim = dim; + } + + public void postRender(PoseStack 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 NarrationPriority narrationPriority() { + return NarrationPriority.NONE; + } + + public void unfocus() { + + } + + public boolean matchesSearch(String query) { + return true; + } + + @Override + public void updateNarration(NarrationElementOutput builder) { + + } + + protected void drawButtonRect(PoseStack 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::getPositionTexShader); + RenderSystem.setShaderTexture(0, net.minecraft.client.gui.components.AbstractWidget.WIDGETS_LOCATION); + RenderSystem.setShaderColor(1.0F, 1.0F, 1.0F, 1.0F); + int i = !enabled ? 0 : hovered ? 2 : 1; + RenderSystem.enableBlend(); + RenderSystem.defaultBlendFunc(); + RenderSystem.enableDepthTest(); + GuiComponent.blit(matrices, x1, y1, 0, 0, 46 + i * 20, width / 2, height, 256, 256); + GuiComponent.blit(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((in