diff options
author | Xander <xander@isxander.dev> | 2022-12-09 16:31:25 +0000 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-12-09 16:31:25 +0000 |
commit | e4856a17133b0567d09cb6db3821674491d57e64 (patch) | |
tree | 0c59597708b3ea9f402ba119490537b5c18fdb93 /src/client/java/dev/isxander/yacl/api | |
parent | e1f6d190d862dd86c251fdd5726efe99f8ec1baf (diff) | |
parent | 49ff470de36e719d5b963de405de891eca2b69d1 (diff) | |
download | YetAnotherConfigLib-e4856a17133b0567d09cb6db3821674491d57e64.tar.gz YetAnotherConfigLib-e4856a17133b0567d09cb6db3821674491d57e64.tar.bz2 YetAnotherConfigLib-e4856a17133b0567d09cb6db3821674491d57e64.zip |
Merge pull request #38 from isXander/update/1.19.3
Diffstat (limited to 'src/client/java/dev/isxander/yacl/api')
11 files changed, 1164 insertions, 0 deletions
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<T> { + 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 <T> Binding<T> generic(T def, Supplier<T> getter, Consumer<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"); + + return new GenericBindingImpl<>(def, getter, setter); + } + + /** + * Creates a {@link Binding} for Minecraft's {@link SimpleOption} + */ + static <T> Binding<T> minecraft(SimpleOption<T> minecraftOption) { + Validate.notNull(minecraftOption, "`minecraftOption` must not be null"); + + return new GenericBindingImpl<>( + ((SimpleOptionAccessor<T>) (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 <T> Binding<T> 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<BiConsumer<YACLScreen, ButtonOption>> { + /** + * Action to be executed upon button press + */ + BiConsumer<YACLScreen, ButtonOption> action(); + + static Builder createBuilder() { + return new Builder(); + } + + class Builder { + private Text name; + private final List<Text> tooltipLines = new ArrayList<>(); + private boolean available = true; + private Function<ButtonOption, Controller<BiConsumer<YACLScreen, ButtonOption>>> controlGetter; + private BiConsumer<YACLScreen, ButtonOption> 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<YACLScreen, ButtonOption> 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<YACLScreen> 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<ButtonOption, Controller<BiConsumer<YACLScreen, ButtonOption>>> 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<OptionGroup> 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<Option<?>> rootOptions = new ArrayList<>(); + private final List<OptionGroup> groups = new ArrayList<>(); + + private final List<Text> 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<Option<?>> 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<OptionGroup> 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<OptionGroup> 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<T> { + /** + * Gets the dedicated {@link Option} for this controller + */ + Option<T> 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<Integer> 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<T> { + /** + * 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<T> controller(); + + /** + * Binding for the option. + * Controls setting, getting and default value. + * + * @see Binding + */ + @NotNull Binding<T> 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<T> typeClass(); + + /** + * Tasks that needs to be executed upon applying changes. + */ + @NotNull ImmutableSet<OptionFlag> 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<Option<T>, T> changedListener); + + /** + * Creates a builder to construct an {@link Option} + * + * @param <T> type of the option's value + * @param typeClass used to capture the type + */ + static <T> Builder<T> createBuilder(Class<T> typeClass) { + return new Builder<>(typeClass); + } + + class Builder<T> { + private Text name = Text.literal("Name not specified!").formatted(Formatting.RED); + + private final List<Function<T, Text>> tooltipGetters = new ArrayList<>(); + + private Function<Option<T>, Controller<T>> controlGetter; + + private Binding<T> binding; + + private boolean available = true; + + private boolean instant = false; + + private final Set<OptionFlag> flags = new HashSet<>(); + + private final Class<T> typeClass; + + private final List<BiConsumer<Option<T>, T>> listeners = new ArrayList<>(); + + private Builder(Class<T> typeClass) { + this.typeClass = typeClass; + } + + /** + * Sets the name to be used by the option. + * + * @see Option#name() + */ + public Builder<T> 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<T> tooltip(@NotNull Function<T, Text>... 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<T> tooltip(@NotNull Text... tooltips) { + Validate.notNull(tooltips, "`tooltips` cannot be empty"); + + this.tooltipGetters.addAll(Stream.of(tooltips).map(text -> (Function<T, Text>) 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<T> controller(@NotNull Function<Option<T>, Controller<T>> 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<T> binding(@NotNull Binding<T> 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<T> 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<T> 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<T> 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<T> flags(@NotNull Collection<OptionFlag> 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 + * <p> + * Does not support {@link Option#flags()}! + */ + public Builder<T> 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<T> listener(@NotNull BiConsumer<Option<T>, 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<T> listeners(@NotNull Collection<BiConsumer<Option<T>, T>> listeners) { + this.listeners.addAll(listeners); + return this; + } + + /** + * Dictates whether the option should require a restart. + * {@link Option#requiresRestart()} + */ + @Deprecated + public Builder<T> requiresRestart(boolean requiresRestart) { + if (requiresRestart) flag(OptionFlag.GAME_RESTART); + else flags.remove(OptionFlag.GAME_RESTART); + + return this; + } + + public Option<T> 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<T, Text> concatenatedTooltipGetter = value -> { + MutableText concatenatedTooltip = Text.empty(); + boolean first = true; + for (Function<T, Text> 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..7a5c23f --- /dev/null +++ b/src/client/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.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<MinecraftClient> { + /** 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<Option<?>> 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<Text> tooltipLines = new ArrayList<>(); + private final List<Option<?>> 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<? extends Option<?>> 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<MinecraftClient, YACLScreen, Screen> screen(); + + static Builder createBuilder() { + return new Builder(); + } + + class Builder { + private Text name; + + private final List<Text> tooltipLines = new ArrayList<>(); + + private BiFunction<MinecraftClient, YACLScreen, Screen> 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<MinecraftClient, YACLScreen, Screen> 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..ae6c060 --- /dev/null +++ b/src/client/java/dev/isxander/yacl/api/YetAnotherConfigLib.java @@ -0,0 +1,151 @@ +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.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.BiFunction; +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<ConfigCategory> 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<YACLScreen> 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(); + } + + /** + * 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 <T> YetAnotherConfigLib create(ConfigInstance<T> configInstance, ConfigBackedBuilder<T> builder) { + return builder.build(configInstance.getDefaults(), configInstance.getConfig(), createBuilder().save(configInstance::save)).build(); + } + + class Builder { + private Text title; + private final List<ConfigCategory> categories = new ArrayList<>(); + private Runnable saveFunction = () -> {}; + private Consumer<YACLScreen> 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<? extends ConfigCategory> 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<YACLScreen> 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); + } + } + + @FunctionalInterface + interface ConfigBackedBuilder<T> { + YetAnotherConfigLib.Builder build(T defaults, T config, YetAnotherConfigLib.Builder builder); + } +} 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<Option<?>, 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<Option<?>> consumer) { + consumeOptions(yacl, (opt) -> { + consumer.accept(opt); + return false; + }); + } +} |