LibJF/libjf-config-ui-tiny-v1/src/client/java/io/gitlab/jfronny/libjf/config/impl/ui/tiny/entry/EntryInfoWidgetBuilder.java

172 lines
8.7 KiB
Java

package io.gitlab.jfronny.libjf.config.impl.ui.tiny.entry;
import io.gitlab.jfronny.commons.throwable.Try;
import io.gitlab.jfronny.libjf.LibJf;
import io.gitlab.jfronny.libjf.config.api.v1.ConfigCategory;
import io.gitlab.jfronny.libjf.config.api.v1.EntryInfo;
import io.gitlab.jfronny.libjf.config.api.v1.type.Type;
import io.gitlab.jfronny.libjf.config.api.v1.ui.tiny.WidgetFactory;
import io.gitlab.jfronny.libjf.config.impl.ui.tiny.WidgetState;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
import net.minecraft.client.gui.widget.ButtonWidget;
import net.minecraft.client.gui.widget.TextFieldWidget;
import net.minecraft.text.Text;
import net.minecraft.util.Formatting;
import java.util.LinkedList;
import java.util.List;
import java.util.function.Function;
import java.util.function.UnaryOperator;
import java.util.regex.Pattern;
@Environment(EnvType.CLIENT)
public class EntryInfoWidgetBuilder {
private static final Pattern INTEGER_ONLY = Pattern.compile("(-?\\d*)");
private static final Pattern DECIMAL_ONLY = Pattern.compile("-?(\\d+\\.?\\d*|\\d*\\.?\\d+|\\.)");
public static List<WidgetState<?>> buildWidgets(ConfigCategory config, List<WidgetState<?>> knownStates) {
List<WidgetState<?>> knownStates2 = new LinkedList<>();
for (EntryInfo<?> info : config.getEntries()) {
WidgetState<?> state = initEntry(config, info, knownStates);
knownStates.add(state);
knownStates2.add(state);
}
return knownStates2;
}
private static <T> WidgetState<T> initEntry(ConfigCategory config, EntryInfo<T> info, List<WidgetState<?>> knownStates) {
Type type = info.getValueType();
WidgetState<T> state = new WidgetState<>();
WidgetFactory factory;
if (type.isInt()) {
factory = isDiscrete(info)
? slider(info, state, t -> (double)(int)t, Double::intValue, true)
: textField(info, state, INTEGER_ONLY, Integer::parseInt, true, info.getMinValue(), info.getMaxValue());
} else if (type.isLong()) {
factory = isDiscrete(info)
? slider(info, state, t -> (double)(long)t, Double::longValue, true)
: textField(info, state, INTEGER_ONLY, Long::parseLong, true, info.getMinValue(), info.getMaxValue());
} else if (type.isFloat()) {
factory = isDiscrete(info)
? slider(info, state, t -> (double)(float)t, Double::floatValue, false)
: textField(info, state, DECIMAL_ONLY, Float::parseFloat, false, info.getMinValue(), info.getMaxValue());
} else if (type.isDouble()) {
factory = isDiscrete(info)
? slider(info, state, t -> t, t -> t, false)
: textField(info, state, DECIMAL_ONLY, Double::parseDouble, false, info.getMinValue(), info.getMaxValue());
} else if (type.isString()) {
factory = textField(info, state, null, String::length, true, Math.min(info.getMinValue(), 0), Math.max(info.getMaxValue(), 1));
} else if (type.isBool()) {
factory = toggle((EntryInfo<Boolean>) info, (WidgetState<Boolean>) state,
value -> !(Boolean) value,
value -> Text.literal(value ? "True" : "False").formatted(value ? Formatting.GREEN : Formatting.RED));
} else if (type.isEnum()) {
T[] values = type.<T>asEnum().options();
factory = toggle(info, state, value -> {
int index = indexOf(values, value) + 1;
return values[index >= values.length ? 0 : index];
}, value -> {
if (type.asClass() == null) {
return Text.translatable(config.getTranslationPrefix() + info.getName() + "." + state.cachedValue);
} else {
return Text.translatable(config.getTranslationPrefix() + "enum." + type.getName() + "." + state.cachedValue);
}
});
} else {
LibJf.LOGGER.error("Unsupported entry type in " + info.getName() + ": " + type.getName() + " - not displaying config control");
factory = null;
}
Try.orThrow(() -> state.initialize(info, knownStates, factory, config.getTranslationPrefix()));
return state;
}
private static int indexOf(Object[] array, Object value) {
for (int i = 0; i < array.length; i++) {
if (array[i] == value) return i;
}
return -1;
}
private static <T> WidgetFactory toggle(EntryInfo<T> info, WidgetState<T> state, UnaryOperator<T> increment, Function<T, Text> valueToText) {
return (screen, textRenderer) -> {
final ButtonWidget button = ButtonWidget.builder(valueToText.apply(state.cachedValue), btn -> {
state.updateCache(increment.apply(state.cachedValue));
btn.setMessage(valueToText.apply(state.cachedValue));
}).position(screen.width - 110, 0).size(info.getWidth(), 20).build();
return new WidgetFactory.Widget(() -> button.setMessage(valueToText.apply(state.cachedValue)), button);
};
}
/**
* @param info The entry to generate a widget for
* @param state The state representation of this widget
* @param pattern The pattern a valid value must abide to
* @param sizeFetcher A function to get a number for size constraints
* @param wholeNumber Whether size constraints are whole numbers
* @param min The minimum size of a valid value
* @param max The maximum size of a valid value
*/
private static <T> WidgetFactory textField(EntryInfo<T> info, WidgetState<T> state, Pattern pattern, Function<String, Number> sizeFetcher, boolean wholeNumber, double min, double max) {
boolean isNumber = pattern != null;
return (screen, textRenderer) -> {
TextFieldWidget widget = new TextFieldWidget(textRenderer, screen.width - 110, 0, info.getWidth(), 20, null);
widget.setText(state.tempValue);
widget.setTextPredicate(currentInput -> {
currentInput = currentInput.trim();
if (!(currentInput.isEmpty() || !isNumber || pattern.matcher(currentInput).matches())) return false;
Number value = 0;
boolean inLimits = false;
state.error = null;
if (!(isNumber && currentInput.isEmpty()) && !currentInput.equals("-") && !currentInput.equals(".")) {
value = sizeFetcher.apply(currentInput);
inLimits = value.doubleValue() >= min && value.doubleValue() <= max;
state.error = inLimits ? null : Text.literal(value.doubleValue() < min ?
"§cMinimum " + (isNumber? "value" : "length") + (wholeNumber ? " is " + (int) min : " is " + min) :
"§cMaximum " + (isNumber? "value" : "length") + (wholeNumber ? " is " + (int) max : " is " + max))
.formatted(Formatting.RED);
}
state.tempValue = currentInput;
widget.setEditableColor(inLimits? 0xFFFFFFFF : 0xFFFF7777);
state.inLimits = inLimits;
screen.done.active = state.knownStates.stream().allMatch(st -> st.inLimits);
if (inLimits) {
state.cachedValue = isNumber ? (T) value : (T) currentInput;
}
return true;
});
return new WidgetFactory.Widget(() -> widget.setText(state.cachedValue == null ? "" : state.cachedValue.toString()), widget);
};
}
private static <T extends Number> WidgetFactory slider(EntryInfo info, WidgetState state, Function<T, Double> t2d, Function<Double, T> d2t, boolean wholeNumber) {
double min = info.getMinValue();
double max = info.getMaxValue();
if (!isDiscrete(min)) throw new IllegalArgumentException("Attempted to create slider with indiscrete minimum");
if (!isDiscrete(max)) throw new IllegalArgumentException("Attempted to create slider with indiscrete maximum");
return (screen, textRenderer) -> {
CustomSlider slider = new CustomSlider(screen.width - 110, 0, info.getWidth(), 20, Double.parseDouble(state.tempValue), min, max, v -> {
state.updateCache(d2t.apply(v));
}, wholeNumber);
return new WidgetFactory.Widget(() -> slider.setValue(t2d.apply((T) state.cachedValue)), slider);
};
}
private static boolean isDiscrete(EntryInfo<?> info) {
return isDiscrete(info.getMinValue()) && isDiscrete(info.getMaxValue());
}
private static boolean isDiscrete(double number) {
return !Double.isNaN(number) && Double.isFinite(number);
}
}