package io.gitlab.jfronny.libjf.config.impl.ui.tiny.entry; import io.gitlab.jfronny.commons.Serializer; import io.gitlab.jfronny.commons.ref.R; import io.gitlab.jfronny.commons.serialize.Transport; import io.gitlab.jfronny.commons.serialize.databind.api.TypeToken; import io.gitlab.jfronny.commons.serialize.json.JsonReader; import io.gitlab.jfronny.commons.throwable.Try; import io.gitlab.jfronny.libjf.LibJf; import io.gitlab.jfronny.libjf.config.api.v2.ConfigCategory; import io.gitlab.jfronny.libjf.config.api.v2.EntryInfo; import io.gitlab.jfronny.libjf.config.api.v2.type.Type; import io.gitlab.jfronny.libjf.config.impl.ConfigCore; import io.gitlab.jfronny.libjf.config.impl.ui.tiny.EditorScreen; 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.client.resource.language.I18n; import net.minecraft.client.toast.SystemToast; import net.minecraft.text.Text; import net.minecraft.util.Formatting; import net.minecraft.util.Language; import java.io.IOException; 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> buildWidgets(ConfigCategory config, List> knownStates, List erroredEntries) { List> knownStates2 = new LinkedList<>(); for (EntryInfo info : config.getEntries()) { if (info.supportsRepresentation()) { WidgetState state = initEntry(config, info, knownStates); knownStates.add(state); knownStates2.add(state); } else { erroredEntries.add(info.getName()); } } return knownStates2; } private static WidgetState initEntry(ConfigCategory config, EntryInfo info, List> knownStates) { Type type = info.getValueType(); WidgetState 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) info, (WidgetState) state, value -> !(Boolean) value, value -> { String customKey = config.getTranslationPrefix() + value; return Language.getInstance().hasTranslation(customKey) ? Text.translatable(customKey) : Text.translatable(ConfigCore.MOD_ID + "." + value); }); } else if (type.isEnum()) { T[] values = type.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() + " - displaying fallback"); factory = jsonScreen(config, info, state); } 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 WidgetFactory toggle(EntryInfo info, WidgetState state, UnaryOperator increment, Function 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)); }) .dimensions(screen.width - 110, 0, info.getWidth(), 20) .build(); return new WidgetFactory.Widget<>(state, value -> button.setMessage(valueToText.apply(value)), button, (width, height) -> button.setX(width - 110)); }; } /** * @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 WidgetFactory textField(EntryInfo info, WidgetState state, Pattern pattern, Function 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<>(state, value -> widget.setText(value == null ? "" : value.toString()), widget, (width, height) -> widget.setX(width - 110)); }; } private static WidgetFactory jsonScreen(ConfigCategory config, EntryInfo info, WidgetState state) { state.managedTemp = false; state.tempValue = null; return (screen, textRenderer) -> { final ButtonWidget button = ButtonWidget.builder(Text.translatable("libjf-config-core-v2.edit"), $ -> { final String jsonified; if (state.tempValue == null) { try { jsonified = LibJf.LENIENT_TRANSPORT.write(writer -> LibJf.MAPPER.serialize(state.cachedValue, writer)); } catch (Throwable e) { LibJf.LOGGER.error("Could not stringify element", e); SystemToast.add( screen.getClient().getToastManager(), SystemToast.Type.PACK_LOAD_FAILURE, Text.translatable("libjf-config-ui-tiny.entry.json.read.fail.title"), Text.translatable("libjf-config-ui-tiny.entry.json.read.fail.description") ); return; } } else { jsonified = state.tempValue; } String key = config.getTranslationPrefix() + info.getName(); screen.getClient().setScreen(new EditorScreen( Text.translatable(key), I18n.hasTranslation(key + ".tooltip") ? Text.translatable(key + ".tooltip") : null, screen, jsonified, json -> { try { state.updateCache(LibJf.LENIENT_TRANSPORT.read( json, (Transport.Returnable) reader -> LibJf.MAPPER .getAdapter((TypeToken) TypeToken.get(info.getValueType().asClass())) .deserialize(reader))); state.tempValue = null; } catch (Throwable e) { LibJf.LOGGER.error("Could not write element", e); SystemToast.add( screen.getClient().getToastManager(), SystemToast.Type.PACK_LOAD_FAILURE, Text.translatable("libjf-config-ui-tiny.entry.json.write.fail.title"), Text.translatable("libjf-config-ui-tiny.entry.json.write.fail.description") ); state.tempValue = json; } } )); }) .dimensions(screen.width - 110, 0, info.getWidth(), 20) .build(); return new WidgetFactory.Widget<>( state, R::nop, button, (width, height) -> button.setX(width - 110) ); }; } private static WidgetFactory slider(EntryInfo info, WidgetState state, Function t2d, Function 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(state, value -> slider.setValue(t2d.apply(value)), slider, (width, height) -> slider.setX(width - 110)); }; } 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); } }