From 980aa089f4123e4fb25876e5f85eae6cc9353fff Mon Sep 17 00:00:00 2001 From: brachy84 Date: Sat, 16 May 2026 22:56:49 +0200 Subject: [PATCH 01/26] json stuff testing --- .../serialization/json/FieldDecoder.java | 6 ++ .../serialization/json/FieldEncoder.java | 6 ++ .../utils/serialization/json/JsonCoder.java | 102 ++++++++++++++++++ .../json/MutableObjectCoder.java | 78 ++++++++++++++ .../brachy/modularui/widget/sizer/Unit.java | 27 ++++- 5 files changed, 215 insertions(+), 4 deletions(-) create mode 100644 src/main/java/brachy/modularui/utils/serialization/json/FieldDecoder.java create mode 100644 src/main/java/brachy/modularui/utils/serialization/json/FieldEncoder.java create mode 100644 src/main/java/brachy/modularui/utils/serialization/json/JsonCoder.java create mode 100644 src/main/java/brachy/modularui/utils/serialization/json/MutableObjectCoder.java diff --git a/src/main/java/brachy/modularui/utils/serialization/json/FieldDecoder.java b/src/main/java/brachy/modularui/utils/serialization/json/FieldDecoder.java new file mode 100644 index 0000000..317aa6b --- /dev/null +++ b/src/main/java/brachy/modularui/utils/serialization/json/FieldDecoder.java @@ -0,0 +1,6 @@ +package brachy.modularui.utils.serialization.json; + +public interface FieldDecoder { + + V decodeField(T holder); +} diff --git a/src/main/java/brachy/modularui/utils/serialization/json/FieldEncoder.java b/src/main/java/brachy/modularui/utils/serialization/json/FieldEncoder.java new file mode 100644 index 0000000..1a1317d --- /dev/null +++ b/src/main/java/brachy/modularui/utils/serialization/json/FieldEncoder.java @@ -0,0 +1,6 @@ +package brachy.modularui.utils.serialization.json; + +public interface FieldEncoder { + + void encodeField(T holder, V value); +} diff --git a/src/main/java/brachy/modularui/utils/serialization/json/JsonCoder.java b/src/main/java/brachy/modularui/utils/serialization/json/JsonCoder.java new file mode 100644 index 0000000..2a5aee6 --- /dev/null +++ b/src/main/java/brachy/modularui/utils/serialization/json/JsonCoder.java @@ -0,0 +1,102 @@ +package brachy.modularui.utils.serialization.json; + +import com.google.gson.JsonElement; +import com.google.gson.JsonPrimitive; + +import java.util.Optional; + +public interface JsonCoder { + + JsonElement encodeJson(T value); + + Optional decodeJson(JsonElement json); + + static JsonCoder empty() { + return (JsonCoder) EMPTY; + } + + JsonCoder EMPTY = new JsonCoder<>() { + @Override + public JsonElement encodeJson(Object value) { + return null; + } + + @Override + public Optional decodeJson(JsonElement json) { + return Optional.empty(); + } + }; + + JsonCoder INT = new JsonCoder<>() { + @Override + public JsonElement encodeJson(Integer value) { + return new JsonPrimitive(value); + } + + @Override + public Optional decodeJson(JsonElement json) { + return Optional.of(json.getAsInt()); + } + }; + + JsonCoder FLOAT = new JsonCoder<>() { + @Override + public JsonElement encodeJson(Float value) { + return new JsonPrimitive(value); + } + + @Override + public Optional decodeJson(JsonElement json) { + return Optional.of(json.getAsFloat()); + } + }; + + JsonCoder DOUBLE = new JsonCoder<>() { + @Override + public JsonElement encodeJson(Double value) { + return new JsonPrimitive(value); + } + + @Override + public Optional decodeJson(JsonElement json) { + return Optional.of(json.getAsDouble()); + } + }; + + JsonCoder BOOL = new JsonCoder<>() { + @Override + public JsonElement encodeJson(Boolean value) { + return new JsonPrimitive(value); + } + + @Override + public Optional decodeJson(JsonElement json) { + return Optional.of(json.getAsBoolean()); + } + }; + + static > JsonCoder ofEnum(Class c) { + return ofEnum(c, c.getEnumConstants()); + } + + static > JsonCoder ofEnum(Class c, T... values) { + return new JsonCoder<>() { + @Override + public JsonElement encodeJson(T value) { + return new JsonPrimitive(value.name()); + } + + @Override + public Optional decodeJson(JsonElement json) { + if (!json.isJsonPrimitive() || !json.getAsJsonPrimitive().isString()) throw new IllegalArgumentException(); + String name = json.getAsString(); + for (T value : values) { + if (value.name().equals(name)) { + return Optional.of(value); + } + } + return Optional.empty(); + } + }; + } +} diff --git a/src/main/java/brachy/modularui/utils/serialization/json/MutableObjectCoder.java b/src/main/java/brachy/modularui/utils/serialization/json/MutableObjectCoder.java new file mode 100644 index 0000000..7f4da4d --- /dev/null +++ b/src/main/java/brachy/modularui/utils/serialization/json/MutableObjectCoder.java @@ -0,0 +1,78 @@ +package brachy.modularui.utils.serialization.json; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import it.unimi.dsi.fastutil.objects.Object2ReferenceLinkedOpenHashMap; + +import java.util.Objects; +import java.util.function.Consumer; +import java.util.function.Predicate; + +public class MutableObjectCoder { + + private final Object2ReferenceLinkedOpenHashMap> fields; + + private MutableObjectCoder(Object2ReferenceLinkedOpenHashMap> fields) { + this.fields = fields; + } + + public void forEachField(Consumer> consumer) { + this.fields.values().forEach(consumer); + } + + public JsonElement encodeJson(T holder) { + JsonObject json = new JsonObject(); + forEachField(f -> encodeField(holder, f, json)); + return json; + } + + private void encodeField(T holder, Field field, JsonObject json) { + json.add(field.name, field.jsonCoder.encodeJson(field.fieldDecoder.decodeField(holder))); + } + + public void decodeJson(T holder, JsonElement jsonElement) { + if (!jsonElement.isJsonObject()) throw new IllegalArgumentException(); + JsonObject json = jsonElement.getAsJsonObject(); + forEachField(f -> decodeField(holder, f, json)); + } + + private void decodeField(T holder, Field field, JsonElement json) { + field.fieldEncoder.encodeField(holder, field.jsonCoder.decodeJson(json).orElseThrow()); + } + + @SuppressWarnings("unchecked") + private Field getField(String name) { + return (Field) this.fields.get(name); + } + + public record Field(String name, JsonCoder jsonCoder, FieldEncoder fieldEncoder, FieldDecoder fieldDecoder) {} + + public static class Builder { + + private final Object2ReferenceLinkedOpenHashMap> fields = new Object2ReferenceLinkedOpenHashMap<>(); + + public Builder add(String name, JsonCoder jsonCoder, FieldEncoder fieldEncoder, FieldDecoder fieldDecoder) { + this.fields.put(name, new Field<>(name, jsonCoder, fieldEncoder, fieldDecoder)); + return this; + } + + public Builder addUncodable(String name, FieldDecoder fieldDecoder) { + return addUncodable(name, fieldDecoder, Objects::isNull); + } + + public Builder addUncodable(String name, FieldDecoder fieldDecoder, Predicate emptyTest) { + return add(name, JsonCoder.EMPTY, (holder, value) -> { + throw new IllegalArgumentException("Unable to write field"); + }, holder -> { + if (!emptyTest.test(fieldDecoder.decodeField(holder))) { + throw new IllegalArgumentException("Unable to read field"); + } + return null; + }); + } + + public MutableObjectCoder build() { + return new MutableObjectCoder<>(this.fields); + } + } +} diff --git a/src/main/java/brachy/modularui/widget/sizer/Unit.java b/src/main/java/brachy/modularui/widget/sizer/Unit.java index 641472b..c9954a6 100644 --- a/src/main/java/brachy/modularui/widget/sizer/Unit.java +++ b/src/main/java/brachy/modularui/widget/sizer/Unit.java @@ -1,6 +1,8 @@ package brachy.modularui.widget.sizer; import brachy.modularui.api.GuiAxis; +import brachy.modularui.utils.serialization.json.JsonCoder; +import brachy.modularui.utils.serialization.json.MutableObjectCoder; import lombok.Getter; import lombok.Setter; @@ -28,17 +30,32 @@ public enum State { public String getText(GuiAxis axis) { return axis.isHorizontal() ? this.xText : this.yText; } + + public static final JsonCoder CODER = JsonCoder.ofEnum(State.class); } + public static final MutableObjectCoder CODER = new MutableObjectCoder.Builder() + .add("autoAnchor", JsonCoder.BOOL, (holder, value) -> holder.autoAnchor = value, holder -> holder.autoAnchor) + .add("value", JsonCoder.FLOAT, (holder, value) -> holder.value = value, holder -> holder.value) + .add("measure", Measure.CODER, (holder, value) -> holder.measure = value, holder -> holder.measure) + .add("anchor", JsonCoder.FLOAT, (holder, value) -> holder.anchor = value, holder -> holder.anchor) + .add("offset", JsonCoder.INT, (holder, value) -> holder.offset = value, holder -> holder.offset) + .add("state", State.CODER, (holder, value) -> holder.state = value, holder -> holder.state) + .addUncodable("valueSupplier", holder -> holder.valueSupplier) + .build(); + @Getter - @Setter private boolean autoAnchor = true; + @Setter + private boolean autoAnchor = true; private float value = 0f; private DoubleSupplier valueSupplier = null; @Getter - @Setter private Measure measure = Measure.PIXEL; + @Setter + private Measure measure = Measure.PIXEL; @Setter private float anchor = 0f; @Getter - @Setter private int offset = 0; + @Setter + private int offset = 0; public State state = State.UNUSED; @@ -103,6 +120,8 @@ public boolean isUnused() { public enum Measure { PIXEL, - RELATIVE + RELATIVE; + + public static final JsonCoder CODER = JsonCoder.ofEnum(Measure.class); } } From 15cc89e425f208044ca1a43ffc02c6844a46319a Mon Sep 17 00:00:00 2001 From: brachy84 Date: Sun, 17 May 2026 12:26:47 +0200 Subject: [PATCH 02/26] codec --- .../{json => codec}/FieldDecoder.java | 2 +- .../{json => codec}/FieldEncoder.java | 2 +- .../serialization/codec/InstanceDecoder.java | 16 ++ .../serialization/codec/MutableCodec.java | 10 + .../serialization/codec/MutableDecoder.java | 37 ++++ .../codec/MutableObjectCodec.java | 204 ++++++++++++++++++ .../utils/serialization/json/JsonCoder.java | 102 --------- .../json/MutableObjectCoder.java | 78 ------- .../brachy/modularui/widget/sizer/Unit.java | 59 +++-- 9 files changed, 312 insertions(+), 198 deletions(-) rename src/main/java/brachy/modularui/utils/serialization/{json => codec}/FieldDecoder.java (57%) rename src/main/java/brachy/modularui/utils/serialization/{json => codec}/FieldEncoder.java (61%) create mode 100644 src/main/java/brachy/modularui/utils/serialization/codec/InstanceDecoder.java create mode 100644 src/main/java/brachy/modularui/utils/serialization/codec/MutableCodec.java create mode 100644 src/main/java/brachy/modularui/utils/serialization/codec/MutableDecoder.java create mode 100644 src/main/java/brachy/modularui/utils/serialization/codec/MutableObjectCodec.java delete mode 100644 src/main/java/brachy/modularui/utils/serialization/json/JsonCoder.java delete mode 100644 src/main/java/brachy/modularui/utils/serialization/json/MutableObjectCoder.java diff --git a/src/main/java/brachy/modularui/utils/serialization/json/FieldDecoder.java b/src/main/java/brachy/modularui/utils/serialization/codec/FieldDecoder.java similarity index 57% rename from src/main/java/brachy/modularui/utils/serialization/json/FieldDecoder.java rename to src/main/java/brachy/modularui/utils/serialization/codec/FieldDecoder.java index 317aa6b..7523607 100644 --- a/src/main/java/brachy/modularui/utils/serialization/json/FieldDecoder.java +++ b/src/main/java/brachy/modularui/utils/serialization/codec/FieldDecoder.java @@ -1,4 +1,4 @@ -package brachy.modularui.utils.serialization.json; +package brachy.modularui.utils.serialization.codec; public interface FieldDecoder { diff --git a/src/main/java/brachy/modularui/utils/serialization/json/FieldEncoder.java b/src/main/java/brachy/modularui/utils/serialization/codec/FieldEncoder.java similarity index 61% rename from src/main/java/brachy/modularui/utils/serialization/json/FieldEncoder.java rename to src/main/java/brachy/modularui/utils/serialization/codec/FieldEncoder.java index 1a1317d..c670018 100644 --- a/src/main/java/brachy/modularui/utils/serialization/json/FieldEncoder.java +++ b/src/main/java/brachy/modularui/utils/serialization/codec/FieldEncoder.java @@ -1,4 +1,4 @@ -package brachy.modularui.utils.serialization.json; +package brachy.modularui.utils.serialization.codec; public interface FieldEncoder { diff --git a/src/main/java/brachy/modularui/utils/serialization/codec/InstanceDecoder.java b/src/main/java/brachy/modularui/utils/serialization/codec/InstanceDecoder.java new file mode 100644 index 0000000..ad13162 --- /dev/null +++ b/src/main/java/brachy/modularui/utils/serialization/codec/InstanceDecoder.java @@ -0,0 +1,16 @@ +package brachy.modularui.utils.serialization.codec; + +import com.mojang.datafixers.util.Pair; +import com.mojang.serialization.DataResult; +import com.mojang.serialization.DynamicOps; + +/** + * Creates an instance and only serializes any immutable fields. + * For no arg constructors this can just be a supplier. + * + * @param type of instance + */ +public interface InstanceDecoder { + + DataResult> decodeInstance(final DynamicOps ops, final T input); +} diff --git a/src/main/java/brachy/modularui/utils/serialization/codec/MutableCodec.java b/src/main/java/brachy/modularui/utils/serialization/codec/MutableCodec.java new file mode 100644 index 0000000..5c94337 --- /dev/null +++ b/src/main/java/brachy/modularui/utils/serialization/codec/MutableCodec.java @@ -0,0 +1,10 @@ +package brachy.modularui.utils.serialization.codec; + +import com.mojang.serialization.Codec; + +/** + * A {@link Codec} that can decode mutable field to an existing instance. + * + * @param type of instance + */ +public interface MutableCodec extends Codec, MutableDecoder {} diff --git a/src/main/java/brachy/modularui/utils/serialization/codec/MutableDecoder.java b/src/main/java/brachy/modularui/utils/serialization/codec/MutableDecoder.java new file mode 100644 index 0000000..6d251f8 --- /dev/null +++ b/src/main/java/brachy/modularui/utils/serialization/codec/MutableDecoder.java @@ -0,0 +1,37 @@ +package brachy.modularui.utils.serialization.codec; + +import com.mojang.datafixers.util.Pair; +import com.mojang.serialization.DataResult; +import com.mojang.serialization.Decoder; +import com.mojang.serialization.Dynamic; +import com.mojang.serialization.DynamicOps; + +/** + * Allows decoded data to be written to an existing instance. + * + * @param type of instance + */ +public interface MutableDecoder extends Decoder, InstanceDecoder { + + DataResult> decode(final DynamicOps ops, final T input, A instance); + + @Override + default DataResult> decode(final DynamicOps ops, final T input) { + var d = decodeInstance(ops, input); + var result = d.result(); + if (result.isEmpty()) return d; + return decode(ops, result.get().getSecond(), result.get().getFirst()); + } + + default DataResult parse(final DynamicOps ops, final T input, A instance) { + return decode(ops, input, instance).map(Pair::getFirst); + } + + default DataResult> decode(final Dynamic input, A instance) { + return decode(input.getOps(), input.getValue(), instance); + } + + default DataResult parse(final Dynamic input, A instance) { + return decode(input, instance).map(Pair::getFirst); + } +} diff --git a/src/main/java/brachy/modularui/utils/serialization/codec/MutableObjectCodec.java b/src/main/java/brachy/modularui/utils/serialization/codec/MutableObjectCodec.java new file mode 100644 index 0000000..3e0a1f8 --- /dev/null +++ b/src/main/java/brachy/modularui/utils/serialization/codec/MutableObjectCodec.java @@ -0,0 +1,204 @@ +package brachy.modularui.utils.serialization.codec; + +import com.mojang.datafixers.util.Pair; +import com.mojang.serialization.Codec; +import com.mojang.serialization.DataResult; +import com.mojang.serialization.DynamicOps; +import com.mojang.serialization.JsonOps; +import com.mojang.serialization.MapLike; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import it.unimi.dsi.fastutil.objects.Object2ReferenceLinkedOpenHashMap; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.function.Consumer; +import java.util.function.Predicate; +import java.util.function.Supplier; +import java.util.stream.Stream; + +public class MutableObjectCodec { + + private final Object2ReferenceLinkedOpenHashMap> fields; + private final InstanceDecoder instanceDecoder; + + private MutableObjectCodec(Object2ReferenceLinkedOpenHashMap> fields, InstanceDecoder instanceDecoder) { + this.fields = fields; + this.instanceDecoder = instanceDecoder; + } + + public void forEachField(Consumer> consumer) { + this.fields.values().forEach(consumer); + } + + public final MutableCodec codec = new MutableCodec() { + + @Override + public DataResult> decodeInstance(DynamicOps ops, J input) { + if (MutableObjectCodec.this.instanceDecoder == null) { + return DataResult.error(() -> "Instance can not be created since no instance decoder was provided."); + } + return MutableObjectCodec.this.instanceDecoder.decodeInstance(ops, input); + } + + @Override + public DataResult> decode(DynamicOps ops, J input, T instance) { + var d = ops.getMap(input); + var map = d.result(); + if (map.isEmpty()) return DataResult.error(() -> d.error().orElseThrow().message()); + List errors = new ArrayList<>(); + forEachField(f -> { + var error = decodeField(instance, f, ops, map.get()); + if (error != null) { + errors.add(error); + } + }); + if (!errors.isEmpty()) { + return DataResult.error(() -> + String.format("Errors while decoding object of type '%s': %s", instance.getClass().getSimpleName(), errors)); + } + return DataResult.success(new Pair<>(instance, input)); + } + + public DataResult> decodeJson(JsonObject json) { + return decode(JsonOps.INSTANCE, json); + } + + public DataResult parseJson(JsonObject json) { + return parse(JsonOps.INSTANCE, json); + } + + public DataResult> decodeJson(JsonObject json, T instance) { + return decode(JsonOps.INSTANCE, json, instance); + } + + public DataResult parseJson(JsonObject json, T instance) { + return parse(JsonOps.INSTANCE, json, instance); + } + + @Override + public DataResult encode(T input, DynamicOps ops, J prefix) { + var builder = Stream.>builder(); + forEachField(f -> { + builder.accept(encodeField(input, f, ops)); + }); + return DataResult.success(ops.createMap(builder.build())); + } + }; + + private Pair encodeField(T holder, Field field, DynamicOps ops) { + V value = field.fieldDecoder.decodeField(holder); + DataResult d = field.codec.encodeStart(ops, value); + return new Pair<>(ops.createString(field.name), d.result().orElseThrow()); + } + + private @Nullable String decodeField(T holder, Field field, DynamicOps ops, MapLike map) { + J element = map.get(field.name); + V value; + if (element == null) { + if (field.defaultSupplier == null) { + return String.format("Field '%s' has no value and is not optional", field.name); + } + value = field.defaultSupplier.get(); + } else { + var d = field.codec.parse(ops, element); + var res = d.result(); + if (res.isEmpty()) return d.error().orElseThrow().message(); + value = res.get(); + } + field.fieldEncoder.encodeField(holder, value); + return null; + } + + @SuppressWarnings("unchecked") + private Field getField(String name) { + return (Field) this.fields.get(name); + } + + public static Builder builder() { + return new Builder<>(); + } + + public static Builder builder(Class c) { + return new Builder<>(); + } + + public static Builder builder(InstanceDecoder instanceDecoder) { + return new Builder().instanceDecoder(instanceDecoder); + } + + public static Builder builder(Supplier instance) { + return new Builder().instance(instance); + } + + public record Field(String name, FieldEncoder fieldEncoder, FieldDecoder fieldDecoder, Codec codec, + Supplier defaultSupplier) { + + public boolean hasDefault() { + return this.defaultSupplier != null; + } + + public V getDefault() { + return this.defaultSupplier.get(); + } + + public boolean isUnencodable() { + return this.codec == null; + } + } + + public static class Builder { + + private final Object2ReferenceLinkedOpenHashMap> fields = new Object2ReferenceLinkedOpenHashMap<>(); + private InstanceDecoder instanceDecoder; + + public Builder instanceDecoder(InstanceDecoder instanceDecoder) { + this.instanceDecoder = instanceDecoder; + return this; + } + + public Builder instance(Supplier instance) { + return instanceDecoder(new InstanceDecoder<>() { + @Override + public DataResult> decodeInstance(DynamicOps ops, J input) { + return DataResult.success(new Pair<>(instance.get(), input)); + } + }); + } + + public Builder add(String name, FieldEncoder fieldEncoder, FieldDecoder fieldDecoder, Codec codec) { + return addDynOpt(name, fieldEncoder, fieldDecoder, codec, null); + } + + public Builder addOpt(String name, FieldEncoder fieldEncoder, FieldDecoder fieldDecoder, Codec codec, V defValue) { + return addDynOpt(name, fieldEncoder, fieldDecoder, codec, () -> defValue); + } + + public Builder addDynOpt(String name, FieldEncoder fieldEncoder, FieldDecoder fieldDecoder, Codec codec, Supplier defaultSupplier) { + this.fields.put(name, new Field<>(name, fieldEncoder, fieldDecoder, codec, defaultSupplier)); + return this; + } + + public Builder addUncodable(String name, FieldDecoder fieldDecoder) { + return addUncodable(name, fieldDecoder, Objects::isNull); + } + + public Builder addUncodable(String name, FieldDecoder fieldDecoder, Predicate emptyTest) { + return add(name, (holder, value) -> { + throw new IllegalArgumentException("Unable to write field"); + }, holder -> { + if (!emptyTest.test(fieldDecoder.decodeField(holder))) { + throw new IllegalArgumentException("Unable to read field"); + } + return null; + }, null); + } + + public MutableObjectCodec build() { + return new MutableObjectCodec<>(this.fields, this.instanceDecoder); + } + } +} diff --git a/src/main/java/brachy/modularui/utils/serialization/json/JsonCoder.java b/src/main/java/brachy/modularui/utils/serialization/json/JsonCoder.java deleted file mode 100644 index 2a5aee6..0000000 --- a/src/main/java/brachy/modularui/utils/serialization/json/JsonCoder.java +++ /dev/null @@ -1,102 +0,0 @@ -package brachy.modularui.utils.serialization.json; - -import com.google.gson.JsonElement; -import com.google.gson.JsonPrimitive; - -import java.util.Optional; - -public interface JsonCoder { - - JsonElement encodeJson(T value); - - Optional decodeJson(JsonElement json); - - static JsonCoder empty() { - return (JsonCoder) EMPTY; - } - - JsonCoder EMPTY = new JsonCoder<>() { - @Override - public JsonElement encodeJson(Object value) { - return null; - } - - @Override - public Optional decodeJson(JsonElement json) { - return Optional.empty(); - } - }; - - JsonCoder INT = new JsonCoder<>() { - @Override - public JsonElement encodeJson(Integer value) { - return new JsonPrimitive(value); - } - - @Override - public Optional decodeJson(JsonElement json) { - return Optional.of(json.getAsInt()); - } - }; - - JsonCoder FLOAT = new JsonCoder<>() { - @Override - public JsonElement encodeJson(Float value) { - return new JsonPrimitive(value); - } - - @Override - public Optional decodeJson(JsonElement json) { - return Optional.of(json.getAsFloat()); - } - }; - - JsonCoder DOUBLE = new JsonCoder<>() { - @Override - public JsonElement encodeJson(Double value) { - return new JsonPrimitive(value); - } - - @Override - public Optional decodeJson(JsonElement json) { - return Optional.of(json.getAsDouble()); - } - }; - - JsonCoder BOOL = new JsonCoder<>() { - @Override - public JsonElement encodeJson(Boolean value) { - return new JsonPrimitive(value); - } - - @Override - public Optional decodeJson(JsonElement json) { - return Optional.of(json.getAsBoolean()); - } - }; - - static > JsonCoder ofEnum(Class c) { - return ofEnum(c, c.getEnumConstants()); - } - - static > JsonCoder ofEnum(Class c, T... values) { - return new JsonCoder<>() { - @Override - public JsonElement encodeJson(T value) { - return new JsonPrimitive(value.name()); - } - - @Override - public Optional decodeJson(JsonElement json) { - if (!json.isJsonPrimitive() || !json.getAsJsonPrimitive().isString()) throw new IllegalArgumentException(); - String name = json.getAsString(); - for (T value : values) { - if (value.name().equals(name)) { - return Optional.of(value); - } - } - return Optional.empty(); - } - }; - } -} diff --git a/src/main/java/brachy/modularui/utils/serialization/json/MutableObjectCoder.java b/src/main/java/brachy/modularui/utils/serialization/json/MutableObjectCoder.java deleted file mode 100644 index 7f4da4d..0000000 --- a/src/main/java/brachy/modularui/utils/serialization/json/MutableObjectCoder.java +++ /dev/null @@ -1,78 +0,0 @@ -package brachy.modularui.utils.serialization.json; - -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import it.unimi.dsi.fastutil.objects.Object2ReferenceLinkedOpenHashMap; - -import java.util.Objects; -import java.util.function.Consumer; -import java.util.function.Predicate; - -public class MutableObjectCoder { - - private final Object2ReferenceLinkedOpenHashMap> fields; - - private MutableObjectCoder(Object2ReferenceLinkedOpenHashMap> fields) { - this.fields = fields; - } - - public void forEachField(Consumer> consumer) { - this.fields.values().forEach(consumer); - } - - public JsonElement encodeJson(T holder) { - JsonObject json = new JsonObject(); - forEachField(f -> encodeField(holder, f, json)); - return json; - } - - private void encodeField(T holder, Field field, JsonObject json) { - json.add(field.name, field.jsonCoder.encodeJson(field.fieldDecoder.decodeField(holder))); - } - - public void decodeJson(T holder, JsonElement jsonElement) { - if (!jsonElement.isJsonObject()) throw new IllegalArgumentException(); - JsonObject json = jsonElement.getAsJsonObject(); - forEachField(f -> decodeField(holder, f, json)); - } - - private void decodeField(T holder, Field field, JsonElement json) { - field.fieldEncoder.encodeField(holder, field.jsonCoder.decodeJson(json).orElseThrow()); - } - - @SuppressWarnings("unchecked") - private Field getField(String name) { - return (Field) this.fields.get(name); - } - - public record Field(String name, JsonCoder jsonCoder, FieldEncoder fieldEncoder, FieldDecoder fieldDecoder) {} - - public static class Builder { - - private final Object2ReferenceLinkedOpenHashMap> fields = new Object2ReferenceLinkedOpenHashMap<>(); - - public Builder add(String name, JsonCoder jsonCoder, FieldEncoder fieldEncoder, FieldDecoder fieldDecoder) { - this.fields.put(name, new Field<>(name, jsonCoder, fieldEncoder, fieldDecoder)); - return this; - } - - public Builder addUncodable(String name, FieldDecoder fieldDecoder) { - return addUncodable(name, fieldDecoder, Objects::isNull); - } - - public Builder addUncodable(String name, FieldDecoder fieldDecoder, Predicate emptyTest) { - return add(name, JsonCoder.EMPTY, (holder, value) -> { - throw new IllegalArgumentException("Unable to write field"); - }, holder -> { - if (!emptyTest.test(fieldDecoder.decodeField(holder))) { - throw new IllegalArgumentException("Unable to read field"); - } - return null; - }); - } - - public MutableObjectCoder build() { - return new MutableObjectCoder<>(this.fields); - } - } -} diff --git a/src/main/java/brachy/modularui/widget/sizer/Unit.java b/src/main/java/brachy/modularui/widget/sizer/Unit.java index c9954a6..1da3896 100644 --- a/src/main/java/brachy/modularui/widget/sizer/Unit.java +++ b/src/main/java/brachy/modularui/widget/sizer/Unit.java @@ -1,28 +1,38 @@ package brachy.modularui.widget.sizer; import brachy.modularui.api.GuiAxis; -import brachy.modularui.utils.serialization.json.JsonCoder; -import brachy.modularui.utils.serialization.json.MutableObjectCoder; +import brachy.modularui.utils.serialization.codec.MutableObjectCodec; +import com.mojang.serialization.Codec; + +import lombok.AccessLevel; import lombok.Getter; import lombok.Setter; + +import net.minecraft.util.StringRepresentable; + import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import java.util.Locale; import java.util.function.DoubleSupplier; @ApiStatus.Internal public class Unit { - public enum State { + public enum State implements StringRepresentable { UNUSED("", ""), START("LEFT", "TOP"), END("RIGHT", "BOTTOM"), SIZE("WIDTH", "HEIGHT"); - public final String xText, yText; + public static final Codec CODEC = StringRepresentable.fromEnum(State::values); + + public final String name, xText, yText; State(String xText, String yText) { + this.name = name().toLowerCase(Locale.ENGLISH); this.xText = xText; this.yText = yText; } @@ -31,23 +41,28 @@ public String getText(GuiAxis axis) { return axis.isHorizontal() ? this.xText : this.yText; } - public static final JsonCoder CODER = JsonCoder.ofEnum(State.class); + + @Override + public @NotNull String getSerializedName() { + return this.name; + } } - public static final MutableObjectCoder CODER = new MutableObjectCoder.Builder() - .add("autoAnchor", JsonCoder.BOOL, (holder, value) -> holder.autoAnchor = value, holder -> holder.autoAnchor) - .add("value", JsonCoder.FLOAT, (holder, value) -> holder.value = value, holder -> holder.value) - .add("measure", Measure.CODER, (holder, value) -> holder.measure = value, holder -> holder.measure) - .add("anchor", JsonCoder.FLOAT, (holder, value) -> holder.anchor = value, holder -> holder.anchor) - .add("offset", JsonCoder.INT, (holder, value) -> holder.offset = value, holder -> holder.offset) - .add("state", State.CODER, (holder, value) -> holder.state = value, holder -> holder.state) - .addUncodable("valueSupplier", holder -> holder.valueSupplier) + public static final MutableObjectCodec CODER = MutableObjectCodec.builder(Unit::new) + .addOpt("autoAnchor", Unit::setAutoAnchor, Unit::isAutoAnchor, Codec.BOOL, true) + .addOpt("value", Unit::setValue, Unit::getValue, Codec.FLOAT, 0f) + .addOpt("measure", Unit::setMeasure, Unit::getMeasure, Measure.CODEC, Measure.PIXEL) + .addOpt("anchor", Unit::setAnchor, Unit::getAnchor, Codec.FLOAT, 0f) + .addOpt("offset", Unit::setOffset, Unit::getOffset, Codec.INT, 0) + .addOpt("state", Unit::setState, Unit::getState, State.CODEC, State.UNUSED) + .addUncodable("valueSupplier", Unit::getValueSupplier) .build(); @Getter @Setter private boolean autoAnchor = true; private float value = 0f; + @Getter(AccessLevel.PRIVATE) private DoubleSupplier valueSupplier = null; @Getter @Setter @@ -56,7 +71,8 @@ public String getText(GuiAxis axis) { @Getter @Setter private int offset = 0; - + @Getter + @Setter(AccessLevel.PRIVATE) public State state = State.UNUSED; public Unit() {} @@ -118,10 +134,21 @@ public boolean isUnused() { return this.state == State.UNUSED; } - public enum Measure { + public enum Measure implements StringRepresentable { PIXEL, RELATIVE; - public static final JsonCoder CODER = JsonCoder.ofEnum(Measure.class); + public static final Codec CODEC = StringRepresentable.fromEnum(Measure::values); + + public final String name; + + Measure() { + this.name = name().toLowerCase(Locale.ENGLISH); + } + + @Override + public @NotNull String getSerializedName() { + return this.name; + } } } From 7360e2f83b4ab8534052eb0c46f64349a7659185 Mon Sep 17 00:00:00 2001 From: brachy84 Date: Sun, 17 May 2026 13:33:13 +0200 Subject: [PATCH 03/26] more codec stuff --- .../codec/MutableObjectCodec.java | 245 ++++++++++++++---- .../brachy/modularui/widget/sizer/Unit.java | 2 +- 2 files changed, 200 insertions(+), 47 deletions(-) diff --git a/src/main/java/brachy/modularui/utils/serialization/codec/MutableObjectCodec.java b/src/main/java/brachy/modularui/utils/serialization/codec/MutableObjectCodec.java index 3e0a1f8..27447ef 100644 --- a/src/main/java/brachy/modularui/utils/serialization/codec/MutableObjectCodec.java +++ b/src/main/java/brachy/modularui/utils/serialization/codec/MutableObjectCodec.java @@ -18,16 +18,26 @@ import java.util.function.Consumer; import java.util.function.Predicate; import java.util.function.Supplier; +import java.util.function.UnaryOperator; import java.util.stream.Stream; +/** + * This can handle (de)serialization and conversion to various data types, editing properties, copying properties and applying default + * properties to (partially) mutable objects. + * + * @param type of property + */ public class MutableObjectCodec { private final Object2ReferenceLinkedOpenHashMap> fields; private final InstanceDecoder instanceDecoder; + private final UnaryOperator baseCopy; - private MutableObjectCodec(Object2ReferenceLinkedOpenHashMap> fields, InstanceDecoder instanceDecoder) { + private MutableObjectCodec(Object2ReferenceLinkedOpenHashMap> fields, + InstanceDecoder instanceDecoder, UnaryOperator baseCopy) { this.fields = fields; this.instanceDecoder = instanceDecoder; + this.baseCopy = baseCopy; } public void forEachField(Consumer> consumer) { @@ -51,10 +61,8 @@ public DataResult> decode(DynamicOps ops, J input, T instance) if (map.isEmpty()) return DataResult.error(() -> d.error().orElseThrow().message()); List errors = new ArrayList<>(); forEachField(f -> { - var error = decodeField(instance, f, ops, map.get()); - if (error != null) { - errors.add(error); - } + var error = f.decode(instance, ops, map.get()); + if (error != null) errors.add(error); }); if (!errors.isEmpty()) { return DataResult.error(() -> @@ -82,40 +90,39 @@ public DataResult parseJson(JsonObject json, T instance) { @Override public DataResult encode(T input, DynamicOps ops, J prefix) { var builder = Stream.>builder(); + List errors = new ArrayList<>(); forEachField(f -> { - builder.accept(encodeField(input, f, ops)); + var error = f.encode(input, ops, builder); + if (error != null) errors.add(error); }); + if (!errors.isEmpty()) { + return DataResult.error(() -> + String.format("Errors while encoding object of type '%s': %s", input.getClass().getSimpleName(), errors)); + } return DataResult.success(ops.createMap(builder.build())); } }; - private Pair encodeField(T holder, Field field, DynamicOps ops) { - V value = field.fieldDecoder.decodeField(holder); - DataResult d = field.codec.encodeStart(ops, value); - return new Pair<>(ops.createString(field.name), d.result().orElseThrow()); + @SuppressWarnings("unchecked") + private Field getField(String name) { + return (Field) this.fields.get(name); } - private @Nullable String decodeField(T holder, Field field, DynamicOps ops, MapLike map) { - J element = map.get(field.name); - V value; - if (element == null) { - if (field.defaultSupplier == null) { - return String.format("Field '%s' has no value and is not optional", field.name); - } - value = field.defaultSupplier.get(); - } else { - var d = field.codec.parse(ops, element); - var res = d.result(); - if (res.isEmpty()) return d.error().orElseThrow().message(); - value = res.get(); + public T copy(T from) { + if (this.baseCopy == null) { + throw new IllegalStateException("Can't copy instance since no base copy function is supplied."); } - field.fieldEncoder.encodeField(holder, value); - return null; + T copy = this.baseCopy.apply(from); + copyFields(from, copy); + return copy; } - @SuppressWarnings("unchecked") - private Field getField(String name) { - return (Field) this.fields.get(name); + public void copyFields(T from, T to) { + forEachField(f -> f.copy(from, to)); + } + + public void applyDefaults(T instance) { + forEachField(f -> f.applyDefault(instance)); } public static Builder builder() { @@ -135,7 +142,7 @@ public static Builder builder(Supplier instance) { } public record Field(String name, FieldEncoder fieldEncoder, FieldDecoder fieldDecoder, Codec codec, - Supplier defaultSupplier) { + Supplier defaultSupplier, Predicate emptyTester) { public boolean hasDefault() { return this.defaultSupplier != null; @@ -148,57 +155,203 @@ public V getDefault() { public boolean isUnencodable() { return this.codec == null; } + + private @Nullable String encode(T holder, DynamicOps ops, Stream.Builder> map) { + V value = this.fieldDecoder.decodeField(holder); + if (isUnencodable()) { + if (!isEmpty(value)) { + return String.format("Field '%s' is unencodeable, but the value is not empty", this.name); + } + return null; + } + var d = this.codec.encodeStart(ops, value); + var res = d.result(); + if (res.isEmpty()) return d.error().orElseThrow().message(); + map.accept(new Pair<>(ops.createString(this.name), res.get())); + return null; + } + + private @Nullable String decode(T holder, DynamicOps ops, MapLike map) { + J element = map.get(this.name); + V value; + if (element == null) { + if (this.defaultSupplier == null) { + return String.format("Field '%s' has no value and is not optional", this.name); + } + value = this.defaultSupplier.get(); + } else { + if (isUnencodable()) { + return String.format("Field '%s' is unencodable, but data still contains value", this.name); + } + var d = this.codec.parse(ops, element); + var res = d.result(); + if (res.isEmpty()) return d.error().orElseThrow().message(); + value = res.get(); + } + this.fieldEncoder.encodeField(holder, value); + return null; + } + + public void copy(T from, T to) { + this.fieldEncoder.encodeField(to, this.fieldDecoder.decodeField(from)); + } + + public void applyDefault(T instance) { + if (hasDefault()) { + this.fieldEncoder.encodeField(instance, getDefault()); + } + } + + public boolean isEmpty(V value) { + return this.emptyTester != null ? this.emptyTester.test(value) : value == null; + } } public static class Builder { private final Object2ReferenceLinkedOpenHashMap> fields = new Object2ReferenceLinkedOpenHashMap<>(); private InstanceDecoder instanceDecoder; - + private UnaryOperator baseCopy; + + /** + * Sets the instance decoder. This is needed when parsing from JSON. The decoder should create a new instance + * and decode any necessary data for that. If the object has a no-arg constructor, then {@link #instance(Supplier)} should be used. + * If this setter is used, then {@link #baseCopy(UnaryOperator)} must also be used. + * + * @param instanceDecoder instance decoder + * @return this + */ public Builder instanceDecoder(InstanceDecoder instanceDecoder) { this.instanceDecoder = instanceDecoder; return this; } + /** + * Sets the instance supplier. This is needed when parsing from JSON and for copying. This MUST always return a new instance. + * + * @param instance instance supplier + * @return this + */ public Builder instance(Supplier instance) { return instanceDecoder(new InstanceDecoder<>() { @Override public DataResult> decodeInstance(DynamicOps ops, J input) { return DataResult.success(new Pair<>(instance.get(), input)); } - }); + }).baseCopy(t -> instance.get()); + } + + /** + * Sets the base copy function. This creates a new instance, but without setting any mutable properties. This MUST always return + * a new instance. If the object has a no-arg constructor, then {@link #instance(Supplier)} should be used. + * + * @param baseCopy base copy function + * @return this + */ + public Builder baseCopy(UnaryOperator baseCopy) { + this.baseCopy = t -> { + T copy = baseCopy.apply(t); + if (copy == t) throw new IllegalArgumentException("The copy function must return a new object!"); + return copy; + }; + return this; } public Builder add(String name, FieldEncoder fieldEncoder, FieldDecoder fieldDecoder, Codec codec) { return addDynOpt(name, fieldEncoder, fieldDecoder, codec, null); } - public Builder addOpt(String name, FieldEncoder fieldEncoder, FieldDecoder fieldDecoder, Codec codec, V defValue) { + /** + * Adds a new non-optional, mutable property. + * + * @see #addDynOpt(String, FieldEncoder, FieldDecoder, Codec, Supplier, Predicate) + */ + public Builder add(String name, FieldEncoder fieldEncoder, FieldDecoder fieldDecoder, + Codec codec, @Nullable Predicate emptyTester) { + return addDynOpt(name, fieldEncoder, fieldDecoder, codec, null, emptyTester); + } + + public Builder addOpt(String name, FieldEncoder fieldEncoder, FieldDecoder fieldDecoder, + Codec codec, @Nullable V defValue) { return addDynOpt(name, fieldEncoder, fieldDecoder, codec, () -> defValue); } - public Builder addDynOpt(String name, FieldEncoder fieldEncoder, FieldDecoder fieldDecoder, Codec codec, Supplier defaultSupplier) { - this.fields.put(name, new Field<>(name, fieldEncoder, fieldDecoder, codec, defaultSupplier)); + /** + * Adds a new optional, mutable property with a const default value. + * + * @param defValue default value, if this is null, the default value is null and this property is still considered optional + * @see #addDynOpt(String, FieldEncoder, FieldDecoder, Codec, Supplier, Predicate) + */ + public Builder addOpt(String name, FieldEncoder fieldEncoder, FieldDecoder fieldDecoder, + Codec codec, @Nullable V defValue, @Nullable Predicate emptyTester) { + return addDynOpt(name, fieldEncoder, fieldDecoder, codec, () -> defValue, emptyTester); + } + + public Builder addDynOpt(String name, FieldEncoder fieldEncoder, FieldDecoder fieldDecoder, + Codec codec, @Nullable Supplier defaultSupplier) { + return addDynOpt(name, fieldEncoder, fieldDecoder, codec, defaultSupplier, null); + } + + /** + * Adds a new optional, mutable property with a dynamic default value. + * + * @param name name of the property, mostly used for en-/decoding + * @param fieldEncoder writes a value to the field + * @param fieldDecoder reads a value from the field + * @param codec handles en-/decoding of a value + * @param defaultSupplier supplier for a default value, if this is non-null, this property is marked as optional + * @param emptyTester a test function to test whether a value is empty, this is currently only used for unencodable values + * @param type of value + * @return this + * @throws NullPointerException if name, fieldEncoder or fieldDecoder is null + */ + public Builder addDynOpt(String name, FieldEncoder fieldEncoder, FieldDecoder fieldDecoder, Codec codec, + @Nullable Supplier defaultSupplier, @Nullable Predicate emptyTester) { + Objects.requireNonNull(name, "Name of field must not be null!"); + Objects.requireNonNull(fieldEncoder, "Field encoder must not be null!"); + Objects.requireNonNull(fieldDecoder, "Field decoder must not be null!"); + this.fields.put(name, new Field<>(name, fieldEncoder, fieldDecoder, codec, defaultSupplier, emptyTester)); return this; } - public Builder addUncodable(String name, FieldDecoder fieldDecoder) { - return addUncodable(name, fieldDecoder, Objects::isNull); + public Builder addUnencodable(String name, FieldEncoder fieldEncoder, FieldDecoder fieldDecoder) { + return addUnencodableDynOpt(name, fieldEncoder, fieldDecoder, null, null); } - public Builder addUncodable(String name, FieldDecoder fieldDecoder, Predicate emptyTest) { - return add(name, (holder, value) -> { - throw new IllegalArgumentException("Unable to write field"); - }, holder -> { - if (!emptyTest.test(fieldDecoder.decodeField(holder))) { - throw new IllegalArgumentException("Unable to read field"); - } - return null; - }, null); + public Builder addUnencodable(String name, FieldEncoder fieldEncoder, FieldDecoder fieldDecoder, + @Nullable Predicate emptyTest) { + return addUnencodableDynOpt(name, fieldEncoder, fieldDecoder, null, emptyTest); + } + + public Builder addUnencodableOpt(String name, FieldEncoder fieldEncoder, FieldDecoder fieldDecoder, + @Nullable V defaultValue) { + return addUnencodableDynOpt(name, fieldEncoder, fieldDecoder, () -> defaultValue, null); + } + + public Builder addUnencodableOpt(String name, FieldEncoder fieldEncoder, FieldDecoder fieldDecoder, + @Nullable V defaultValue, @Nullable Predicate emptyTest) { + return addUnencodableDynOpt(name, fieldEncoder, fieldDecoder, () -> defaultValue, emptyTest); + } + + public Builder addUnencodableDynOpt(String name, FieldEncoder fieldEncoder, FieldDecoder fieldDecoder, + @Nullable Supplier defaultSupplier) { + return addUnencodableDynOpt(name, fieldEncoder, fieldDecoder, defaultSupplier, null); + } + + /** + * Adds a new unencodable, optional, mutable property with a dynamic default value. + * Unencodable means it cannot be converted to a data format like JSON. This is the case for functions. + * These unencodable values are still important for copying and applying default values. + * + * @see #addDynOpt(String, FieldEncoder, FieldDecoder, Codec, Supplier, Predicate) + */ + public Builder addUnencodableDynOpt(String name, FieldEncoder fieldEncoder, FieldDecoder fieldDecoder, + @Nullable Supplier defaultSupplier, @Nullable Predicate emptyTest) { + return addDynOpt(name, fieldEncoder, fieldDecoder, null, defaultSupplier, emptyTest); } public MutableObjectCodec build() { - return new MutableObjectCodec<>(this.fields, this.instanceDecoder); + return new MutableObjectCodec<>(this.fields, this.instanceDecoder, this.baseCopy); } } } diff --git a/src/main/java/brachy/modularui/widget/sizer/Unit.java b/src/main/java/brachy/modularui/widget/sizer/Unit.java index 1da3896..c957f17 100644 --- a/src/main/java/brachy/modularui/widget/sizer/Unit.java +++ b/src/main/java/brachy/modularui/widget/sizer/Unit.java @@ -55,7 +55,7 @@ public String getText(GuiAxis axis) { .addOpt("anchor", Unit::setAnchor, Unit::getAnchor, Codec.FLOAT, 0f) .addOpt("offset", Unit::setOffset, Unit::getOffset, Codec.INT, 0) .addOpt("state", Unit::setState, Unit::getState, State.CODEC, State.UNUSED) - .addUncodable("valueSupplier", Unit::getValueSupplier) + .addUnencodableOpt("valueSupplier", Unit::setValue, Unit::getValueSupplier, null) .build(); @Getter From 2f085c19a902e8c3858bbcfd9be27351bf1d30e2 Mon Sep 17 00:00:00 2001 From: brachy84 Date: Sun, 17 May 2026 16:26:33 +0200 Subject: [PATCH 04/26] working codecs for Unit, DimensionSizer & StandardResizer --- .../java/brachy/modularui/ClientProxy.java | 26 ++++ .../serialization/codec/InstanceDecoder.java | 17 +++ .../serialization/codec/MutableCodec.java | 33 +++- .../codec/MutableObjectCodec.java | 142 +++++++++--------- .../utils/serialization/json/JsonHelper.java | 31 +++- .../widget/sizer/DimensionSizer.java | 70 ++++++++- .../widget/sizer/StandardResizer.java | 25 ++- .../brachy/modularui/widget/sizer/Unit.java | 82 +++++----- 8 files changed, 306 insertions(+), 120 deletions(-) diff --git a/src/main/java/brachy/modularui/ClientProxy.java b/src/main/java/brachy/modularui/ClientProxy.java index 2be93d7..1c2a387 100644 --- a/src/main/java/brachy/modularui/ClientProxy.java +++ b/src/main/java/brachy/modularui/ClientProxy.java @@ -16,6 +16,12 @@ import brachy.modularui.network.ModularNetwork; import brachy.modularui.theme.ThemeManager; +import brachy.modularui.utils.serialization.json.JsonHelper; +import brachy.modularui.widget.Widget; + +import brachy.modularui.widget.sizer.StandardResizer; + +import com.mojang.serialization.JsonOps; import net.minecraft.client.Minecraft; import net.minecraft.client.Timer; import net.minecraft.client.gui.screens.inventory.tooltip.ClientTooltipComponent; @@ -30,6 +36,7 @@ import lombok.Getter; +import java.util.Objects; import java.util.function.Function; public class ClientProxy extends CommonProxy { @@ -58,6 +65,25 @@ protected void onInit(FMLCommonSetupEvent event) { // enable stencil bits, must call on render thread RenderSystem.recordRenderCall(() -> Minecraft.getInstance().getMainRenderTarget().enableStencil()); } + + // CODEC tests + // TODO remove when done + Widget w = new Widget<>(); + w.left(5); + w.bottomRel(0.75f, -67, 0.42f); + w.size(20); + w.decoration(); + StandardResizer r1 = w.resizer(); + var s1 = JsonHelper.toJsonString(StandardResizer.CODEC, r1); + ModularUI.LOGGER.info("Resizer Json of {}:\n{}", r1, s1); + + Widget w2 = new Widget<>(); + StandardResizer r2 = JsonHelper.fromJsonString(StandardResizer.CODEC, s1, w2.resizer()); + + var s2 = JsonHelper.toJsonString(StandardResizer.CODEC, r2); + boolean eq = Objects.equals(s1, s2); + ModularUI.LOGGER.info("Resizer Json of {}:\n{}", r2, s2); + ModularUI.LOGGER.info("Equals: {}", eq); } private void onRegisterClientTooltipComponents(RegisterClientTooltipComponentFactoriesEvent event) { diff --git a/src/main/java/brachy/modularui/utils/serialization/codec/InstanceDecoder.java b/src/main/java/brachy/modularui/utils/serialization/codec/InstanceDecoder.java index ad13162..ff73c10 100644 --- a/src/main/java/brachy/modularui/utils/serialization/codec/InstanceDecoder.java +++ b/src/main/java/brachy/modularui/utils/serialization/codec/InstanceDecoder.java @@ -2,6 +2,7 @@ import com.mojang.datafixers.util.Pair; import com.mojang.serialization.DataResult; +import com.mojang.serialization.Dynamic; import com.mojang.serialization.DynamicOps; /** @@ -13,4 +14,20 @@ public interface InstanceDecoder { DataResult> decodeInstance(final DynamicOps ops, final T input); + + default DataResult parseInstance(final DynamicOps ops, final T input) { + return decodeInstance(ops, input).map(Pair::getFirst); + } + + default DataResult> decodeInstance(final Dynamic dynamic) { + return decodeInstance(dynamic.getOps(), dynamic.getValue()); + } + + default DataResult parseInstance(final Dynamic dynamic) { + return decodeInstance(dynamic.getOps(), dynamic.getValue()).map(Pair::getFirst); + } + + default boolean canDecodeInstance() { + return true; + } } diff --git a/src/main/java/brachy/modularui/utils/serialization/codec/MutableCodec.java b/src/main/java/brachy/modularui/utils/serialization/codec/MutableCodec.java index 5c94337..399db46 100644 --- a/src/main/java/brachy/modularui/utils/serialization/codec/MutableCodec.java +++ b/src/main/java/brachy/modularui/utils/serialization/codec/MutableCodec.java @@ -1,10 +1,41 @@ package brachy.modularui.utils.serialization.codec; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; + +import com.mojang.datafixers.util.Pair; import com.mojang.serialization.Codec; +import com.mojang.serialization.DataResult; +import com.mojang.serialization.JsonOps; /** * A {@link Codec} that can decode mutable field to an existing instance. * * @param type of instance */ -public interface MutableCodec extends Codec, MutableDecoder {} +public interface MutableCodec extends Codec, MutableDecoder { + + default DataResult> decodeJson(JsonObject json) { + return decode(JsonOps.INSTANCE, json); + } + + default DataResult parseJson(JsonObject json) { + return parse(JsonOps.INSTANCE, json); + } + + default DataResult> decodeJson(JsonObject json, A instance) { + return decode(JsonOps.INSTANCE, json, instance); + } + + default DataResult parseJson(JsonObject json, A instance) { + return parse(JsonOps.INSTANCE, json, instance); + } + + default DataResult encodeJson(A instance) { + return encodeStart(JsonOps.INSTANCE, instance); + } + + default DataResult encodeJson(A instance, JsonElement prefix) { + return encode(instance, JsonOps.INSTANCE, prefix); + } +} diff --git a/src/main/java/brachy/modularui/utils/serialization/codec/MutableObjectCodec.java b/src/main/java/brachy/modularui/utils/serialization/codec/MutableObjectCodec.java index 27447ef..1244192 100644 --- a/src/main/java/brachy/modularui/utils/serialization/codec/MutableObjectCodec.java +++ b/src/main/java/brachy/modularui/utils/serialization/codec/MutableObjectCodec.java @@ -4,11 +4,8 @@ import com.mojang.serialization.Codec; import com.mojang.serialization.DataResult; import com.mojang.serialization.DynamicOps; -import com.mojang.serialization.JsonOps; import com.mojang.serialization.MapLike; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; import it.unimi.dsi.fastutil.objects.Object2ReferenceLinkedOpenHashMap; import org.jetbrains.annotations.Nullable; @@ -27,7 +24,7 @@ * * @param type of property */ -public class MutableObjectCodec { +public class MutableObjectCodec implements MutableCodec { private final Object2ReferenceLinkedOpenHashMap> fields; private final InstanceDecoder instanceDecoder; @@ -44,65 +41,6 @@ public void forEachField(Consumer> consumer) { this.fields.values().forEach(consumer); } - public final MutableCodec codec = new MutableCodec() { - - @Override - public DataResult> decodeInstance(DynamicOps ops, J input) { - if (MutableObjectCodec.this.instanceDecoder == null) { - return DataResult.error(() -> "Instance can not be created since no instance decoder was provided."); - } - return MutableObjectCodec.this.instanceDecoder.decodeInstance(ops, input); - } - - @Override - public DataResult> decode(DynamicOps ops, J input, T instance) { - var d = ops.getMap(input); - var map = d.result(); - if (map.isEmpty()) return DataResult.error(() -> d.error().orElseThrow().message()); - List errors = new ArrayList<>(); - forEachField(f -> { - var error = f.decode(instance, ops, map.get()); - if (error != null) errors.add(error); - }); - if (!errors.isEmpty()) { - return DataResult.error(() -> - String.format("Errors while decoding object of type '%s': %s", instance.getClass().getSimpleName(), errors)); - } - return DataResult.success(new Pair<>(instance, input)); - } - - public DataResult> decodeJson(JsonObject json) { - return decode(JsonOps.INSTANCE, json); - } - - public DataResult parseJson(JsonObject json) { - return parse(JsonOps.INSTANCE, json); - } - - public DataResult> decodeJson(JsonObject json, T instance) { - return decode(JsonOps.INSTANCE, json, instance); - } - - public DataResult parseJson(JsonObject json, T instance) { - return parse(JsonOps.INSTANCE, json, instance); - } - - @Override - public DataResult encode(T input, DynamicOps ops, J prefix) { - var builder = Stream.>builder(); - List errors = new ArrayList<>(); - forEachField(f -> { - var error = f.encode(input, ops, builder); - if (error != null) errors.add(error); - }); - if (!errors.isEmpty()) { - return DataResult.error(() -> - String.format("Errors while encoding object of type '%s': %s", input.getClass().getSimpleName(), errors)); - } - return DataResult.success(ops.createMap(builder.build())); - } - }; - @SuppressWarnings("unchecked") private Field getField(String name) { return (Field) this.fields.get(name); @@ -125,6 +63,57 @@ public void applyDefaults(T instance) { forEachField(f -> f.applyDefault(instance)); } + @Override + public DataResult> decodeInstance(DynamicOps ops, J input) { + if (MutableObjectCodec.this.instanceDecoder == null) { + return DataResult.error(() -> "Instance can not be created since no instance decoder was provided."); + } + return MutableObjectCodec.this.instanceDecoder.decodeInstance(ops, input); + } + + @Override + public boolean canDecodeInstance() { + return this.instanceDecoder != null; + } + + @Override + public DataResult> decode(DynamicOps ops, J input, T instance) { + if (Objects.equals(input, ops.empty())) { + return DataResult.success(new Pair<>(null, ops.empty())); + } + var d = ops.getMap(input); + var map = d.result(); + if (map.isEmpty()) return DataResult.error(() -> d.error().orElseThrow().message()); + List errors = new ArrayList<>(); + forEachField(f -> { + var error = f.decode(instance, ops, map.get()); + if (error != null) errors.add(error); + }); + if (!errors.isEmpty()) { + return DataResult.error(() -> + String.format("Errors while decoding object of type '%s': %s", instance.getClass().getSimpleName(), errors)); + } + return DataResult.success(new Pair<>(instance, input)); + } + + @Override + public DataResult encode(T input, DynamicOps ops, J prefix) { + if (input == null) { + return DataResult.success(ops.empty()); + } + var builder = Stream.>builder(); + List errors = new ArrayList<>(); + forEachField(f -> { + var error = f.encode(input, ops, builder); + if (error != null) errors.add(error); + }); + if (!errors.isEmpty()) { + return DataResult.error(() -> + String.format("Errors while encoding object of type '%s': %s", input.getClass().getSimpleName(), errors)); + } + return DataResult.success(ops.createMap(builder.build())); + } + public static Builder builder() { return new Builder<>(); } @@ -175,14 +164,31 @@ public boolean isUnencodable() { J element = map.get(this.name); V value; if (element == null) { - if (this.defaultSupplier == null) { + if (!hasDefault()) { return String.format("Field '%s' has no value and is not optional", this.name); } - value = this.defaultSupplier.get(); - } else { - if (isUnencodable()) { - return String.format("Field '%s' is unencodable, but data still contains value", this.name); + value = getDefault(); + } else if (isUnencodable()) { + return String.format("Field '%s' is unencodable, but data still contains value", this.name); + } else if (this.codec instanceof MutableCodec mutableCodec) { + value = this.fieldDecoder.decodeField(holder); + if (value == null) { + if (mutableCodec.canDecodeInstance()) { + var d = mutableCodec.parseInstance(ops, element); + var res = d.result(); + if (res.isEmpty()) return d.error().orElseThrow().message(); + value = res.get(); + } + if (value == null && hasDefault()) value = getDefault(); + if (value == null) { + return String.format("Field '%s' is unable to decode instance and the holder has no default value and this property has no default value", this.name); + } } + var d = mutableCodec.parse(ops, element, value); + var res = d.result(); + if (res.isEmpty()) return d.error().orElseThrow().message(); + value = res.get(); + } else { var d = this.codec.parse(ops, element); var res = d.result(); if (res.isEmpty()) return d.error().orElseThrow().message(); diff --git a/src/main/java/brachy/modularui/utils/serialization/json/JsonHelper.java b/src/main/java/brachy/modularui/utils/serialization/json/JsonHelper.java index d5a76b2..fb3784a 100755 --- a/src/main/java/brachy/modularui/utils/serialization/json/JsonHelper.java +++ b/src/main/java/brachy/modularui/utils/serialization/json/JsonHelper.java @@ -1,17 +1,25 @@ package brachy.modularui.utils.serialization.json; +import brachy.modularui.ModularUI; import brachy.modularui.api.drawable.IDrawable; import brachy.modularui.drawable.DrawableSerialization; import brachy.modularui.utils.Alignment; import brachy.modularui.utils.Color; +import brachy.modularui.utils.serialization.codec.MutableCodec; + import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.JsonDeserializationContext; import com.google.gson.JsonElement; +import com.google.gson.JsonNull; import com.google.gson.JsonObject; import com.google.gson.JsonParser; import com.google.gson.JsonSerializationContext; + +import com.mojang.serialization.Codec; +import com.mojang.serialization.JsonOps; + import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -45,7 +53,6 @@ public JsonElement serialize(Object o, Type type) { } }; - public static JsonElement serialize(Object object) { return GSON.toJsonTree(object); } @@ -232,4 +239,26 @@ public static JsonObject makeJson(Consumer writer) { writer.accept(json); return json; } + + public static JsonElement toJson(Codec codec, T input) { + var d = codec.encodeStart(JsonOps.INSTANCE, input); + if (d.error().isPresent()) ModularUI.LOGGER.error("Error encoding '{}' to json: {}", input, d.error().get()); + return d.result().orElse(JsonNull.INSTANCE); + } + + public static String toJsonString(Codec codec, T input) { + return GSON.toJson(toJson(codec, input)); + } + + public static T fromJsonString(MutableCodec codec, String json, T instance) { + var d = codec.parse(JsonOps.INSTANCE, JsonParser.parseString(json), instance); + if (d.error().isPresent()) ModularUI.LOGGER.error("Error decoding from json: {}", d.error().get()); + return instance; + } + + public static T fromJsonString(Codec codec, String json) { + var d = codec.parse(JsonOps.INSTANCE, JsonParser.parseString(json)); + if (d.error().isPresent()) ModularUI.LOGGER.error("Error decoding from json: {}", d.error().get()); + return d.result().orElseThrow(); + } } diff --git a/src/main/java/brachy/modularui/widget/sizer/DimensionSizer.java b/src/main/java/brachy/modularui/widget/sizer/DimensionSizer.java index 67df805..402716a 100644 --- a/src/main/java/brachy/modularui/widget/sizer/DimensionSizer.java +++ b/src/main/java/brachy/modularui/widget/sizer/DimensionSizer.java @@ -6,6 +6,11 @@ import brachy.modularui.api.GuiAxis; import brachy.modularui.api.widget.IWidget; +import brachy.modularui.utils.serialization.codec.MutableObjectCodec; + +import com.mojang.serialization.Codec; + +import lombok.AccessLevel; import lombok.Getter; import lombok.Setter; import org.jetbrains.annotations.ApiStatus; @@ -19,19 +24,25 @@ @ApiStatus.Internal public class DimensionSizer { + public static final MutableObjectCodec CODEC = MutableObjectCodec.builder(DimensionSizer.class) + .baseCopy(sizer -> new DimensionSizer(sizer.resizer, sizer.axis)) + .addOpt("coverChildrenMinSize", DimensionSizer::setCoverChildrenMinSize, DimensionSizer::getCoverChildrenMinSize, Codec.INT, -1) + .addDynOpt("start", DimensionSizer::setStart, DimensionSizer::getStart, Unit.CODEC, Unit::new) + .addDynOpt("end", DimensionSizer::setEnd, DimensionSizer::getEnd, Unit.CODEC, Unit::new) + .addDynOpt("size", DimensionSizer::setSize, DimensionSizer::getSize, Unit.CODEC, Unit::new) + .build(); + private final ResizeNode resizer; private final GuiAxis axis; private final Unit p1 = new Unit(), p2 = new Unit(); - private Unit start, end, size; + @Getter(AccessLevel.PRIVATE) private Unit start, end, size; private Unit next = p1; @Getter private int coverChildrenMinSize = -1; @Setter private boolean expanded = false; - @Setter - private boolean cancelAutoMovement = false; @Getter private boolean posCalculated = false, sizeCalculated = false; @@ -83,10 +94,14 @@ public void resetSize() { } public void setCoverChildren(int minSize, IWidget widget) { - getSize(widget); + if (minSize >= 0) getSize(widget); this.coverChildrenMinSize = minSize; } + private void setCoverChildrenMinSize(int minSize) { + setCoverChildren(minSize, null); + } + public void setUnit(Unit unit, Unit.State pos) { switch (pos) { case START -> getStart(null).copyPropertiesOf(unit); @@ -271,9 +286,7 @@ public int postApply(ResizeNode resizer, ResizeNode relativeTo, int p0, int p1) p = calcPoint(this.end, s, parentSize, parentCalculated) - s; } else { p = area.getRelativePoint(this.axis) + p0/* + area.getMargin().getStart(this.axis)*/; - if (!this.cancelAutoMovement) { - moveAmount = -p0; - } + moveAmount = -p0; } area.setRelativePoint(this.axis, p); this.posCalculated = true; @@ -434,12 +447,53 @@ protected Unit getSize(IWidget widget) { return this.size; } + private void setStart(Unit unit) { + if (unit == null || unit.isUnused()) { + if (this.start != null) { + this.start.reset(); + if (this.next != this.start && !this.next.isUnused()) { + this.next = this.start; + } + this.start = null; + } + return; + } + if (this.start != unit) getStart(null).copyPropertiesOf(unit); + } + + private void setEnd(Unit unit) { + if (unit == null || unit.isUnused()) { + if (this.end != null) { + this.end.reset(); + if (this.next != this.end && !this.next.isUnused()) { + this.next = this.end; + } + this.end = null; + } + return; + } + if (this.end != unit) getEnd(null).copyPropertiesOf(unit); + } + + private void setSize(Unit unit) { + if (unit == null || unit.isUnused()) { + if (this.size != null) { + this.size.reset(); + if (this.next != this.size && !this.next.isUnused()) { + this.next = this.size; + } + this.size = null; + } + return; + } + if (this.size != unit) getSize(null).copyPropertiesOf(unit); + } + public void copyPropertiesOf(DimensionSizer sizer) { reset(); if (sizer.start != null) getStart(null).copyPropertiesOf(sizer.start); if (sizer.end != null) getEnd(null).copyPropertiesOf(sizer.end); if (sizer.size != null) getSize(null).copyPropertiesOf(sizer.size); this.coverChildrenMinSize = sizer.coverChildrenMinSize; - this.cancelAutoMovement = sizer.cancelAutoMovement; } } diff --git a/src/main/java/brachy/modularui/widget/sizer/StandardResizer.java b/src/main/java/brachy/modularui/widget/sizer/StandardResizer.java index 919dee7..beb1407 100644 --- a/src/main/java/brachy/modularui/widget/sizer/StandardResizer.java +++ b/src/main/java/brachy/modularui/widget/sizer/StandardResizer.java @@ -10,9 +10,14 @@ import brachy.modularui.api.widget.IWidget; import brachy.modularui.core.mixins.client.SlotAccessor; import brachy.modularui.utils.TreeUtil; +import brachy.modularui.utils.serialization.codec.MutableObjectCodec; import brachy.modularui.widgets.layout.IExpander; +import com.mojang.serialization.Codec; + +import lombok.AccessLevel; import lombok.Getter; +import lombok.Setter; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.Nullable; @@ -24,8 +29,16 @@ */ public class StandardResizer extends WidgetResizeNode implements IPositioned { - private final DimensionSizer x; - private final DimensionSizer y; + public static final MutableObjectCodec CODEC = MutableObjectCodec.builder(StandardResizer.class) + .baseCopy(resizer -> new StandardResizer(resizer.getWidget())) + .addOpt("expanded", StandardResizer::expanded, StandardResizer::isExpanded, Codec.BOOL, false) + .addOpt("decoration", StandardResizer::decoration, StandardResizer::isDecoration, Codec.BOOL, false) + .add("x", StandardResizer::setX, StandardResizer::getX, DimensionSizer.CODEC) + .add("y", StandardResizer::setY, StandardResizer::getY, DimensionSizer.CODEC) + .build(); + + @Getter(AccessLevel.PRIVATE) private final DimensionSizer x; + @Getter(AccessLevel.PRIVATE) private final DimensionSizer y; @Getter private boolean expanded = false; @Getter private boolean decoration = false; @@ -43,6 +56,14 @@ protected DimensionSizer createDimensionSizer(GuiAxis axis) { return new DimensionSizer(this, axis); } + private void setX(DimensionSizer ds) { + if (ds != this.x) this.x.copyPropertiesOf(ds); + } + + private void setY(DimensionSizer ds) { + if (ds != this.y) this.y.copyPropertiesOf(ds); + } + @Override public void initialize(ResizeNode defaultParent, ResizeNode root) { super.initialize(defaultParent, root); diff --git a/src/main/java/brachy/modularui/widget/sizer/Unit.java b/src/main/java/brachy/modularui/widget/sizer/Unit.java index c957f17..6d5a314 100644 --- a/src/main/java/brachy/modularui/widget/sizer/Unit.java +++ b/src/main/java/brachy/modularui/widget/sizer/Unit.java @@ -3,14 +3,12 @@ import brachy.modularui.api.GuiAxis; import brachy.modularui.utils.serialization.codec.MutableObjectCodec; +import net.minecraft.util.StringRepresentable; import com.mojang.serialization.Codec; import lombok.AccessLevel; import lombok.Getter; import lombok.Setter; - -import net.minecraft.util.StringRepresentable; - import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; @@ -20,35 +18,7 @@ @ApiStatus.Internal public class Unit { - public enum State implements StringRepresentable { - - UNUSED("", ""), - START("LEFT", "TOP"), - END("RIGHT", "BOTTOM"), - SIZE("WIDTH", "HEIGHT"); - - public static final Codec CODEC = StringRepresentable.fromEnum(State::values); - - public final String name, xText, yText; - - State(String xText, String yText) { - this.name = name().toLowerCase(Locale.ENGLISH); - this.xText = xText; - this.yText = yText; - } - - public String getText(GuiAxis axis) { - return axis.isHorizontal() ? this.xText : this.yText; - } - - - @Override - public @NotNull String getSerializedName() { - return this.name; - } - } - - public static final MutableObjectCodec CODER = MutableObjectCodec.builder(Unit::new) + public static final MutableObjectCodec CODEC = MutableObjectCodec.builder(Unit::new) .addOpt("autoAnchor", Unit::setAutoAnchor, Unit::isAutoAnchor, Codec.BOOL, true) .addOpt("value", Unit::setValue, Unit::getValue, Codec.FLOAT, 0f) .addOpt("measure", Unit::setMeasure, Unit::getMeasure, Measure.CODEC, Measure.PIXEL) @@ -60,22 +30,25 @@ public String getText(GuiAxis axis) { @Getter @Setter - private boolean autoAnchor = true; - private float value = 0f; + private boolean autoAnchor; + private float value; @Getter(AccessLevel.PRIVATE) - private DoubleSupplier valueSupplier = null; + private DoubleSupplier valueSupplier; @Getter @Setter - private Measure measure = Measure.PIXEL; - @Setter private float anchor = 0f; + private Measure measure; + @Setter + private float anchor; @Getter @Setter - private int offset = 0; + private int offset; @Getter @Setter(AccessLevel.PRIVATE) - public State state = State.UNUSED; + public State state; - public Unit() {} + public Unit() { + reset(); + } public void reset() { this.state = State.UNUSED; @@ -135,6 +108,7 @@ public boolean isUnused() { } public enum Measure implements StringRepresentable { + PIXEL, RELATIVE; @@ -146,6 +120,34 @@ public enum Measure implements StringRepresentable { this.name = name().toLowerCase(Locale.ENGLISH); } + @Override + public @NotNull String getSerializedName() { + return this.name; + } + } + + public enum State implements StringRepresentable { + + UNUSED("", ""), + START("LEFT", "TOP"), + END("RIGHT", "BOTTOM"), + SIZE("WIDTH", "HEIGHT"); + + public static final Codec CODEC = StringRepresentable.fromEnum(State::values); + + public final String name, xText, yText; + + State(String xText, String yText) { + this.name = name().toLowerCase(Locale.ENGLISH); + this.xText = xText; + this.yText = yText; + } + + public String getText(GuiAxis axis) { + return axis.isHorizontal() ? this.xText : this.yText; + } + + @Override public @NotNull String getSerializedName() { return this.name; From 75d61e3d40c00c71f65bab6cfd8a6c2fcaaf47ca Mon Sep 17 00:00:00 2001 From: brachy84 Date: Sun, 17 May 2026 20:05:03 +0200 Subject: [PATCH 05/26] more codecs --- .../modularui/api/drawable/IDrawable.java | 12 + .../brachy/modularui/api/widget/IWidget.java | 17 ++ .../brachy/modularui/drawable/Rectangle.java | 23 +- .../modularui/screen/ClientScreenHandler.java | 4 +- .../brachy/modularui/utils/Alignment.java | 62 +++++- .../java/brachy/modularui/utils/Color.java | 79 +++++-- .../serialization/codec/CodecRegistry.java | 41 ++++ .../utils/serialization/codec/CodecUtil.java | 60 +++++ .../serialization/codec/FieldDecoder.java | 6 - .../serialization/codec/FieldEncoder.java | 6 - .../serialization/codec/FieldReader.java | 6 + .../serialization/codec/FieldWriter.java | 6 + .../codec/MutableObjectCodec.java | 205 ++++++++++++++---- .../modularui/widget/AbstractWidget.java | 2 +- .../brachy/modularui/widgets/layout/Flow.java | 2 +- 15 files changed, 450 insertions(+), 81 deletions(-) create mode 100644 src/main/java/brachy/modularui/utils/serialization/codec/CodecRegistry.java create mode 100644 src/main/java/brachy/modularui/utils/serialization/codec/CodecUtil.java delete mode 100644 src/main/java/brachy/modularui/utils/serialization/codec/FieldDecoder.java delete mode 100644 src/main/java/brachy/modularui/utils/serialization/codec/FieldEncoder.java create mode 100644 src/main/java/brachy/modularui/utils/serialization/codec/FieldReader.java create mode 100644 src/main/java/brachy/modularui/utils/serialization/codec/FieldWriter.java diff --git a/src/main/java/brachy/modularui/api/drawable/IDrawable.java b/src/main/java/brachy/modularui/api/drawable/IDrawable.java index 04fcfe9..19a75a8 100644 --- a/src/main/java/brachy/modularui/api/drawable/IDrawable.java +++ b/src/main/java/brachy/modularui/api/drawable/IDrawable.java @@ -9,14 +9,19 @@ import brachy.modularui.theme.WidgetTheme; import brachy.modularui.theme.WidgetThemeEntry; import brachy.modularui.utils.Color; +import brachy.modularui.utils.serialization.codec.CodecRegistry; import brachy.modularui.widget.Widget; import brachy.modularui.widget.sizer.Area; +import com.mojang.serialization.Codec; +import net.minecraft.util.ExtraCodecs; import net.minecraftforge.api.distmarker.Dist; import net.minecraftforge.api.distmarker.OnlyIn; import org.jetbrains.annotations.Nullable; +import java.util.Locale; + /** * An object which can be drawn at any size. This is mainly used for backgrounds and overlays in * {@link IWidget}. @@ -34,6 +39,9 @@ static IDrawable of(IDrawable... drawables) { } } + CodecRegistry CODECS = new CodecRegistry<>(); + Codec CODEC_DISPATCH = Codec.STRING.dispatch(IDrawable::getTypeName, CODECS::getNullable); + /** * Draws this drawable at the given position with the given size. It's the implementors responsibility to properly * apply the widget theme by calling {@link #applyColor(int)} before drawing. @@ -164,6 +172,10 @@ default IDrawable getSubArea(float u0, float v0, float u1, float v1) { return new SubAreaDrawable(this).uv(u0, v0, u1, v1); } + default String getTypeName() { + return getClass().getSimpleName(); + } + /** * An empty drawable. Does nothing. */ diff --git a/src/main/java/brachy/modularui/api/widget/IWidget.java b/src/main/java/brachy/modularui/api/widget/IWidget.java index 93e85e0..bc6b37f 100644 --- a/src/main/java/brachy/modularui/api/widget/IWidget.java +++ b/src/main/java/brachy/modularui/api/widget/IWidget.java @@ -10,10 +10,15 @@ import brachy.modularui.utils.FormattingUtil; import brachy.modularui.utils.ObjectList; import brachy.modularui.utils.Stencil; +import brachy.modularui.utils.serialization.codec.CodecRegistry; import brachy.modularui.widget.sizer.Area; import brachy.modularui.widget.sizer.StandardResizer; +import com.google.common.base.CaseFormat; import com.google.common.base.CharMatcher; + +import com.mojang.serialization.Codec; + import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -27,6 +32,9 @@ */ public interface IWidget extends ITreeNode { + CodecRegistry CODECS = new CodecRegistry<>(); + Codec CODEC = Codec.STRING.dispatch("widget", IWidget::getTypeName, CODECS::getNullable); + String WIDGET_TRANSLATION_KEY_FORMAT = "widget.%s.name"; /** * This char matcher is used to remove any non-{@code [a-z0-9_.-]} characters in translation keys. @@ -377,6 +385,15 @@ default boolean areAncestorsEnabled() { @Nullable String getName(); + /** + * The type name of this widget. This is used for codecs. + * + * @return the simple class name or other fitting name + */ + default String getTypeName() { + return getClass().getSimpleName(); + } + default boolean isName(String name) { return name.equals(getName()); } diff --git a/src/main/java/brachy/modularui/drawable/Rectangle.java b/src/main/java/brachy/modularui/drawable/Rectangle.java index a11c398..b29289c 100644 --- a/src/main/java/brachy/modularui/drawable/Rectangle.java +++ b/src/main/java/brachy/modularui/drawable/Rectangle.java @@ -9,9 +9,11 @@ import brachy.modularui.theme.WidgetTheme; import brachy.modularui.utils.Color; import brachy.modularui.utils.Interpolations; +import brachy.modularui.utils.serialization.codec.MutableObjectCodec; import brachy.modularui.utils.serialization.json.JsonHelper; import com.mojang.blaze3d.vertex.VertexConsumer; +import com.mojang.serialization.Codec; import net.minecraftforge.api.distmarker.Dist; import net.minecraftforge.api.distmarker.OnlyIn; @@ -27,9 +29,28 @@ @Accessors(fluent = true, chain = true) public class Rectangle implements IDrawable, IJsonSerializable, IAnimatable { - private int cornerRadius, colorTL, colorTR, colorBL, colorBR; + public static final MutableObjectCodec CODEC = MutableObjectCodec.builder(Rectangle::new) + .addOpt("colorTopLeft", Rectangle::colorTL, Rectangle::colorTL, Color.CODEC, Color.WHITE.main, "color", "colorTop", "colorLeft", "colorTL") + .addOpt("colorTopRight", Rectangle::colorTR, Rectangle::colorTR, Color.CODEC, Color.WHITE.main, "color", "colorTop", "colorRight", "colorTR") + .addOpt("colorBottomLeft", Rectangle::colorBL, Rectangle::colorBL, Color.CODEC, Color.WHITE.main, "color", "colorBottom", "colorLeft", "colorBL") + .addOpt("colorBottomRight", Rectangle::colorBR, Rectangle::colorBR, Color.CODEC, Color.WHITE.main, "color", "colorBottom", "colorRight", "colorBR") + .addOpt("cornerRadius", Rectangle::cornerRadius, Rectangle::cornerRadius, Codec.INT, 0) + .addOpt("cornerSegments", Rectangle::cornerSegments, Rectangle::cornerSegments, Codec.INT, 8) + .addOpt("borderThickness", Rectangle::borderThickness, Rectangle::borderThickness, Codec.FLOAT, 0f) + .addOpt("canApplyTheme", Rectangle::canApplyTheme, Rectangle::canApplyTheme, Codec.BOOL, false) + .drawableRegistry(Rectangle.class) + .build(); + + @Getter + @Setter + private int colorTL, colorTR, colorBL, colorBR; + @Getter + private int cornerRadius; + @Getter @Setter private int cornerSegments; + @Getter + @Setter private float borderThickness; @Getter @Setter diff --git a/src/main/java/brachy/modularui/screen/ClientScreenHandler.java b/src/main/java/brachy/modularui/screen/ClientScreenHandler.java index 13f5c07..c312256 100644 --- a/src/main/java/brachy/modularui/screen/ClientScreenHandler.java +++ b/src/main/java/brachy/modularui/screen/ClientScreenHandler.java @@ -579,8 +579,8 @@ public static void drawDebugScreen(GuiGraphics graphics, @Nullable ModularScreen int mouseX = context.getAbsMouseX(), mouseY = context.getAbsMouseY(); int screenH = muiScreen.getScreenArea().height; - int outlineColor = Color.parseString(ModularUIConfig.DEBUG_OUTLINE_COLOR.get(), DEFAULT_DEBUG_OUTLINE_COLOR, true); - int textColor = Color.parseString(ModularUIConfig.DEBUG_TEXT_COLOR.get(), DEFAULT_DEBUG_TEXT_COLOR, true); + int outlineColor = Color.parseString(ModularUIConfig.DEBUG_OUTLINE_COLOR.get(), DEFAULT_DEBUG_OUTLINE_COLOR).resultOrPartial(s -> {}).orElseThrow(); + int textColor = Color.parseString(ModularUIConfig.DEBUG_TEXT_COLOR.get(), DEFAULT_DEBUG_TEXT_COLOR).resultOrPartial(s -> {}).orElseThrow(); float scale = ModularUIConfig.Dev.scale(); int shift = (int) (11 * scale + 0.5f); int lineY = screenH - shift - 2; diff --git a/src/main/java/brachy/modularui/utils/Alignment.java b/src/main/java/brachy/modularui/utils/Alignment.java index 7f1ae0c..572c7ed 100644 --- a/src/main/java/brachy/modularui/utils/Alignment.java +++ b/src/main/java/brachy/modularui/utils/Alignment.java @@ -1,5 +1,6 @@ package brachy.modularui.utils; +import brachy.modularui.utils.serialization.codec.CodecUtil; import brachy.modularui.utils.serialization.json.JsonHelper; import com.google.common.base.CaseFormat; @@ -10,16 +11,30 @@ import com.google.gson.JsonPrimitive; import com.google.gson.JsonSerializationContext; import com.google.gson.JsonSerializer; + +import com.mojang.serialization.Codec; + +import com.mojang.serialization.codecs.RecordCodecBuilder; + import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import lombok.AccessLevel; +import lombok.Getter; + +import net.minecraft.util.ExtraCodecs; +import net.minecraft.util.StringRepresentable; + +import org.jetbrains.annotations.NotNull; import java.lang.reflect.Type; +import java.util.Locale; import java.util.Map; public class Alignment { private static final Map ALIGNMENT_MAP = new Object2ObjectOpenHashMap<>(); - public final float x, y; + @Getter public final float x, y; + @Getter(AccessLevel.PRIVATE) private final String name; public static final Alignment TopLeft = new Alignment(0, 0, "TopLeft"); public static final Alignment TopCenter = new Alignment(0.5f, 0, "TopCenter"); @@ -53,6 +68,7 @@ public Alignment(float x, float y) { private Alignment(float x, float y, String name) { this.x = x; this.y = y; + this.name = name; if (name != null) { ALIGNMENT_MAP.put(name, this); ALIGNMENT_MAP.put(CaseFormat.UPPER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, name), this); @@ -66,7 +82,7 @@ private Alignment(float x, float y, String name) { * Defines how elements should be aligned on the main axis. * In a row this would mean the x coordinates. */ - public enum MainAxis { + public enum MainAxis implements StringRepresentable { /** * All children will be put at the start of the Flow next to each other. @@ -95,14 +111,27 @@ public enum MainAxis { * flow has exactly one * child, then this behaves the same as {@link #CENTER}. */ - SPACE_AROUND + SPACE_AROUND; + + public static final Codec CODEC = StringRepresentable.fromEnum(MainAxis::values); + + public final String name; + + MainAxis() { + this.name = name().toLowerCase(Locale.ENGLISH); + } + + @Override + public @NotNull String getSerializedName() { + return this.name; + } } /** * Defines how elements should be aligned on the cross axis. * In a row this would mean the y coordinates. */ - public enum CrossAxis { + public enum CrossAxis implements StringRepresentable { /** * All children will be put at the start of the Flow next to each other. @@ -115,9 +144,32 @@ public enum CrossAxis { /** * All children will be put at the end of the Flow next to each other (this does not reverse children order). */ - END + END; + + public static final Codec CODEC = StringRepresentable.fromEnum(CrossAxis::values); + + public final String name; + + CrossAxis() { + this.name = name().toLowerCase(Locale.ENGLISH); + } + + @Override + public @NotNull String getSerializedName() { + return this.name; + } } + private static final Codec CODEC_OF_INSTANCE = RecordCodecBuilder.create(instance -> instance.group( + Codec.FLOAT.fieldOf("x").forGetter(Alignment::getX), + Codec.FLOAT.fieldOf("y").forGetter(Alignment::getY) + ).apply(instance, Alignment::new)); + + private static final Codec CODEC_OF_NAME = ExtraCodecs.stringResolverCodec(Alignment::getName, ALIGNMENT_MAP::get); + + public static final Codec CODEC = CodecUtil.chainedCodec(CODEC_OF_NAME, CODEC_OF_INSTANCE); + + @Deprecated public static class Json implements JsonDeserializer, JsonSerializer { @Override diff --git a/src/main/java/brachy/modularui/utils/Color.java b/src/main/java/brachy/modularui/utils/Color.java index 607e3a3..3fb4433 100644 --- a/src/main/java/brachy/modularui/utils/Color.java +++ b/src/main/java/brachy/modularui/utils/Color.java @@ -2,8 +2,12 @@ import brachy.modularui.ModularUI; import brachy.modularui.api.drawable.IInterpolation; +import brachy.modularui.utils.serialization.codec.CodecUtil; import brachy.modularui.utils.serialization.json.JsonHelper; +import com.mojang.serialization.Codec; +import com.mojang.serialization.DataResult; +import com.mojang.serialization.codecs.RecordCodecBuilder; import net.minecraft.util.Mth; import com.mojang.blaze3d.systems.RenderSystem; import net.minecraftforge.api.distmarker.Dist; @@ -893,8 +897,45 @@ public static String componentToFullHexString(int component) { return s; } - public static int parseString(String colorString) { - return parseString(colorString, WHITE.main, false); + public static final Codec CODEC_STRING = Codec.STRING.comapFlatMap(Color::parseString, c -> "#" + argbToFullHexString(c)); + + public static final Codec CODEC_ARGB = RecordCodecBuilder.create(instance -> instance.group( + Codec.FLOAT.fieldOf("r").forGetter(Color::getRedF), + Codec.FLOAT.fieldOf("g").forGetter(Color::getGreenF), + Codec.FLOAT.fieldOf("b").forGetter(Color::getBlueF), + Codec.FLOAT.optionalFieldOf("a", 1f).forGetter(Color::getAlphaF) + ).apply(instance, Color::argb)); + + public static final Codec CODEC_HSV = RecordCodecBuilder.create(instance -> instance.group( + Codec.FLOAT.fieldOf("h").forGetter(Color::getHue), + Codec.FLOAT.fieldOf("s").forGetter(Color::getHSVSaturation), + Codec.FLOAT.fieldOf("v").forGetter(Color::getValue), + Codec.FLOAT.optionalFieldOf("a", 1f).forGetter(Color::getAlphaF) + ).apply(instance, Color::ofHSV)); + + public static final Codec CODEC_HSL = RecordCodecBuilder.create(instance -> instance.group( + Codec.FLOAT.fieldOf("h").forGetter(Color::getHue), + Codec.FLOAT.fieldOf("s").forGetter(Color::getHSLSaturation), + Codec.FLOAT.fieldOf("l").forGetter(Color::getLightness), + Codec.FLOAT.optionalFieldOf("a", 1f).forGetter(Color::getAlphaF) + ).apply(instance, Color::ofHSL)); + + public static final Codec CODEC_CMYK = RecordCodecBuilder.create(instance -> instance.group( + Codec.FLOAT.fieldOf("c").forGetter(Color::getCyan), + Codec.FLOAT.fieldOf("m").forGetter(Color::getMagenta), + Codec.FLOAT.fieldOf("y").forGetter(Color::getYellow), + Codec.FLOAT.fieldOf("k").forGetter(Color::getBlack), + Codec.FLOAT.optionalFieldOf("a", 1f).forGetter(Color::getAlphaF) + ).apply(instance, Color::ofCMYK)); + + /** + * Encodes into hex string and decodes, hex string, arg obj, hsv obj, hsl obj and then cmyk obj. + */ + public static final Codec CODEC = Codec.of(CODEC_STRING, + CodecUtil.chainedDecoder(CODEC_STRING, CODEC_ARGB, CODEC_HSV, CODEC_HSL, CODEC_CMYK), "Codec[Generic color to ARGB]"); + + public static DataResult parseString(String colorString) { + return parseString(colorString, WHITE.main); } /** @@ -913,33 +954,34 @@ public static int parseString(String colorString) { * @param fallback the returned color value when the string is invalid * @return the parsed color ARGB. */ - public static int parseString(String colorString, int fallback, boolean silent) { - if (colorString.isEmpty()) return fallback; + public static DataResult parseString(String colorString, int fallback) { + if (colorString.isEmpty()) return DataResult.success(fallback); char c = colorString.charAt(0); // a normal int string if (Character.isDigit(c) || c == '-' || c == '#') { try { int color = (int) (long) Long.decode(colorString); // bruh if (color != 0 && getAlpha(color) == 0) { - return withAlpha(color, 255); + return DataResult.success(withAlpha(color, 255)); } - return color; + return DataResult.success(color); } catch (NumberFormatException e) { - ModularUI.LOGGER.error("Failed to decode color of string '{}'. Exception: ", colorString); - ModularUI.LOGGER.catching(e); - return fallback; + String finalColorString = colorString; + return DataResult.error(() -> String.format("Failed to decode color of string '%s'.", finalColorString), fallback); } } if ("invisible".equals(colorString)) { - return withAlpha(WHITE.main, 0); + return DataResult.success(withAlpha(WHITE.main, 0)); } int i = colorString.indexOf(':'); int index = 0; + String fail = null; if (i > 0) { try { index = Integer.parseInt(colorString.substring(i + 1)); } catch (NumberFormatException e) { + fail = colorString.substring(i + 1); ModularUI.LOGGER.error("[THEME] If the color is a word then after teh : must come a negative or " + "positive integer, but got '{}'", colorString.substring(i + 1)); } @@ -947,12 +989,16 @@ public static int parseString(String colorString, int fallback, boolean silent) } ColorShade colorShade = ColorShade.getFromName(colorString); if (colorShade != null) { - if (index == 0) return colorShade.main; - if (index > 0) return colorShade.brighterSafe(index - 1); - return colorShade.darkerSafe(-index - 1); + if (fail != null) { + String finalFail = fail; + return DataResult.error(() -> String.format("If the color is a word then after teh : must come a negative or positive integer, but got '%s'.", finalFail), colorShade.main); + } + if (index == 0) return DataResult.success(colorShade.main); + if (index > 0) return DataResult.success(colorShade.brighterSafe(index - 1)); + return DataResult.success(colorShade.darkerSafe(-index - 1)); } - ModularUI.LOGGER.error("[THEME] No color shade for name '{}' was found", colorString); - return fallback; + String finalColorString1 = colorString; + return DataResult.error(() -> String.format("No color shade for name '%s' was found", finalColorString1), fallback); } /** @@ -962,9 +1008,10 @@ public static int parseString(String colorString, int fallback, boolean silent) * @return ARGB color * @throws JsonParseException if color could not be parsed */ + @Deprecated public static int ofJson(JsonElement jsonElement) { if (jsonElement.isJsonPrimitive()) { - return parseString(jsonElement.getAsString()); + return parseString(jsonElement.getAsString()).resultOrPartial(s -> {}).orElse(WHITE.main); } if (jsonElement.isJsonObject()) { JsonObject json = jsonElement.getAsJsonObject(); diff --git a/src/main/java/brachy/modularui/utils/serialization/codec/CodecRegistry.java b/src/main/java/brachy/modularui/utils/serialization/codec/CodecRegistry.java new file mode 100644 index 0000000..8991cdb --- /dev/null +++ b/src/main/java/brachy/modularui/utils/serialization/codec/CodecRegistry.java @@ -0,0 +1,41 @@ +package brachy.modularui.utils.serialization.codec; + +import com.mojang.serialization.Codec; + +import it.unimi.dsi.fastutil.objects.Object2ReferenceOpenHashMap; +import org.jetbrains.annotations.Nullable; + +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +public class CodecRegistry { + + private final Map> drawableCodecs = new Object2ReferenceOpenHashMap<>(); + + public @Nullable Codec getNullable(String name) { + return this.drawableCodecs.get(name); + } + + public @Nullable Codec get(String name) { + return Objects.requireNonNull(getNullable(name)); + } + + public Optional> getOptional(String name) { + return Optional.ofNullable(getNullable(name)); + } + + public Codec getOrElse(String name, Codec codec) { + return this.drawableCodecs.getOrDefault(name, codec); + } + + public Codec register(String name, Codec codec) { + this.drawableCodecs.put(name, Objects.requireNonNull(codec)); + return codec; + } + + public Codec register(Codec codec, String... names) { + for (String name : names) this.drawableCodecs.put(name, codec); + return codec; + } +} diff --git a/src/main/java/brachy/modularui/utils/serialization/codec/CodecUtil.java b/src/main/java/brachy/modularui/utils/serialization/codec/CodecUtil.java new file mode 100644 index 0000000..fe59b44 --- /dev/null +++ b/src/main/java/brachy/modularui/utils/serialization/codec/CodecUtil.java @@ -0,0 +1,60 @@ +package brachy.modularui.utils.serialization.codec; + +import com.mojang.datafixers.util.Pair; +import com.mojang.serialization.Codec; +import com.mojang.serialization.DataResult; +import com.mojang.serialization.Decoder; +import com.mojang.serialization.DynamicOps; + +public class CodecUtil { + + @SafeVarargs + public static Codec chainedCodec(Codec... codecs) { + if (codecs == null || codecs.length == 0) throw new NullPointerException(); + if (codecs.length == 1) return codecs[0]; + return new Codec<>() { + @Override + public DataResult> decode(DynamicOps ops, T input) { + StringBuilder message = new StringBuilder(); + DataResult> last = null; + for (var codec : codecs) { + last = codec.decode(ops, input); + if (last.result().isPresent()) return last; + message.append(last.error().orElseThrow().message()).append("; "); + } + return last.mapError(s -> message.substring(0, message.length() - 2)); + } + + @Override + public DataResult encode(A input, DynamicOps ops, T prefix) { + StringBuilder message = new StringBuilder(); + DataResult last = null; + for (var codec : codecs) { + last = codec.encode(input, ops, prefix); + if (last.result().isPresent()) return last; + message.append(last.error().orElseThrow().message()).append("; "); + } + return last.mapError(s -> message.substring(0, message.length() - 2)); + } + }; + } + + @SafeVarargs + public static Decoder chainedDecoder(Decoder... decoder) { + if (decoder == null || decoder.length == 0) throw new NullPointerException(); + if (decoder.length == 1) return decoder[0]; + return new Decoder() { + @Override + public DataResult> decode(DynamicOps ops, T input) { + StringBuilder message = new StringBuilder(); + DataResult> last = null; + for (var codec : decoder) { + last = codec.decode(ops, input); + if (last.result().isPresent()) return last; + message.append(last.error().orElseThrow().message()).append("; "); + } + return last.mapError(s -> message.substring(0, message.length() - 2)); + } + }; + } +} diff --git a/src/main/java/brachy/modularui/utils/serialization/codec/FieldDecoder.java b/src/main/java/brachy/modularui/utils/serialization/codec/FieldDecoder.java deleted file mode 100644 index 7523607..0000000 --- a/src/main/java/brachy/modularui/utils/serialization/codec/FieldDecoder.java +++ /dev/null @@ -1,6 +0,0 @@ -package brachy.modularui.utils.serialization.codec; - -public interface FieldDecoder { - - V decodeField(T holder); -} diff --git a/src/main/java/brachy/modularui/utils/serialization/codec/FieldEncoder.java b/src/main/java/brachy/modularui/utils/serialization/codec/FieldEncoder.java deleted file mode 100644 index c670018..0000000 --- a/src/main/java/brachy/modularui/utils/serialization/codec/FieldEncoder.java +++ /dev/null @@ -1,6 +0,0 @@ -package brachy.modularui.utils.serialization.codec; - -public interface FieldEncoder { - - void encodeField(T holder, V value); -} diff --git a/src/main/java/brachy/modularui/utils/serialization/codec/FieldReader.java b/src/main/java/brachy/modularui/utils/serialization/codec/FieldReader.java new file mode 100644 index 0000000..6d4dae3 --- /dev/null +++ b/src/main/java/brachy/modularui/utils/serialization/codec/FieldReader.java @@ -0,0 +1,6 @@ +package brachy.modularui.utils.serialization.codec; + +public interface FieldReader { + + V readField(T holder); +} diff --git a/src/main/java/brachy/modularui/utils/serialization/codec/FieldWriter.java b/src/main/java/brachy/modularui/utils/serialization/codec/FieldWriter.java new file mode 100644 index 0000000..889e5c4 --- /dev/null +++ b/src/main/java/brachy/modularui/utils/serialization/codec/FieldWriter.java @@ -0,0 +1,6 @@ +package brachy.modularui.utils.serialization.codec; + +public interface FieldWriter { + + void writeField(T holder, V value); +} diff --git a/src/main/java/brachy/modularui/utils/serialization/codec/MutableObjectCodec.java b/src/main/java/brachy/modularui/utils/serialization/codec/MutableObjectCodec.java index 1244192..3a305b8 100644 --- a/src/main/java/brachy/modularui/utils/serialization/codec/MutableObjectCodec.java +++ b/src/main/java/brachy/modularui/utils/serialization/codec/MutableObjectCodec.java @@ -1,5 +1,11 @@ package brachy.modularui.utils.serialization.codec; +import brachy.modularui.api.drawable.IDrawable; + +import brachy.modularui.api.widget.IWidget; + +import com.google.common.base.CaseFormat; + import com.mojang.datafixers.util.Pair; import com.mojang.serialization.Codec; import com.mojang.serialization.DataResult; @@ -47,6 +53,7 @@ private Field getField(String name) { } public T copy(T from) { + if (from == null) return null; if (this.baseCopy == null) { throw new IllegalStateException("Can't copy instance since no base copy function is supplied."); } @@ -130,8 +137,8 @@ public static Builder builder(Supplier instance) { return new Builder().instance(instance); } - public record Field(String name, FieldEncoder fieldEncoder, FieldDecoder fieldDecoder, Codec codec, - Supplier defaultSupplier, Predicate emptyTester) { + public record Field(String name, FieldWriter fieldWriter, FieldReader fieldReader, Codec codec, + Supplier defaultSupplier, Predicate emptyTester, String[] altNames) { public boolean hasDefault() { return this.defaultSupplier != null; @@ -146,7 +153,7 @@ public boolean isUnencodable() { } private @Nullable String encode(T holder, DynamicOps ops, Stream.Builder> map) { - V value = this.fieldDecoder.decodeField(holder); + V value = this.fieldReader.readField(holder); if (isUnencodable()) { if (!isEmpty(value)) { return String.format("Field '%s' is unencodeable, but the value is not empty", this.name); @@ -162,6 +169,12 @@ public boolean isUnencodable() { private @Nullable String decode(T holder, DynamicOps ops, MapLike map) { J element = map.get(this.name); + if (element == null && this.altNames != null) { + for (String alt : this.altNames) { + element = map.get(alt); + if (element != null) break; + } + } V value; if (element == null) { if (!hasDefault()) { @@ -171,7 +184,7 @@ public boolean isUnencodable() { } else if (isUnencodable()) { return String.format("Field '%s' is unencodable, but data still contains value", this.name); } else if (this.codec instanceof MutableCodec mutableCodec) { - value = this.fieldDecoder.decodeField(holder); + value = this.fieldReader.readField(holder); if (value == null) { if (mutableCodec.canDecodeInstance()) { var d = mutableCodec.parseInstance(ops, element); @@ -194,17 +207,21 @@ public boolean isUnencodable() { if (res.isEmpty()) return d.error().orElseThrow().message(); value = res.get(); } - this.fieldEncoder.encodeField(holder, value); + this.fieldWriter.writeField(holder, value); return null; } public void copy(T from, T to) { - this.fieldEncoder.encodeField(to, this.fieldDecoder.decodeField(from)); + V value = this.fieldReader.readField(from); + if (this.codec instanceof MutableObjectCodec mutableObjectCodec) { + value = mutableObjectCodec.copy(value); + } + this.fieldWriter.writeField(to, value); } public void applyDefault(T instance) { if (hasDefault()) { - this.fieldEncoder.encodeField(instance, getDefault()); + this.fieldWriter.writeField(instance, getDefault()); } } @@ -218,6 +235,8 @@ public static class Builder { private final Object2ReferenceLinkedOpenHashMap> fields = new Object2ReferenceLinkedOpenHashMap<>(); private InstanceDecoder instanceDecoder; private UnaryOperator baseCopy; + private CodecRegistry registry; + private String[] names; /** * Sets the instance decoder. This is needed when parsing from JSON. The decoder should create a new instance @@ -263,47 +282,76 @@ public Builder baseCopy(UnaryOperator baseCopy) { return this; } - public Builder add(String name, FieldEncoder fieldEncoder, FieldDecoder fieldDecoder, Codec codec) { - return addDynOpt(name, fieldEncoder, fieldDecoder, codec, null); + public Builder add(String name, FieldWriter fieldWriter, FieldReader fieldReader, Codec codec) { + return addDynOpt(name, fieldWriter, fieldReader, codec, null); + } + + public Builder add(String name, FieldWriter fieldWriter, FieldReader fieldReader, Codec codec, String @Nullable ... altNames) { + return addDynOpt(name, fieldWriter, fieldReader, codec, null, altNames); + } + + public Builder add(String name, FieldWriter fieldWriter, FieldReader fieldReader, + Codec codec, @Nullable Predicate emptyTester) { + return addDynOpt(name, fieldWriter, fieldReader, codec, null, emptyTester); } /** * Adds a new non-optional, mutable property. * - * @see #addDynOpt(String, FieldEncoder, FieldDecoder, Codec, Supplier, Predicate) + * @see #addDynOpt(String, FieldWriter, FieldReader, Codec, Supplier, Predicate) */ - public Builder add(String name, FieldEncoder fieldEncoder, FieldDecoder fieldDecoder, - Codec codec, @Nullable Predicate emptyTester) { - return addDynOpt(name, fieldEncoder, fieldDecoder, codec, null, emptyTester); + public Builder add(String name, FieldWriter fieldWriter, FieldReader fieldReader, + Codec codec, @Nullable Predicate emptyTester, String @Nullable ... altNames) { + return addDynOpt(name, fieldWriter, fieldReader, codec, null, emptyTester, altNames); } - public Builder addOpt(String name, FieldEncoder fieldEncoder, FieldDecoder fieldDecoder, + public Builder addOpt(String name, FieldWriter fieldWriter, FieldReader fieldReader, Codec codec, @Nullable V defValue) { - return addDynOpt(name, fieldEncoder, fieldDecoder, codec, () -> defValue); + return addDynOpt(name, fieldWriter, fieldReader, codec, () -> defValue); + } + + public Builder addOpt(String name, FieldWriter fieldWriter, FieldReader fieldReader, + Codec codec, @Nullable V defValue, String @Nullable ... altNames) { + return addDynOpt(name, fieldWriter, fieldReader, codec, () -> defValue, altNames); + } + + public Builder addOpt(String name, FieldWriter fieldWriter, FieldReader fieldReader, + Codec codec, @Nullable V defValue, @Nullable Predicate emptyTester) { + return addDynOpt(name, fieldWriter, fieldReader, codec, () -> defValue, emptyTester, (String[]) null); } /** * Adds a new optional, mutable property with a const default value. * * @param defValue default value, if this is null, the default value is null and this property is still considered optional - * @see #addDynOpt(String, FieldEncoder, FieldDecoder, Codec, Supplier, Predicate) + * @see #addDynOpt(String, FieldWriter, FieldReader, Codec, Supplier, Predicate) */ - public Builder addOpt(String name, FieldEncoder fieldEncoder, FieldDecoder fieldDecoder, - Codec codec, @Nullable V defValue, @Nullable Predicate emptyTester) { - return addDynOpt(name, fieldEncoder, fieldDecoder, codec, () -> defValue, emptyTester); + public Builder addOpt(String name, FieldWriter fieldWriter, FieldReader fieldReader, + Codec codec, @Nullable V defValue, @Nullable Predicate emptyTester, String @Nullable ... altNames) { + return addDynOpt(name, fieldWriter, fieldReader, codec, () -> defValue, emptyTester, altNames); } - public Builder addDynOpt(String name, FieldEncoder fieldEncoder, FieldDecoder fieldDecoder, + public Builder addDynOpt(String name, FieldWriter fieldWriter, FieldReader fieldReader, Codec codec, @Nullable Supplier defaultSupplier) { - return addDynOpt(name, fieldEncoder, fieldDecoder, codec, defaultSupplier, null); + return addDynOpt(name, fieldWriter, fieldReader, codec, defaultSupplier, null, (String[]) null); + } + + public Builder addDynOpt(String name, FieldWriter fieldWriter, FieldReader fieldReader, + Codec codec, @Nullable Supplier defaultSupplier, String @Nullable ... altNames) { + return addDynOpt(name, fieldWriter, fieldReader, codec, defaultSupplier, null, altNames); + } + + public Builder addDynOpt(String name, FieldWriter fieldWriter, FieldReader fieldReader, Codec codec, + @Nullable Supplier defaultSupplier, @Nullable Predicate emptyTester) { + return addDynOpt(name, fieldWriter, fieldReader, codec, defaultSupplier, emptyTester, (String[]) null); } /** * Adds a new optional, mutable property with a dynamic default value. * * @param name name of the property, mostly used for en-/decoding - * @param fieldEncoder writes a value to the field - * @param fieldDecoder reads a value from the field + * @param fieldWriter writes a value to the field + * @param fieldReader reads a value from the field * @param codec handles en-/decoding of a value * @param defaultSupplier supplier for a default value, if this is non-null, this property is marked as optional * @param emptyTester a test function to test whether a value is empty, this is currently only used for unencodable values @@ -311,37 +359,66 @@ public Builder addDynOpt(String name, FieldEncoder fieldEncoder, Fi * @return this * @throws NullPointerException if name, fieldEncoder or fieldDecoder is null */ - public Builder addDynOpt(String name, FieldEncoder fieldEncoder, FieldDecoder fieldDecoder, Codec codec, - @Nullable Supplier defaultSupplier, @Nullable Predicate emptyTester) { + public Builder addDynOpt(String name, FieldWriter fieldWriter, FieldReader fieldReader, Codec codec, + @Nullable Supplier defaultSupplier, @Nullable Predicate emptyTester, String @Nullable ... altNames) { Objects.requireNonNull(name, "Name of field must not be null!"); - Objects.requireNonNull(fieldEncoder, "Field encoder must not be null!"); - Objects.requireNonNull(fieldDecoder, "Field decoder must not be null!"); - this.fields.put(name, new Field<>(name, fieldEncoder, fieldDecoder, codec, defaultSupplier, emptyTester)); + Objects.requireNonNull(fieldWriter, "Field encoder must not be null!"); + Objects.requireNonNull(fieldReader, "Field decoder must not be null!"); + this.fields.put(name, new Field<>(name, fieldWriter, fieldReader, codec, defaultSupplier, emptyTester, altNames)); return this; } - public Builder addUnencodable(String name, FieldEncoder fieldEncoder, FieldDecoder fieldDecoder) { - return addUnencodableDynOpt(name, fieldEncoder, fieldDecoder, null, null); + public Builder addUnencodable(String name, FieldWriter fieldWriter, FieldReader fieldReader) { + return addUnencodableDynOpt(name, fieldWriter, fieldReader, null, null, (String[]) null); } - public Builder addUnencodable(String name, FieldEncoder fieldEncoder, FieldDecoder fieldDecoder, + public Builder addUnencodable(String name, FieldWriter fieldWriter, FieldReader fieldReader, String @Nullable ... altNames) { + return addUnencodableDynOpt(name, fieldWriter, fieldReader, null, null, altNames); + } + + public Builder addUnencodable(String name, FieldWriter fieldWriter, FieldReader fieldReader, @Nullable Predicate emptyTest) { - return addUnencodableDynOpt(name, fieldEncoder, fieldDecoder, null, emptyTest); + return addUnencodableDynOpt(name, fieldWriter, fieldReader, null, emptyTest); + } + + public Builder addUnencodable(String name, FieldWriter fieldWriter, FieldReader fieldReader, + @Nullable Predicate emptyTest, String @Nullable ... altNames) { + return addUnencodableDynOpt(name, fieldWriter, fieldReader, null, emptyTest, altNames); } - public Builder addUnencodableOpt(String name, FieldEncoder fieldEncoder, FieldDecoder fieldDecoder, + public Builder addUnencodableOpt(String name, FieldWriter fieldWriter, FieldReader fieldReader, @Nullable V defaultValue) { - return addUnencodableDynOpt(name, fieldEncoder, fieldDecoder, () -> defaultValue, null); + return addUnencodableDynOpt(name, fieldWriter, fieldReader, () -> defaultValue, null, (String[]) null); } - public Builder addUnencodableOpt(String name, FieldEncoder fieldEncoder, FieldDecoder fieldDecoder, + public Builder addUnencodableOpt(String name, FieldWriter fieldWriter, FieldReader fieldReader, + @Nullable V defaultValue, String @Nullable ... altNames) { + return addUnencodableDynOpt(name, fieldWriter, fieldReader, () -> defaultValue, null, altNames); + } + + public Builder addUnencodableOpt(String name, FieldWriter fieldWriter, FieldReader fieldReader, @Nullable V defaultValue, @Nullable Predicate emptyTest) { - return addUnencodableDynOpt(name, fieldEncoder, fieldDecoder, () -> defaultValue, emptyTest); + return addUnencodableDynOpt(name, fieldWriter, fieldReader, () -> defaultValue, emptyTest); + } + + public Builder addUnencodableOpt(String name, FieldWriter fieldWriter, FieldReader fieldReader, + @Nullable V defaultValue, @Nullable Predicate emptyTest, String @Nullable ... altNames) { + return addUnencodableDynOpt(name, fieldWriter, fieldReader, () -> defaultValue, emptyTest, altNames); } - public Builder addUnencodableDynOpt(String name, FieldEncoder fieldEncoder, FieldDecoder fieldDecoder, + public Builder addUnencodableDynOpt(String name, FieldWriter fieldWriter, FieldReader fieldReader, @Nullable Supplier defaultSupplier) { - return addUnencodableDynOpt(name, fieldEncoder, fieldDecoder, defaultSupplier, null); + return addUnencodableDynOpt(name, fieldWriter, fieldReader, defaultSupplier, null, (String[]) null); + } + + public Builder addUnencodableDynOpt(String name, FieldWriter fieldWriter, FieldReader fieldReader, + @Nullable Supplier defaultSupplier, String @Nullable ... altNames) { + return addUnencodableDynOpt(name, fieldWriter, fieldReader, defaultSupplier, null, altNames); + } + + public Builder addUnencodableDynOpt(String name, FieldWriter fieldWriter, FieldReader fieldReader, + @Nullable Supplier defaultSupplier, @Nullable Predicate emptyTest) { + return addDynOpt(name, fieldWriter, fieldReader, null, defaultSupplier, emptyTest); } /** @@ -349,15 +426,57 @@ public Builder addUnencodableDynOpt(String name, FieldEncoder field * Unencodable means it cannot be converted to a data format like JSON. This is the case for functions. * These unencodable values are still important for copying and applying default values. * - * @see #addDynOpt(String, FieldEncoder, FieldDecoder, Codec, Supplier, Predicate) + * @see #addDynOpt(String, FieldWriter, FieldReader, Codec, Supplier, Predicate) */ - public Builder addUnencodableDynOpt(String name, FieldEncoder fieldEncoder, FieldDecoder fieldDecoder, - @Nullable Supplier defaultSupplier, @Nullable Predicate emptyTest) { - return addDynOpt(name, fieldEncoder, fieldDecoder, null, defaultSupplier, emptyTest); + public Builder addUnencodableDynOpt(String name, FieldWriter fieldWriter, FieldReader fieldReader, + @Nullable Supplier defaultSupplier, @Nullable Predicate emptyTest, String @Nullable ... altNames) { + return addDynOpt(name, fieldWriter, fieldReader, null, defaultSupplier, emptyTest, altNames); + } + + public Builder registry(CodecRegistry registry, String... names) { + this.registry = registry; + this.names = names; + return this; + } + + public Builder registryTypeName(CodecRegistry registry, String typeName) { + return registry(registry, typeName, CaseFormat.UPPER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, typeName)); + } + + public Builder registryTypeName(CodecRegistry registry, Class typeName) { + return registryTypeName(registry, typeName.getSimpleName()); + } + + public Builder drawableRegistry(Class type) { + return drawableRegistry(type, type.getSimpleName()); + } + + @SuppressWarnings("unchecked") + public Builder drawableRegistry(Class type, String typeName) { + if (!IDrawable.class.isAssignableFrom(type)) { + throw new IllegalArgumentException("Type '" + typeName + "' is not an IDrawable!"); + } + return registryTypeName((CodecRegistry) IDrawable.CODECS, typeName); + } + + public Builder widgetRegistry(Class type) { + return widgetRegistry(type, type.getSimpleName()); + } + + @SuppressWarnings("unchecked") + public Builder widgetRegistry(Class type, String typeName) { + if (!IWidget.class.isAssignableFrom(type)) { + throw new IllegalArgumentException("Type '" + typeName + "' is not an IWidget!"); + } + return registryTypeName((CodecRegistry) IWidget.CODECS, typeName); } public MutableObjectCodec build() { - return new MutableObjectCodec<>(this.fields, this.instanceDecoder, this.baseCopy); + var c = new MutableObjectCodec<>(this.fields, this.instanceDecoder, this.baseCopy); + if (this.registry != null) { + this.registry.register(c, this.names); + } + return c; } } } diff --git a/src/main/java/brachy/modularui/widget/AbstractWidget.java b/src/main/java/brachy/modularui/widget/AbstractWidget.java index 4e37f33..8ecdeeb 100644 --- a/src/main/java/brachy/modularui/widget/AbstractWidget.java +++ b/src/main/java/brachy/modularui/widget/AbstractWidget.java @@ -338,7 +338,7 @@ public boolean nameContains(String part) { * * @return the simple class name or other fitting name */ - protected String getTypeName() { + public String getTypeName() { return getClass().getSimpleName(); } diff --git a/src/main/java/brachy/modularui/widgets/layout/Flow.java b/src/main/java/brachy/modularui/widgets/layout/Flow.java index adeaa48..905f72a 100644 --- a/src/main/java/brachy/modularui/widgets/layout/Flow.java +++ b/src/main/java/brachy/modularui/widgets/layout/Flow.java @@ -390,7 +390,7 @@ public Flow crossAxisChildPadding(int crossAxisChildPadding) { } @Override - protected String getTypeName() { + public String getTypeName() { return this.axis.isHorizontal() ? "Row" : "Column"; } } From 226bdf4fea38f6c1eb7d2a2b18b9a72f2461e9de Mon Sep 17 00:00:00 2001 From: brachy84 Date: Sun, 17 May 2026 20:32:31 +0200 Subject: [PATCH 06/26] some tweaks --- .../brachy/modularui/drawable/Rectangle.java | 3 +- .../brachy/modularui/utils/Alignment.java | 14 +-- .../java/brachy/modularui/utils/Color.java | 7 +- .../utils/serialization/codec/CodecUtil.java | 8 +- .../codec/MutableObjectCodec.java | 110 +++++++++++++----- 5 files changed, 98 insertions(+), 44 deletions(-) diff --git a/src/main/java/brachy/modularui/drawable/Rectangle.java b/src/main/java/brachy/modularui/drawable/Rectangle.java index b29289c..f758ffd 100644 --- a/src/main/java/brachy/modularui/drawable/Rectangle.java +++ b/src/main/java/brachy/modularui/drawable/Rectangle.java @@ -29,7 +29,7 @@ @Accessors(fluent = true, chain = true) public class Rectangle implements IDrawable, IJsonSerializable, IAnimatable { - public static final MutableObjectCodec CODEC = MutableObjectCodec.builder(Rectangle::new) + public static final MutableObjectCodec CODEC = MutableObjectCodec.drawableBuilder(Rectangle::new) .addOpt("colorTopLeft", Rectangle::colorTL, Rectangle::colorTL, Color.CODEC, Color.WHITE.main, "color", "colorTop", "colorLeft", "colorTL") .addOpt("colorTopRight", Rectangle::colorTR, Rectangle::colorTR, Color.CODEC, Color.WHITE.main, "color", "colorTop", "colorRight", "colorTR") .addOpt("colorBottomLeft", Rectangle::colorBL, Rectangle::colorBL, Color.CODEC, Color.WHITE.main, "color", "colorBottom", "colorLeft", "colorBL") @@ -38,7 +38,6 @@ public class Rectangle implements IDrawable, IJsonSerializable, IAnim .addOpt("cornerSegments", Rectangle::cornerSegments, Rectangle::cornerSegments, Codec.INT, 8) .addOpt("borderThickness", Rectangle::borderThickness, Rectangle::borderThickness, Codec.FLOAT, 0f) .addOpt("canApplyTheme", Rectangle::canApplyTheme, Rectangle::canApplyTheme, Codec.BOOL, false) - .drawableRegistry(Rectangle.class) .build(); @Getter diff --git a/src/main/java/brachy/modularui/utils/Alignment.java b/src/main/java/brachy/modularui/utils/Alignment.java index 572c7ed..9ced6e7 100644 --- a/src/main/java/brachy/modularui/utils/Alignment.java +++ b/src/main/java/brachy/modularui/utils/Alignment.java @@ -3,6 +3,11 @@ import brachy.modularui.utils.serialization.codec.CodecUtil; import brachy.modularui.utils.serialization.json.JsonHelper; +import net.minecraft.util.ExtraCodecs; +import net.minecraft.util.StringRepresentable; +import com.mojang.serialization.Codec; +import com.mojang.serialization.codecs.RecordCodecBuilder; + import com.google.common.base.CaseFormat; import com.google.gson.JsonDeserializationContext; import com.google.gson.JsonDeserializer; @@ -11,18 +16,9 @@ import com.google.gson.JsonPrimitive; import com.google.gson.JsonSerializationContext; import com.google.gson.JsonSerializer; - -import com.mojang.serialization.Codec; - -import com.mojang.serialization.codecs.RecordCodecBuilder; - import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; import lombok.AccessLevel; import lombok.Getter; - -import net.minecraft.util.ExtraCodecs; -import net.minecraft.util.StringRepresentable; - import org.jetbrains.annotations.NotNull; import java.lang.reflect.Type; diff --git a/src/main/java/brachy/modularui/utils/Color.java b/src/main/java/brachy/modularui/utils/Color.java index 3fb4433..6324190 100644 --- a/src/main/java/brachy/modularui/utils/Color.java +++ b/src/main/java/brachy/modularui/utils/Color.java @@ -5,11 +5,11 @@ import brachy.modularui.utils.serialization.codec.CodecUtil; import brachy.modularui.utils.serialization.json.JsonHelper; +import net.minecraft.util.Mth; +import com.mojang.blaze3d.systems.RenderSystem; import com.mojang.serialization.Codec; import com.mojang.serialization.DataResult; import com.mojang.serialization.codecs.RecordCodecBuilder; -import net.minecraft.util.Mth; -import com.mojang.blaze3d.systems.RenderSystem; import net.minecraftforge.api.distmarker.Dist; import net.minecraftforge.api.distmarker.OnlyIn; @@ -931,8 +931,7 @@ public static String componentToFullHexString(int component) { /** * Encodes into hex string and decodes, hex string, arg obj, hsv obj, hsl obj and then cmyk obj. */ - public static final Codec CODEC = Codec.of(CODEC_STRING, - CodecUtil.chainedDecoder(CODEC_STRING, CODEC_ARGB, CODEC_HSV, CODEC_HSL, CODEC_CMYK), "Codec[Generic color to ARGB]"); + public static final Codec CODEC = CodecUtil.codecOf(CODEC_STRING, CODEC_STRING, CODEC_ARGB, CODEC_HSV, CODEC_HSL, CODEC_CMYK); public static DataResult parseString(String colorString) { return parseString(colorString, WHITE.main); diff --git a/src/main/java/brachy/modularui/utils/serialization/codec/CodecUtil.java b/src/main/java/brachy/modularui/utils/serialization/codec/CodecUtil.java index fe59b44..8307d18 100644 --- a/src/main/java/brachy/modularui/utils/serialization/codec/CodecUtil.java +++ b/src/main/java/brachy/modularui/utils/serialization/codec/CodecUtil.java @@ -5,6 +5,7 @@ import com.mojang.serialization.DataResult; import com.mojang.serialization.Decoder; import com.mojang.serialization.DynamicOps; +import com.mojang.serialization.Encoder; public class CodecUtil { @@ -43,7 +44,7 @@ public DataResult encode(A input, DynamicOps ops, T prefix) { public static Decoder chainedDecoder(Decoder... decoder) { if (decoder == null || decoder.length == 0) throw new NullPointerException(); if (decoder.length == 1) return decoder[0]; - return new Decoder() { + return new Decoder<>() { @Override public DataResult> decode(DynamicOps ops, T input) { StringBuilder message = new StringBuilder(); @@ -57,4 +58,9 @@ public DataResult> decode(DynamicOps ops, T input) { } }; } + + @SafeVarargs + public static Codec codecOf(Encoder encoder, Decoder... decoder) { + return Codec.of(encoder, chainedDecoder(decoder)); + } } diff --git a/src/main/java/brachy/modularui/utils/serialization/codec/MutableObjectCodec.java b/src/main/java/brachy/modularui/utils/serialization/codec/MutableObjectCodec.java index 3a305b8..74c1b38 100644 --- a/src/main/java/brachy/modularui/utils/serialization/codec/MutableObjectCodec.java +++ b/src/main/java/brachy/modularui/utils/serialization/codec/MutableObjectCodec.java @@ -1,17 +1,15 @@ package brachy.modularui.utils.serialization.codec; import brachy.modularui.api.drawable.IDrawable; - import brachy.modularui.api.widget.IWidget; -import com.google.common.base.CaseFormat; - import com.mojang.datafixers.util.Pair; import com.mojang.serialization.Codec; import com.mojang.serialization.DataResult; import com.mojang.serialization.DynamicOps; import com.mojang.serialization.MapLike; +import com.google.common.base.CaseFormat; import it.unimi.dsi.fastutil.objects.Object2ReferenceLinkedOpenHashMap; import org.jetbrains.annotations.Nullable; @@ -137,6 +135,70 @@ public static Builder builder(Supplier instance) { return new Builder().instance(instance); } + public static Builder drawableBuilder(Class type) { + return new DrawableBuilder<>(type.getTypeName()); + } + + public static Builder drawableBuilder(String typeName) { + return new DrawableBuilder<>(typeName); + } + + public static Builder drawableBuilder(Class type, String typeName) { + return new DrawableBuilder<>(typeName); + } + + public static Builder drawableBuilder(String typeName, InstanceDecoder instanceDecoder) { + return new DrawableBuilder(typeName).instanceDecoder(instanceDecoder); + } + + public static Builder drawableBuilder(String typeName, Supplier instance) { + return new DrawableBuilder(typeName).instance(instance); + } + + public static Builder drawableBuilder(Class type, InstanceDecoder instanceDecoder) { + return new DrawableBuilder(type.getSimpleName()).instanceDecoder(instanceDecoder); + } + + public static Builder drawableBuilder(Supplier instance) { + return drawableBuilder(instance.get().getTypeName(), instance); + } + + public static Builder drawableBuilder(Class type, Supplier instance) { + return new DrawableBuilder(type.getSimpleName()).instance(instance); + } + + public static Builder widgetBuilder(Class type) { + return new WidgetBuilder<>(type.getTypeName()); + } + + public static Builder widgetBuilder(String typeName) { + return new WidgetBuilder<>(typeName); + } + + public static Builder widgetBuilder(Class type, String typeName) { + return new WidgetBuilder<>(typeName); + } + + public static Builder widgetBuilder(String typeName, InstanceDecoder instanceDecoder) { + return new WidgetBuilder(typeName).instanceDecoder(instanceDecoder); + } + + public static Builder widgetBuilder(String typeName, Supplier instance) { + return new WidgetBuilder(typeName).instance(instance); + } + + public static Builder widgetBuilder(Class type, InstanceDecoder instanceDecoder) { + return new WidgetBuilder(type.getSimpleName()).instanceDecoder(instanceDecoder); + } + + public static Builder widgetBuilder(Class type, Supplier instance) { + return new WidgetBuilder(type.getSimpleName()).instance(instance); + } + + public static Builder widgetBuilder(Supplier instance) { + return widgetBuilder(instance.get().getTypeName(), instance); + } + public record Field(String name, FieldWriter fieldWriter, FieldReader fieldReader, Codec codec, Supplier defaultSupplier, Predicate emptyTester, String[] altNames) { @@ -350,8 +412,8 @@ public Builder addDynOpt(String name, FieldWriter fieldWriter, Fiel * Adds a new optional, mutable property with a dynamic default value. * * @param name name of the property, mostly used for en-/decoding - * @param fieldWriter writes a value to the field - * @param fieldReader reads a value from the field + * @param fieldWriter writes a value to the field + * @param fieldReader reads a value from the field * @param codec handles en-/decoding of a value * @param defaultSupplier supplier for a default value, if this is non-null, this property is marked as optional * @param emptyTester a test function to test whether a value is empty, this is currently only used for unencodable values @@ -447,36 +509,28 @@ public Builder registryTypeName(CodecRegistry registry, Class typeName) return registryTypeName(registry, typeName.getSimpleName()); } - public Builder drawableRegistry(Class type) { - return drawableRegistry(type, type.getSimpleName()); - } - - @SuppressWarnings("unchecked") - public Builder drawableRegistry(Class type, String typeName) { - if (!IDrawable.class.isAssignableFrom(type)) { - throw new IllegalArgumentException("Type '" + typeName + "' is not an IDrawable!"); + public MutableObjectCodec build() { + var c = new MutableObjectCodec<>(this.fields, this.instanceDecoder, this.baseCopy); + if (this.registry != null) { + this.registry.register(c, this.names); } - return registryTypeName((CodecRegistry) IDrawable.CODECS, typeName); + return c; } + } - public Builder widgetRegistry(Class type) { - return widgetRegistry(type, type.getSimpleName()); - } + public static class DrawableBuilder extends Builder { @SuppressWarnings("unchecked") - public Builder widgetRegistry(Class type, String typeName) { - if (!IWidget.class.isAssignableFrom(type)) { - throw new IllegalArgumentException("Type '" + typeName + "' is not an IWidget!"); - } - return registryTypeName((CodecRegistry) IWidget.CODECS, typeName); + public DrawableBuilder(String typeName) { + registryTypeName((CodecRegistry) IDrawable.CODECS, typeName); } + } - public MutableObjectCodec build() { - var c = new MutableObjectCodec<>(this.fields, this.instanceDecoder, this.baseCopy); - if (this.registry != null) { - this.registry.register(c, this.names); - } - return c; + @SuppressWarnings("unchecked") + public static class WidgetBuilder extends Builder { + + public WidgetBuilder(String typeName) { + registryTypeName((CodecRegistry) IWidget.CODECS, typeName); } } } From c7a508c4d87378cb7c73de2e78f67357afd00665 Mon Sep 17 00:00:00 2001 From: brachy84 Date: Mon, 18 May 2026 09:14:35 +0200 Subject: [PATCH 07/26] texture codec --- .../modularui/api/IJsonSerializable.java | 1 + .../drawable/AdaptableUITexture.java | 25 ++- .../brachy/modularui/drawable/ColorType.java | 6 + .../modularui/drawable/TiledUITexture.java | 11 +- .../brachy/modularui/drawable/UITexture.java | 189 +++++++++++++++--- .../serialization/codec/CodecRegistry.java | 6 +- 6 files changed, 201 insertions(+), 37 deletions(-) diff --git a/src/main/java/brachy/modularui/api/IJsonSerializable.java b/src/main/java/brachy/modularui/api/IJsonSerializable.java index 88271d1..d04cc17 100755 --- a/src/main/java/brachy/modularui/api/IJsonSerializable.java +++ b/src/main/java/brachy/modularui/api/IJsonSerializable.java @@ -8,6 +8,7 @@ import com.google.gson.JsonObject; import org.jetbrains.annotations.ApiStatus; +@Deprecated public interface IJsonSerializable> { /** diff --git a/src/main/java/brachy/modularui/drawable/AdaptableUITexture.java b/src/main/java/brachy/modularui/drawable/AdaptableUITexture.java index 7013503..e23b021 100644 --- a/src/main/java/brachy/modularui/drawable/AdaptableUITexture.java +++ b/src/main/java/brachy/modularui/drawable/AdaptableUITexture.java @@ -2,11 +2,23 @@ import brachy.modularui.screen.viewport.GuiContext; +import brachy.modularui.utils.Color; + +import com.mojang.serialization.Codec; +import com.mojang.serialization.codecs.RecordCodecBuilder; + +import lombok.Getter; + +import lombok.experimental.Accessors; + import net.minecraft.client.renderer.GameRenderer; import net.minecraft.resources.ResourceLocation; import com.mojang.blaze3d.systems.RenderSystem; import com.google.gson.JsonObject; + +import net.minecraft.util.ExtraCodecs; + import org.joml.Matrix4f; import java.util.Objects; @@ -15,10 +27,11 @@ * This class is a 9-slice texture. It can be created using * {@link UITexture.Builder#adaptable(int, int, int, int)}. */ +@Accessors(fluent = true) public class AdaptableUITexture extends UITexture { - private final int imageWidth, imageHeight, bl, bt, br, bb; - private final boolean tiled; + @Getter private final int imageWidth, imageHeight, bl, bt, br, bb; + @Getter private final boolean tiled; /** * Use {@link UITexture#builder()} with {@link Builder#adaptable(int, int)} @@ -226,6 +239,14 @@ public AdaptableUITexture withColorOverride(int color) { return (AdaptableUITexture) super.withColorOverride(color); } + @Override + public Builder toBuilder() { + return super.toBuilder() + .imageSize(this.imageWidth, this.imageHeight) + .adaptable(this.bl, this.bt, this.br, this.bb) + .tiled(); + } + @Override public boolean equals(Object o) { return o != null && getClass() == o.getClass() && isEqual((AdaptableUITexture) o); diff --git a/src/main/java/brachy/modularui/drawable/ColorType.java b/src/main/java/brachy/modularui/drawable/ColorType.java index c26c67d..abb32d7 100755 --- a/src/main/java/brachy/modularui/drawable/ColorType.java +++ b/src/main/java/brachy/modularui/drawable/ColorType.java @@ -2,15 +2,21 @@ import brachy.modularui.theme.WidgetTheme; +import com.mojang.serialization.Codec; + import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; import lombok.Getter; +import net.minecraft.util.ExtraCodecs; + import java.util.Map; import java.util.Objects; import java.util.function.ToIntFunction; public class ColorType { + public static final Codec CODEC = ExtraCodecs.stringResolverCodec(ColorType::getName, ColorType::get); + private static final Map COLOR_TYPES = new Object2ObjectOpenHashMap<>(); public static ColorType get(String name) { diff --git a/src/main/java/brachy/modularui/drawable/TiledUITexture.java b/src/main/java/brachy/modularui/drawable/TiledUITexture.java index d2c3393..1f57310 100644 --- a/src/main/java/brachy/modularui/drawable/TiledUITexture.java +++ b/src/main/java/brachy/modularui/drawable/TiledUITexture.java @@ -2,6 +2,9 @@ import brachy.modularui.screen.viewport.GuiContext; +import lombok.Getter; +import lombok.experimental.Accessors; + import net.minecraft.client.renderer.GameRenderer; import net.minecraft.resources.ResourceLocation; import com.mojang.blaze3d.systems.RenderSystem; @@ -10,9 +13,10 @@ import java.util.Objects; +@Accessors(fluent = true) public class TiledUITexture extends UITexture { - private final int imageWidth, imageHeight; + @Getter private final int imageWidth, imageHeight; /** * Use {@link UITexture#builder()} with {@link Builder#tiled()} @@ -59,6 +63,11 @@ public TiledUITexture withColorOverride(int color) { return (TiledUITexture) super.withColorOverride(color); } + @Override + public Builder toBuilder() { + return super.toBuilder().tiled(this.imageWidth, this.imageHeight); + } + @Override public boolean equals(Object o) { return o != null && getClass() == o.getClass() && isEqual((TiledUITexture) o); diff --git a/src/main/java/brachy/modularui/drawable/UITexture.java b/src/main/java/brachy/modularui/drawable/UITexture.java index b17ef6c..d208ffb 100644 --- a/src/main/java/brachy/modularui/drawable/UITexture.java +++ b/src/main/java/brachy/modularui/drawable/UITexture.java @@ -7,17 +7,22 @@ import brachy.modularui.theme.WidgetTheme; import brachy.modularui.utils.Color; import brachy.modularui.utils.Interpolations; +import brachy.modularui.utils.serialization.codec.CodecUtil; +import brachy.modularui.utils.serialization.codec.MutableObjectCodec; import brachy.modularui.utils.serialization.json.JsonHelper; -import brachy.modularui.widget.sizer.Area; import net.minecraft.resources.FileToIdConverter; import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.ExtraCodecs; +import com.mojang.serialization.Codec; +import com.mojang.serialization.DataResult; import net.minecraftforge.api.distmarker.Dist; import net.minecraftforge.api.distmarker.OnlyIn; import com.google.gson.JsonObject; import com.google.gson.JsonParseException; import lombok.Getter; +import lombok.Setter; import lombok.experimental.Accessors; import org.jetbrains.annotations.Nullable; @@ -26,6 +31,10 @@ @Accessors(fluent = true, chain = true) public class UITexture implements IDrawable, IJsonSerializable { + public static final Codec CODEC_FROM_BUILDER = Builder.CODEC.xmap(Builder::buildForCodec, UITexture::toBuilder); + public static final Codec CODEC_FROM_NAME = ExtraCodecs.stringResolverCodec(DrawableSerialization::getTextureId, DrawableSerialization::getTexture); + public static final Codec CODEC = IDrawable.CODECS.register("texture", CodecUtil.chainedCodec(CODEC_FROM_NAME, CODEC_FROM_BUILDER)); + public static final UITexture DEFAULT = fullImage("gui/options_background", ColorType.DEFAULT); public static final FileToIdConverter GUI_TEXTURE_ID_CONVERTER = new FileToIdConverter("textures/gui", ".png"); @@ -49,15 +58,14 @@ static UITexture icon(String name, int x, int y) { private static final String TEXTURES_PREFIX = "textures/"; private static final String PNG_SUFFIX = ".png"; - @Getter - public final ResourceLocation location; - public final float u0, v0, u1, v1; + @Getter public final ResourceLocation location; + @Getter public final float u0, v0, u1, v1; @Getter @Nullable public final ColorType colorType; - public final boolean nonOpaque; + @Getter public final boolean nonOpaque; - protected int colorOverride = 0; + @Getter protected int colorOverride = 0; /** * Creates a drawable texture @@ -285,6 +293,14 @@ protected void saveTextureToJson(JsonObject json) { json.addProperty("colorOverride", this.colorOverride); } + public Builder toBuilder() { + return builder() + .location(this.location) + .subAreaUV(this.u0, this.v0, this.u1, this.v1) + .colorType(this.colorType) + .nonOpaque(this.nonOpaque); + } + @Override public boolean equals(Object o) { return o != null && getClass() == o.getClass() && isEqual((UITexture) o); @@ -321,18 +337,44 @@ public static void setDefaultImageSize(int w, int h) { /** * A builder class to help create image textures. */ + @Accessors(fluent = false) public static class Builder { - private ResourceLocation location; + public static final MutableObjectCodec CODEC = MutableObjectCodec.builder(UITexture::builder) + .add("location", Builder::location, Builder::getLocation, ResourceLocation.CODEC) + .addDynOpt("imageWidth", Builder::setIw, Builder::getIw, Codec.INT, () -> UITexture.defaultImageWidth, "iw") + .addDynOpt("imageHeight", Builder::setIh, Builder::getIh, Codec.INT, () -> UITexture.defaultImageHeight, "ih") + .addOpt("x", Builder::setX, Builder::getX, Codec.INT, 0) + .addOpt("y", Builder::setY, Builder::getY, Codec.INT, 0) + .addOpt("w", Builder::setW, Builder::getW, Codec.INT, 0) + .addOpt("h", Builder::setH, Builder::getH, Codec.INT, 0) + .addOpt("u0", Builder::setU0, Builder::getU0, Codec.FLOAT, 0f, "uStart") + .addOpt("v0", Builder::setV0, Builder::getV0, Codec.FLOAT, 0f, "vStart") + .addOpt("u1", Builder::setU1, Builder::getU1, Codec.FLOAT, 1f, "uEnd") + .addOpt("v1", Builder::setV1, Builder::getV1, Codec.FLOAT, 1f, "vEnd") + .addOpt("bl", Builder::setBl, Builder::getBl, Codec.INT, 0, "borderLeft", "borderX", "border") + .addOpt("bt", Builder::setBt, Builder::getBt, Codec.INT, 0, "borderTop", "borderY", "border") + .addOpt("br", Builder::setBr, Builder::getBr, Codec.INT, 0, "borderRight", "borderX", "border") + .addOpt("bb", Builder::setBb, Builder::getBb, Codec.INT, 0, "borderBottom", "borderY", "border") + .addOpt("name", Builder::name, Builder::getName, Codec.STRING, null) + .addOpt("tiled", Builder::tiled, Builder::isTiled, Codec.BOOL, false) + .addOpt("colorType", Builder::colorType, Builder::getColorType, ColorType.CODEC, null) + .addOpt("nonOpaque", Builder::nonOpaque, Builder::isNonOpaque, Codec.BOOL, false) + .build(); + + @Getter private ResourceLocation location; + @Getter + @Setter private int iw = defaultImageWidth, ih = defaultImageHeight; - private int x, y, w, h; - private float u0 = 0, v0 = 0, u1 = 1, v1 = 1; - private Mode mode = Mode.FULL; - private int bl = 0, bt = 0, br = 0, bb = 0; - private String name; - private boolean tiled = false; - private ColorType colorType = null; - private boolean nonOpaque = false; + @Getter private int x, y, w, h; + @Getter private float u0 = 0, v0 = 0, u1 = 1, v1 = 1; + @Getter private Mode mode = Mode.FULL; + @Getter private int bl = 0, bt = 0, br = 0, bb = 0; + @Getter private String name; + @Getter private boolean tiled = false; + @Getter private ColorType colorType = null; + @Getter private boolean nonOpaque = false; + @Getter private int colorOverride = 0; /** * @param loc location of the image to draw @@ -386,7 +428,11 @@ public Builder tiled(int imageWidth, int imageHeight) { * This will make the image be drawn tiled rather than stretched. */ public Builder tiled() { - this.tiled = true; + return tiled(true); + } + + public Builder tiled(boolean tiled) { + this.tiled = tiled; return this; } @@ -563,7 +609,11 @@ public Builder name(String name) { * Sets this texture as at least partially transparent, will not disable glBlend when drawing. */ public Builder nonOpaque() { - this.nonOpaque = true; + return nonOpaque(true); + } + + public Builder nonOpaque(boolean nonOpaque) { + this.nonOpaque = nonOpaque; return this; } @@ -573,16 +623,31 @@ public Builder nonOpaque() { * @return the created texture */ public UITexture build() { - UITexture texture = create(); - DrawableSerialization.registerTexture(this.name, texture); - return texture; + return create() + .resultOrPartial(s -> { + throw new IllegalArgumentException(s); + }).map(texture -> { + DrawableSerialization.registerTexture(this.name, texture); + return texture; + }).map(texture -> this.colorOverride != 0 ? texture.withColorOverride(this.colorOverride) : texture) + .orElseThrow(); } - private UITexture create() { + private UITexture buildForCodec() { + // no error throwing and no drawable registration + return create() + .resultOrPartial(ModularUI.LOGGER::error) + .map(texture -> this.colorOverride != 0 ? texture.withColorOverride(this.colorOverride) : texture) + .orElseThrow(); + } + + private DataResult create() { if (this.location == null) { - throw new NullPointerException("Location must not be null"); + return DataResult.error(() -> "Location must not be null"); + } + if (this.iw <= 0 || this.ih <= 0) { + return DataResult.error(() -> "Image size must be > 0"); } - if (this.iw <= 0 || this.ih <= 0) throw new IllegalArgumentException("Image size must be > 0"); if (this.mode == Mode.FULL) { this.u0 = 0; this.v0 = 0; @@ -590,6 +655,9 @@ private UITexture create() { this.v1 = 1; this.mode = Mode.RELATIVE; } else if (this.mode == Mode.PIXEL) { + if (this.x < 0 || this.y < 0 || this.w > this.iw || this.h > this.ih) { + return DataResult.error(() -> "X and Y must be > 0 and W and H must by smaller than the specified image size"); + } float tw = 1f / this.iw, th = 1f / this.ih; this.u0 = this.x * tw; this.v0 = this.y * th; @@ -598,19 +666,78 @@ private UITexture create() { this.mode = Mode.RELATIVE; } if (this.mode == Mode.RELATIVE) { - if (this.u0 < 0 || this.v0 < 0 || this.u1 > 1 || this.v1 > 1) - throw new IllegalArgumentException("UV values must be 0 - 1"); + if (this.u0 < 0 || this.v0 < 0 || this.u1 > 1 || this.v1 > 1) { + return DataResult.error(() -> "UV values must be 0 - 1"); + } if (this.bl > 0 || this.bt > 0 || this.br > 0 || this.bb > 0) { - return new AdaptableUITexture(this.location, this.u0, this.v0, this.u1, this.v1, this.colorType, - this.nonOpaque, 0, this.iw, this.ih, this.bl, this.bt, this.br, this.bb, this.tiled); + return DataResult.success(new AdaptableUITexture(this.location, this.u0, this.v0, this.u1, this.v1, this.colorType, + this.nonOpaque, 0, this.iw, this.ih, this.bl, this.bt, this.br, this.bb, this.tiled)); } if (this.tiled) { - return new TiledUITexture(this.location, this.u0, this.v0, this.u1, this.v1, - this.colorType, this.nonOpaque, 0, this.iw, this.ih); + return DataResult.success(new TiledUITexture(this.location, this.u0, this.v0, this.u1, this.v1, + this.colorType, this.nonOpaque, 0, this.iw, this.ih)); } - return new UITexture(this.location, this.u0, this.v0, this.u1, this.v1, this.colorType, this.nonOpaque); + return DataResult.success(new UITexture(this.location, this.u0, this.v0, this.u1, this.v1, this.colorType, this.nonOpaque)); } - throw new IllegalStateException(); + return DataResult.error(() -> "Unknown error"); + } + + // Setters for codec + + private void setX(int x) { + this.x = x; + if (x > 0) this.mode = Mode.PIXEL; + } + + private void setY(int y) { + this.y = y; + if (y > 0) this.mode = Mode.PIXEL; + } + + private void setW(int w) { + this.w = w; + if (w > 0) this.mode = Mode.PIXEL; + } + + private void setH(int h) { + this.h = h; + if (h > 0) this.mode = Mode.PIXEL; + } + + private void setU0(float u0) { + this.u0 = u0; + if (u0 > 0) this.mode = Mode.RELATIVE; + } + + private void setV0(float v0) { + this.v0 = v0; + if (v0 > 0) this.mode = Mode.RELATIVE; + } + + private void setU1(float u1) { + this.u1 = u1; + if (u1 < 1) this.mode = Mode.RELATIVE; + } + + private void setV1(float v1) { + this.v1 = v1; + if (v1 < 1) this.mode = Mode.RELATIVE; + } + + private void setBl(int bl) { + this.bl = bl; + } + + private void setBb(int bb) { + this.bb = bb; + } + + private void setBr(int br) { + this.br = br; + } + + private void setBt(int bt) { + this.bt = bt; } } diff --git a/src/main/java/brachy/modularui/utils/serialization/codec/CodecRegistry.java b/src/main/java/brachy/modularui/utils/serialization/codec/CodecRegistry.java index 8991cdb..7f5773c 100644 --- a/src/main/java/brachy/modularui/utils/serialization/codec/CodecRegistry.java +++ b/src/main/java/brachy/modularui/utils/serialization/codec/CodecRegistry.java @@ -17,7 +17,7 @@ public class CodecRegistry { return this.drawableCodecs.get(name); } - public @Nullable Codec get(String name) { + public Codec get(String name) { return Objects.requireNonNull(getNullable(name)); } @@ -29,12 +29,12 @@ public Codec getOrElse(String name, Codec codec) { return this.drawableCodecs.getOrDefault(name, codec); } - public Codec register(String name, Codec codec) { + public synchronized Codec register(String name, Codec codec) { this.drawableCodecs.put(name, Objects.requireNonNull(codec)); return codec; } - public Codec register(Codec codec, String... names) { + public synchronized Codec register(Codec codec, String... names) { for (String name : names) this.drawableCodecs.put(name, codec); return codec; } From 045d3bdeddfde84d1eb51b3bfa674d9271c680a0 Mon Sep 17 00:00:00 2001 From: brachy84 Date: Mon, 18 May 2026 14:13:03 +0200 Subject: [PATCH 08/26] drawable codec & some fixes --- .../modularui/api/drawable/IDrawable.java | 37 +++++++++------ .../brachy/modularui/drawable/Circle.java | 35 +++++++-------- .../modularui/drawable/DrawableStack.java | 19 ++++++++ .../brachy/modularui/drawable/Rectangle.java | 5 +++ .../brachy/modularui/drawable/UITexture.java | 7 ++- .../modularui/test/TestBlockEntity.java | 2 +- .../brachy/modularui/utils/Alignment.java | 30 +++++++++---- .../utils/serialization/codec/CodecUtil.java | 45 +++++++++++++++++++ .../codec/MutableObjectCodec.java | 4 ++ .../utils/serialization/json/JsonHelper.java | 16 +++++-- 10 files changed, 153 insertions(+), 47 deletions(-) diff --git a/src/main/java/brachy/modularui/api/drawable/IDrawable.java b/src/main/java/brachy/modularui/api/drawable/IDrawable.java index 19a75a8..b800871 100644 --- a/src/main/java/brachy/modularui/api/drawable/IDrawable.java +++ b/src/main/java/brachy/modularui/api/drawable/IDrawable.java @@ -10,18 +10,17 @@ import brachy.modularui.theme.WidgetThemeEntry; import brachy.modularui.utils.Color; import brachy.modularui.utils.serialization.codec.CodecRegistry; +import brachy.modularui.utils.serialization.codec.CodecUtil; import brachy.modularui.widget.Widget; import brachy.modularui.widget.sizer.Area; import com.mojang.serialization.Codec; -import net.minecraft.util.ExtraCodecs; +import com.mojang.serialization.DataResult; import net.minecraftforge.api.distmarker.Dist; import net.minecraftforge.api.distmarker.OnlyIn; import org.jetbrains.annotations.Nullable; -import java.util.Locale; - /** * An object which can be drawn at any size. This is mainly used for backgrounds and overlays in * {@link IWidget}. @@ -39,8 +38,28 @@ static IDrawable of(IDrawable... drawables) { } } + /** + * An empty drawable. Does nothing. + */ + IDrawable EMPTY = (context, x, y, width, height, widgetTheme) -> {}; + + /** + * An empty drawable used to mark hover textures as "should not be used"! + */ + IDrawable NONE = (context, x, y, width, height, widgetTheme) -> {}; + CodecRegistry CODECS = new CodecRegistry<>(); - Codec CODEC_DISPATCH = Codec.STRING.dispatch(IDrawable::getTypeName, CODECS::getNullable); + Codec CODEC_DISPATCH = CodecUtil.dispatchNullable(Codec.STRING, IDrawable::getTypeName, CODECS::getNullable); + Codec CODEC_EMPTY_NONE = Codec.STRING.flatXmap(s -> { + if (s == null || s.equals("empty") || s.equals("null")) return DataResult.success(EMPTY); + if (s.equals("none")) return DataResult.success(NONE); + return DataResult.error(() -> "Only valid options are empty, null and none"); + }, d -> { + if (d == EMPTY) return DataResult.success("empty"); + if (d == NONE) return DataResult.success("none"); + return DataResult.error(() -> "Only works for empty and none"); + }); + Codec CODEC = CodecUtil.chainedCodec(CODEC_EMPTY_NONE, DrawableStack.CODEC, CODEC_DISPATCH); /** * Draws this drawable at the given position with the given size. It's the implementors responsibility to properly @@ -176,16 +195,6 @@ default String getTypeName() { return getClass().getSimpleName(); } - /** - * An empty drawable. Does nothing. - */ - IDrawable EMPTY = (context, x, y, width, height, widgetTheme) -> {}; - - /** - * An empty drawable used to mark hover textures as "should not be used"! - */ - IDrawable NONE = (context, x, y, width, height, widgetTheme) -> {}; - static boolean isVisible(@Nullable IDrawable drawable) { if (drawable == null || drawable == EMPTY || drawable == NONE) return false; if (drawable instanceof DrawableStack array) { diff --git a/src/main/java/brachy/modularui/drawable/Circle.java b/src/main/java/brachy/modularui/drawable/Circle.java index 56ffbbb..a4ae06f 100644 --- a/src/main/java/brachy/modularui/drawable/Circle.java +++ b/src/main/java/brachy/modularui/drawable/Circle.java @@ -7,18 +7,28 @@ import brachy.modularui.theme.WidgetTheme; import brachy.modularui.utils.Color; import brachy.modularui.utils.Interpolations; +import brachy.modularui.utils.serialization.codec.MutableObjectCodec; import brachy.modularui.utils.serialization.json.JsonHelper; +import com.mojang.serialization.Codec; import net.minecraftforge.api.distmarker.Dist; import net.minecraftforge.api.distmarker.OnlyIn; import com.google.gson.JsonObject; +import lombok.Getter; import lombok.Setter; import lombok.experimental.Accessors; @Accessors(fluent = true, chain = true) public class Circle implements IDrawable, IJsonSerializable, IAnimatable { + public static final MutableObjectCodec CODEC = MutableObjectCodec.drawableBuilder(Circle::new) + .addOpt("colorInner", Circle::colorInner, Circle::colorInner, Codec.INT, 0, "color") + .addOpt("colorOuter", Circle::colorOuter, Circle::colorOuter, Codec.INT, 0, "color") + .addOpt("segments", Circle::segments, Circle::segments, Codec.INT, 40) + .build(); + + @Getter @Setter private int colorInner, colorOuter, segments; @@ -28,22 +38,6 @@ public Circle() { this.segments = 40; } - public Circle setColorInner(int colorInner) { - return colorInner(colorInner); - } - - public Circle setColorOuter(int colorOuter) { - return colorOuter(colorOuter); - } - - public Circle setColor(int inner, int outer) { - return color(inner, outer); - } - - public Circle setSegments(int segments) { - return segments(segments); - } - public Circle color(int inner, int outer) { this.colorInner = inner; this.colorOuter = outer; @@ -88,7 +82,12 @@ public Circle interpolate(Circle start, Circle end, float t) { @Override public Circle copyOrImmutable() { return new Circle() - .setColor(this.colorInner, this.colorOuter) - .setSegments(this.segments); + .color(this.colorInner, this.colorOuter) + .segments(this.segments); + } + + @Override + public String getTypeName() { + return "circle"; } } diff --git a/src/main/java/brachy/modularui/drawable/DrawableStack.java b/src/main/java/brachy/modularui/drawable/DrawableStack.java index 86a74cb..c2f2e37 100644 --- a/src/main/java/brachy/modularui/drawable/DrawableStack.java +++ b/src/main/java/brachy/modularui/drawable/DrawableStack.java @@ -5,8 +5,11 @@ import brachy.modularui.api.drawable.IDrawable; import brachy.modularui.screen.viewport.GuiContext; import brachy.modularui.theme.WidgetTheme; +import brachy.modularui.utils.serialization.codec.CodecUtil; import brachy.modularui.utils.serialization.json.JsonHelper; +import net.minecraft.util.ExtraCodecs; +import com.mojang.serialization.Codec; import net.minecraftforge.api.distmarker.Dist; import net.minecraftforge.api.distmarker.OnlyIn; @@ -16,6 +19,7 @@ import com.google.gson.JsonParseException; import java.util.ArrayList; +import java.util.Collections; import java.util.List; /** @@ -23,9 +27,24 @@ */ public record DrawableStack(IDrawable... drawables) implements IDrawable, IJsonSerializable { + public static final Codec CODEC = ExtraCodecs.lazyInitializedCodec(() -> + CodecUtil.checkedEncoder(IDrawable.CODEC.listOf().xmap(DrawableStack::fromList, DrawableStack::toList), + d -> d instanceof DrawableStack)); + public static final IDrawable[] EMPTY_BACKGROUND = {}; public static final DrawableStack EMPTY = new DrawableStack(EMPTY_BACKGROUND); + public static IDrawable fromList(List list) { + return IDrawable.of(list.toArray(IDrawable[]::new)); + } + + public static List toList(IDrawable drawable) { + if (drawable instanceof DrawableStack stack) { + return List.of(stack.drawables); + } + return Collections.singletonList(drawable); + } + public DrawableStack(IDrawable... drawables) { this.drawables = drawables == null || drawables.length == 0 ? EMPTY_BACKGROUND : drawables; } diff --git a/src/main/java/brachy/modularui/drawable/Rectangle.java b/src/main/java/brachy/modularui/drawable/Rectangle.java index f758ffd..0f12998 100644 --- a/src/main/java/brachy/modularui/drawable/Rectangle.java +++ b/src/main/java/brachy/modularui/drawable/Rectangle.java @@ -226,4 +226,9 @@ public Rectangle copyOrImmutable() { .cornerSegments(this.cornerSegments) .canApplyTheme(this.canApplyTheme); } + + @Override + public String getTypeName() { + return "rectangle"; + } } diff --git a/src/main/java/brachy/modularui/drawable/UITexture.java b/src/main/java/brachy/modularui/drawable/UITexture.java index d208ffb..abd6ab4 100644 --- a/src/main/java/brachy/modularui/drawable/UITexture.java +++ b/src/main/java/brachy/modularui/drawable/UITexture.java @@ -293,6 +293,11 @@ protected void saveTextureToJson(JsonObject json) { json.addProperty("colorOverride", this.colorOverride); } + @Override + public String getTypeName() { + return "texture"; + } + public Builder toBuilder() { return builder() .location(this.location) @@ -627,7 +632,7 @@ public UITexture build() { .resultOrPartial(s -> { throw new IllegalArgumentException(s); }).map(texture -> { - DrawableSerialization.registerTexture(this.name, texture); + //DrawableSerialization.registerTexture(this.name, texture); return texture; }).map(texture -> this.colorOverride != 0 ? texture.withColorOverride(this.colorOverride) : texture) .orElseThrow(); diff --git a/src/main/java/brachy/modularui/test/TestBlockEntity.java b/src/main/java/brachy/modularui/test/TestBlockEntity.java index d9c3fce..b1f1c58 100644 --- a/src/main/java/brachy/modularui/test/TestBlockEntity.java +++ b/src/main/java/brachy/modularui/test/TestBlockEntity.java @@ -246,7 +246,7 @@ public ModularPanel buildUI(PosGuiData guiData, PanelSyncManager syncManager, tooltip.addDrawableLine(GuiTextures.MUI_LOGO.asIcon().size(50).alignment(Alignment.TopCenter)); tooltip.addLine(Text.str("And here a circle:")); tooltip.addDrawableLine(new Circle() - .setColor(Color.RED.darker(2), Color.RED.brighter(2)) + .color(Color.RED.darker(2), Color.RED.brighter(2)) .asIcon() .size(20)) .addDrawableLine(new ItemDrawable(new ItemStack(Items.DIAMOND)).asIcon()) diff --git a/src/main/java/brachy/modularui/utils/Alignment.java b/src/main/java/brachy/modularui/utils/Alignment.java index 9ced6e7..d7f874b 100644 --- a/src/main/java/brachy/modularui/utils/Alignment.java +++ b/src/main/java/brachy/modularui/utils/Alignment.java @@ -24,11 +24,20 @@ import java.lang.reflect.Type; import java.util.Locale; import java.util.Map; +import java.util.Objects; public class Alignment { private static final Map ALIGNMENT_MAP = new Object2ObjectOpenHashMap<>(); + private static final Codec CODEC_OF_INSTANCE = RecordCodecBuilder.create(instance -> instance.group( + Codec.FLOAT.fieldOf("x").forGetter(Alignment::getX), + Codec.FLOAT.fieldOf("y").forGetter(Alignment::getY) + ).apply(instance, Alignment::new)); + private static final Codec CODEC_OF_NAME = ExtraCodecs.stringResolverCodec(Alignment::getName, ALIGNMENT_MAP::get); + + public static final Codec CODEC = CodecUtil.chainedCodec(CODEC_OF_NAME, CODEC_OF_INSTANCE); + @Getter public final float x, y; @Getter(AccessLevel.PRIVATE) private final String name; @@ -74,6 +83,18 @@ private Alignment(float x, float y, String name) { } } + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + Alignment alignment = (Alignment) o; + return Float.compare(x, alignment.x) == 0 && Float.compare(y, alignment.y) == 0; + } + + @Override + public int hashCode() { + return Objects.hash(x, y); + } + /** * Defines how elements should be aligned on the main axis. * In a row this would mean the x coordinates. @@ -156,15 +177,6 @@ public enum CrossAxis implements StringRepresentable { } } - private static final Codec CODEC_OF_INSTANCE = RecordCodecBuilder.create(instance -> instance.group( - Codec.FLOAT.fieldOf("x").forGetter(Alignment::getX), - Codec.FLOAT.fieldOf("y").forGetter(Alignment::getY) - ).apply(instance, Alignment::new)); - - private static final Codec CODEC_OF_NAME = ExtraCodecs.stringResolverCodec(Alignment::getName, ALIGNMENT_MAP::get); - - public static final Codec CODEC = CodecUtil.chainedCodec(CODEC_OF_NAME, CODEC_OF_INSTANCE); - @Deprecated public static class Json implements JsonDeserializer, JsonSerializer { diff --git a/src/main/java/brachy/modularui/utils/serialization/codec/CodecUtil.java b/src/main/java/brachy/modularui/utils/serialization/codec/CodecUtil.java index 8307d18..9f3091a 100644 --- a/src/main/java/brachy/modularui/utils/serialization/codec/CodecUtil.java +++ b/src/main/java/brachy/modularui/utils/serialization/codec/CodecUtil.java @@ -7,6 +7,9 @@ import com.mojang.serialization.DynamicOps; import com.mojang.serialization.Encoder; +import java.util.function.Function; +import java.util.function.Predicate; + public class CodecUtil { @SafeVarargs @@ -63,4 +66,46 @@ public DataResult> decode(DynamicOps ops, T input) { public static Codec codecOf(Encoder encoder, Decoder... decoder) { return Codec.of(encoder, chainedDecoder(decoder)); } + + public static Codec dispatchNullable(Codec keyCodec, Function type, Function> codec) { + return dispatchNullable("type", keyCodec, type, codec); + } + + public static Codec dispatchNullable(String key, Codec keyCodec, Function type, Function> codec) { + return keyCodec.partialDispatch(key, e -> { + A a = type.apply(e); + return a == null ? DataResult.error(() -> "No key found") : DataResult.success(a); + }, a -> { + Codec e = codec.apply(a); + return e == null ? DataResult.error(() -> "No codec found for key " + a) : DataResult.success(e); + }); + } + + public static Encoder checked(Encoder codec, Predicate test) { + return new Encoder<>() { + @Override + public DataResult encode(A input, DynamicOps ops, T prefix) { + return test.test(input) ? codec.encode(input, ops, prefix) : DataResult.error(() -> "Codec " + codec + " can't handle value " + input); + } + }; + } + + public static Codec checkedEncoder(Codec codec, Predicate test) { + return new Codec<>() { + @Override + public DataResult> decode(DynamicOps ops, T input) { + return codec.decode(ops, input); + } + + @Override + public DataResult encode(A input, DynamicOps ops, T prefix) { + return test.test(input) ? codec.encode(input, ops, prefix) : DataResult.error(() -> "Codec " + codec + " can't handle value " + input); + } + + @Override + public String toString() { + return super.toString() + "[Checked encoder]"; + } + }; + } } diff --git a/src/main/java/brachy/modularui/utils/serialization/codec/MutableObjectCodec.java b/src/main/java/brachy/modularui/utils/serialization/codec/MutableObjectCodec.java index 74c1b38..43147d6 100644 --- a/src/main/java/brachy/modularui/utils/serialization/codec/MutableObjectCodec.java +++ b/src/main/java/brachy/modularui/utils/serialization/codec/MutableObjectCodec.java @@ -222,6 +222,10 @@ public boolean isUnencodable() { } return null; } + if (value == null) { + if (hasDefault()) return null; + return String.format("Field '%s' is not optional, but is trying to encode a null value", this.name); + } var d = this.codec.encodeStart(ops, value); var res = d.result(); if (res.isEmpty()) return d.error().orElseThrow().message(); diff --git a/src/main/java/brachy/modularui/utils/serialization/json/JsonHelper.java b/src/main/java/brachy/modularui/utils/serialization/json/JsonHelper.java index fb3784a..ae2893a 100755 --- a/src/main/java/brachy/modularui/utils/serialization/json/JsonHelper.java +++ b/src/main/java/brachy/modularui/utils/serialization/json/JsonHelper.java @@ -250,15 +250,23 @@ public static String toJsonString(Codec codec, T input) { return GSON.toJson(toJson(codec, input)); } - public static T fromJsonString(MutableCodec codec, String json, T instance) { - var d = codec.parse(JsonOps.INSTANCE, JsonParser.parseString(json), instance); + public static T fromJson(MutableCodec codec, JsonElement json, T instance) { + var d = codec.parse(JsonOps.INSTANCE, json, instance); if (d.error().isPresent()) ModularUI.LOGGER.error("Error decoding from json: {}", d.error().get()); return instance; } - public static T fromJsonString(Codec codec, String json) { - var d = codec.parse(JsonOps.INSTANCE, JsonParser.parseString(json)); + public static T fromJson(Codec codec, JsonElement json) { + var d = codec.parse(JsonOps.INSTANCE, json); if (d.error().isPresent()) ModularUI.LOGGER.error("Error decoding from json: {}", d.error().get()); return d.result().orElseThrow(); } + + public static T fromJsonString(MutableCodec codec, String json, T instance) { + return fromJson(codec, JsonParser.parseString(json), instance); + } + + public static T fromJsonString(Codec codec, String json) { + return fromJson(codec, JsonParser.parseString(json)); + } } From 6ac36f187a60c55891b44d1a7891ee786d235c66 Mon Sep 17 00:00:00 2001 From: brachy84 Date: Mon, 18 May 2026 14:13:47 +0200 Subject: [PATCH 09/26] fix color conversion inaccuracy --- src/main/java/brachy/modularui/utils/Color.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main/java/brachy/modularui/utils/Color.java b/src/main/java/brachy/modularui/utils/Color.java index 6324190..f4dd195 100644 --- a/src/main/java/brachy/modularui/utils/Color.java +++ b/src/main/java/brachy/modularui/utils/Color.java @@ -18,6 +18,7 @@ import com.google.gson.JsonParseException; import java.util.Locale; +import java.util.Random; import java.util.function.ToIntFunction; /** @@ -46,7 +47,7 @@ public static int argb(int red, int green, int blue, int alpha) { * Creates a color int. All values should be 0 - 1 */ public static int argb(float red, float green, float blue, float alpha) { - return argb((int) (red * 255), (int) (green * 255), (int) (blue * 255), (int) (alpha * 255)); + return argb(Math.round(red * 255), Math.round(green * 255), Math.round(blue * 255), Math.round(alpha * 255)); } /** @@ -60,7 +61,7 @@ public static int rgba(int red, int green, int blue, int alpha) { * Creates a color int. All values should be 0 - 1 */ public static int rgba(float red, float green, float blue, float alpha) { - return rgba((int) (red * 255), (int) (green * 255), (int) (blue * 255), (int) (alpha * 255)); + return rgba(Math.round(red * 255), Math.round(green * 255), Math.round(blue * 255), Math.round(alpha * 255)); } /** @@ -728,7 +729,7 @@ public static int mix(int argb1, int argb2) { */ public static int average(int... colors) { float r = 0, g = 0, b = 0; - int a = 0; + float a = 0; for (int color : colors) { r += getRedSq(color); g += getGreenSq(color); From fbbfe7757eca1ccda0d3b592edf62749602a794b Mon Sep 17 00:00:00 2001 From: brachy84 Date: Mon, 18 May 2026 14:14:15 +0200 Subject: [PATCH 10/26] unit tests for codecs & colors --- build.gradle | 4 + dependencies.gradle | 4 + .../java/brachy/modularui/utils/Color.java | 28 +++ src/test/java/brachy/modularui/CodecTest.java | 190 ++++++++++++++++++ src/test/java/brachy/modularui/ColorTest.java | 94 +++++++++ src/test/java/brachy/modularui/TestUtil.java | 19 ++ 6 files changed, 339 insertions(+) create mode 100644 src/test/java/brachy/modularui/CodecTest.java create mode 100644 src/test/java/brachy/modularui/ColorTest.java create mode 100644 src/test/java/brachy/modularui/TestUtil.java diff --git a/build.gradle b/build.gradle index 359b7d5..fab55c9 100755 --- a/build.gradle +++ b/build.gradle @@ -86,3 +86,7 @@ tasks.withType(JavaCompile).configureEach { lombok { version = "1.18.38" } + +test { + useJUnitPlatform() +} diff --git a/dependencies.gradle b/dependencies.gradle index 3743b18..668a51e 100755 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -1,6 +1,10 @@ dependencies { compileOnly(libs.jetbrains.annotations) + testImplementation 'org.hamcrest:hamcrest:2.2' + testImplementation 'org.junit.jupiter:junit-jupiter:5.9.2' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + // Math Parser jarJar(implementation(libs.evalEx.get())) additionalRuntimeClasspath(libs.evalEx.get()) diff --git a/src/main/java/brachy/modularui/utils/Color.java b/src/main/java/brachy/modularui/utils/Color.java index f4dd195..b0237a5 100644 --- a/src/main/java/brachy/modularui/utils/Color.java +++ b/src/main/java/brachy/modularui/utils/Color.java @@ -838,6 +838,34 @@ public static void resetGlColor() { setGlColorOpaque(WHITE.main); } + public static int getLargestDiff(int argb1, int argb2) { + return Math.max(Math.abs(getRed(argb1) - getRed(argb2)), Math.max(Math.abs(getGreen(argb1) - getGreen(argb2)), Math.abs(getBlue(argb1) - getBlue(argb2)))); + } + + public static float getLargestDiffFloat(int argb1, int argb2) { + return Math.max(Math.abs(getRedF(argb1) - getRedF(argb2)), Math.max(Math.abs(getGreenF(argb1) - getGreenF(argb2)), Math.abs(getBlueF(argb1) - getBlueF(argb2)))); + } + + public static boolean areSameColor(int argb1, int argb2) { + return areSameColor(argb1, argb2, 0); + } + + public static boolean areSameColor(int argb1, int argb2, int tolerance) { + return getLargestDiff(argb1, argb2) <= tolerance; + } + + public static boolean areSameColor(int argb1, int argb2, float tolerance) { + return getLargestDiffFloat(argb1, argb2) <= tolerance; + } + + public static int random(Random rnd, int alpha) { + return argb(rnd.nextInt(256), rnd.nextInt(256), rnd.nextInt(256), alpha); + } + + public static int random(Random rnd) { + return rnd.nextInt(); + } + /** * Returns a six digit hex string representation of a color component with upper case letters. Alpha is ignored. * diff --git a/src/test/java/brachy/modularui/CodecTest.java b/src/test/java/brachy/modularui/CodecTest.java new file mode 100644 index 0000000..327d496 --- /dev/null +++ b/src/test/java/brachy/modularui/CodecTest.java @@ -0,0 +1,190 @@ +package brachy.modularui; + +import brachy.modularui.api.drawable.IDrawable; +import brachy.modularui.api.widget.IWidget; +import brachy.modularui.drawable.Circle; +import brachy.modularui.drawable.GuiTextures; +import brachy.modularui.drawable.Rectangle; +import brachy.modularui.utils.Alignment; +import brachy.modularui.utils.Color; +import brachy.modularui.utils.serialization.codec.MutableCodec; +import brachy.modularui.utils.serialization.codec.MutableObjectCodec; +import brachy.modularui.utils.serialization.json.JsonHelper; +import brachy.modularui.widget.Widget; +import brachy.modularui.widget.sizer.StandardResizer; + +import net.minecraft.DetectedVersion; +import net.minecraft.SharedConstants; +import net.minecraft.server.Bootstrap; +import com.mojang.serialization.Codec; +import com.mojang.serialization.JsonOps; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; +import org.junit.jupiter.api.Test; + +import java.util.Random; +import java.util.function.Supplier; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class CodecTest { + + @Test + void resizer() { + resizerTest(new Widget<>().left(5) + .bottomRel(0.75f, -67, 0.42f) + .size(20) + .decoration()); + } + + @Test + void drawable() { + SharedConstants.setVersion(DetectedVersion.BUILT_IN); + Bootstrap.bootStrap(); + drawableTest(IDrawable.EMPTY); + drawableTest(IDrawable.NONE); + decodeTest(new JsonPrimitive("null"), IDrawable.CODEC, IDrawable.EMPTY); + drawableTest(IDrawable.of(new Rectangle().color(Color.GREEN.main), GuiTextures.BOOKMARK, new Circle().color(Color.RED.main, Color.BLUE.main))); + } + + @Test + void alignment() { + test(Alignment.CODEC, Alignment.TopLeft, true); + test(Alignment.CODEC, Alignment.TopCenter, true); + test(Alignment.CODEC, Alignment.TopRight, true); + test(Alignment.CODEC, Alignment.CenterLeft, true); + test(Alignment.CODEC, Alignment.Center, true); + test(Alignment.CODEC, Alignment.CenterRight, true); + test(Alignment.CODEC, Alignment.BottomLeft, true); + test(Alignment.CODEC, Alignment.BottomCenter, true); + test(Alignment.CODEC, Alignment.BottomRight, true); + + TestUtil.repeatRnd(10, rnd -> test(Alignment.CODEC, new Alignment(rnd.nextFloat(), rnd.nextFloat()), true)); + + testEnum(Alignment.MainAxis.CODEC, Alignment.MainAxis.class); + testEnum(Alignment.CrossAxis.CODEC, Alignment.CrossAxis.class); + } + + @Test + void color() { + TestUtil.repeatRnd(10, CodecTest::testColorHex); + TestUtil.repeatRnd(10, CodecTest::testColorRGB); + TestUtil.repeatRnd(10, CodecTest::testColorHSV); + TestUtil.repeatRnd(10, CodecTest::testColorHSL); + TestUtil.repeatRnd(10, CodecTest::testColorCMYK); + } + + private static void testColorHex(Random rnd) { + int c = rnd.nextInt(); + JsonElement json = new JsonPrimitive("#" + Color.argbToFullHexString(c)); + testColor(c, json); + } + + private static void testColorRGB(Random rnd) { + int c = rnd.nextInt(); + JsonObject json = new JsonObject(); + json.addProperty("r", Color.getRedF(c)); + json.addProperty("g", Color.getGreenF(c)); + json.addProperty("b", Color.getBlueF(c)); + json.addProperty("a", Color.getAlphaF(c)); + testColor(c, json); + } + + private static void testColorHSV(Random rnd) { + int c = rnd.nextInt(); + JsonObject json = new JsonObject(); + json.addProperty("h", Color.getHue(c)); + json.addProperty("s", Color.getHSVSaturation(c)); + json.addProperty("v", Color.getValue(c)); + json.addProperty("a", Color.getAlphaF(c)); + testColor(c, json); + } + + private static void testColorHSL(Random rnd) { + int c = rnd.nextInt(); + JsonObject json = new JsonObject(); + json.addProperty("h", Color.getHue(c)); + json.addProperty("s", Color.getHSLSaturation(c)); + json.addProperty("l", Color.getLightness(c)); + json.addProperty("a", Color.getAlphaF(c)); + testColor(c, json); + } + + private static void testColorCMYK(Random rnd) { + int c = rnd.nextInt(); + JsonObject json = new JsonObject(); + json.addProperty("c", Color.getCyan(c)); + json.addProperty("m", Color.getMagenta(c)); + json.addProperty("y", Color.getYellow(c)); + json.addProperty("k", Color.getBlack(c)); + json.addProperty("a", Color.getAlphaF(c)); + testColor(c, json); + } + + private static void testColor(int color, JsonElement json) { + int c2 = fromJson(Color.CODEC, json); + ColorTest.assertColor(color, c2); + JsonElement j2 = toJson(Color.CODEC, c2); + int c3 = fromJson(Color.CODEC, j2); + ColorTest.assertColor(c2, c3); + } + + private static > void testEnum(Codec codec, Class c) { + for (E e : c.getEnumConstants()) { + test(codec, e, true); + } + } + + private static void resizerTest(IWidget widget) { + test(StandardResizer.CODEC, widget.resizer(), () -> new Widget<>().resizer(), false); + } + + private static void drawableTest(IDrawable widget) { + test(IDrawable.CODEC, widget, false); + } + + private static void test(MutableObjectCodec codec, A obj1, Supplier supplier, boolean checkObjEquals) { + JsonElement json1 = toJson(codec, obj1); + A obj2 = fromJson(codec, json1, supplier.get()); + if (checkObjEquals) assertEquals(obj1, obj2); + JsonElement json2 = toJson(codec, obj2); + assertEquals(json1, json2); + } + + private static void test(Codec codec, A obj, boolean checkObjEquals) { + JsonElement json1 = toJson(codec, obj); + A obj2 = fromJson(codec, json1); + if (checkObjEquals) assertEquals(obj, obj2); + JsonElement json2 = toJson(codec, obj2); + assertEquals(json1, json2); + System.out.println(JsonHelper.GSON.toJson(json1)); + } + + private static void decodeTest(JsonElement json, Codec codec, A obj) { + assertEquals(obj, fromJson(codec, json)); + } + + public static T fromJson(MutableCodec codec, JsonElement json, T instance) { + var d = codec.parse(JsonOps.INSTANCE, json, instance); + var err = d.error(); + assertTrue(err.isEmpty(), () -> "Expected no error, but got: " + err.get().message()); + return instance; + } + + public static T fromJson(Codec codec, JsonElement json) { + var d = codec.parse(JsonOps.INSTANCE, json); + var err = d.error(); + assertTrue(err.isEmpty(), () -> "Expected no error, but got: " + err.get().message()); + return d.result().orElseThrow(); + } + + public static JsonElement toJson(Codec codec, T input) { + var d = codec.encodeStart(JsonOps.INSTANCE, input); + var err = d.error(); + assertTrue(err.isEmpty(), () -> "Expected no error, but got: " + err.get().message()); + return d.result().orElseThrow(); + } +} diff --git a/src/test/java/brachy/modularui/ColorTest.java b/src/test/java/brachy/modularui/ColorTest.java new file mode 100644 index 0000000..036bf62 --- /dev/null +++ b/src/test/java/brachy/modularui/ColorTest.java @@ -0,0 +1,94 @@ +package brachy.modularui; + +import brachy.modularui.utils.Color; + +import org.apache.commons.lang3.mutable.MutableInt; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; + +import static org.junit.jupiter.api.AssertionFailureBuilder.assertionFailure; +import static brachy.modularui.utils.Color.*; + +public class ColorTest { + + @Test + void rgbConversion() { + // tests conversion accuracy by repeatedly converting the same color for random colors + TestUtil.repeatRnd(10, rnd -> { + int start = Color.random(rnd, 0); + MutableInt color = new MutableInt(start); + TestUtil.repeat(10, i -> { + int c = color.intValue(); + color.setValue(argb(getRed(c), getGreen(c), getBlue(c), 0)); + assertColor(c, color.intValue(), 0, "Iteration: " + i); + }); + assertColor(start, color.intValue(), "Final assert"); + }); + } + + @Test + void hsvConversion() { + TestUtil.repeatRnd(10, rnd -> { + int start = Color.random(rnd, 0); + MutableInt color = new MutableInt(start); + TestUtil.repeat(10, i -> { + int c = color.intValue(); + color.setValue(ofHSV(getHue(c), getHSVSaturation(c), getValue(c), 0)); + assertColor(c, color.intValue(), "Iteration: " + i); + }); + assertColor(start, color.intValue(), "Final assert"); + }); + } + + @Test + void hslConversion() { + TestUtil.repeatRnd(10, rnd -> { + int start = Color.random(rnd, 0); + MutableInt color = new MutableInt(start); + TestUtil.repeat(10, i -> { + int c = color.intValue(); + color.setValue(ofHSL(getHue(c), getHSLSaturation(c), getLightness(c), 0)); + assertColor(c, color.intValue(), "Iteration: " + i); + }); + assertColor(start, color.intValue(), "Final assert"); + }); + } + + @Test + void cmykConversion() { + TestUtil.repeatRnd(10, rnd -> { + int start = Color.random(rnd, 0); + MutableInt color = new MutableInt(start); + TestUtil.repeat(10, i -> { + int c = color.intValue(); + color.setValue(ofCMYK(getCyan(c), getMagenta(c), getYellow(c), getBlack(c))); + assertColor(c, color.intValue(), "Iteration: " + i); + }); + assertColor(start, color.intValue(), "Final assert"); + }); + } + + static void assertColor(int c1, int c2) { + assertColor(c1, c2, null); + } + + static void assertColor(int c1, int c2, int tolerance) { + assertColor(c1, c2, tolerance, null); + } + + static void assertColor(int c1, int c2, String extraMsg) { + assertColor(c1, c2, 0, extraMsg); + } + + static void assertColor(int c1, int c2, int tolerance, String extraMsg) { + if (!Color.areSameColor(c1, c2, tolerance)) { + throw assertionFailure() + .expected(Arrays.toString(Color.getARGBValues(c1))) + .actual(Arrays.toString(Color.getARGBValues(c2))) + .reason("Color components differ by " + Color.getLargestDiff(c1, c2) + ", but only " + tolerance + " is allowed. ") + .message(extraMsg) + .build(); + } + } +} diff --git a/src/test/java/brachy/modularui/TestUtil.java b/src/test/java/brachy/modularui/TestUtil.java new file mode 100644 index 0000000..5987b2e --- /dev/null +++ b/src/test/java/brachy/modularui/TestUtil.java @@ -0,0 +1,19 @@ +package brachy.modularui; + +import java.util.Random; +import java.util.function.Consumer; +import java.util.function.IntConsumer; + +public class TestUtil { + + public static void repeat(int n, IntConsumer consumer) { + for (int i = 0; i < n; i++) { + consumer.accept(n); + } + } + + public static void repeatRnd(int n, Consumer consumer) { + Random rnd = new Random(); + repeat(n, i -> consumer.accept(rnd)); + } +} From 951e8b3a800514d7242e4db02d592f6b20268ffd Mon Sep 17 00:00:00 2001 From: brachy84 Date: Mon, 18 May 2026 16:16:58 +0200 Subject: [PATCH 11/26] ModularComponent codec --- .../java/brachy/modularui/ClientProxy.java | 26 ------ .../brachy/modularui/api/drawable/Text.java | 8 ++ .../core/mixins/common/TextColorMixin.java | 24 +++++ .../drawable/text/ModularComponent.java | 17 ++++ .../utils/serialization/codec/CodecUtil.java | 21 +++++ .../serialization/codec/MutableDecoder.java | 2 +- .../codec/MutableObjectCodec.java | 88 +++++++------------ src/main/resources/modularui.mixins.json | 47 +++++----- src/test/java/brachy/modularui/CodecTest.java | 13 +++ 9 files changed, 142 insertions(+), 104 deletions(-) create mode 100644 src/main/java/brachy/modularui/core/mixins/common/TextColorMixin.java diff --git a/src/main/java/brachy/modularui/ClientProxy.java b/src/main/java/brachy/modularui/ClientProxy.java index 1c2a387..2be93d7 100644 --- a/src/main/java/brachy/modularui/ClientProxy.java +++ b/src/main/java/brachy/modularui/ClientProxy.java @@ -16,12 +16,6 @@ import brachy.modularui.network.ModularNetwork; import brachy.modularui.theme.ThemeManager; -import brachy.modularui.utils.serialization.json.JsonHelper; -import brachy.modularui.widget.Widget; - -import brachy.modularui.widget.sizer.StandardResizer; - -import com.mojang.serialization.JsonOps; import net.minecraft.client.Minecraft; import net.minecraft.client.Timer; import net.minecraft.client.gui.screens.inventory.tooltip.ClientTooltipComponent; @@ -36,7 +30,6 @@ import lombok.Getter; -import java.util.Objects; import java.util.function.Function; public class ClientProxy extends CommonProxy { @@ -65,25 +58,6 @@ protected void onInit(FMLCommonSetupEvent event) { // enable stencil bits, must call on render thread RenderSystem.recordRenderCall(() -> Minecraft.getInstance().getMainRenderTarget().enableStencil()); } - - // CODEC tests - // TODO remove when done - Widget w = new Widget<>(); - w.left(5); - w.bottomRel(0.75f, -67, 0.42f); - w.size(20); - w.decoration(); - StandardResizer r1 = w.resizer(); - var s1 = JsonHelper.toJsonString(StandardResizer.CODEC, r1); - ModularUI.LOGGER.info("Resizer Json of {}:\n{}", r1, s1); - - Widget w2 = new Widget<>(); - StandardResizer r2 = JsonHelper.fromJsonString(StandardResizer.CODEC, s1, w2.resizer()); - - var s2 = JsonHelper.toJsonString(StandardResizer.CODEC, r2); - boolean eq = Objects.equals(s1, s2); - ModularUI.LOGGER.info("Resizer Json of {}:\n{}", r2, s2); - ModularUI.LOGGER.info("Equals: {}", eq); } private void onRegisterClientTooltipComponents(RegisterClientTooltipComponentFactoriesEvent event) { diff --git a/src/main/java/brachy/modularui/api/drawable/Text.java b/src/main/java/brachy/modularui/api/drawable/Text.java index 947745f..5b784e8 100644 --- a/src/main/java/brachy/modularui/api/drawable/Text.java +++ b/src/main/java/brachy/modularui/api/drawable/Text.java @@ -9,6 +9,7 @@ import brachy.modularui.theme.WidgetTheme; import brachy.modularui.utils.Alignment; +import com.mojang.serialization.Codec; import net.minecraft.ChatFormatting; import net.minecraft.network.chat.Component; import net.minecraft.network.chat.MutableComponent; @@ -27,6 +28,8 @@ */ public interface Text extends IDrawable, IJsonSerializable { + Codec CODEC = ModularComponent.CODEC; + int TEXT_COLOR = 0xFF404040; TextRenderer renderer = new TextRenderer(); @@ -234,6 +237,11 @@ default KeyIcon asTextIcon() { return new KeyIcon(this); } + @Override + default String getTypeName() { + return "text"; + } + @Override default void loadFromJson(JsonObject json) { if (json.has("color") || json.has("shadow") || json.has("align") || json.has("alignment") || diff --git a/src/main/java/brachy/modularui/core/mixins/common/TextColorMixin.java b/src/main/java/brachy/modularui/core/mixins/common/TextColorMixin.java new file mode 100644 index 0000000..4748356 --- /dev/null +++ b/src/main/java/brachy/modularui/core/mixins/common/TextColorMixin.java @@ -0,0 +1,24 @@ +package brachy.modularui.core.mixins.common; + +import net.minecraft.network.chat.TextColor; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(TextColor.class) +public class TextColorMixin { + + /** + * @reason Minecraft uses Integer.parseInt which fails when alpha is specified because of signed vs. unsigned + */ + @Inject(method = "parseColor", at = @At(value = "INVOKE", target = "Ljava/lang/Integer;parseInt(Ljava/lang/String;I)I"), cancellable = true) + private static void fixDecode(String hexString, CallbackInfoReturnable cir) { + try { + cir.setReturnValue(TextColor.fromRgb((int) (long) Long.decode(hexString))); + } catch (NumberFormatException e) { + cir.setReturnValue(null); + } + } +} diff --git a/src/main/java/brachy/modularui/drawable/text/ModularComponent.java b/src/main/java/brachy/modularui/drawable/text/ModularComponent.java index a7ee775..67b784b 100644 --- a/src/main/java/brachy/modularui/drawable/text/ModularComponent.java +++ b/src/main/java/brachy/modularui/drawable/text/ModularComponent.java @@ -4,6 +4,7 @@ import brachy.modularui.screen.viewport.GuiContext; import brachy.modularui.theme.WidgetTheme; import brachy.modularui.utils.Alignment; +import brachy.modularui.utils.serialization.codec.MutableObjectCodec; import brachy.modularui.utils.serialization.json.JsonHelper; import brachy.modularui.widgets.TextWidget; @@ -20,6 +21,8 @@ import net.minecraft.network.chat.contents.ScoreContents; import net.minecraft.network.chat.contents.SelectorContents; import net.minecraft.network.chat.contents.TranslatableContents; +import net.minecraft.util.ExtraCodecs; +import com.mojang.serialization.Codec; import com.google.gson.JsonObject; import lombok.Getter; @@ -34,6 +37,15 @@ public class ModularComponent extends MutableComponent implements Text { + // TODO: This currently doesn't not handle nested components correctly. I might have to write a complete custom codec for this. + public static final MutableObjectCodec CODEC = MutableObjectCodec.drawableBuilder(ModularComponent.class, "Text") + .wrapped(ExtraCodecs.COMPONENT.xmap(ModularComponent::of, mc -> mc)) + .addOpt("alignment", ModularComponent::alignment, ModularComponent::getAlignment, Alignment.CODEC, Alignment.Center) + .addOpt("scale", ModularComponent::scale, ModularComponent::getScale, Codec.FLOAT, 1f) + .addOpt("shadow", ModularComponent::shadow, ModularComponent::getShadow, Codec.BOOL, null) + .addUnencodable("dynamicColor", ModularComponent::color, ModularComponent::getDynamicColor) + .build(); + public static ModularComponent literal(String text) { return ModularComponent.create(new LiteralContents(text)); } @@ -79,6 +91,7 @@ public static ModularComponent create(@NotNull ComponentContents contents) { } public static ModularComponent of(Component component) { + if (component instanceof ModularComponent mc) return mc; return new ModularComponent(component.getContents(), component.getSiblings(), component.getStyle()); } @@ -163,6 +176,10 @@ public ModularComponent scale(float scale) { return this; } + public ModularComponent color(Integer color) { + return color != null ? color((int) color) : color((IntSupplier) null); + } + @Override public ModularComponent color(@Nullable IntSupplier color) { this.dynamicColor = color; diff --git a/src/main/java/brachy/modularui/utils/serialization/codec/CodecUtil.java b/src/main/java/brachy/modularui/utils/serialization/codec/CodecUtil.java index 9f3091a..6df976e 100644 --- a/src/main/java/brachy/modularui/utils/serialization/codec/CodecUtil.java +++ b/src/main/java/brachy/modularui/utils/serialization/codec/CodecUtil.java @@ -7,8 +7,12 @@ import com.mojang.serialization.DynamicOps; import com.mojang.serialization.Encoder; +import org.jetbrains.annotations.Nullable; + +import java.util.Objects; import java.util.function.Function; import java.util.function.Predicate; +import java.util.stream.Stream; public class CodecUtil { @@ -108,4 +112,21 @@ public String toString() { } }; } + + public static @Nullable Stream.Builder> mergePrefixToMapBuilder(DynamicOps ops, @Nullable T prefix) { + return mergePrefixToMapBuilder(ops, prefix, null); + } + + public static @Nullable Stream.Builder> mergePrefixToMapBuilder(DynamicOps ops, @Nullable T prefix, @Nullable Stream.Builder> mapValues) { + if (mapValues == null) mapValues = Stream.builder(); + if (prefix != null && !Objects.equals(prefix, ops.empty())) { + // add values of prefix map + // this is more performant than mergeToMap of DynamicOps + var prefixMap = ops.getMapValues(prefix); + var res = prefixMap.result(); + if (res.isEmpty()) return null; // prefix is not empty and is not a map + res.get().forEach(mapValues); + } + return mapValues; + } } diff --git a/src/main/java/brachy/modularui/utils/serialization/codec/MutableDecoder.java b/src/main/java/brachy/modularui/utils/serialization/codec/MutableDecoder.java index 6d251f8..7afde0a 100644 --- a/src/main/java/brachy/modularui/utils/serialization/codec/MutableDecoder.java +++ b/src/main/java/brachy/modularui/utils/serialization/codec/MutableDecoder.java @@ -20,7 +20,7 @@ default DataResult> decode(final DynamicOps ops, final T input var d = decodeInstance(ops, input); var result = d.result(); if (result.isEmpty()) return d; - return decode(ops, result.get().getSecond(), result.get().getFirst()); + return decode(ops, input, result.get().getFirst()); } default DataResult parse(final DynamicOps ops, final T input, A instance) { diff --git a/src/main/java/brachy/modularui/utils/serialization/codec/MutableObjectCodec.java b/src/main/java/brachy/modularui/utils/serialization/codec/MutableObjectCodec.java index 43147d6..078a50e 100644 --- a/src/main/java/brachy/modularui/utils/serialization/codec/MutableObjectCodec.java +++ b/src/main/java/brachy/modularui/utils/serialization/codec/MutableObjectCodec.java @@ -33,12 +33,14 @@ public class MutableObjectCodec implements MutableCodec { private final Object2ReferenceLinkedOpenHashMap> fields; private final InstanceDecoder instanceDecoder; private final UnaryOperator baseCopy; + private final Codec wrapped; private MutableObjectCodec(Object2ReferenceLinkedOpenHashMap> fields, - InstanceDecoder instanceDecoder, UnaryOperator baseCopy) { + InstanceDecoder instanceDecoder, UnaryOperator baseCopy, Codec wrapped) { this.fields = fields; this.instanceDecoder = instanceDecoder; this.baseCopy = baseCopy; + this.wrapped = wrapped; } public void forEachField(Consumer> consumer) { @@ -70,15 +72,14 @@ public void applyDefaults(T instance) { @Override public DataResult> decodeInstance(DynamicOps ops, J input) { - if (MutableObjectCodec.this.instanceDecoder == null) { - return DataResult.error(() -> "Instance can not be created since no instance decoder was provided."); - } - return MutableObjectCodec.this.instanceDecoder.decodeInstance(ops, input); + if (this.wrapped != null) return this.wrapped.decode(ops, input); + if (this.instanceDecoder != null) return this.instanceDecoder.decodeInstance(ops, input); + return DataResult.error(() -> "Instance can not be created since no instance decoder or wrapped codec was provided."); } @Override public boolean canDecodeInstance() { - return this.instanceDecoder != null; + return this.instanceDecoder != null || this.wrapped != null; } @Override @@ -106,17 +107,24 @@ public DataResult encode(T input, DynamicOps ops, J prefix) { if (input == null) { return DataResult.success(ops.empty()); } - var builder = Stream.>builder(); + if (this.wrapped != null) { + var d = this.wrapped.encode(input, ops, prefix); + var res = d.result(); + if (res.isEmpty()) return d; + prefix = res.get(); + } + var mapValues = CodecUtil.mergePrefixToMapBuilder(ops, prefix); + if (mapValues == null) return DataResult.error(() -> "Prefix is not empty and is not a map"); List errors = new ArrayList<>(); forEachField(f -> { - var error = f.encode(input, ops, builder); + var error = f.encode(input, ops, mapValues); if (error != null) errors.add(error); }); if (!errors.isEmpty()) { return DataResult.error(() -> String.format("Errors while encoding object of type '%s': %s", input.getClass().getSimpleName(), errors)); } - return DataResult.success(ops.createMap(builder.build())); + return DataResult.success(ops.createMap(mapValues.build())); } public static Builder builder() { @@ -244,7 +252,7 @@ public boolean isUnencodable() { V value; if (element == null) { if (!hasDefault()) { - return String.format("Field '%s' has no value and is not optional", this.name); + return isUnencodable() ? null : String.format("Field '%s' has no value and is not optional", this.name); } value = getDefault(); } else if (isUnencodable()) { @@ -303,14 +311,12 @@ public static class Builder { private UnaryOperator baseCopy; private CodecRegistry registry; private String[] names; + private Codec wrapped; /** * Sets the instance decoder. This is needed when parsing from JSON. The decoder should create a new instance * and decode any necessary data for that. If the object has a no-arg constructor, then {@link #instance(Supplier)} should be used. * If this setter is used, then {@link #baseCopy(UnaryOperator)} must also be used. - * - * @param instanceDecoder instance decoder - * @return this */ public Builder instanceDecoder(InstanceDecoder instanceDecoder) { this.instanceDecoder = instanceDecoder; @@ -319,9 +325,6 @@ public Builder instanceDecoder(InstanceDecoder instanceDecoder) { /** * Sets the instance supplier. This is needed when parsing from JSON and for copying. This MUST always return a new instance. - * - * @param instance instance supplier - * @return this */ public Builder instance(Supplier instance) { return instanceDecoder(new InstanceDecoder<>() { @@ -335,9 +338,6 @@ public DataResult> decodeInstance(DynamicOps ops, J input) { /** * Sets the base copy function. This creates a new instance, but without setting any mutable properties. This MUST always return * a new instance. If the object has a no-arg constructor, then {@link #instance(Supplier)} should be used. - * - * @param baseCopy base copy function - * @return this */ public Builder baseCopy(UnaryOperator baseCopy) { this.baseCopy = t -> { @@ -348,6 +348,15 @@ public Builder baseCopy(UnaryOperator baseCopy) { return this; } + /** + * When this object is encoded and decoded this codec will be called first. This is useful when this object is based on another + * object which already has a codec. Example: {@link brachy.modularui.drawable.text.ModularComponent#CODEC ModularComponent.CODEC} + */ + public Builder wrapped(Codec codec) { + this.wrapped = codec; + return this; + } + public Builder add(String name, FieldWriter fieldWriter, FieldReader fieldReader, Codec codec) { return addDynOpt(name, fieldWriter, fieldReader, codec, null); } @@ -435,11 +444,7 @@ public Builder addDynOpt(String name, FieldWriter fieldWriter, Fiel } public Builder addUnencodable(String name, FieldWriter fieldWriter, FieldReader fieldReader) { - return addUnencodableDynOpt(name, fieldWriter, fieldReader, null, null, (String[]) null); - } - - public Builder addUnencodable(String name, FieldWriter fieldWriter, FieldReader fieldReader, String @Nullable ... altNames) { - return addUnencodableDynOpt(name, fieldWriter, fieldReader, null, null, altNames); + return addUnencodableDynOpt(name, fieldWriter, fieldReader, null, null); } public Builder addUnencodable(String name, FieldWriter fieldWriter, FieldReader fieldReader, @@ -447,19 +452,9 @@ public Builder addUnencodable(String name, FieldWriter fieldWriter, return addUnencodableDynOpt(name, fieldWriter, fieldReader, null, emptyTest); } - public Builder addUnencodable(String name, FieldWriter fieldWriter, FieldReader fieldReader, - @Nullable Predicate emptyTest, String @Nullable ... altNames) { - return addUnencodableDynOpt(name, fieldWriter, fieldReader, null, emptyTest, altNames); - } - public Builder addUnencodableOpt(String name, FieldWriter fieldWriter, FieldReader fieldReader, @Nullable V defaultValue) { - return addUnencodableDynOpt(name, fieldWriter, fieldReader, () -> defaultValue, null, (String[]) null); - } - - public Builder addUnencodableOpt(String name, FieldWriter fieldWriter, FieldReader fieldReader, - @Nullable V defaultValue, String @Nullable ... altNames) { - return addUnencodableDynOpt(name, fieldWriter, fieldReader, () -> defaultValue, null, altNames); + return addUnencodableDynOpt(name, fieldWriter, fieldReader, () -> defaultValue, null); } public Builder addUnencodableOpt(String name, FieldWriter fieldWriter, FieldReader fieldReader, @@ -467,24 +462,9 @@ public Builder addUnencodableOpt(String name, FieldWriter fieldWrit return addUnencodableDynOpt(name, fieldWriter, fieldReader, () -> defaultValue, emptyTest); } - public Builder addUnencodableOpt(String name, FieldWriter fieldWriter, FieldReader fieldReader, - @Nullable V defaultValue, @Nullable Predicate emptyTest, String @Nullable ... altNames) { - return addUnencodableDynOpt(name, fieldWriter, fieldReader, () -> defaultValue, emptyTest, altNames); - } - public Builder addUnencodableDynOpt(String name, FieldWriter fieldWriter, FieldReader fieldReader, @Nullable Supplier defaultSupplier) { - return addUnencodableDynOpt(name, fieldWriter, fieldReader, defaultSupplier, null, (String[]) null); - } - - public Builder addUnencodableDynOpt(String name, FieldWriter fieldWriter, FieldReader fieldReader, - @Nullable Supplier defaultSupplier, String @Nullable ... altNames) { - return addUnencodableDynOpt(name, fieldWriter, fieldReader, defaultSupplier, null, altNames); - } - - public Builder addUnencodableDynOpt(String name, FieldWriter fieldWriter, FieldReader fieldReader, - @Nullable Supplier defaultSupplier, @Nullable Predicate emptyTest) { - return addDynOpt(name, fieldWriter, fieldReader, null, defaultSupplier, emptyTest); + return addUnencodableDynOpt(name, fieldWriter, fieldReader, defaultSupplier, null); } /** @@ -495,8 +475,8 @@ public Builder addUnencodableDynOpt(String name, FieldWriter fieldW * @see #addDynOpt(String, FieldWriter, FieldReader, Codec, Supplier, Predicate) */ public Builder addUnencodableDynOpt(String name, FieldWriter fieldWriter, FieldReader fieldReader, - @Nullable Supplier defaultSupplier, @Nullable Predicate emptyTest, String @Nullable ... altNames) { - return addDynOpt(name, fieldWriter, fieldReader, null, defaultSupplier, emptyTest, altNames); + @Nullable Supplier defaultSupplier, @Nullable Predicate emptyTest) { + return addDynOpt(name, fieldWriter, fieldReader, null, defaultSupplier, emptyTest); } public Builder registry(CodecRegistry registry, String... names) { @@ -514,7 +494,7 @@ public Builder registryTypeName(CodecRegistry registry, Class typeName) } public MutableObjectCodec build() { - var c = new MutableObjectCodec<>(this.fields, this.instanceDecoder, this.baseCopy); + var c = new MutableObjectCodec<>(this.fields, this.instanceDecoder, this.baseCopy, this.wrapped); if (this.registry != null) { this.registry.register(c, this.names); } diff --git a/src/main/resources/modularui.mixins.json b/src/main/resources/modularui.mixins.json index 460bec4..3fd2e7e 100755 --- a/src/main/resources/modularui.mixins.json +++ b/src/main/resources/modularui.mixins.json @@ -1,25 +1,26 @@ { - "required": true, - "minVersion": "0.8", - "refmap": "modularui.refmap.json", - "package": "brachy.modularui.core.mixins", - "compatibilityLevel": "JAVA_17", - "client": [ - "client.AbstractContainerMenuAccessor", - "client.AbstractContainerScreenAccessor", - "client.AbstractContainerScreenMixin", - "client.MinecraftMixin", - "client.ScreenAccessor", - "client.SlotAccessor", - "client.StringSplitterAccessor", - "jei.IngredientListOverlayAccessor" - ], - "mixins": [ - "common.CombinedInvWrapperAccessor", - "common.ComponentMixin" - ], - "injectors": { - "defaultRequire": 1, - "maxShiftBy": 5 - } + "required": true, + "minVersion": "0.8", + "refmap": "modularui.refmap.json", + "package": "brachy.modularui.core.mixins", + "compatibilityLevel": "JAVA_17", + "client": [ + "client.AbstractContainerMenuAccessor", + "client.AbstractContainerScreenAccessor", + "client.AbstractContainerScreenMixin", + "client.MinecraftMixin", + "client.ScreenAccessor", + "client.SlotAccessor", + "client.StringSplitterAccessor", + "jei.IngredientListOverlayAccessor" + ], + "mixins": [ + "common.CombinedInvWrapperAccessor", + "common.ComponentMixin", + "common.TextColorMixin" + ], + "injectors": { + "defaultRequire": 1, + "maxShiftBy": 5 + } } diff --git a/src/test/java/brachy/modularui/CodecTest.java b/src/test/java/brachy/modularui/CodecTest.java index 327d496..25a2cf4 100644 --- a/src/test/java/brachy/modularui/CodecTest.java +++ b/src/test/java/brachy/modularui/CodecTest.java @@ -1,10 +1,12 @@ package brachy.modularui; import brachy.modularui.api.drawable.IDrawable; +import brachy.modularui.api.drawable.Text; import brachy.modularui.api.widget.IWidget; import brachy.modularui.drawable.Circle; import brachy.modularui.drawable.GuiTextures; import brachy.modularui.drawable.Rectangle; +import brachy.modularui.drawable.text.ModularComponent; import brachy.modularui.utils.Alignment; import brachy.modularui.utils.Color; import brachy.modularui.utils.serialization.codec.MutableCodec; @@ -50,6 +52,17 @@ void drawable() { drawableTest(IDrawable.of(new Rectangle().color(Color.GREEN.main), GuiTextures.BOOKMARK, new Circle().color(Color.RED.main, Color.BLUE.main))); } + @Test + void text() { + // NOTE: integer colors do not work properly when they have an alpha value due to a Minecraft bug. + // I fixed this in TextColorMixin, but mixins are not applied in testing. + test(ModularComponent.CODEC, Text.str("Hello"), true); + test(ModularComponent.CODEC, Text.str("World").style(Text.UNDERLINE).color(Color.withAlpha(Color.GREEN.main, 0)), true); + test(ModularComponent.CODEC, Text.comp( + Text.str("Hello ").color(Color.withAlpha(Color.BLUE.main, 0)), + Text.lang("World").scale(1.5f)).alignment(Alignment.BottomCenter), true); + } + @Test void alignment() { test(Alignment.CODEC, Alignment.TopLeft, true); From fc47917419da08599efdeb41e379f8111cf74ea6 Mon Sep 17 00:00:00 2001 From: brachy84 Date: Mon, 18 May 2026 16:40:03 +0200 Subject: [PATCH 12/26] Redirect instead of Inject --- .../core/mixins/common/TextColorMixin.java | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/src/main/java/brachy/modularui/core/mixins/common/TextColorMixin.java b/src/main/java/brachy/modularui/core/mixins/common/TextColorMixin.java index 4748356..81ae8ea 100644 --- a/src/main/java/brachy/modularui/core/mixins/common/TextColorMixin.java +++ b/src/main/java/brachy/modularui/core/mixins/common/TextColorMixin.java @@ -4,8 +4,7 @@ import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; +import org.spongepowered.asm.mixin.injection.Redirect; @Mixin(TextColor.class) public class TextColorMixin { @@ -13,12 +12,8 @@ public class TextColorMixin { /** * @reason Minecraft uses Integer.parseInt which fails when alpha is specified because of signed vs. unsigned */ - @Inject(method = "parseColor", at = @At(value = "INVOKE", target = "Ljava/lang/Integer;parseInt(Ljava/lang/String;I)I"), cancellable = true) - private static void fixDecode(String hexString, CallbackInfoReturnable cir) { - try { - cir.setReturnValue(TextColor.fromRgb((int) (long) Long.decode(hexString))); - } catch (NumberFormatException e) { - cir.setReturnValue(null); - } + @Redirect(method = "parseColor", at = @At(value = "INVOKE", target = "Ljava/lang/Integer;parseInt(Ljava/lang/String;I)I")) + private static int fixDecode(String s, int radix) { + return (int) Long.parseLong(s, 16); } } From 526ee035374456996a7dbe160343ed143fd3cb71 Mon Sep 17 00:00:00 2001 From: brachy84 Date: Mon, 18 May 2026 16:40:25 +0200 Subject: [PATCH 13/26] rip radix --- .../brachy/modularui/core/mixins/common/TextColorMixin.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/brachy/modularui/core/mixins/common/TextColorMixin.java b/src/main/java/brachy/modularui/core/mixins/common/TextColorMixin.java index 81ae8ea..bf5109b 100644 --- a/src/main/java/brachy/modularui/core/mixins/common/TextColorMixin.java +++ b/src/main/java/brachy/modularui/core/mixins/common/TextColorMixin.java @@ -14,6 +14,6 @@ public class TextColorMixin { */ @Redirect(method = "parseColor", at = @At(value = "INVOKE", target = "Ljava/lang/Integer;parseInt(Ljava/lang/String;I)I")) private static int fixDecode(String s, int radix) { - return (int) Long.parseLong(s, 16); + return (int) Long.parseLong(s, radix); } } From e4527addffae9f59d6ccd77aac044fcf8dbadbe3 Mon Sep 17 00:00:00 2001 From: brachy84 Date: Mon, 18 May 2026 18:08:28 +0200 Subject: [PATCH 14/26] someone is not going to like this --- .../common/ComponentSerializerMixin.java | 55 +++++++++++++++++++ .../codec/MutableObjectCodec.java | 20 +++++++ src/main/resources/modularui.mixins.json | 47 ++++++++-------- 3 files changed, 99 insertions(+), 23 deletions(-) create mode 100644 src/main/java/brachy/modularui/core/mixins/common/ComponentSerializerMixin.java diff --git a/src/main/java/brachy/modularui/core/mixins/common/ComponentSerializerMixin.java b/src/main/java/brachy/modularui/core/mixins/common/ComponentSerializerMixin.java new file mode 100644 index 0000000..8ee1d52 --- /dev/null +++ b/src/main/java/brachy/modularui/core/mixins/common/ComponentSerializerMixin.java @@ -0,0 +1,55 @@ +package brachy.modularui.core.mixins.common; + +import brachy.modularui.ModularUI; +import brachy.modularui.drawable.text.ModularComponent; + +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.google.gson.JsonSerializationContext; +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; + +import net.minecraft.network.chat.Component; + +import net.minecraft.network.chat.MutableComponent; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +import java.lang.reflect.Type; + +/** + * We need this since nested components are potentially modular too + */ +@Mixin(Component.Serializer.class) +public class ComponentSerializerMixin { + + @WrapOperation( + method = "serialize(Lnet/minecraft/network/chat/Component;Ljava/lang/reflect/Type;Lcom/google/gson/JsonSerializationContext;)Lcom/google/gson/JsonElement;", + at = @At(value = "INVOKE", + target = "Lnet/minecraft/network/chat/Component$Serializer;serialize(Lnet/minecraft/network/chat/Component;Ljava/lang/reflect/Type;Lcom/google/gson/JsonSerializationContext;)Lcom/google/gson/JsonElement;")) + public JsonElement serialize(Component.Serializer instance, Component src, Type typeOfSrc, JsonSerializationContext context, Operation original) { + if (src instanceof ModularComponent mc) { + var d = ModularComponent.CODEC.encodeJson(mc); + var res = d.result(); + if (res.isPresent()) return res.get(); + ModularUI.LOGGER.error("Error encoding nested ModularComponent: {}", d.error().orElseThrow().message()); + } + return original.call(instance, src, typeOfSrc, context); + } + + @WrapOperation(method = "deserialize(Lcom/google/gson/JsonElement;Ljava/lang/reflect/Type;Lcom/google/gson/JsonDeserializationContext;)Lnet/minecraft/network/chat/MutableComponent;", at = @At(value = "INVOKE", target = "Lnet/minecraft/network/chat/Component$Serializer;deserialize(Lcom/google/gson/JsonElement;Ljava/lang/reflect/Type;Lcom/google/gson/JsonDeserializationContext;)Lnet/minecraft/network/chat/MutableComponent;")) + public MutableComponent deserialize(Component.Serializer instance, JsonElement json, Type typeOfT, JsonDeserializationContext context, Operation original) { + MutableComponent comp = original.call(instance, json, typeOfT, context); + if (comp.getClass() == MutableComponent.class && json instanceof JsonObject jsonObj && ModularComponent.CODEC.hasAnyField(jsonObj)) { + ModularComponent mc = comp.asModular(); + var d = ModularComponent.CODEC.parseJson(jsonObj, mc); + var res = d.result(); + if (res.isEmpty()) throw new JsonParseException(d.error().orElseThrow().message()); + return res.get(); + } + return comp; + } +} diff --git a/src/main/java/brachy/modularui/utils/serialization/codec/MutableObjectCodec.java b/src/main/java/brachy/modularui/utils/serialization/codec/MutableObjectCodec.java index 078a50e..c42978b 100644 --- a/src/main/java/brachy/modularui/utils/serialization/codec/MutableObjectCodec.java +++ b/src/main/java/brachy/modularui/utils/serialization/codec/MutableObjectCodec.java @@ -10,6 +10,7 @@ import com.mojang.serialization.MapLike; import com.google.common.base.CaseFormat; +import com.google.gson.JsonObject; import it.unimi.dsi.fastutil.objects.Object2ReferenceLinkedOpenHashMap; import org.jetbrains.annotations.Nullable; @@ -47,6 +48,25 @@ public void forEachField(Consumer> consumer) { this.fields.values().forEach(consumer); } + public boolean testEachField(Predicate> test) { + for (Field f : this.fields.values()) { + if (!test.test(f)) return false; + } + return true; + } + + public boolean hasAnyField(JsonObject json) { + return !testEachField(f -> { + if (json.has(f.name)) return false; + if (f.altNames != null) { + for (String alt : f.altNames) { + if (json.has(alt)) return false; + } + } + return true; + }); + } + @SuppressWarnings("unchecked") private Field getField(String name) { return (Field) this.fields.get(name); diff --git a/src/main/resources/modularui.mixins.json b/src/main/resources/modularui.mixins.json index 3fd2e7e..d409616 100755 --- a/src/main/resources/modularui.mixins.json +++ b/src/main/resources/modularui.mixins.json @@ -1,26 +1,27 @@ { - "required": true, - "minVersion": "0.8", - "refmap": "modularui.refmap.json", - "package": "brachy.modularui.core.mixins", - "compatibilityLevel": "JAVA_17", - "client": [ - "client.AbstractContainerMenuAccessor", - "client.AbstractContainerScreenAccessor", - "client.AbstractContainerScreenMixin", - "client.MinecraftMixin", - "client.ScreenAccessor", - "client.SlotAccessor", - "client.StringSplitterAccessor", - "jei.IngredientListOverlayAccessor" - ], - "mixins": [ - "common.CombinedInvWrapperAccessor", - "common.ComponentMixin", - "common.TextColorMixin" - ], - "injectors": { - "defaultRequire": 1, - "maxShiftBy": 5 + "required": true, + "minVersion": "0.8", + "refmap": "modularui.refmap.json", + "package": "brachy.modularui.core.mixins", + "compatibilityLevel": "JAVA_17", + "client": [ + "client.AbstractContainerMenuAccessor", + "client.AbstractContainerScreenAccessor", + "client.AbstractContainerScreenMixin", + "client.MinecraftMixin", + "client.ScreenAccessor", + "client.SlotAccessor", + "client.StringSplitterAccessor", + "jei.IngredientListOverlayAccessor" + ], + "mixins": [ + "common.CombinedInvWrapperAccessor", + "common.ComponentMixin", + "common.ComponentSerializerMixin", + "common.TextColorMixin" + ], + "injectors": { + "defaultRequire": 1, + "maxShiftBy": 5 } } From 9858c295a6b3ca7cc4fb35ccead3325f575ef2bd Mon Sep 17 00:00:00 2001 From: brachy84 Date: Mon, 18 May 2026 20:14:45 +0200 Subject: [PATCH 15/26] dont encode unset fields & restructure a little bit --- .../brachy/modularui/drawable/Circle.java | 4 +- .../brachy/modularui/drawable/Rectangle.java | 12 +- .../brachy/modularui/drawable/UITexture.java | 45 ++-- .../utils/serialization/codec/Field.java | 135 +++++++++++ .../codec/MutableObjectCodec.java | 223 ++++-------------- src/test/java/brachy/modularui/CodecTest.java | 6 +- src/test/java/brachy/modularui/TestUtil.java | 9 + 7 files changed, 223 insertions(+), 211 deletions(-) create mode 100644 src/main/java/brachy/modularui/utils/serialization/codec/Field.java diff --git a/src/main/java/brachy/modularui/drawable/Circle.java b/src/main/java/brachy/modularui/drawable/Circle.java index a4ae06f..369f7f6 100644 --- a/src/main/java/brachy/modularui/drawable/Circle.java +++ b/src/main/java/brachy/modularui/drawable/Circle.java @@ -23,8 +23,8 @@ public class Circle implements IDrawable, IJsonSerializable, IAnimatable { public static final MutableObjectCodec CODEC = MutableObjectCodec.drawableBuilder(Circle::new) - .addOpt("colorInner", Circle::colorInner, Circle::colorInner, Codec.INT, 0, "color") - .addOpt("colorOuter", Circle::colorOuter, Circle::colorOuter, Codec.INT, 0, "color") + .addOpt("colorInner", Circle::colorInner, Circle::colorInner, Codec.INT, 0).alias("color") + .addOpt("colorOuter", Circle::colorOuter, Circle::colorOuter, Codec.INT, 0).alias("color") .addOpt("segments", Circle::segments, Circle::segments, Codec.INT, 40) .build(); diff --git a/src/main/java/brachy/modularui/drawable/Rectangle.java b/src/main/java/brachy/modularui/drawable/Rectangle.java index 0f12998..d96f2bb 100644 --- a/src/main/java/brachy/modularui/drawable/Rectangle.java +++ b/src/main/java/brachy/modularui/drawable/Rectangle.java @@ -30,10 +30,14 @@ public class Rectangle implements IDrawable, IJsonSerializable, IAnimatable { public static final MutableObjectCodec CODEC = MutableObjectCodec.drawableBuilder(Rectangle::new) - .addOpt("colorTopLeft", Rectangle::colorTL, Rectangle::colorTL, Color.CODEC, Color.WHITE.main, "color", "colorTop", "colorLeft", "colorTL") - .addOpt("colorTopRight", Rectangle::colorTR, Rectangle::colorTR, Color.CODEC, Color.WHITE.main, "color", "colorTop", "colorRight", "colorTR") - .addOpt("colorBottomLeft", Rectangle::colorBL, Rectangle::colorBL, Color.CODEC, Color.WHITE.main, "color", "colorBottom", "colorLeft", "colorBL") - .addOpt("colorBottomRight", Rectangle::colorBR, Rectangle::colorBR, Color.CODEC, Color.WHITE.main, "color", "colorBottom", "colorRight", "colorBR") + .addOpt("colorTopLeft", Rectangle::colorTL, Rectangle::colorTL, Color.CODEC, Color.WHITE.main) + .alias("colorTL", "colorLeft", "colorTop", "color") + .addOpt("colorTopRight", Rectangle::colorTR, Rectangle::colorTR, Color.CODEC, Color.WHITE.main) + .alias("colorTR", "colorRight", "colorTop", "color") + .addOpt("colorBottomLeft", Rectangle::colorBL, Rectangle::colorBL, Color.CODEC, Color.WHITE.main) + .alias("colorBL", "colorLeft", "colorBottom", "color") + .addOpt("colorBottomRight", Rectangle::colorBR, Rectangle::colorBR, Color.CODEC, Color.WHITE.main) + .alias("colorBR", "colorRight", "colorBottom", "color") .addOpt("cornerRadius", Rectangle::cornerRadius, Rectangle::cornerRadius, Codec.INT, 0) .addOpt("cornerSegments", Rectangle::cornerSegments, Rectangle::cornerSegments, Codec.INT, 8) .addOpt("borderThickness", Rectangle::borderThickness, Rectangle::borderThickness, Codec.FLOAT, 0f) diff --git a/src/main/java/brachy/modularui/drawable/UITexture.java b/src/main/java/brachy/modularui/drawable/UITexture.java index abd6ab4..b1f3f6a 100644 --- a/src/main/java/brachy/modularui/drawable/UITexture.java +++ b/src/main/java/brachy/modularui/drawable/UITexture.java @@ -228,8 +228,8 @@ public static UITexture parseFromJson(JsonObject json) { } Builder builder = builder(); builder.location(JsonHelper.getString(json, ModularUI.MOD_ID + ":gui/widgets/error", "location")) - .imageSize(JsonHelper.getInt(json, defaultImageWidth, "imageWidth", "iw"), - JsonHelper.getInt(json, defaultImageHeight, "imageHeight", "ih")); + .imageSize(JsonHelper.getInt(json, -1, "imageWidth", "iw"), + JsonHelper.getInt(json, -1, "imageHeight", "ih")); boolean mode1 = json.has("x") || json.has("y") || json.has("w") || json.has("h") || json.has("width") || json.has("height"); boolean mode2 = json.has("u0") || json.has("v0") || json.has("u1") || json.has("u1"); @@ -332,13 +332,6 @@ public UITexture withColorOverride(int color) { return t; } - private static int defaultImageWidth = 16, defaultImageHeight = 16; - - public static void setDefaultImageSize(int w, int h) { - defaultImageWidth = w; - defaultImageHeight = h; - } - /** * A builder class to help create image textures. */ @@ -347,30 +340,31 @@ public static class Builder { public static final MutableObjectCodec CODEC = MutableObjectCodec.builder(UITexture::builder) .add("location", Builder::location, Builder::getLocation, ResourceLocation.CODEC) - .addDynOpt("imageWidth", Builder::setIw, Builder::getIw, Codec.INT, () -> UITexture.defaultImageWidth, "iw") - .addDynOpt("imageHeight", Builder::setIh, Builder::getIh, Codec.INT, () -> UITexture.defaultImageHeight, "ih") + .addOpt("imageWidth", Builder::setIw, Builder::getIw, Codec.INT, -1).alias("iw") + .addOpt("imageHeight", Builder::setIh, Builder::getIh, Codec.INT, -1).alias("ih") .addOpt("x", Builder::setX, Builder::getX, Codec.INT, 0) .addOpt("y", Builder::setY, Builder::getY, Codec.INT, 0) .addOpt("w", Builder::setW, Builder::getW, Codec.INT, 0) .addOpt("h", Builder::setH, Builder::getH, Codec.INT, 0) - .addOpt("u0", Builder::setU0, Builder::getU0, Codec.FLOAT, 0f, "uStart") - .addOpt("v0", Builder::setV0, Builder::getV0, Codec.FLOAT, 0f, "vStart") - .addOpt("u1", Builder::setU1, Builder::getU1, Codec.FLOAT, 1f, "uEnd") - .addOpt("v1", Builder::setV1, Builder::getV1, Codec.FLOAT, 1f, "vEnd") - .addOpt("bl", Builder::setBl, Builder::getBl, Codec.INT, 0, "borderLeft", "borderX", "border") - .addOpt("bt", Builder::setBt, Builder::getBt, Codec.INT, 0, "borderTop", "borderY", "border") - .addOpt("br", Builder::setBr, Builder::getBr, Codec.INT, 0, "borderRight", "borderX", "border") - .addOpt("bb", Builder::setBb, Builder::getBb, Codec.INT, 0, "borderBottom", "borderY", "border") + .addOpt("u0", Builder::setU0, Builder::getU0, Codec.FLOAT, 0f).alias("uStart") + .addOpt("v0", Builder::setV0, Builder::getV0, Codec.FLOAT, 0f).alias("vStart") + .addOpt("u1", Builder::setU1, Builder::getU1, Codec.FLOAT, 1f).alias("uEnd") + .addOpt("v1", Builder::setV1, Builder::getV1, Codec.FLOAT, 1f).alias("vEnd") + .addOpt("bl", Builder::setBl, Builder::getBl, Codec.INT, 0).alias("borderLeft", "borderX", "border") + .addOpt("bt", Builder::setBt, Builder::getBt, Codec.INT, 0).alias("borderTop", "borderY", "border") + .addOpt("br", Builder::setBr, Builder::getBr, Codec.INT, 0).alias("borderRight", "borderX", "border") + .addOpt("bb", Builder::setBb, Builder::getBb, Codec.INT, 0).alias("borderBottom", "borderY", "border") .addOpt("name", Builder::name, Builder::getName, Codec.STRING, null) .addOpt("tiled", Builder::tiled, Builder::isTiled, Codec.BOOL, false) .addOpt("colorType", Builder::colorType, Builder::getColorType, ColorType.CODEC, null) .addOpt("nonOpaque", Builder::nonOpaque, Builder::isNonOpaque, Codec.BOOL, false) + .addOpt("colorOverride", Builder::colorOverride, Builder::getColorOverride, Codec.INT, 0) .build(); @Getter private ResourceLocation location; @Getter @Setter - private int iw = defaultImageWidth, ih = defaultImageHeight; + private int iw = -1, ih = -1; @Getter private int x, y, w, h; @Getter private float u0 = 0, v0 = 0, u1 = 1, v1 = 1; @Getter private Mode mode = Mode.FULL; @@ -622,6 +616,11 @@ public Builder nonOpaque(boolean nonOpaque) { return this; } + public Builder colorOverride(int colorOverride) { + this.colorOverride = colorOverride; + return this; + } + /** * Creates the texture * @@ -650,9 +649,6 @@ private DataResult create() { if (this.location == null) { return DataResult.error(() -> "Location must not be null"); } - if (this.iw <= 0 || this.ih <= 0) { - return DataResult.error(() -> "Image size must be > 0"); - } if (this.mode == Mode.FULL) { this.u0 = 0; this.v0 = 0; @@ -660,6 +656,7 @@ private DataResult create() { this.v1 = 1; this.mode = Mode.RELATIVE; } else if (this.mode == Mode.PIXEL) { + if (this.iw <= 0 || this.ih <= 0) return DataResult.error(() -> "Image size must be > 0 for sub area via xywh or ltrb"); if (this.x < 0 || this.y < 0 || this.w > this.iw || this.h > this.ih) { return DataResult.error(() -> "X and Y must be > 0 and W and H must by smaller than the specified image size"); } @@ -675,10 +672,12 @@ private DataResult create() { return DataResult.error(() -> "UV values must be 0 - 1"); } if (this.bl > 0 || this.bt > 0 || this.br > 0 || this.bb > 0) { + if (this.iw <= 0 || this.ih <= 0) return DataResult.error(() -> "Image size must be > 0 for adaptable textures (border > 0)"); return DataResult.success(new AdaptableUITexture(this.location, this.u0, this.v0, this.u1, this.v1, this.colorType, this.nonOpaque, 0, this.iw, this.ih, this.bl, this.bt, this.br, this.bb, this.tiled)); } if (this.tiled) { + if (this.iw <= 0 || this.ih <= 0) return DataResult.error(() -> "Image size must be > 0 for tiled textures"); return DataResult.success(new TiledUITexture(this.location, this.u0, this.v0, this.u1, this.v1, this.colorType, this.nonOpaque, 0, this.iw, this.ih)); } diff --git a/src/main/java/brachy/modularui/utils/serialization/codec/Field.java b/src/main/java/brachy/modularui/utils/serialization/codec/Field.java new file mode 100644 index 0000000..e7c9b2d --- /dev/null +++ b/src/main/java/brachy/modularui/utils/serialization/codec/Field.java @@ -0,0 +1,135 @@ +package brachy.modularui.utils.serialization.codec; + +import com.mojang.datafixers.util.Pair; +import com.mojang.serialization.Codec; +import com.mojang.serialization.DynamicOps; +import com.mojang.serialization.MapLike; + +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; +import org.jetbrains.annotations.Nullable; + +import java.util.Objects; +import java.util.function.Supplier; +import java.util.stream.Stream; + +@Accessors(fluent = true) +public final class Field { + + @Getter private final String name; + @Getter private final FieldWriter fieldWriter; + @Getter private final FieldReader fieldReader; + @Getter private final Codec codec; + @Getter private final Supplier defaultSupplier; + @Getter + @Setter + private String[] altNames; + @Getter + @Setter + private boolean alwaysEncode; + + public Field(String name, FieldWriter fieldWriter, FieldReader fieldReader, Codec codec, Supplier defaultSupplier) { + this.name = name; + this.fieldWriter = fieldWriter; + this.fieldReader = fieldReader; + this.codec = codec; + this.defaultSupplier = defaultSupplier; + } + + public boolean hasDefault() { + return this.defaultSupplier != null; + } + + public V getDefault() { + return this.defaultSupplier.get(); + } + + public boolean isUnencodable() { + return this.codec == null; + } + + public @Nullable String encode(T holder, DynamicOps ops, Stream.Builder> map) { + V value = this.fieldReader.readField(holder); + if (isUnencodable()) { + if (!isEmpty(value)) { + return String.format("Field '%s' is unencodeable, but the value is not empty", this.name); + } + return null; + } + if (value == null) { + if (hasDefault()) return null; + return String.format("Field '%s' is not optional, but is trying to encode a null value", this.name); + } + if (!alwaysEncode() && hasDefault() && Objects.equals(value, getDefault())) { + return null; + } + var d = this.codec.encodeStart(ops, value); + var res = d.result(); + if (res.isEmpty()) return d.error().orElseThrow().message(); + map.accept(new Pair<>(ops.createString(this.name), res.get())); + return null; + } + + public @Nullable String decode(T holder, DynamicOps ops, MapLike map) { + J element = map.get(this.name); + if (element == null && this.altNames != null) { + for (String alt : this.altNames) { + element = map.get(alt); + if (element != null) break; + } + } + V value; + if (element == null) { + if (!hasDefault()) { + return isUnencodable() ? null : String.format("Field '%s' has no value and is not optional", this.name); + } + value = getDefault(); + } else if (isUnencodable()) { + return String.format("Field '%s' is unencodable, but data still contains value", this.name); + } else if (this.codec instanceof MutableCodec mutableCodec) { + value = this.fieldReader.readField(holder); + if (value == null) { + if (mutableCodec.canDecodeInstance()) { + var d = mutableCodec.parseInstance(ops, element); + var res = d.result(); + if (res.isEmpty()) return d.error().orElseThrow().message(); + value = res.get(); + } + if (value == null && hasDefault()) value = getDefault(); + if (value == null) { + return String.format("Field '%s' is unable to decode instance and the holder has no default value and this property has no default value", this.name); + } + } + var d = mutableCodec.parse(ops, element, value); + var res = d.result(); + if (res.isEmpty()) return d.error().orElseThrow().message(); + value = res.get(); + } else { + var d = this.codec.parse(ops, element); + var res = d.result(); + if (res.isEmpty()) return d.error().orElseThrow().message(); + value = res.get(); + } + this.fieldWriter.writeField(holder, value); + return null; + } + + public void copy(T from, T to) { + V value = this.fieldReader.readField(from); + if (this.codec instanceof MutableObjectCodec mutableObjectCodec) { + value = mutableObjectCodec.copy(value); + } + this.fieldWriter.writeField(to, value); + } + + public void applyDefault(T instance) { + if (hasDefault()) { + this.fieldWriter.writeField(instance, getDefault()); + } + } + + public boolean isEmpty(V value) { + return value == null; + } +} diff --git a/src/main/java/brachy/modularui/utils/serialization/codec/MutableObjectCodec.java b/src/main/java/brachy/modularui/utils/serialization/codec/MutableObjectCodec.java index c42978b..308a644 100644 --- a/src/main/java/brachy/modularui/utils/serialization/codec/MutableObjectCodec.java +++ b/src/main/java/brachy/modularui/utils/serialization/codec/MutableObjectCodec.java @@ -7,7 +7,6 @@ import com.mojang.serialization.Codec; import com.mojang.serialization.DataResult; import com.mojang.serialization.DynamicOps; -import com.mojang.serialization.MapLike; import com.google.common.base.CaseFormat; import com.google.gson.JsonObject; @@ -21,7 +20,6 @@ import java.util.function.Predicate; import java.util.function.Supplier; import java.util.function.UnaryOperator; -import java.util.stream.Stream; /** * This can handle (de)serialization and conversion to various data types, editing properties, copying properties and applying default @@ -57,9 +55,9 @@ public boolean testEachField(Predicate> test) { public boolean hasAnyField(JsonObject json) { return !testEachField(f -> { - if (json.has(f.name)) return false; - if (f.altNames != null) { - for (String alt : f.altNames) { + if (json.has(f.name())) return false; + if (f.altNames() != null) { + for (String alt : f.altNames()) { if (json.has(alt)) return false; } } @@ -227,103 +225,6 @@ public static Builder widgetBuilder(Supplier instance) return widgetBuilder(instance.get().getTypeName(), instance); } - public record Field(String name, FieldWriter fieldWriter, FieldReader fieldReader, Codec codec, - Supplier defaultSupplier, Predicate emptyTester, String[] altNames) { - - public boolean hasDefault() { - return this.defaultSupplier != null; - } - - public V getDefault() { - return this.defaultSupplier.get(); - } - - public boolean isUnencodable() { - return this.codec == null; - } - - private @Nullable String encode(T holder, DynamicOps ops, Stream.Builder> map) { - V value = this.fieldReader.readField(holder); - if (isUnencodable()) { - if (!isEmpty(value)) { - return String.format("Field '%s' is unencodeable, but the value is not empty", this.name); - } - return null; - } - if (value == null) { - if (hasDefault()) return null; - return String.format("Field '%s' is not optional, but is trying to encode a null value", this.name); - } - var d = this.codec.encodeStart(ops, value); - var res = d.result(); - if (res.isEmpty()) return d.error().orElseThrow().message(); - map.accept(new Pair<>(ops.createString(this.name), res.get())); - return null; - } - - private @Nullable String decode(T holder, DynamicOps ops, MapLike map) { - J element = map.get(this.name); - if (element == null && this.altNames != null) { - for (String alt : this.altNames) { - element = map.get(alt); - if (element != null) break; - } - } - V value; - if (element == null) { - if (!hasDefault()) { - return isUnencodable() ? null : String.format("Field '%s' has no value and is not optional", this.name); - } - value = getDefault(); - } else if (isUnencodable()) { - return String.format("Field '%s' is unencodable, but data still contains value", this.name); - } else if (this.codec instanceof MutableCodec mutableCodec) { - value = this.fieldReader.readField(holder); - if (value == null) { - if (mutableCodec.canDecodeInstance()) { - var d = mutableCodec.parseInstance(ops, element); - var res = d.result(); - if (res.isEmpty()) return d.error().orElseThrow().message(); - value = res.get(); - } - if (value == null && hasDefault()) value = getDefault(); - if (value == null) { - return String.format("Field '%s' is unable to decode instance and the holder has no default value and this property has no default value", this.name); - } - } - var d = mutableCodec.parse(ops, element, value); - var res = d.result(); - if (res.isEmpty()) return d.error().orElseThrow().message(); - value = res.get(); - } else { - var d = this.codec.parse(ops, element); - var res = d.result(); - if (res.isEmpty()) return d.error().orElseThrow().message(); - value = res.get(); - } - this.fieldWriter.writeField(holder, value); - return null; - } - - public void copy(T from, T to) { - V value = this.fieldReader.readField(from); - if (this.codec instanceof MutableObjectCodec mutableObjectCodec) { - value = mutableObjectCodec.copy(value); - } - this.fieldWriter.writeField(to, value); - } - - public void applyDefault(T instance) { - if (hasDefault()) { - this.fieldWriter.writeField(instance, getDefault()); - } - } - - public boolean isEmpty(V value) { - return this.emptyTester != null ? this.emptyTester.test(value) : value == null; - } - } - public static class Builder { private final Object2ReferenceLinkedOpenHashMap> fields = new Object2ReferenceLinkedOpenHashMap<>(); @@ -333,6 +234,8 @@ public static class Builder { private String[] names; private Codec wrapped; + private Field lastField; + /** * Sets the instance decoder. This is needed when parsing from JSON. The decoder should create a new instance * and decode any necessary data for that. If the object has a no-arg constructor, then {@link #instance(Supplier)} should be used. @@ -377,68 +280,43 @@ public Builder wrapped(Codec codec) { return this; } - public Builder add(String name, FieldWriter fieldWriter, FieldReader fieldReader, Codec codec) { - return addDynOpt(name, fieldWriter, fieldReader, codec, null); + public Builder registry(CodecRegistry registry, String... names) { + this.registry = registry; + this.names = names; + return this; } - public Builder add(String name, FieldWriter fieldWriter, FieldReader fieldReader, Codec codec, String @Nullable ... altNames) { - return addDynOpt(name, fieldWriter, fieldReader, codec, null, altNames); + public Builder registryTypeName(CodecRegistry registry, String typeName) { + return registry(registry, typeName, CaseFormat.UPPER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, typeName)); } - public Builder add(String name, FieldWriter fieldWriter, FieldReader fieldReader, - Codec codec, @Nullable Predicate emptyTester) { - return addDynOpt(name, fieldWriter, fieldReader, codec, null, emptyTester); + public Builder registryTypeName(CodecRegistry registry, Class typeName) { + return registryTypeName(registry, typeName.getSimpleName()); } /** * Adds a new non-optional, mutable property. * - * @see #addDynOpt(String, FieldWriter, FieldReader, Codec, Supplier, Predicate) + * @see #addDynOpt(String, FieldWriter, FieldReader, Codec, Supplier) */ - public Builder add(String name, FieldWriter fieldWriter, FieldReader fieldReader, - Codec codec, @Nullable Predicate emptyTester, String @Nullable ... altNames) { - return addDynOpt(name, fieldWriter, fieldReader, codec, null, emptyTester, altNames); - } - - public Builder addOpt(String name, FieldWriter fieldWriter, FieldReader fieldReader, - Codec codec, @Nullable V defValue) { - return addDynOpt(name, fieldWriter, fieldReader, codec, () -> defValue); - } - - public Builder addOpt(String name, FieldWriter fieldWriter, FieldReader fieldReader, - Codec codec, @Nullable V defValue, String @Nullable ... altNames) { - return addDynOpt(name, fieldWriter, fieldReader, codec, () -> defValue, altNames); - } - - public Builder addOpt(String name, FieldWriter fieldWriter, FieldReader fieldReader, - Codec codec, @Nullable V defValue, @Nullable Predicate emptyTester) { - return addDynOpt(name, fieldWriter, fieldReader, codec, () -> defValue, emptyTester, (String[]) null); + public Builder add(String name, FieldWriter fieldWriter, FieldReader fieldReader, Codec codec) { + return addDynOpt(name, fieldWriter, fieldReader, codec, null); } /** * Adds a new optional, mutable property with a const default value. * * @param defValue default value, if this is null, the default value is null and this property is still considered optional - * @see #addDynOpt(String, FieldWriter, FieldReader, Codec, Supplier, Predicate) + * @see #addDynOpt(String, FieldWriter, FieldReader, Codec, Supplier) */ public Builder addOpt(String name, FieldWriter fieldWriter, FieldReader fieldReader, - Codec codec, @Nullable V defValue, @Nullable Predicate emptyTester, String @Nullable ... altNames) { - return addDynOpt(name, fieldWriter, fieldReader, codec, () -> defValue, emptyTester, altNames); - } - - public Builder addDynOpt(String name, FieldWriter fieldWriter, FieldReader fieldReader, - Codec codec, @Nullable Supplier defaultSupplier) { - return addDynOpt(name, fieldWriter, fieldReader, codec, defaultSupplier, null, (String[]) null); - } - - public Builder addDynOpt(String name, FieldWriter fieldWriter, FieldReader fieldReader, - Codec codec, @Nullable Supplier defaultSupplier, String @Nullable ... altNames) { - return addDynOpt(name, fieldWriter, fieldReader, codec, defaultSupplier, null, altNames); - } - - public Builder addDynOpt(String name, FieldWriter fieldWriter, FieldReader fieldReader, Codec codec, - @Nullable Supplier defaultSupplier, @Nullable Predicate emptyTester) { - return addDynOpt(name, fieldWriter, fieldReader, codec, defaultSupplier, emptyTester, (String[]) null); + Codec codec, @Nullable V defValue) { + Objects.requireNonNull(name, "Name of field must not be null!"); + Objects.requireNonNull(fieldWriter, "Field encoder must not be null!"); + Objects.requireNonNull(fieldReader, "Field decoder must not be null!"); + this.lastField = new Field<>(name, fieldWriter, fieldReader, codec, () -> defValue); + this.fields.put(name, this.lastField); + return this; } /** @@ -449,42 +327,28 @@ public Builder addDynOpt(String name, FieldWriter fieldWriter, Fiel * @param fieldReader reads a value from the field * @param codec handles en-/decoding of a value * @param defaultSupplier supplier for a default value, if this is non-null, this property is marked as optional - * @param emptyTester a test function to test whether a value is empty, this is currently only used for unencodable values * @param type of value * @return this * @throws NullPointerException if name, fieldEncoder or fieldDecoder is null */ public Builder addDynOpt(String name, FieldWriter fieldWriter, FieldReader fieldReader, Codec codec, - @Nullable Supplier defaultSupplier, @Nullable Predicate emptyTester, String @Nullable ... altNames) { + @Nullable Supplier defaultSupplier) { Objects.requireNonNull(name, "Name of field must not be null!"); Objects.requireNonNull(fieldWriter, "Field encoder must not be null!"); Objects.requireNonNull(fieldReader, "Field decoder must not be null!"); - this.fields.put(name, new Field<>(name, fieldWriter, fieldReader, codec, defaultSupplier, emptyTester, altNames)); + this.lastField = new Field<>(name, fieldWriter, fieldReader, codec, defaultSupplier); + this.lastField.alwaysEncode(true); + this.fields.put(name, this.lastField); return this; } public Builder addUnencodable(String name, FieldWriter fieldWriter, FieldReader fieldReader) { - return addUnencodableDynOpt(name, fieldWriter, fieldReader, null, null); - } - - public Builder addUnencodable(String name, FieldWriter fieldWriter, FieldReader fieldReader, - @Nullable Predicate emptyTest) { - return addUnencodableDynOpt(name, fieldWriter, fieldReader, null, emptyTest); + return add(name, fieldWriter, fieldReader, null); } public Builder addUnencodableOpt(String name, FieldWriter fieldWriter, FieldReader fieldReader, @Nullable V defaultValue) { - return addUnencodableDynOpt(name, fieldWriter, fieldReader, () -> defaultValue, null); - } - - public Builder addUnencodableOpt(String name, FieldWriter fieldWriter, FieldReader fieldReader, - @Nullable V defaultValue, @Nullable Predicate emptyTest) { - return addUnencodableDynOpt(name, fieldWriter, fieldReader, () -> defaultValue, emptyTest); - } - - public Builder addUnencodableDynOpt(String name, FieldWriter fieldWriter, FieldReader fieldReader, - @Nullable Supplier defaultSupplier) { - return addUnencodableDynOpt(name, fieldWriter, fieldReader, defaultSupplier, null); + return addOpt(name, fieldWriter, fieldReader, null, defaultValue); } /** @@ -492,25 +356,30 @@ public Builder addUnencodableDynOpt(String name, FieldWriter fieldW * Unencodable means it cannot be converted to a data format like JSON. This is the case for functions. * These unencodable values are still important for copying and applying default values. * - * @see #addDynOpt(String, FieldWriter, FieldReader, Codec, Supplier, Predicate) + * @see #addDynOpt(String, FieldWriter, FieldReader, Codec, Supplier) */ public Builder addUnencodableDynOpt(String name, FieldWriter fieldWriter, FieldReader fieldReader, - @Nullable Supplier defaultSupplier, @Nullable Predicate emptyTest) { - return addDynOpt(name, fieldWriter, fieldReader, null, defaultSupplier, emptyTest); + @Nullable Supplier defaultSupplier) { + return addDynOpt(name, fieldWriter, fieldReader, null, defaultSupplier); } - public Builder registry(CodecRegistry registry, String... names) { - this.registry = registry; - this.names = names; - return this; + public Builder alwaysEncode() { + return alwaysEncode(true); } - public Builder registryTypeName(CodecRegistry registry, String typeName) { - return registry(registry, typeName, CaseFormat.UPPER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, typeName)); + public Builder alwaysEncode(boolean alwaysEncode) { + return doOnField(f -> f.alwaysEncode(alwaysEncode)); } - public Builder registryTypeName(CodecRegistry registry, Class typeName) { - return registryTypeName(registry, typeName.getSimpleName()); + public Builder alias(String... alias) { + return doOnField(f -> f.altNames(alias)); + } + + private Builder doOnField(Consumer> consumer) { + if (this.lastField != null) { + consumer.accept(this.lastField); + } + return this; } public MutableObjectCodec build() { diff --git a/src/test/java/brachy/modularui/CodecTest.java b/src/test/java/brachy/modularui/CodecTest.java index 25a2cf4..9278765 100644 --- a/src/test/java/brachy/modularui/CodecTest.java +++ b/src/test/java/brachy/modularui/CodecTest.java @@ -15,9 +15,6 @@ import brachy.modularui.widget.Widget; import brachy.modularui.widget.sizer.StandardResizer; -import net.minecraft.DetectedVersion; -import net.minecraft.SharedConstants; -import net.minecraft.server.Bootstrap; import com.mojang.serialization.Codec; import com.mojang.serialization.JsonOps; @@ -44,8 +41,7 @@ void resizer() { @Test void drawable() { - SharedConstants.setVersion(DetectedVersion.BUILT_IN); - Bootstrap.bootStrap(); + TestUtil.bootstrap(); drawableTest(IDrawable.EMPTY); drawableTest(IDrawable.NONE); decodeTest(new JsonPrimitive("null"), IDrawable.CODEC, IDrawable.EMPTY); diff --git a/src/test/java/brachy/modularui/TestUtil.java b/src/test/java/brachy/modularui/TestUtil.java index 5987b2e..ccf6259 100644 --- a/src/test/java/brachy/modularui/TestUtil.java +++ b/src/test/java/brachy/modularui/TestUtil.java @@ -1,11 +1,20 @@ package brachy.modularui; +import net.minecraft.DetectedVersion; +import net.minecraft.SharedConstants; +import net.minecraft.server.Bootstrap; + import java.util.Random; import java.util.function.Consumer; import java.util.function.IntConsumer; public class TestUtil { + public static void bootstrap() { + SharedConstants.setVersion(DetectedVersion.BUILT_IN); + Bootstrap.bootStrap(); + } + public static void repeat(int n, IntConsumer consumer) { for (int i = 0; i < n; i++) { consumer.accept(n); From ab82c6c7a52185b3e90c187bba1668b60ec13e5d Mon Sep 17 00:00:00 2001 From: brachy84 Date: Mon, 18 May 2026 21:47:49 +0200 Subject: [PATCH 16/26] Icon codec --- .../java/brachy/modularui/drawable/Icon.java | 40 +++++++++++++++---- .../utils/serialization/codec/Field.java | 21 +++++++--- .../codec/MutableObjectCodec.java | 8 +++- .../brachy/modularui/widget/sizer/Box.java | 11 +++++ .../widget/sizer/DimensionSizer.java | 6 +-- .../brachy/modularui/widget/sizer/Unit.java | 2 + src/test/java/brachy/modularui/CodecTest.java | 1 + 7 files changed, 71 insertions(+), 18 deletions(-) diff --git a/src/main/java/brachy/modularui/drawable/Icon.java b/src/main/java/brachy/modularui/drawable/Icon.java index ff8b2fa..ead9156 100644 --- a/src/main/java/brachy/modularui/drawable/Icon.java +++ b/src/main/java/brachy/modularui/drawable/Icon.java @@ -7,9 +7,11 @@ import brachy.modularui.screen.viewport.GuiContext; import brachy.modularui.theme.WidgetTheme; import brachy.modularui.utils.Alignment; +import brachy.modularui.utils.serialization.codec.MutableObjectCodec; import brachy.modularui.utils.serialization.json.JsonHelper; import brachy.modularui.widget.sizer.Box; +import com.mojang.serialization.Codec; import net.minecraftforge.api.distmarker.Dist; import net.minecraftforge.api.distmarker.OnlyIn; @@ -21,14 +23,24 @@ */ public class Icon implements IIcon, IJsonSerializable { - private final IDrawable drawable; - @Getter - private int width = 0, height = 0; - private float aspectRatio = 0; - @Getter - private Alignment alignment = Alignment.Center; - @Getter - private final Box margin = new Box(); + public static final MutableObjectCodec CODEC = MutableObjectCodec.drawableBuilder(Icon::new) + .add("drawable", Icon::drawable, Icon::getDrawable, IDrawable.CODEC) + .addOpt("width", Icon::width, Icon::getWidth, Codec.INT, 0) + .addOpt("height", Icon::height, Icon::getHeight, Codec.INT, 0) + .addOpt("aspectRatio", Icon::aspectRatio, Icon::getAspectRatio, Codec.FLOAT, 0f) + .addOpt("alignment", Icon::alignment, Icon::getAlignment, Alignment.CODEC, Alignment.Center) + .addOpt("margin", Icon::margin, Icon::getMargin, Box.CODEC, Box.ZERO) + .build(); + + @Getter private IDrawable drawable; + @Getter private int width = 0, height = 0; + @Getter private float aspectRatio = 0; + @Getter private Alignment alignment = Alignment.Center; + @Getter private final Box margin = new Box(); + + private Icon() { + this.drawable = IDrawable.EMPTY; + } public Icon(IDrawable drawable) { this.drawable = drawable; @@ -85,6 +97,11 @@ public IDrawable getWrappedDrawable() { return drawable; } + public Icon drawable(IDrawable drawable) { + this.drawable = drawable; + return this; + } + public Icon expandWidth() { return width(0); } @@ -160,6 +177,13 @@ public Icon marginBottom(int val) { return this; } + public Icon margin(Box box) { + if (box != null && box != this.margin) { + Box.CODEC.copyFields(box, this.margin); + } + return this; + } + @Override public void loadFromJson(JsonObject json) { this.width = (json.has("autoWidth") || json.has("autoSize")) && diff --git a/src/main/java/brachy/modularui/utils/serialization/codec/Field.java b/src/main/java/brachy/modularui/utils/serialization/codec/Field.java index e7c9b2d..c54f3d4 100644 --- a/src/main/java/brachy/modularui/utils/serialization/codec/Field.java +++ b/src/main/java/brachy/modularui/utils/serialization/codec/Field.java @@ -22,6 +22,7 @@ public final class Field { @Getter private final FieldReader fieldReader; @Getter private final Codec codec; @Getter private final Supplier defaultSupplier; + @Getter private final boolean dynamicSupplier; @Getter @Setter private String[] altNames; @@ -29,22 +30,32 @@ public final class Field { @Setter private boolean alwaysEncode; - public Field(String name, FieldWriter fieldWriter, FieldReader fieldReader, Codec codec, Supplier defaultSupplier) { + Field(String name, FieldWriter fieldWriter, FieldReader fieldReader, Codec codec, Supplier defaultSupplier, boolean dynamicSupplier) { this.name = name; this.fieldWriter = fieldWriter; this.fieldReader = fieldReader; this.codec = codec; this.defaultSupplier = defaultSupplier; + this.dynamicSupplier = dynamicSupplier; } public boolean hasDefault() { return this.defaultSupplier != null; } - public V getDefault() { + private V getDefault() { + // could return a non-dynamic, modifiable instance return this.defaultSupplier.get(); } + public V getModifiableDefault() { + V v = getDefault(); + if (!this.dynamicSupplier && this.codec instanceof MutableObjectCodec moc && moc.canCopy()) { + v = moc.copy(v); + } + return v; + } + public boolean isUnencodable() { return this.codec == null; } @@ -84,7 +95,7 @@ public boolean isUnencodable() { if (!hasDefault()) { return isUnencodable() ? null : String.format("Field '%s' has no value and is not optional", this.name); } - value = getDefault(); + value = getModifiableDefault(); } else if (isUnencodable()) { return String.format("Field '%s' is unencodable, but data still contains value", this.name); } else if (this.codec instanceof MutableCodec mutableCodec) { @@ -96,7 +107,7 @@ public boolean isUnencodable() { if (res.isEmpty()) return d.error().orElseThrow().message(); value = res.get(); } - if (value == null && hasDefault()) value = getDefault(); + if (value == null && hasDefault()) value = getModifiableDefault(); if (value == null) { return String.format("Field '%s' is unable to decode instance and the holder has no default value and this property has no default value", this.name); } @@ -125,7 +136,7 @@ public void copy(T from, T to) { public void applyDefault(T instance) { if (hasDefault()) { - this.fieldWriter.writeField(instance, getDefault()); + this.fieldWriter.writeField(instance, getModifiableDefault()); } } diff --git a/src/main/java/brachy/modularui/utils/serialization/codec/MutableObjectCodec.java b/src/main/java/brachy/modularui/utils/serialization/codec/MutableObjectCodec.java index 308a644..0edc452 100644 --- a/src/main/java/brachy/modularui/utils/serialization/codec/MutableObjectCodec.java +++ b/src/main/java/brachy/modularui/utils/serialization/codec/MutableObjectCodec.java @@ -70,6 +70,10 @@ private Field getField(String name) { return (Field) this.fields.get(name); } + public boolean canCopy() { + return this.baseCopy != null; + } + public T copy(T from) { if (from == null) return null; if (this.baseCopy == null) { @@ -314,7 +318,7 @@ public Builder addOpt(String name, FieldWriter fieldWriter, FieldRe Objects.requireNonNull(name, "Name of field must not be null!"); Objects.requireNonNull(fieldWriter, "Field encoder must not be null!"); Objects.requireNonNull(fieldReader, "Field decoder must not be null!"); - this.lastField = new Field<>(name, fieldWriter, fieldReader, codec, () -> defValue); + this.lastField = new Field<>(name, fieldWriter, fieldReader, codec, () -> defValue, false); this.fields.put(name, this.lastField); return this; } @@ -336,7 +340,7 @@ public Builder addDynOpt(String name, FieldWriter fieldWriter, Fiel Objects.requireNonNull(name, "Name of field must not be null!"); Objects.requireNonNull(fieldWriter, "Field encoder must not be null!"); Objects.requireNonNull(fieldReader, "Field decoder must not be null!"); - this.lastField = new Field<>(name, fieldWriter, fieldReader, codec, defaultSupplier); + this.lastField = new Field<>(name, fieldWriter, fieldReader, codec, defaultSupplier, defaultSupplier != null); this.lastField.alwaysEncode(true); this.fields.put(name, this.lastField); return this; diff --git a/src/main/java/brachy/modularui/widget/sizer/Box.java b/src/main/java/brachy/modularui/widget/sizer/Box.java index 7b24c50..f2574ef 100644 --- a/src/main/java/brachy/modularui/widget/sizer/Box.java +++ b/src/main/java/brachy/modularui/widget/sizer/Box.java @@ -3,9 +3,13 @@ import brachy.modularui.animation.IAnimatable; import brachy.modularui.api.GuiAxis; import brachy.modularui.utils.Interpolations; +import brachy.modularui.utils.serialization.codec.MutableObjectCodec; import brachy.modularui.utils.serialization.json.JsonHelper; import com.google.gson.JsonObject; + +import com.mojang.serialization.Codec; + import lombok.Getter; import lombok.Setter; import lombok.experimental.Accessors; @@ -25,6 +29,13 @@ public class Box implements IAnimatable { public static final Box ONE = new Box().all(1); + public static final MutableObjectCodec CODEC = MutableObjectCodec.builder(Box::new) + .addOpt("left", Box::left, Box::left, Codec.INT, 0).alias("x", "all") + .addOpt("top", Box::top, Box::top, Codec.INT, 0).alias("y", "all") + .addOpt("right", Box::right, Box::right, Codec.INT, 0).alias("x", "all") + .addOpt("bottom", Box::bottom, Box::bottom, Codec.INT, 0).alias("y", "all") + .build(); + @Getter @Setter protected int left; diff --git a/src/main/java/brachy/modularui/widget/sizer/DimensionSizer.java b/src/main/java/brachy/modularui/widget/sizer/DimensionSizer.java index 402716a..3a3a7a0 100644 --- a/src/main/java/brachy/modularui/widget/sizer/DimensionSizer.java +++ b/src/main/java/brachy/modularui/widget/sizer/DimensionSizer.java @@ -27,9 +27,9 @@ public class DimensionSizer { public static final MutableObjectCodec CODEC = MutableObjectCodec.builder(DimensionSizer.class) .baseCopy(sizer -> new DimensionSizer(sizer.resizer, sizer.axis)) .addOpt("coverChildrenMinSize", DimensionSizer::setCoverChildrenMinSize, DimensionSizer::getCoverChildrenMinSize, Codec.INT, -1) - .addDynOpt("start", DimensionSizer::setStart, DimensionSizer::getStart, Unit.CODEC, Unit::new) - .addDynOpt("end", DimensionSizer::setEnd, DimensionSizer::getEnd, Unit.CODEC, Unit::new) - .addDynOpt("size", DimensionSizer::setSize, DimensionSizer::getSize, Unit.CODEC, Unit::new) + .addOpt("start", DimensionSizer::setStart, DimensionSizer::getStart, Unit.CODEC, Unit.ZERO) + .addOpt("end", DimensionSizer::setEnd, DimensionSizer::getEnd, Unit.CODEC, Unit.ZERO) + .addOpt("size", DimensionSizer::setSize, DimensionSizer::getSize, Unit.CODEC, Unit.ZERO) .build(); private final ResizeNode resizer; diff --git a/src/main/java/brachy/modularui/widget/sizer/Unit.java b/src/main/java/brachy/modularui/widget/sizer/Unit.java index 6d5a314..bfc2403 100644 --- a/src/main/java/brachy/modularui/widget/sizer/Unit.java +++ b/src/main/java/brachy/modularui/widget/sizer/Unit.java @@ -28,6 +28,8 @@ public class Unit { .addUnencodableOpt("valueSupplier", Unit::setValue, Unit::getValueSupplier, null) .build(); + static final Unit ZERO = new Unit(); + @Getter @Setter private boolean autoAnchor; diff --git a/src/test/java/brachy/modularui/CodecTest.java b/src/test/java/brachy/modularui/CodecTest.java index 9278765..dea9c88 100644 --- a/src/test/java/brachy/modularui/CodecTest.java +++ b/src/test/java/brachy/modularui/CodecTest.java @@ -161,6 +161,7 @@ private static void test(MutableObjectCodec codec, A obj1, Supplier su if (checkObjEquals) assertEquals(obj1, obj2); JsonElement json2 = toJson(codec, obj2); assertEquals(json1, json2); + System.out.println(JsonHelper.GSON.toJson(json1)); } private static void test(Codec codec, A obj, boolean checkObjEquals) { From 224c9e86c65b39fc6b1699cc4b34b0ad0f31971e Mon Sep 17 00:00:00 2001 From: brachy84 Date: Mon, 18 May 2026 21:48:16 +0200 Subject: [PATCH 17/26] ItemDrawable codec & merge IngredientDrawable --- .../java/brachy/modularui/ClientProxy.java | 20 +++ .../drawable/DrawableSerialization.java | 1 - .../drawable/IngredientDrawable.java | 61 ------- .../modularui/drawable/ItemDrawable.java | 160 ++++++++++-------- .../utils/serialization/codec/CodecUtil.java | 12 ++ 5 files changed, 119 insertions(+), 135 deletions(-) delete mode 100755 src/main/java/brachy/modularui/drawable/IngredientDrawable.java diff --git a/src/main/java/brachy/modularui/ClientProxy.java b/src/main/java/brachy/modularui/ClientProxy.java index 2be93d7..f620d34 100644 --- a/src/main/java/brachy/modularui/ClientProxy.java +++ b/src/main/java/brachy/modularui/ClientProxy.java @@ -2,6 +2,7 @@ import brachy.modularui.animation.AnimatorManager; import brachy.modularui.api.drawable.IIcon; +import brachy.modularui.api.drawable.Text; import brachy.modularui.client.CursorHandler; import brachy.modularui.client.component.DrawableTooltipComponent; import brachy.modularui.client.component.TooltipComponentIcon; @@ -12,14 +13,19 @@ import brachy.modularui.drawable.Icon; import brachy.modularui.drawable.InteractableIcon; import brachy.modularui.drawable.text.KeyIcon; +import brachy.modularui.drawable.text.ModularComponent; import brachy.modularui.drawable.text.TextIcon; import brachy.modularui.network.ModularNetwork; import brachy.modularui.theme.ThemeManager; +import brachy.modularui.utils.Alignment; +import brachy.modularui.utils.Color; +import brachy.modularui.utils.serialization.json.JsonHelper; import net.minecraft.client.Minecraft; import net.minecraft.client.Timer; import net.minecraft.client.gui.screens.inventory.tooltip.ClientTooltipComponent; import com.mojang.blaze3d.systems.RenderSystem; +import com.mojang.serialization.Codec; import net.minecraftforge.client.event.RegisterClientReloadListenersEvent; import net.minecraftforge.client.event.RegisterClientTooltipComponentFactoriesEvent; import net.minecraftforge.common.MinecraftForge; @@ -28,8 +34,10 @@ import net.minecraftforge.fml.event.lifecycle.FMLCommonSetupEvent; import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext; +import com.google.gson.JsonElement; import lombok.Getter; +import java.util.Objects; import java.util.function.Function; public class ClientProxy extends CommonProxy { @@ -58,6 +66,18 @@ protected void onInit(FMLCommonSetupEvent event) { // enable stencil bits, must call on render thread RenderSystem.recordRenderCall(() -> Minecraft.getInstance().getMainRenderTarget().enableStencil()); } + test(ModularComponent.CODEC, Text.comp( + Text.str("Hello ").color(Color.withAlpha(Color.BLUE.main, 0)), + Text.lang("World").scale(1.5f)).alignment(Alignment.BottomCenter), true); + } + + private static void test(Codec codec, A obj, boolean checkObjEquals) { + JsonElement json1 = JsonHelper.toJson(codec, obj); + A obj2 = JsonHelper.fromJson(codec, json1); + if (checkObjEquals) ModularUI.LOGGER.info("Equals: {}", Objects.equals(obj, obj2)); + JsonElement json2 = JsonHelper.toJson(codec, obj2); + ModularUI.LOGGER.info("Equals: {}", Objects.equals(json1, json2)); + ModularUI.LOGGER.info(JsonHelper.GSON.toJson(json1)); } private void onRegisterClientTooltipComponents(RegisterClientTooltipComponentFactoriesEvent event) { diff --git a/src/main/java/brachy/modularui/drawable/DrawableSerialization.java b/src/main/java/brachy/modularui/drawable/DrawableSerialization.java index 570b705..d457c60 100644 --- a/src/main/java/brachy/modularui/drawable/DrawableSerialization.java +++ b/src/main/java/brachy/modularui/drawable/DrawableSerialization.java @@ -113,7 +113,6 @@ public static void init() { registerDrawableType("color", Rectangle.class, json -> new Rectangle()); registerDrawableType("rectangle", Rectangle.class, json -> new Rectangle()); registerDrawableType("ellipse", Circle.class, json -> new Circle()); - registerDrawableType("item", ItemDrawable.class, ItemDrawable::ofJson); registerDrawableType("icon", Icon.class, Icon::ofJson); registerDrawableType("stack", DrawableStack.class, DrawableStack::parseJson); registerDrawableType("scrollbar", Scrollbar.class, Scrollbar::ofJson); diff --git a/src/main/java/brachy/modularui/drawable/IngredientDrawable.java b/src/main/java/brachy/modularui/drawable/IngredientDrawable.java deleted file mode 100755 index f567cc0..0000000 --- a/src/main/java/brachy/modularui/drawable/IngredientDrawable.java +++ /dev/null @@ -1,61 +0,0 @@ -package brachy.modularui.drawable; - -import brachy.modularui.api.IJsonSerializable; -import brachy.modularui.api.drawable.IDrawable; -import brachy.modularui.screen.viewport.GuiContext; -import brachy.modularui.theme.WidgetTheme; - -import net.minecraft.Util; -import net.minecraft.world.item.ItemStack; -import net.minecraft.world.item.crafting.Ingredient; -import net.minecraftforge.api.distmarker.Dist; -import net.minecraftforge.api.distmarker.OnlyIn; - -import lombok.Getter; -import lombok.Setter; -import lombok.experimental.Tolerate; - -public class IngredientDrawable implements IDrawable, IJsonSerializable { - - @Getter - @Setter - private ItemStack[] items; - @Getter - @Setter - private int cycleTime = 1000; - - public IngredientDrawable(Ingredient ingredient) { - this(ingredient.getItems()); - } - - public IngredientDrawable(ItemStack... items) { - setItems(items); - } - - @OnlyIn(Dist.CLIENT) - @Override - public void draw(GuiContext context, int x, int y, int width, int height, WidgetTheme widgetTheme) { - if (this.items.length == 0) return; - ItemStack item = this.items[(int) (Util.getMillis() % (this.cycleTime * this.items.length)) / this.cycleTime]; - if (item != null) { - GuiDraw.drawItem(context.getGraphics(), item, x, y, width, height, context.getCurrentDrawingZ()); - } - } - - /** - * Sets how many milliseconds each item shows up - * - * @param cycleTime time per item in milliseconds - * @return this - */ - @Tolerate - public IngredientDrawable cycleTime(int cycleTime) { - this.cycleTime = cycleTime; - return this; - } - - @Tolerate - public void setItems(Ingredient ingredient) { - setItems(ingredient.getItems()); - } -} diff --git a/src/main/java/brachy/modularui/drawable/ItemDrawable.java b/src/main/java/brachy/modularui/drawable/ItemDrawable.java index 000cf47..14e8f40 100755 --- a/src/main/java/brachy/modularui/drawable/ItemDrawable.java +++ b/src/main/java/brachy/modularui/drawable/ItemDrawable.java @@ -4,137 +4,151 @@ import brachy.modularui.api.drawable.IDrawable; import brachy.modularui.screen.viewport.GuiContext; import brachy.modularui.theme.WidgetTheme; -import brachy.modularui.utils.serialization.json.JsonHelper; -import brachy.modularui.widget.Widget; +import brachy.modularui.utils.serialization.codec.CodecUtil; +import brachy.modularui.utils.serialization.codec.MutableObjectCodec; -import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.Util; import net.minecraft.nbt.CompoundTag; -import net.minecraft.nbt.NbtOps; -import net.minecraft.resources.ResourceLocation; -import net.minecraft.world.item.Item; import net.minecraft.world.item.ItemStack; -import net.minecraft.world.level.block.Block; -import com.mojang.serialization.JsonOps; +import net.minecraft.world.item.crafting.Ingredient; +import net.minecraft.world.level.ItemLike; +import com.mojang.serialization.Codec; import net.minecraftforge.api.distmarker.Dist; import net.minecraftforge.api.distmarker.OnlyIn; -import com.google.gson.JsonObject; -import com.google.gson.JsonParseException; import lombok.Getter; -import org.jetbrains.annotations.NotNull; +import lombok.Setter; +import lombok.experimental.Accessors; +import org.apache.commons.lang3.ArrayUtils; import org.jetbrains.annotations.Nullable; -import java.util.NoSuchElementException; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +@Accessors(fluent = true, chain = true) public class ItemDrawable implements IDrawable, IJsonSerializable { + public static final Codec CODEC = MutableObjectCodec.drawableBuilder(ItemDrawable::new) + .add("items", ItemDrawable::items, ItemDrawable::getItemList, CodecUtil.listLike(ItemStack.CODEC)).alias("item") + .addOpt("cycleTime", ItemDrawable::cycleTime, ItemDrawable::cycleTime, Codec.INT, 1000) + .build(); + + @Getter + private ItemStack[] items; @Getter - private ItemStack item = ItemStack.EMPTY; + @Setter + private int cycleTime = 1000; - public ItemDrawable() {} + private ItemDrawable() { + this(new ItemStack[0]); + } - public ItemDrawable(@NotNull ItemStack item) { - setItem(item); + public ItemDrawable(Ingredient ingredient) { + this(ingredient.getItems()); } - public ItemDrawable(@NotNull Item item) { - setItem(item); + public ItemDrawable(ItemStack... items) { + items(items); } - public ItemDrawable(@NotNull Item item, int amount) { - setItem(item, amount); + public ItemDrawable(ItemStack item) { + item(item); } - public ItemDrawable(@NotNull Item item, int amount, @Nullable CompoundTag nbt) { - setItem(item, amount, nbt); + public ItemDrawable(ItemLike item) { + item(item); } - public ItemDrawable(@NotNull Block item) { - setItem(item); + public ItemDrawable(ItemLike item, int amount) { + item(item, amount); } - public ItemDrawable(@NotNull Block item, int amount) { - setItem(new ItemStack(item, amount)); + public ItemDrawable(ItemLike item, int amount, @Nullable CompoundTag tag) { + item(item, amount, tag); } @OnlyIn(Dist.CLIENT) @Override public void draw(GuiContext context, int x, int y, int width, int height, WidgetTheme widgetTheme) { - applyColor(widgetTheme.getColor()); - GuiDraw.drawItem(context.getGraphics(), this.item, x, y, width, height, context.getCurrentDrawingZ()); + if (this.items.length == 0) return; + ItemStack item = this.items.length == 1 ? this.items[0] : + this.items[(int) (Util.getMillis() % (this.cycleTime * this.items.length)) / this.cycleTime]; + if (item != null) { + GuiDraw.drawItem(context.getGraphics(), item, x, y, width, height, context.getCurrentDrawingZ()); + } } @Override - public int getDefaultWidth() { + public int getDefaultHeight() { return 16; } @Override - public int getDefaultHeight() { + public int getDefaultWidth() { return 16; } - @Override - public Widget asWidget() { - return IDrawable.super.asWidget().size(16); + public void ingredient(Ingredient ingredient) { + items(ingredient.getItems()); + } + + public ItemDrawable items(Collection items) { + return items(items.toArray(ItemStack[]::new)); } - public ItemDrawable setItem(@NotNull ItemStack item) { - this.item = item; + public ItemDrawable items(ItemStack... items) { + this.items = items; return this; } - public ItemDrawable setItem(@NotNull Item item) { - return setItem(item, 1, null); + public ItemDrawable item(ItemStack item) { + if (this.items.length != 1) { + this.items = new ItemStack[1]; + } + this.items[0] = item; + return this; } - public ItemDrawable setItem(@NotNull Item item, int amount) { - return setItem(item, amount, null); + public ItemDrawable item(ItemLike item) { + return item(item.asItem(), 1, null); } - public ItemDrawable setItem(@NotNull Item item, int amount, @Nullable CompoundTag nbt) { + public ItemDrawable item(ItemLike item, int amount) { + return item(item, amount, null); + } + + public ItemDrawable item(ItemLike item, int amount, @Nullable CompoundTag tag) { ItemStack itemStack = new ItemStack(item, amount); - itemStack.setTag(nbt); - return setItem(itemStack); + itemStack.setTag(tag); + return item(itemStack); } - public ItemDrawable setItem(@NotNull Block item) { - return setItem(item, 1); + public ItemDrawable addItem(ItemStack item) { + this.items = ArrayUtils.add(this.items, item); + return this; } - public ItemDrawable setItem(@NotNull Block item, int amount) { - return setItem(new ItemStack(item, amount)); + public ItemDrawable addItem(ItemLike item) { + return addItem(item.asItem(), 1, null); } - public static ItemDrawable ofJson(JsonObject json) { - String itemName = JsonHelper.getString(json, null, "item"); - if (itemName == null) throw new JsonParseException("Item property not found!"); - if (itemName.isEmpty()) return new ItemDrawable(); - ItemStack stack; - try { - ResourceLocation id = new ResourceLocation(itemName); - stack = new ItemStack(BuiltInRegistries.ITEM.get(id)); - } catch (NoSuchElementException e) { - throw new JsonParseException(e); - } - if (json.has("nbt")) { - CompoundTag nbt = (CompoundTag) JsonOps.INSTANCE.convertTo(NbtOps.INSTANCE, - JsonHelper.getObject(json, new JsonObject(), o -> o, "nbt")); - stack.setTag(nbt); - } - return new ItemDrawable(stack); + public ItemDrawable addItem(ItemLike item, int amount) { + return addItem(item, amount, null); + } + + public ItemDrawable addItem(ItemLike item, int amount, @Nullable CompoundTag tag) { + ItemStack itemStack = new ItemStack(item, amount); + itemStack.setTag(tag); + return addItem(itemStack); + } + + public List getItemList() { + return Arrays.asList(this.items); } @Override - public boolean saveToJson(JsonObject json) { - if (this.item == null || this.item.isEmpty()) { - json.addProperty("item", ""); - return true; - } - json.addProperty("item", this.item.getItemHolder().unwrapKey().get().location().toString()); - if (this.item.hasTag()) { - json.addProperty("nbt", this.item.getTag().toString()); - } - return true; + public String getTypeName() { + return "item"; } } diff --git a/src/main/java/brachy/modularui/utils/serialization/codec/CodecUtil.java b/src/main/java/brachy/modularui/utils/serialization/codec/CodecUtil.java index 6df976e..3cbc4e0 100644 --- a/src/main/java/brachy/modularui/utils/serialization/codec/CodecUtil.java +++ b/src/main/java/brachy/modularui/utils/serialization/codec/CodecUtil.java @@ -9,6 +9,8 @@ import org.jetbrains.annotations.Nullable; +import java.util.Collections; +import java.util.List; import java.util.Objects; import java.util.function.Function; import java.util.function.Predicate; @@ -129,4 +131,14 @@ public String toString() { } return mapValues; } + + /** + * Creates a codec that accepts either a list or a single element and turns it into a list. + */ + public static Codec> listLike(Codec codec) { + return chainedCodec(codec.flatComapMap(Collections::singletonList, list -> { + if (list.size() != 1) return DataResult.error(() -> "List must contain exactly one element"); + return DataResult.success(list.get(0)); + }), codec.listOf()); + } } From d53a0d0159c21b855f2836b08fc985edbd56df4d Mon Sep 17 00:00:00 2001 From: brachy84 Date: Mon, 18 May 2026 22:01:40 +0200 Subject: [PATCH 18/26] make FluidDrawable consistent with ItemDrawable --- .../modularui/drawable/FluidDrawable.java | 118 ++++++++++++++++-- .../brachy/modularui/drawable/GuiDraw.java | 7 +- 2 files changed, 110 insertions(+), 15 deletions(-) diff --git a/src/main/java/brachy/modularui/drawable/FluidDrawable.java b/src/main/java/brachy/modularui/drawable/FluidDrawable.java index f0622eb..a20b40a 100644 --- a/src/main/java/brachy/modularui/drawable/FluidDrawable.java +++ b/src/main/java/brachy/modularui/drawable/FluidDrawable.java @@ -3,29 +3,69 @@ import brachy.modularui.api.drawable.IDrawable; import brachy.modularui.screen.viewport.GuiContext; import brachy.modularui.theme.WidgetTheme; +import brachy.modularui.utils.serialization.codec.CodecUtil; +import brachy.modularui.utils.serialization.codec.MutableObjectCodec; import brachy.modularui.widget.Widget; +import net.minecraft.Util; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.world.level.material.Fluid; +import com.mojang.serialization.Codec; import net.minecraftforge.fluids.FluidStack; -import org.jetbrains.annotations.NotNull; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; +import org.apache.commons.lang3.ArrayUtils; +import org.jetbrains.annotations.Nullable; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +@Accessors(fluent = true, chain = true) public class FluidDrawable implements IDrawable { - private FluidStack fluid = null; + public static final Codec CODEC = MutableObjectCodec.drawableBuilder(FluidDrawable::new) + .add("fluids", FluidDrawable::fluids, FluidDrawable::getFluidList, CodecUtil.listLike(FluidStack.CODEC)).alias("fluid") + .addOpt("cycleTime", FluidDrawable::cycleTime, FluidDrawable::cycleTime, Codec.INT, 1000) + .build(); + + @Getter + private FluidStack[] fluids; + @Getter + @Setter + private int cycleTime; + + public FluidDrawable() { + this(new FluidStack[0]); + } + + public FluidDrawable(FluidStack... fluid) { + fluids(fluid); + } + + public FluidDrawable(FluidStack fluid) { + fluid(fluid); + } + + public FluidDrawable(Fluid fluid) { + fluid(fluid); + } - public FluidDrawable() {} + public FluidDrawable(Fluid fluid, int amount) { + fluid(fluid, amount); + } - /** - * Takes a fluid stack, it can be null but will not draw anything - * - * @param fluid - fluid stack to draw - */ - public FluidDrawable(@NotNull FluidStack fluid) { - setFluid(fluid); + public FluidDrawable(Fluid fluid, int amount, @Nullable CompoundTag tag) { + fluid(fluid, amount, tag); } @Override public void draw(GuiContext context, int x, int y, int width, int height, WidgetTheme widgetTheme) { + if (this.fluids.length == 0) return; + FluidStack fluid = this.fluids.length == 1 ? this.fluids[0] : + this.fluids[(int) (Util.getMillis() % (this.cycleTime * this.fluids.length)) / this.cycleTime]; GuiDraw.drawFluidTexture(context.getGraphics(), fluid, x, y, width, height, context.getCurrentDrawingZ()); } @@ -44,8 +84,62 @@ public Widget asWidget() { return IDrawable.super.asWidget().size(16); } - public FluidDrawable setFluid(FluidStack fluid) { - this.fluid = fluid; + public List getFluidList() { + return Arrays.asList(this.fluids); + } + + public FluidDrawable fluids(Collection fluids) { + return fluids(fluids.toArray(FluidStack[]::new)); + } + + public FluidDrawable fluids(FluidStack... fluids) { + this.fluids = fluids; + return this; + } + + public FluidDrawable fluid(FluidStack fluid) { + if (this.fluids.length != 1) { + this.fluids = new FluidStack[1]; + } + this.fluids[0] = fluid; return this; } + + public FluidDrawable fluid(Fluid fluid) { + return fluid(fluid, 1, null); + } + + public FluidDrawable fluid(Fluid fluid, int amount) { + return fluid(fluid, amount, null); + } + + public FluidDrawable fluid(Fluid fluid, int amount, @Nullable CompoundTag tag) { + FluidStack fluidStack = new FluidStack(fluid, amount); + fluidStack.setTag(tag); + return fluid(fluidStack); + } + + public FluidDrawable addFluid(FluidStack fluid) { + this.fluids = ArrayUtils.add(this.fluids, fluid); + return this; + } + + public FluidDrawable addFluid(Fluid fluid) { + return addFluid(fluid, 1, null); + } + + public FluidDrawable addFluid(Fluid fluid, int amount) { + return addFluid(fluid, amount, null); + } + + public FluidDrawable addFluid(Fluid fluid, int amount, @Nullable CompoundTag tag) { + FluidStack fluidStack = new FluidStack(fluid, amount); + fluidStack.setTag(tag); + return addFluid(fluidStack); + } + + @Override + public String getTypeName() { + return "fluid"; + } } diff --git a/src/main/java/brachy/modularui/drawable/GuiDraw.java b/src/main/java/brachy/modularui/drawable/GuiDraw.java index e1ffd07..250e18e 100644 --- a/src/main/java/brachy/modularui/drawable/GuiDraw.java +++ b/src/main/java/brachy/modularui/drawable/GuiDraw.java @@ -4,7 +4,6 @@ import brachy.modularui.api.drawable.IRichTextBuilder; import brachy.modularui.client.GuiSpriteManager; import brachy.modularui.drawable.text.TextRenderer; -import brachy.modularui.screen.RichTooltip; import brachy.modularui.screen.event.RichTooltipEvent; import brachy.modularui.screen.viewport.GuiContext; import brachy.modularui.screen.viewport.ModularGuiContext; @@ -601,8 +600,10 @@ public static void drawEntityLookingAtAngle(GuiGraphics graph oldYHeadRotO = livingEntity.yHeadRotO; oldYHeadRot = livingEntity.yHeadRot; - livingEntity.yBodyRotO = livingEntity.yBodyRot = 180.0f + xAngle * 20.0f; - livingEntity.yHeadRotO = livingEntity.yHeadRot = entity.getYRot(); + livingEntity.yBodyRot = 180.0f + xAngle * 20.0f; + livingEntity.yHeadRot = entity.getYRot(); + livingEntity.yBodyRotO = livingEntity.yBodyRot; + livingEntity.yHeadRotO = livingEntity.yHeadRot; } // skip rotating the render by 180° on the Z axis here, because we always do that in setupDrawEntity From e822daea04d88b66cbf49a7cd7cdd386b458644f Mon Sep 17 00:00:00 2001 From: brachy84 Date: Mon, 18 May 2026 22:07:16 +0200 Subject: [PATCH 19/26] last drawable codec --- .../modularui/drawable/SubAreaDrawable.java | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/main/java/brachy/modularui/drawable/SubAreaDrawable.java b/src/main/java/brachy/modularui/drawable/SubAreaDrawable.java index 76fcd5e..2c0a513 100644 --- a/src/main/java/brachy/modularui/drawable/SubAreaDrawable.java +++ b/src/main/java/brachy/modularui/drawable/SubAreaDrawable.java @@ -1,19 +1,34 @@ package brachy.modularui.drawable; import brachy.modularui.api.drawable.IDrawable; - import brachy.modularui.screen.viewport.GuiContext; import brachy.modularui.theme.WidgetTheme; - import brachy.modularui.utils.Interpolations; import brachy.modularui.utils.math.MathUtils; +import brachy.modularui.utils.serialization.codec.MutableObjectCodec; + +import com.mojang.serialization.Codec; +import lombok.Getter; import org.jetbrains.annotations.Nullable; public class SubAreaDrawable extends DelegateDrawable { + public static final Codec CODEC = MutableObjectCodec.drawableBuilder(SubAreaDrawable::new) + .add("drawable", SubAreaDrawable::drawable, SubAreaDrawable::getWrappedDrawable, IDrawable.CODEC) + .addOpt("u0", SubAreaDrawable::u0, SubAreaDrawable::getU0, Codec.FLOAT, 0f).alias("uStart") + .addOpt("v0", SubAreaDrawable::v0, SubAreaDrawable::getV0, Codec.FLOAT, 0f).alias("vStart") + .addOpt("u1", SubAreaDrawable::u1, SubAreaDrawable::getU1, Codec.FLOAT, 1f).alias("uEnd") + .addOpt("v1", SubAreaDrawable::v1, SubAreaDrawable::getV1, Codec.FLOAT, 1f).alias("vEnd") + .build(); + + @Getter private float u0 = 0, v0 = 0, u1 = 1, v1 = 1; + private SubAreaDrawable() { + super(IDrawable.EMPTY); + } + public SubAreaDrawable(@Nullable IDrawable drawable) { super(drawable); } From 0e0259368223770ac348e54fd2eb2870daa9c218 Mon Sep 17 00:00:00 2001 From: brachy84 Date: Tue, 19 May 2026 19:31:05 +0200 Subject: [PATCH 20/26] some fixes --- .../modularui/drawable/FluidDrawable.java | 7 +++++-- .../brachy/modularui/drawable/ItemDrawable.java | 7 +++++-- .../brachy/modularui/drawable/Scrollbar.java | 10 ++++++++++ .../brachy/modularui/drawable/UITexture.java | 8 +++++--- .../utils/serialization/json/JsonHelper.java | 17 ++++++++--------- 5 files changed, 33 insertions(+), 16 deletions(-) diff --git a/src/main/java/brachy/modularui/drawable/FluidDrawable.java b/src/main/java/brachy/modularui/drawable/FluidDrawable.java index a20b40a..c6a5c34 100644 --- a/src/main/java/brachy/modularui/drawable/FluidDrawable.java +++ b/src/main/java/brachy/modularui/drawable/FluidDrawable.java @@ -31,8 +31,7 @@ public class FluidDrawable implements IDrawable { .addOpt("cycleTime", FluidDrawable::cycleTime, FluidDrawable::cycleTime, Codec.INT, 1000) .build(); - @Getter - private FluidStack[] fluids; + private FluidStack[] fluids = new FluidStack[0]; @Getter @Setter private int cycleTime; @@ -88,6 +87,10 @@ public List getFluidList() { return Arrays.asList(this.fluids); } + public FluidStack[] getFluids() { + return this.fluids; + } + public FluidDrawable fluids(Collection fluids) { return fluids(fluids.toArray(FluidStack[]::new)); } diff --git a/src/main/java/brachy/modularui/drawable/ItemDrawable.java b/src/main/java/brachy/modularui/drawable/ItemDrawable.java index 14e8f40..144a96a 100755 --- a/src/main/java/brachy/modularui/drawable/ItemDrawable.java +++ b/src/main/java/brachy/modularui/drawable/ItemDrawable.java @@ -34,8 +34,7 @@ public class ItemDrawable implements IDrawable, IJsonSerializable .addOpt("cycleTime", ItemDrawable::cycleTime, ItemDrawable::cycleTime, Codec.INT, 1000) .build(); - @Getter - private ItemStack[] items; + private ItemStack[] items = new ItemStack[0]; @Getter @Setter private int cycleTime = 1000; @@ -89,6 +88,10 @@ public int getDefaultWidth() { return 16; } + public ItemStack[] getItems() { + return this.items; + } + public void ingredient(Ingredient ingredient) { items(ingredient.getItems()); } diff --git a/src/main/java/brachy/modularui/drawable/Scrollbar.java b/src/main/java/brachy/modularui/drawable/Scrollbar.java index f3b35c4..c1713e7 100755 --- a/src/main/java/brachy/modularui/drawable/Scrollbar.java +++ b/src/main/java/brachy/modularui/drawable/Scrollbar.java @@ -7,6 +7,8 @@ import brachy.modularui.utils.Color; import brachy.modularui.utils.serialization.json.JsonHelper; +import com.mojang.serialization.Codec; + import com.google.gson.JsonObject; public record Scrollbar(boolean striped) implements IDrawable, IJsonSerializable { @@ -14,6 +16,14 @@ public record Scrollbar(boolean striped) implements IDrawable, IJsonSerializable public static final Scrollbar DEFAULT = new Scrollbar(false); public static final Scrollbar VANILLA = new Scrollbar(true); + public static Scrollbar get(boolean striped) { + return striped ? VANILLA : DEFAULT; + } + + public static final Codec CODEC = IDrawable.CODECS.register( + Codec.BOOL.fieldOf("striped").xmap(Scrollbar::get, Scrollbar::striped).codec(), + "scrollbar", "Scrollbar"); + public static Scrollbar ofJson(JsonObject json) { if (JsonHelper.getBoolean(json, false, "striped", "vanilla")) { return VANILLA; diff --git a/src/main/java/brachy/modularui/drawable/UITexture.java b/src/main/java/brachy/modularui/drawable/UITexture.java index b1f3f6a..ebbfaaa 100644 --- a/src/main/java/brachy/modularui/drawable/UITexture.java +++ b/src/main/java/brachy/modularui/drawable/UITexture.java @@ -33,7 +33,8 @@ public class UITexture implements IDrawable, IJsonSerializable { public static final Codec CODEC_FROM_BUILDER = Builder.CODEC.xmap(Builder::buildForCodec, UITexture::toBuilder); public static final Codec CODEC_FROM_NAME = ExtraCodecs.stringResolverCodec(DrawableSerialization::getTextureId, DrawableSerialization::getTexture); - public static final Codec CODEC = IDrawable.CODECS.register("texture", CodecUtil.chainedCodec(CODEC_FROM_NAME, CODEC_FROM_BUILDER)); + public static final Codec CODEC = IDrawable.CODECS.register("texture", + CodecUtil.chainedCodec(CODEC_FROM_NAME.fieldOf("name").codec(), CODEC_FROM_BUILDER)); public static final UITexture DEFAULT = fullImage("gui/options_background", ColorType.DEFAULT); public static final FileToIdConverter GUI_TEXTURE_ID_CONVERTER = new FileToIdConverter("textures/gui", ".png"); @@ -631,7 +632,7 @@ public UITexture build() { .resultOrPartial(s -> { throw new IllegalArgumentException(s); }).map(texture -> { - //DrawableSerialization.registerTexture(this.name, texture); + DrawableSerialization.registerTexture(this.name, texture); return texture; }).map(texture -> this.colorOverride != 0 ? texture.withColorOverride(this.colorOverride) : texture) .orElseThrow(); @@ -672,7 +673,8 @@ private DataResult create() { return DataResult.error(() -> "UV values must be 0 - 1"); } if (this.bl > 0 || this.bt > 0 || this.br > 0 || this.bb > 0) { - if (this.iw <= 0 || this.ih <= 0) return DataResult.error(() -> "Image size must be > 0 for adaptable textures (border > 0)"); + if (this.iw <= 0 || this.ih <= 0) + return DataResult.error(() -> "Image size must be > 0 for adaptable textures (border > 0)"); return DataResult.success(new AdaptableUITexture(this.location, this.u0, this.v0, this.u1, this.v1, this.colorType, this.nonOpaque, 0, this.iw, this.ih, this.bl, this.bt, this.br, this.bb, this.tiled)); } diff --git a/src/main/java/brachy/modularui/utils/serialization/json/JsonHelper.java b/src/main/java/brachy/modularui/utils/serialization/json/JsonHelper.java index ae2893a..fc4a67a 100755 --- a/src/main/java/brachy/modularui/utils/serialization/json/JsonHelper.java +++ b/src/main/java/brachy/modularui/utils/serialization/json/JsonHelper.java @@ -5,9 +5,12 @@ import brachy.modularui.drawable.DrawableSerialization; import brachy.modularui.utils.Alignment; import brachy.modularui.utils.Color; - import brachy.modularui.utils.serialization.codec.MutableCodec; +import com.mojang.serialization.Decoder; +import com.mojang.serialization.Encoder; +import com.mojang.serialization.JsonOps; + import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.JsonDeserializationContext; @@ -16,10 +19,6 @@ import com.google.gson.JsonObject; import com.google.gson.JsonParser; import com.google.gson.JsonSerializationContext; - -import com.mojang.serialization.Codec; -import com.mojang.serialization.JsonOps; - import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -240,13 +239,13 @@ public static JsonObject makeJson(Consumer writer) { return json; } - public static JsonElement toJson(Codec codec, T input) { + public static JsonElement toJson(Encoder codec, T input) { var d = codec.encodeStart(JsonOps.INSTANCE, input); if (d.error().isPresent()) ModularUI.LOGGER.error("Error encoding '{}' to json: {}", input, d.error().get()); return d.result().orElse(JsonNull.INSTANCE); } - public static String toJsonString(Codec codec, T input) { + public static String toJsonString(Encoder codec, T input) { return GSON.toJson(toJson(codec, input)); } @@ -256,7 +255,7 @@ public static T fromJson(MutableCodec codec, JsonElement json, T instance return instance; } - public static T fromJson(Codec codec, JsonElement json) { + public static T fromJson(Decoder codec, JsonElement json) { var d = codec.parse(JsonOps.INSTANCE, json); if (d.error().isPresent()) ModularUI.LOGGER.error("Error decoding from json: {}", d.error().get()); return d.result().orElseThrow(); @@ -266,7 +265,7 @@ public static T fromJsonString(MutableCodec codec, String json, T instanc return fromJson(codec, JsonParser.parseString(json), instance); } - public static T fromJsonString(Codec codec, String json) { + public static T fromJsonString(Decoder codec, String json) { return fromJson(codec, JsonParser.parseString(json)); } } From d0eab8dd9c3c245a291640a13eb5db3146e6bf46 Mon Sep 17 00:00:00 2001 From: brachy84 Date: Tue, 19 May 2026 19:51:54 +0200 Subject: [PATCH 21/26] widget theme json parsing via codec --- .../java/brachy/modularui/api/ITheme.java | 37 ++++++- .../java/brachy/modularui/api/IThemeApi.java | 86 +++++++++------- .../modularui/overlay/DebugOverlay.java | 19 +++- .../brachy/modularui/theme/ImmutableJson.java | 44 +++++++++ .../theme/JsonWidgetThemeStorage.java | 19 ++++ .../modularui/theme/SelectableTheme.java | 47 +++++---- .../brachy/modularui/theme/SlotTheme.java | 8 -- .../modularui/theme/TextFieldTheme.java | 9 -- .../java/brachy/modularui/theme/ThemeAPI.java | 8 +- .../brachy/modularui/theme/ThemeManager.java | 69 +++++++------ .../brachy/modularui/theme/WidgetTheme.java | 38 ++------ .../modularui/theme/WidgetThemeCodec.java | 67 +++++++++++++ .../modularui/theme/WidgetThemeEntry.java | 44 +++++++++ .../modularui/theme/WidgetThemeField.java | 49 ++++++++++ .../modularui/theme/WidgetThemeKey.java | 45 +++++---- .../theme/WidgetThemeKeyBuilder.java | 97 +++++++++++++------ .../modularui/theme/WidgetThemeMap.java | 14 +-- .../modularui/theme/WidgetThemeMerger.java | 56 +++++++++++ .../modularui/theme/WidgetThemeParser.java | 22 ----- .../utils/serialization/codec/CodecUtil.java | 97 +++++++++++++++++-- .../assets/modularui/themes/context_menu.json | 2 +- 21 files changed, 655 insertions(+), 222 deletions(-) create mode 100644 src/main/java/brachy/modularui/theme/ImmutableJson.java create mode 100644 src/main/java/brachy/modularui/theme/JsonWidgetThemeStorage.java create mode 100644 src/main/java/brachy/modularui/theme/WidgetThemeCodec.java create mode 100644 src/main/java/brachy/modularui/theme/WidgetThemeField.java create mode 100644 src/main/java/brachy/modularui/theme/WidgetThemeMerger.java delete mode 100755 src/main/java/brachy/modularui/theme/WidgetThemeParser.java diff --git a/src/main/java/brachy/modularui/api/ITheme.java b/src/main/java/brachy/modularui/api/ITheme.java index 85660f9..5313f4c 100644 --- a/src/main/java/brachy/modularui/api/ITheme.java +++ b/src/main/java/brachy/modularui/api/ITheme.java @@ -6,16 +6,51 @@ import brachy.modularui.theme.WidgetTheme; import brachy.modularui.theme.WidgetThemeEntry; import brachy.modularui.theme.WidgetThemeKey; +import brachy.modularui.utils.serialization.codec.CodecUtil; +import com.mojang.serialization.DataResult; +import com.mojang.serialization.DynamicOps; +import com.mojang.serialization.Encoder; + +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; import org.jetbrains.annotations.UnmodifiableView; +import java.util.ArrayList; import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; /** - * A theme is parsed from json and contains style information like color or background texture. + * A theme is parsed from JSON and contains style information like color or background texture. */ public interface ITheme { + Encoder ENCODER = new Encoder<>() { + @Override + public DataResult encode(ITheme input, DynamicOps ops, T prefix) { + prefix = ops.set(prefix, "id", ops.createString(input.getId())); + prefix = ops.set(prefix, "parent", ops.createString(input.getParentTheme().getId())); + List errors = new ArrayList<>(); + prefix = input.getFallback().encodeFallback(ops, prefix, errors); + var mapBuilder = CodecUtil.mergePrefixToMapBuilder(ops, prefix); + Map> encodedThemes = new Object2ObjectOpenHashMap<>(); + for (WidgetThemeEntry entry : input.getWidgetThemes()) { + if (entry.key() == IThemeApi.FALLBACK) continue; + var set = encodedThemes.computeIfAbsent(entry.key().getName(), k -> new ObjectOpenHashSet<>()); + if (entry.key().isSubWidgetTheme() && set.contains(entry.theme()) && set.contains(entry.hoverTheme())) continue; + entry.encode(ops, mapBuilder, errors); + set.add(entry.theme()); + set.add(entry.hoverTheme()); + } + if (!errors.isEmpty()) { + return DataResult.error(() -> String.format("Error while encoding theme '%s': %s", input.getId(), errors)); + } + return DataResult.success(ops.createMap(mapBuilder.build())); + } + }; + /** * @return the master default theme. */ diff --git a/src/main/java/brachy/modularui/api/IThemeApi.java b/src/main/java/brachy/modularui/api/IThemeApi.java index 6f27180..689c121 100644 --- a/src/main/java/brachy/modularui/api/IThemeApi.java +++ b/src/main/java/brachy/modularui/api/IThemeApi.java @@ -11,11 +11,15 @@ import brachy.modularui.theme.ThemeAPI; import brachy.modularui.theme.ThemeBuilder; import brachy.modularui.theme.WidgetTheme; +import brachy.modularui.theme.WidgetThemeCodec; import brachy.modularui.theme.WidgetThemeKey; import brachy.modularui.theme.WidgetThemeKeyBuilder; -import brachy.modularui.theme.WidgetThemeParser; +import brachy.modularui.theme.WidgetThemeMerger; +import brachy.modularui.utils.Color; import brachy.modularui.utils.serialization.json.JsonBuilder; +import com.mojang.serialization.Codec; + import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; @@ -30,46 +34,85 @@ @ApiStatus.NonExtendable public interface IThemeApi { + // properties + String PARENT = "parent"; + String DEFAULT_WIDTH = "defaultWidth"; + String DEFAULT_HEIGHT = "defaultHeight"; + String BACKGROUND = "background"; + String COLOR = "color"; + String TEXT_COLOR = "textColor"; + String TEXT_SHADOW = "textShadow"; + String ICON_COLOR = "iconColor"; + String SLOT_HOVER_COLOR = "slotHoverColor"; + String MARKED_COLOR = "markedColor"; + String HINT_COLOR = "hintColor"; + String SELECTED_BACKGROUND = "selectedBackground"; + String SELECTED_COLOR = "selectedColor"; + String SELECTED_TEXT_COLOR = "selectedTextColor"; + String SELECTED_TEXT_SHADOW = "selectedTextShadow"; + String SELECTED_ICON_COLOR = "selectedIconColor"; + // widget themes WidgetThemeKey FALLBACK = get().widgetThemeKeyBuilder("default", WidgetTheme.class) .defaultTheme(WidgetTheme.darkTextNoShadow(18, 18, null)) + .field(DEFAULT_WIDTH, int.class, Codec.INT, WidgetTheme::getDefaultWidth) + .field(DEFAULT_HEIGHT, int.class, Codec.INT, WidgetTheme::getDefaultHeight) + .field(BACKGROUND, IDrawable.class, IDrawable.CODEC, WidgetTheme::getBackground) + .fallbackField(COLOR, int.class, Color.CODEC, WidgetTheme::getColor) + .fallbackField(TEXT_COLOR, int.class, Color.CODEC, WidgetTheme::getTextColor) + .fallbackField(TEXT_SHADOW, boolean.class, Codec.BOOL, WidgetTheme::isTextShadow) + .fallbackField(ICON_COLOR, int.class, Color.CODEC, WidgetTheme::getIconColor) .register(); WidgetThemeKey PANEL = get().widgetThemeKeyBuilder("panel", WidgetTheme.class) .defaultTheme(WidgetTheme.darkTextNoShadow(176, 166, GuiTextures.MC_BACKGROUND)) + .fieldsOf(FALLBACK) .register(); WidgetThemeKey BUTTON = get().widgetThemeKeyBuilder("button", WidgetTheme.class) .defaultTheme(WidgetTheme.whiteTextShadow(18, 18, GuiTextures.MC_BUTTON)) .defaultHoverTheme(WidgetTheme.whiteTextShadow(18, 18, GuiTextures.MC_BUTTON_HOVERED)) + .fieldsOf(FALLBACK) .register(); WidgetThemeKey CLOSE_BUTTON = get().widgetThemeKeyBuilder("closeButton", WidgetTheme.class) .defaultTheme(WidgetTheme.whiteTextShadow(10, 10, GuiTextures.MC_BUTTON)) .defaultHoverTheme(WidgetTheme.whiteTextShadow(10, 10, GuiTextures.MC_BUTTON_HOVERED)) + .fieldsOf(FALLBACK) .register(); WidgetThemeKey SCROLLBAR = get().widgetThemeKeyBuilder("scrollbar", WidgetTheme.class) .defaultTheme(WidgetTheme.darkTextNoShadow(4, 4, Scrollbar.VANILLA)) + .fieldsOf(FALLBACK) .register(); WidgetThemeKey ITEM_SLOT = get().widgetThemeKeyBuilder("itemSlot", SlotTheme.class) .defaultTheme(new SlotTheme(GuiTextures.SLOT_ITEM)) + .fieldsOf(FALLBACK) + .field(SLOT_HOVER_COLOR, int.class, Color.CODEC, SlotTheme::getSlotHoverColor) .register(); WidgetThemeKey FLUID_SLOT = get().widgetThemeKeyBuilder("fluidSlot", SlotTheme.class) .defaultTheme(new SlotTheme(GuiTextures.SLOT_FLUID)) + .fieldsOf(ITEM_SLOT) .register(); WidgetThemeKey TEXT_FIELD = get().widgetThemeKeyBuilder("textField", TextFieldTheme.class) .defaultTheme(new TextFieldTheme(0xFF2F72A8, 0xFF5F5F5F)) + .fieldsOf(FALLBACK) + .field(MARKED_COLOR, int.class, Color.CODEC, TextFieldTheme::getMarkedColor) + .field(HINT_COLOR, int.class, Color.CODEC, TextFieldTheme::getHintColor) .register(); WidgetThemeKey TOGGLE_BUTTON = get().widgetThemeKeyBuilder("toggleButton", SelectableTheme.class) - .defaultTheme( - SelectableTheme.whiteTextShadow(18, 18, GuiTextures.MC_BUTTON, GuiTextures.MC_BUTTON_DISABLED)) - .defaultHoverTheme(SelectableTheme.whiteTextShadow(18, 18, GuiTextures.MC_BUTTON_HOVERED, - IDrawable.NONE)) + .defaultTheme(SelectableTheme.whiteTextShadow(18, 18, GuiTextures.MC_BUTTON, GuiTextures.MC_BUTTON_DISABLED)) + .defaultHoverTheme(SelectableTheme.whiteTextShadow(18, 18, GuiTextures.MC_BUTTON_HOVERED, IDrawable.NONE)) + .fieldsOf(FALLBACK) + .field(SELECTED_BACKGROUND, IDrawable.class, IDrawable.CODEC, SelectableTheme::getSelectedBackground) + .field(SELECTED_COLOR, int.class, Color.CODEC, SelectableTheme::getSelectedColor) + .field(SELECTED_TEXT_COLOR, int.class, Color.CODEC, SelectableTheme::getSelectedTextColor) + .field(SELECTED_TEXT_SHADOW, boolean.class, Codec.BOOL, SelectableTheme::isSelectedTextShadow) + .field(SELECTED_ICON_COLOR, int.class, Color.CODEC, SelectableTheme::getSelectedIconColor) .register(); // subwidget themes @@ -81,24 +124,6 @@ public interface IThemeApi { String HOVER_SUFFIX = ":hover"; - // properties - String PARENT = "parent"; - String DEFAULT_WIDTH = "defaultWidth"; - String DEFAULT_HEIGHT = "defaultHeight"; - String BACKGROUND = "background"; - String HOVER_BACKGROUND = "hoverBackground"; - String COLOR = "color"; - String TEXT_COLOR = "textColor"; - String TEXT_SHADOW = "textShadow"; - String ICON_COLOR = "iconColor"; - String SLOT_HOVER_COLOR = "slotHoverColor"; - String MARKED_COLOR = "markedColor"; - String HINT_COLOR = "hintColor"; - String SELECTED_BACKGROUND = "selectedBackground"; - String SELECTED_COLOR = "selectedColor"; - String SELECTED_TEXT_COLOR = "selectedTextColor"; - String SELECTED_TEXT_SHADOW = "selectedTextShadow"; - String SELECTED_ICON_COLOR = "selectedIconColor"; /** * @return the default api implementation @@ -219,21 +244,12 @@ default void registerThemeForScreen(String owner, String name, String theme) { */ void registerThemeForScreen(String screen, String theme); - /** - * Registers a widget theme. It is recommended to store the resulting key in a static variable and make it - * accessible by public methods. - * - * @param id id of the widget theme - * @param defaultTheme the fallback widget theme - * @param defaultHoverTheme the fallback hover widget theme - * @param parser the widget theme json parser function. This is usually another constructor. - * @return key to access the widget theme - */ + @Deprecated WidgetThemeKey registerWidgetTheme(String id, T defaultTheme, T defaultHoverTheme, - WidgetThemeParser parser); + WidgetThemeMerger merger, WidgetThemeCodec codec); default WidgetThemeKeyBuilder widgetThemeKeyBuilder(String id, Class type) { - return new WidgetThemeKeyBuilder<>(id); + return new WidgetThemeKeyBuilder<>(id, type); } @UnmodifiableView diff --git a/src/main/java/brachy/modularui/overlay/DebugOverlay.java b/src/main/java/brachy/modularui/overlay/DebugOverlay.java index 0cfdf43..6493c38 100644 --- a/src/main/java/brachy/modularui/overlay/DebugOverlay.java +++ b/src/main/java/brachy/modularui/overlay/DebugOverlay.java @@ -3,6 +3,7 @@ import brachy.modularui.ModularUI; import brachy.modularui.ModularUIConfig; import brachy.modularui.api.IMuiScreen; +import brachy.modularui.api.ITheme; import brachy.modularui.api.drawable.IIcon; import brachy.modularui.api.drawable.Text; import brachy.modularui.api.value.IBoolValue; @@ -12,9 +13,11 @@ import brachy.modularui.drawable.Rectangle; import brachy.modularui.screen.CustomModularScreen; import brachy.modularui.screen.ModularPanel; +import brachy.modularui.screen.viewport.GuiContext; import brachy.modularui.screen.viewport.ModularGuiContext; import brachy.modularui.utils.Color; import brachy.modularui.utils.TreeUtil; +import brachy.modularui.utils.serialization.json.JsonHelper; import brachy.modularui.value.BoolValue; import brachy.modularui.widget.WidgetTree; import brachy.modularui.widgets.ButtonWidget; @@ -64,7 +67,7 @@ public DebugOverlay(IMuiScreen screen) { .widthRel(1f) .invisible() .overlay(Text.str("Print widget trees")) - .onMousePressed((context1, b) -> this.logWidgetTrees(b))) + .onMousePressed(this::logWidgetTrees)) .child(new ButtonWidget<>().name("print_resizer_tree_button") .height(12) .widthRel(1f) @@ -74,6 +77,12 @@ public DebugOverlay(IMuiScreen screen) { TreeUtil.print(parent.screen().getResizeNode()); return true; })) + .child(new ButtonWidget<>().name("print_theme") + .height(12) + .widthRel(1f) + .invisible() + .overlay(Text.str("Print Theme json")) + .onMousePressed(this::logTheme)) .child(new ContextMenuButton<>("menu_hover_info") .height(10) .widthRel(1f) @@ -134,10 +143,16 @@ public static IWidget toggleOption(int i, String name, String field) { .name(Text.str(name))); } - private boolean logWidgetTrees(int b) { + private boolean logWidgetTrees(GuiContext context, int b) { for (ModularPanel panel : parent.screen().getPanelManager().getOpenPanels()) { WidgetTree.print(panel); } return true; } + + private boolean logTheme(GuiContext context, int b) { + var s = JsonHelper.toJsonString(ITheme.ENCODER, this.parent.screen().getCurrentTheme()); + ModularUI.LOGGER.info("Theme JSON of screen {}:\n{}", this.parent.screen(), s); + return true; + } } diff --git a/src/main/java/brachy/modularui/theme/ImmutableJson.java b/src/main/java/brachy/modularui/theme/ImmutableJson.java new file mode 100644 index 0000000..7b5b999 --- /dev/null +++ b/src/main/java/brachy/modularui/theme/ImmutableJson.java @@ -0,0 +1,44 @@ +package brachy.modularui.theme; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import it.unimi.dsi.fastutil.objects.Object2ReferenceOpenHashMap; + +import java.util.Collections; +import java.util.Map; + +public class ImmutableJson { + + public static final ImmutableJson EMPTY = new ImmutableJson(Collections.emptyMap()); + + private final Map json; + + public static ImmutableJson of(JsonObject json) { + if (json == null || json.asMap().isEmpty()) { + return EMPTY; + } + return new ImmutableJson(json); + } + + private ImmutableJson(Map json) { + this.json = json; + } + + private ImmutableJson(JsonObject json) { + this(new Object2ReferenceOpenHashMap<>(json.asMap())); + } + + public boolean has(String key) { + return this.json.containsKey(key); + } + + public JsonElement get(String key) { + return this.json.get(key); + } + + public JsonObject toJson() { + JsonObject json = new JsonObject(); + json.asMap().putAll(this.json); + return json; + } +} diff --git a/src/main/java/brachy/modularui/theme/JsonWidgetThemeStorage.java b/src/main/java/brachy/modularui/theme/JsonWidgetThemeStorage.java new file mode 100644 index 0000000..cbdf47c --- /dev/null +++ b/src/main/java/brachy/modularui/theme/JsonWidgetThemeStorage.java @@ -0,0 +1,19 @@ +package brachy.modularui.theme; + +import it.unimi.dsi.fastutil.objects.Reference2ReferenceOpenHashMap; + +import java.util.Map; + +class JsonWidgetThemeStorage { + + private final Map jsons = new Reference2ReferenceOpenHashMap<>(); + + public ImmutableJson get(WidgetThemeKey key, T theme) { + ImmutableJson json = this.jsons.get(theme); + if (json == null) { + json = ImmutableJson.of(key.encodeJson(theme)); + this.jsons.put(theme, json); + } + return json; + } +} diff --git a/src/main/java/brachy/modularui/theme/SelectableTheme.java b/src/main/java/brachy/modularui/theme/SelectableTheme.java index 82c8b5a..e166133 100755 --- a/src/main/java/brachy/modularui/theme/SelectableTheme.java +++ b/src/main/java/brachy/modularui/theme/SelectableTheme.java @@ -5,9 +5,7 @@ import brachy.modularui.drawable.DrawableSerialization; import brachy.modularui.utils.Color; import brachy.modularui.utils.serialization.json.JsonBuilder; -import brachy.modularui.utils.serialization.json.JsonHelper; -import com.google.gson.JsonObject; import lombok.Getter; import org.jetbrains.annotations.Nullable; @@ -34,25 +32,16 @@ public SelectableTheme(int defaultWidth, int defaultHeight, @Nullable IDrawable int color, int textColor, boolean textShadow, int iconColor, @Nullable IDrawable selectedBackground, int selectedColor, int selectedTextColor, boolean selectedTextShadow, int selectedIconColor) { - super(defaultWidth, defaultHeight, background, color, textColor, textShadow, iconColor); - this.selected = new WidgetTheme(defaultWidth, defaultHeight, selectedBackground, selectedColor, - selectedTextColor, selectedTextShadow, selectedIconColor); + this(defaultWidth, defaultHeight, background, color, textColor, textShadow, iconColor, + new WidgetTheme(defaultWidth, defaultHeight, selectedBackground, selectedColor, + selectedTextColor, selectedTextShadow, selectedIconColor)); } - public SelectableTheme(SelectableTheme parent, JsonObject json, JsonObject fallback) { - super(parent, json, fallback); - IDrawable selectedBackground = JsonHelper.deserializeWithFallback(json, fallback, IDrawable.class, - parent.getSelected().getBackground(), IThemeApi.SELECTED_BACKGROUND); - int selectedColor = JsonHelper.getColorWithFallback(json, fallback, - parent.getSelected().getColor(), IThemeApi.SELECTED_COLOR); - int selectedTextColor = JsonHelper.getColorWithFallback(json, fallback, - parent.getSelected().getTextColor(), IThemeApi.SELECTED_TEXT_COLOR); - boolean selectedTextShadow = JsonHelper.getBoolWithFallback(json, fallback, - parent.getSelected().isTextShadow(), IThemeApi.SELECTED_TEXT_SHADOW); - int selectedIconColor = JsonHelper.getColorWithFallback(json, fallback, - parent.getSelected().getIconColor(), IThemeApi.SELECTED_ICON_COLOR); - this.selected = new WidgetTheme(getDefaultWidth(), getDefaultHeight(), selectedBackground, selectedColor, - selectedTextColor, selectedTextShadow, selectedIconColor); + public SelectableTheme(int defaultWidth, int defaultHeight, @Nullable IDrawable background, + int color, int textColor, boolean textShadow, int iconColor, + WidgetTheme selected) { + super(defaultWidth, defaultHeight, background, color, textColor, textShadow, iconColor); + this.selected = selected; } @Override @@ -62,6 +51,26 @@ public WidgetTheme withNoHoverBackground() { this.selected.isTextShadow(), this.selected.getIconColor()); } + public @Nullable IDrawable getSelectedBackground() { + return this.selected.getBackground(); + } + + public int getSelectedColor() { + return this.selected.getColor(); + } + + public int getSelectedTextColor() { + return this.selected.getTextColor(); + } + + public boolean isSelectedTextShadow() { + return this.selected.isTextShadow(); + } + + public int getSelectedIconColor() { + return this.selected.getIconColor(); + } + public static class Builder> extends WidgetThemeBuilder { diff --git a/src/main/java/brachy/modularui/theme/SlotTheme.java b/src/main/java/brachy/modularui/theme/SlotTheme.java index 3317be9..e8211da 100755 --- a/src/main/java/brachy/modularui/theme/SlotTheme.java +++ b/src/main/java/brachy/modularui/theme/SlotTheme.java @@ -3,9 +3,7 @@ import brachy.modularui.api.IThemeApi; import brachy.modularui.api.drawable.IDrawable; import brachy.modularui.utils.Color; -import brachy.modularui.utils.serialization.json.JsonHelper; -import com.google.gson.JsonObject; import lombok.Getter; import org.jetbrains.annotations.Nullable; @@ -28,12 +26,6 @@ public SlotTheme(int defaultWidth, int defaultHeight, @Nullable IDrawable backgr this.slotHoverColor = slotHoverColor; } - public SlotTheme(SlotTheme parent, JsonObject json, JsonObject fallback) { - super(parent, json, fallback); - this.slotHoverColor = JsonHelper.getColorWithFallback(json, fallback, parent.getSlotHoverColor(), - IThemeApi.SLOT_HOVER_COLOR); - } - @Override public WidgetTheme withNoHoverBackground() { return new SlotTheme(getDefaultWidth(), getDefaultHeight(), IDrawable.NONE, getColor(), getTextColor(), diff --git a/src/main/java/brachy/modularui/theme/TextFieldTheme.java b/src/main/java/brachy/modularui/theme/TextFieldTheme.java index 7481177..f925bd1 100755 --- a/src/main/java/brachy/modularui/theme/TextFieldTheme.java +++ b/src/main/java/brachy/modularui/theme/TextFieldTheme.java @@ -4,9 +4,7 @@ import brachy.modularui.api.drawable.IDrawable; import brachy.modularui.drawable.GuiTextures; import brachy.modularui.utils.Color; -import brachy.modularui.utils.serialization.json.JsonHelper; -import com.google.gson.JsonObject; import lombok.Getter; import org.jetbrains.annotations.Nullable; @@ -29,13 +27,6 @@ public TextFieldTheme(int defaultWidth, int defaultHeight, @Nullable IDrawable b this.hintColor = hintColor; } - public TextFieldTheme(TextFieldTheme parent, JsonObject json, JsonObject fallback) { - super(parent, json, fallback); - this.markedColor = JsonHelper.getColorWithFallback(json, fallback, parent.getMarkedColor(), - IThemeApi.MARKED_COLOR); - this.hintColor = JsonHelper.getColorWithFallback(json, fallback, parent.getHintColor(), IThemeApi.HINT_COLOR); - } - @Override public WidgetTheme withNoHoverBackground() { return new TextFieldTheme(getDefaultWidth(), getDefaultHeight(), IDrawable.NONE, getColor(), getTextColor(), diff --git a/src/main/java/brachy/modularui/theme/ThemeAPI.java b/src/main/java/brachy/modularui/theme/ThemeAPI.java index f5849c7..c42ed12 100644 --- a/src/main/java/brachy/modularui/theme/ThemeAPI.java +++ b/src/main/java/brachy/modularui/theme/ThemeAPI.java @@ -99,11 +99,11 @@ public void registerThemeForScreen(String screen, String theme) { @SuppressWarnings("unchecked") @Override - public WidgetThemeKey registerWidgetTheme(String id, T defaultTheme, T defaultHoverTheme, - WidgetThemeParser parser) { + public WidgetThemeKey registerWidgetTheme(String id, T defaultTheme, T defaultHoverTheme, WidgetThemeMerger merger, WidgetThemeCodec codec) { Objects.requireNonNull(id, "Id for widget theme must not be null"); Objects.requireNonNull(defaultTheme, "Default widget theme must not be null, but is null for id '" + id + "'."); - Objects.requireNonNull(parser, "Parser for widget theme must not be null, but is null for id '" + id + "'."); + Objects.requireNonNull(merger, "Merger for widget theme must not be null, but is null for id '" + id + "'."); + Objects.requireNonNull(codec, "Codec for widget theme must not be null, but is null for id '" + id + "'."); if (WidgetThemeKey.getFromFullName(id) != null) { throw new IllegalStateException("there already is a widget theme for id '" + id + "' registered."); } @@ -112,7 +112,7 @@ public WidgetThemeKey registerWidgetTheme(String id, "' is invalid. Id must only contain letters, numbers, underscores and minus."); } Class type = (Class) defaultTheme.getClass(); - return new WidgetThemeKey<>(type, id, defaultTheme, defaultHoverTheme, parser); + return new WidgetThemeKey<>(type, id, defaultTheme, defaultHoverTheme, merger, codec); } @Override diff --git a/src/main/java/brachy/modularui/theme/ThemeManager.java b/src/main/java/brachy/modularui/theme/ThemeManager.java index 6a3d480..3f199d7 100755 --- a/src/main/java/brachy/modularui/theme/ThemeManager.java +++ b/src/main/java/brachy/modularui/theme/ThemeManager.java @@ -13,6 +13,7 @@ import net.minecraft.server.packs.resources.ResourceManager; import net.minecraft.server.packs.resources.SimplePreparableReloadListener; import net.minecraft.util.profiling.ProfilerFiller; +import com.mojang.serialization.JsonOps; import net.minecraftforge.api.distmarker.Dist; import net.minecraftforge.api.distmarker.OnlyIn; import net.minecraftforge.common.MinecraftForge; @@ -44,9 +45,10 @@ public class ThemeManager extends SimplePreparableReloadListener defaultFallbackWidgetTheme = IThemeApi.get().getDefaultTheme() - .getWidgetTheme(IThemeApi.FALLBACK); - private static final JsonObject emptyJson = new JsonObject(); + protected static final WidgetThemeEntry defaultFallbackWidgetTheme = IThemeApi.get() + .getDefaultTheme().getWidgetTheme(IThemeApi.FALLBACK); + + private static JsonWidgetThemeStorage jsons; public ThemeManager() {} @@ -163,10 +165,12 @@ protected void apply(@NotNull Map> themes, } while (changed); // finally parse and register themes + jsons = new JsonWidgetThemeStorage(); for (ThemeJson themeJson : sortedThemes.values()) { Theme theme = themeJson.deserialize(); ThemeAPI.INSTANCE.registerTheme(theme); } + jsons = null; validateJsonScreenThemes(); MinecraftForge.EVENT_BUS.post(new ReloadThemeEvent.Post()); @@ -316,32 +320,39 @@ private Theme deserialize() { WidgetThemeMap widgetThemes = new WidgetThemeMap(); WidgetThemeEntry parentWidgetTheme = parent.getFallback(); // fallback theme of parent // fallback theme of new theme - WidgetTheme fallback = new WidgetTheme(parentWidgetTheme.theme(), jsonBuilder.getJson(), null); + var incompleteFallbackJson = ImmutableJson.of(jsonBuilder.getJson()); // themes should only inherit values which are declared here + var fallbackJson = IThemeApi.FALLBACK.getMerger().merge( + jsonBuilder.getJson(), + ThemeManager.jsons.get(IThemeApi.FALLBACK, parentWidgetTheme.theme()), + ImmutableJson.EMPTY); + WidgetTheme fallback = IThemeApi.FALLBACK.parseJson(fallbackJson); WidgetTheme fallbackHover = fallback; + var immutableFallbackJson = ThemeManager.jsons.get(IThemeApi.FALLBACK, fallback); JsonObject hoverJson = getJson(jsonBuilder.getJson(), IThemeApi.HOVER_SUFFIX); - if (hoverJson == null) + if (hoverJson == null) { hoverJson = getJson(jsonBuilder.getJson(), IThemeApi.FALLBACK.getFullName() + IThemeApi.HOVER_SUFFIX); + } if (hoverJson != null) { - fallbackHover = new WidgetTheme(fallback, hoverJson, null); + hoverJson = IThemeApi.FALLBACK.getMerger().merge(hoverJson, immutableFallbackJson, ImmutableJson.EMPTY); + fallbackHover = IThemeApi.FALLBACK.parseJson(hoverJson); } - widgetThemes.putTheme(IThemeApi.FALLBACK, - new WidgetThemeEntry<>(IThemeApi.FALLBACK, fallback, fallbackHover)); + widgetThemes.register(IThemeApi.FALLBACK, fallback, fallbackHover); // parse all main widget themes for (WidgetThemeKey key : ThemeAPI.INSTANCE.getWidgetThemeKeys()) { if (key != IThemeApi.FALLBACK) { - parse(widgetThemes, parent, key, jsonBuilder); + parse(widgetThemes, parent, key, jsonBuilder, incompleteFallbackJson); } } return new Theme(this.id, parent, widgetThemes); } private void parse(WidgetThemeMap map, ITheme parent, WidgetThemeKey key, - JsonBuilder json) { - WidgetThemeParser parser = key.getParser(); - JsonObject widgetThemeJson = getJson(json.getJson(), key.getFullName()); - boolean definedStandard = widgetThemeJson != null; + JsonBuilder json, ImmutableJson fallback) { + WidgetThemeMerger merger = key.getMerger(); + JsonObject rawWidgetThemeJson = getJson(json.getJson(), key.getFullName()); + boolean definedStandard = rawWidgetThemeJson != null; JsonObject widgetThemeHoverJson = getJson(json.getJson(), key.getFullName() + IThemeApi.HOVER_SUFFIX); boolean definedHover = widgetThemeHoverJson != null; @@ -355,34 +366,36 @@ private void parse(WidgetThemeMap map, ITheme parent, Wi return; } // we still need to parse non-inherited values (fallback) - widgetThemeJson = emptyJson; - widgetThemeHoverJson = emptyJson; + rawWidgetThemeJson = new JsonObject(); + widgetThemeHoverJson = new JsonObject(); } } - JsonObject fallback = key.isSubWidgetTheme() ? null : json.getJson(); - T widgetTheme; - if (widgetThemeJson != null) { - T parentWidgetTheme = key.isSubWidgetTheme() ? map.getTheme(key.getParent()).theme() : - parent.getWidgetTheme(key).theme(); + if (key.isSubWidgetTheme()) fallback = ImmutableJson.EMPTY; + ImmutableJson widgetThemeJson; + if (rawWidgetThemeJson != null) { + T parentWidgetTheme = key.isSubWidgetTheme() ? map.getTheme(key.getParent()).theme() : parent.getWidgetTheme(key).theme(); // sub widget themes strictly only inherit from their parent widget theme and not the parent theme - widgetTheme = parser.parse(parentWidgetTheme, widgetThemeJson, fallback); + widgetThemeJson = ImmutableJson.of(merger.merge(rawWidgetThemeJson, ThemeManager.jsons.get(key, parentWidgetTheme), fallback)); } else { - widgetTheme = parent.getWidgetTheme(key).theme(); + widgetThemeJson = ThemeManager.jsons.get(key, parent.getWidgetTheme(key).theme()); } if (!definedHover && definedStandard) { // marker to use the standard theme background on hover - widgetThemeJson.addProperty(IThemeApi.BACKGROUND, "none"); - widgetThemeHoverJson = widgetThemeJson; + rawWidgetThemeJson.addProperty(IThemeApi.BACKGROUND, "none"); + widgetThemeHoverJson = rawWidgetThemeJson; } // only inherit from the widget theme if it was actually defined, otherwise use parent - T parentWidgetHoverTheme = definedStandard ? widgetTheme : - parent.getWidgetTheme(key).hoverTheme(); - T widgetThemeHover = parser.parse(parentWidgetHoverTheme, widgetThemeHoverJson, fallback); + ImmutableJson parentWidgetHoverTheme = definedStandard ? widgetThemeJson : ThemeManager.jsons.get(key, parent.getWidgetTheme(key).hoverTheme()); + JsonObject widgetThemeHover = merger.merge(widgetThemeHoverJson, parentWidgetHoverTheme, fallback); + var immutableHoverWidgetTheme = ImmutableJson.of(widgetThemeHover); + + T widgetThemeInstance = key.getCodec().parse(JsonOps.INSTANCE, widgetThemeJson.toJson()).getOrThrow(false, s -> {}); + T widgetThemeHoverInstance = key.getCodec().parse(JsonOps.INSTANCE, immutableHoverWidgetTheme.toJson()).getOrThrow(false, s -> {}); - map.putTheme(key, new WidgetThemeEntry<>(key, widgetTheme, widgetThemeHover)); + map.register(key, widgetThemeInstance, widgetThemeHoverInstance); } private JsonObject getJson(JsonObject json, String key) { diff --git a/src/main/java/brachy/modularui/theme/WidgetTheme.java b/src/main/java/brachy/modularui/theme/WidgetTheme.java index ee3e094..8bb38ad 100755 --- a/src/main/java/brachy/modularui/theme/WidgetTheme.java +++ b/src/main/java/brachy/modularui/theme/WidgetTheme.java @@ -1,9 +1,7 @@ package brachy.modularui.theme; -import brachy.modularui.api.IThemeApi; import brachy.modularui.api.drawable.IDrawable; import brachy.modularui.utils.Color; -import brachy.modularui.utils.serialization.json.JsonHelper; import com.google.gson.JsonElement; import com.google.gson.JsonObject; @@ -16,21 +14,15 @@ public static WidgetThemeEntry getDefault() { return ThemeAPI.DEFAULT_THEME.getFallback(); } - @Getter - private final int defaultWidth; - @Getter - private final int defaultHeight; + @Getter private final int defaultWidth; + @Getter private final int defaultHeight; @Getter @Nullable private final IDrawable background; - @Getter - private final int color; - @Getter - private final int textColor; - @Getter - private final boolean textShadow; - @Getter - private final int iconColor; + @Getter private final int color; + @Getter private final int textColor; + @Getter private final boolean textShadow; + @Getter private final int iconColor; public static WidgetTheme whiteTextShadow(int defaultWidth, int defaultHeight, @Nullable IDrawable background) { return new WidgetTheme(defaultWidth, defaultHeight, background, Color.WHITE.main, @@ -53,24 +45,6 @@ public WidgetTheme(int defaultWidth, int defaultHeight, @Nullable IDrawable back this.iconColor = iconColor == 0 ? color : iconColor; } - public WidgetTheme(WidgetTheme parent, JsonObject json, JsonObject fallback) { - this.defaultWidth = JsonHelper.getInt(json, parent.getDefaultWidth(), "w", "width"); - this.defaultHeight = JsonHelper.getInt(json, parent.getDefaultHeight(), "h", "height"); - this.background = JsonHelper.deserialize(json, IDrawable.class, parent.getBackground(), - IThemeApi.BACKGROUND, "bg"); - // color, textColor, textShadow and iconColor inherit from fallback first and then from parent widget theme - this.color = JsonHelper.getColorWithFallback(json, inherits(json, IThemeApi.COLOR) ? null : fallback, - parent.getColor(), IThemeApi.COLOR); - int textColor = JsonHelper.getColorWithFallback(json, inherits(json, IThemeApi.TEXT_COLOR) ? null : fallback, - parent.getTextColor(), IThemeApi.TEXT_COLOR); - this.textColor = textColor == 0 ? color : textColor; - this.textShadow = JsonHelper.getBoolWithFallback(json, inherits(json, IThemeApi.TEXT_SHADOW) ? null : fallback, - parent.isTextShadow(), IThemeApi.TEXT_SHADOW); - int iconColor = JsonHelper.getColorWithFallback(json, inherits(json, IThemeApi.ICON_COLOR) ? null : fallback, - parent.getTextColor(), IThemeApi.TEXT_COLOR); - this.iconColor = iconColor == 0 ? color : iconColor; - } - protected static boolean inherits(JsonObject json, String property) { if (!json.has("inherit")) return false; JsonElement element = json.get("inherit"); diff --git a/src/main/java/brachy/modularui/theme/WidgetThemeCodec.java b/src/main/java/brachy/modularui/theme/WidgetThemeCodec.java new file mode 100644 index 0000000..3dfb86d --- /dev/null +++ b/src/main/java/brachy/modularui/theme/WidgetThemeCodec.java @@ -0,0 +1,67 @@ +package brachy.modularui.theme; + +import brachy.modularui.utils.serialization.codec.CodecUtil; + +import com.mojang.datafixers.util.Pair; +import com.mojang.serialization.Codec; +import com.mojang.serialization.DataResult; +import com.mojang.serialization.DynamicOps; + +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import lombok.AccessLevel; +import lombok.Getter; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; +import java.util.List; + +public class WidgetThemeCodec implements Codec { + + @Getter + private final Class type; + @Getter(AccessLevel.PACKAGE) + private final List> fields; + private final Constructor ctor; + + public WidgetThemeCodec(Class type, List> fields, Constructor ctor) { + this.type = type; + this.fields = fields; + this.ctor = ctor; + } + + @Override + public DataResult> decode(DynamicOps ops, J input) { + var d = ops.getMapValues(input); + var res = d.result(); + if (res.isEmpty()) return DataResult.error(() -> d.error().orElseThrow().message()); + var map = new Object2ObjectOpenHashMap(); + res.get().forEach(p -> map.put(ops.getStringValue(p.getFirst()).result().orElseThrow(), p.getSecond())); + List errors = new ArrayList<>(); + List args = new ArrayList<>(); + this.fields.forEach(f -> args.add(f.decode(ops, map, errors))); + if (!errors.isEmpty()) { + return DataResult.error(() -> + String.format("Errors while decoding widget theme: %s", errors)); + } + + T instance = null; + try { + instance = this.ctor.newInstance(args.toArray(Object[]::new)); + } catch (InstantiationException | IllegalAccessException | InvocationTargetException | IllegalArgumentException e) { + return DataResult.error(e::getMessage); + } + return DataResult.success(new Pair<>(instance, input)); + } + + @Override + public DataResult encode(T input, DynamicOps ops, J prefix) { + var mapBuilder = CodecUtil.mergePrefixToMapBuilder(ops, prefix); + List errors = new ArrayList<>(); + this.fields.forEach(f -> f.encode(input, ops, mapBuilder, errors)); + if (!errors.isEmpty()) { + return DataResult.error(() -> "Error while encoding widget theme of type " + input.getClass().getSimpleName() + ": " + errors); + } + return DataResult.success(ops.createMap(mapBuilder.build())); + } +} diff --git a/src/main/java/brachy/modularui/theme/WidgetThemeEntry.java b/src/main/java/brachy/modularui/theme/WidgetThemeEntry.java index 7397c95..1105bf0 100755 --- a/src/main/java/brachy/modularui/theme/WidgetThemeEntry.java +++ b/src/main/java/brachy/modularui/theme/WidgetThemeEntry.java @@ -1,5 +1,13 @@ package brachy.modularui.theme; +import brachy.modularui.api.IThemeApi; + +import com.mojang.datafixers.util.Pair; +import com.mojang.serialization.DynamicOps; + +import java.util.List; +import java.util.stream.Stream; + public record WidgetThemeEntry(WidgetThemeKey key, T theme, T hoverTheme) { public WidgetThemeEntry(WidgetThemeKey key, T theme) { @@ -19,4 +27,40 @@ public WidgetThemeEntry expectType(Class expectedT String.format("Got widget theme with invalid type. Got type '%s', but expected type '%s'", this.key.getWidgetThemeType().getSimpleName(), expectedType.getSimpleName())); } + + public J encodeFallback(DynamicOps ops, J prefix, List errors) { + var d = key().getCodec().encode(theme(), ops, prefix); + var res = d.result(); + if (res.isEmpty()) { + errors.add(d.error().orElseThrow().message()); + } else { + prefix = res.get(); + } + if (theme() == hoverTheme()) return prefix; + d = key().getCodec().encode(hoverTheme(), ops, prefix); + res = d.result(); + if (res.isEmpty()) { + errors.add(d.error().orElseThrow().message()); + return prefix; + } + return res.get(); + } + + public void encode(DynamicOps ops, Stream.Builder> mapBuilder, List errors) { + var d = key.getCodec().encodeStart(ops, theme); + var res = d.result(); + if (res.isEmpty()) { + errors.add(d.error().orElseThrow().message()); + } else { + mapBuilder.accept(new Pair<>(ops.createString(key.getFullName()), res.get())); + } + if (theme == hoverTheme) return; + d = key.getCodec().encodeStart(ops, theme); + res = d.result(); + if (res.isEmpty()) { + errors.add(d.error().orElseThrow().message()); + return; + } + mapBuilder.accept(new Pair<>(ops.createString(key.getFullName() + IThemeApi.HOVER_SUFFIX), res.get())); + } } diff --git a/src/main/java/brachy/modularui/theme/WidgetThemeField.java b/src/main/java/brachy/modularui/theme/WidgetThemeField.java new file mode 100644 index 0000000..c26a07a --- /dev/null +++ b/src/main/java/brachy/modularui/theme/WidgetThemeField.java @@ -0,0 +1,49 @@ +package brachy.modularui.theme; + +import brachy.modularui.utils.serialization.codec.FieldReader; + +import com.mojang.datafixers.util.Pair; +import com.mojang.serialization.Codec; +import com.mojang.serialization.DynamicOps; + +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; + +public record WidgetThemeField(String name, Class type, Codec codec, FieldReader fieldReader, + boolean canFallback) { + + void encode(T widgetTheme, DynamicOps ops, Stream.Builder> mapBuilder, List errors) { + var value = fieldReader().readField(widgetTheme); + J data; + if (value == null) { + data = ops.empty(); + } else { + var d = codec().encodeStart(ops, value); + var res = d.result(); + if (res.isEmpty()) { + errors.add(d.error().orElseThrow().message()); + return; + } + data = res.get(); + } + mapBuilder.accept(new Pair<>(ops.createString(name()), data)); + } + + V decode(DynamicOps ops, Map map, List errors) { + J element = map.get(name()); + if (element == null) { + if (!map.containsKey(name())) { + errors.add(String.format("Field '%s' in widget theme of type %s was not found", name(), type().getSimpleName())); + } + return null; + } + var d = codec().parse(ops, element); + var res = d.result(); + if (res.isEmpty()) { + errors.add(d.error().orElseThrow().message()); + return null; + } + return res.get(); + } +} diff --git a/src/main/java/brachy/modularui/theme/WidgetThemeKey.java b/src/main/java/brachy/modularui/theme/WidgetThemeKey.java index 4e55f74..f5706cb 100755 --- a/src/main/java/brachy/modularui/theme/WidgetThemeKey.java +++ b/src/main/java/brachy/modularui/theme/WidgetThemeKey.java @@ -1,6 +1,9 @@ package brachy.modularui.theme; -import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import com.mojang.serialization.JsonOps; + +import com.google.gson.JsonObject; +import it.unimi.dsi.fastutil.objects.Object2ReferenceOpenHashMap; import lombok.Getter; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -10,7 +13,7 @@ public class WidgetThemeKey implements Comparable> { - private static final Map> KEYS = new Object2ObjectOpenHashMap<>(); + private static final Map> KEYS = new Object2ReferenceOpenHashMap<>(); @Nullable public static WidgetThemeKey getFromFullName(String key) { @@ -20,36 +23,34 @@ public static WidgetThemeKey getFromFullName(String key) { @Nullable @Getter private final WidgetThemeKey parent; - private final Class type; - @Getter - private final String name; + @Getter private final Class type; + @Getter private final String name; @Nullable @Getter private final String subName; - @Getter - private final T defaultValue; - @Getter - private final T defaultHoverValue; - @Getter - private final WidgetThemeParser parser; + @Getter private final T defaultValue; + @Getter private final T defaultHoverValue; + @Getter private final WidgetThemeMerger merger; + @Getter private final WidgetThemeCodec codec; - WidgetThemeKey(Class type, String name, T defaultValue, WidgetThemeParser parser) { - this(type, name, defaultValue, defaultValue, parser); + WidgetThemeKey(Class type, String name, T defaultValue, WidgetThemeMerger merger, WidgetThemeCodec codec) { + this(type, name, defaultValue, defaultValue, merger, codec); } - WidgetThemeKey(Class type, String name, T defaultValue, T defaultHoverValue, WidgetThemeParser parser) { - this(null, type, name, null, defaultValue, defaultHoverValue, parser); + WidgetThemeKey(Class type, String name, T defaultValue, T defaultHoverValue, WidgetThemeMerger merger, WidgetThemeCodec codec) { + this(null, type, name, null, defaultValue, defaultHoverValue, merger, codec); } WidgetThemeKey(@Nullable WidgetThemeKey parent, Class type, String name, @Nullable String subName, - T defaultValue, T defaultHoverValue, WidgetThemeParser parser) { + T defaultValue, T defaultHoverValue, WidgetThemeMerger merger, WidgetThemeCodec codec) { this.parent = parent; this.type = type; this.name = name; this.subName = subName; this.defaultValue = defaultValue; this.defaultHoverValue = defaultHoverValue; - this.parser = parser; + this.merger = merger; + this.codec = codec; KEYS.put(getFullName(), this); ThemeAPI.INSTANCE.registerWidgetThemeKey(this); } @@ -71,7 +72,15 @@ public WidgetThemeKey createSubKey(String subName, @Nullable T defaultValue, return new WidgetThemeKey<>(this, type, name, subName, defaultValue != null ? defaultValue : getDefaultValue(), defaultHoverValue != null ? defaultHoverValue : getDefaultHoverValue(), - this.parser); + this.merger, this.codec); + } + + public T parseJson(JsonObject json) { + return getCodec().parse(JsonOps.INSTANCE, json).getOrThrow(false, s -> {}); + } + + public JsonObject encodeJson(T theme) { + return getCodec().encodeStart(JsonOps.INSTANCE, theme).getOrThrow(false, s -> {}).getAsJsonObject(); } public Class getWidgetThemeType() { diff --git a/src/main/java/brachy/modularui/theme/WidgetThemeKeyBuilder.java b/src/main/java/brachy/modularui/theme/WidgetThemeKeyBuilder.java index f261560..75b22ff 100755 --- a/src/main/java/brachy/modularui/theme/WidgetThemeKeyBuilder.java +++ b/src/main/java/brachy/modularui/theme/WidgetThemeKeyBuilder.java @@ -1,22 +1,28 @@ package brachy.modularui.theme; import brachy.modularui.api.IThemeApi; +import brachy.modularui.utils.serialization.codec.FieldReader; -import com.google.gson.JsonObject; +import com.mojang.serialization.Codec; import java.lang.reflect.Constructor; -import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; import java.util.Objects; public class WidgetThemeKeyBuilder { private final String id; + private final Class type; private T defaultTheme; private T defaultHoverTheme; - private WidgetThemeParser parser; + private final List> fields = new ArrayList<>(); + private WidgetThemeKey parser; - public WidgetThemeKeyBuilder(String id) { + public WidgetThemeKeyBuilder(String id, Class type) { this.id = id; + this.type = type; } public WidgetThemeKeyBuilder defaultTheme(T defaultTheme) { @@ -29,8 +35,37 @@ public WidgetThemeKeyBuilder defaultHoverTheme(T defaultHoverTheme) { return this; } - public WidgetThemeKeyBuilder parser(WidgetThemeParser parser) { - this.parser = parser; + /** + * Adds all fields of another widget theme at index 0 of the list. + * This means new fields added in these widget theme are at the end. + * The widget theme constructor must have the parameters in this order too. + */ + public WidgetThemeKeyBuilder fieldsOf(WidgetThemeKey key) { + this.parser = key; + return this; + } + + public WidgetThemeKeyBuilder field(String name, Class type, Codec codec, FieldReader fieldReader) { + return field(name, type, codec, fieldReader, false); + } + + public WidgetThemeKeyBuilder fallbackField(String name, Class type, Codec codec, FieldReader fieldReader) { + return field(name, type, codec, fieldReader, true); + } + + /** + * Adds a field to the codec. The widget theme must have a public constructor with all the fields in the same order they were added. + * The constructor is found and used via reflection. + * + * @param name name of the field, used to read and write in JSON + * @param type type of the field in the constructor, must be exact + * @param codec codec for this field + * @param fieldReader reads the field from a widget theme + * @param canFallback determines if values are inherited from parent or fallback first, if this is true fallback is used first, + * but can be disabled individually via the "inherited" property in JSON + */ + public WidgetThemeKeyBuilder field(String name, Class type, Codec codec, FieldReader fieldReader, boolean canFallback) { + this.fields.add(new WidgetThemeField<>(name, type, codec, fieldReader, canFallback)); return this; } @@ -39,7 +74,34 @@ public WidgetThemeKey register() { Objects.requireNonNull(this.id, "Id for widget theme must not be null"); Objects.requireNonNull(this.defaultTheme, "Default theme for widget theme must not be null, but is null for id '" + this.id + "'."); - if (parser == null) parser = createParserWithReflection(); + WidgetThemeMerger merger = null; + WidgetThemeCodec codec = null; + if (this.parser != null) { + if (!this.fields.isEmpty()) { + int i = 0; + for (WidgetThemeField f : this.parser.getCodec().getFields()) { + this.fields.add(i++, (WidgetThemeField) f); + } + } else { + if (this.type == this.parser.getType()) { + codec = (WidgetThemeCodec) this.parser.getCodec(); + } + merger = (WidgetThemeMerger) this.parser.getMerger(); + } + } + if (this.fields.isEmpty() && (merger == null || codec == null)) throw new IllegalArgumentException("No fields for codec provided!"); + if (merger == null) merger = new WidgetThemeMerger<>(this.fields); + if (codec == null) { + Constructor ctor; + Class[] types = this.fields.stream().map(WidgetThemeField::type).toArray(Class[]::new); + try { + ctor = this.type.getDeclaredConstructor(this.fields.stream().map(WidgetThemeField::type).toArray(Class[]::new)); + } catch (NoSuchMethodException e) { + String sTypes = Arrays.stream(types).map(Class::getSimpleName).reduce("", (a, b) -> a + b + ", "); + throw new IllegalArgumentException("No constructor found for the fields with the given types in that order. Types: " + sTypes); + } + codec = new WidgetThemeCodec<>(this.type, this.fields, ctor); + } if (defaultHoverTheme == null) { WidgetTheme hover = this.defaultTheme.withNoHoverBackground(); if (hover.getClass() != this.defaultTheme.getClass()) { @@ -49,25 +111,6 @@ public WidgetThemeKey register() { } defaultHoverTheme = (T) hover; } - return IThemeApi.get().registerWidgetTheme(this.id, this.defaultTheme, this.defaultHoverTheme, this.parser); - } - - @SuppressWarnings("unchecked") - private WidgetThemeParser createParserWithReflection() { - Class type = (Class) this.defaultTheme.getClass(); - try { - Constructor ctor = type.getConstructor(type, JsonObject.class, JsonObject.class); - return (parent, json, fallback) -> { - try { - return ctor.newInstance(parent, json, fallback); - } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) { - throw new RuntimeException("Failed to instantiate widget theme of type " + type.getSimpleName(), e); - } - }; - } catch (NoSuchMethodException e) { - throw new RuntimeException(String.format( - "No constructor with signature '%s(%s parent, JsonObject json, JsonObject fallback)' found", - type.getSimpleName(), type.getSimpleName())); - } + return IThemeApi.get().registerWidgetTheme(this.id, this.defaultTheme, this.defaultHoverTheme, merger, codec); } } diff --git a/src/main/java/brachy/modularui/theme/WidgetThemeMap.java b/src/main/java/brachy/modularui/theme/WidgetThemeMap.java index ffc999e..e9b505a 100755 --- a/src/main/java/brachy/modularui/theme/WidgetThemeMap.java +++ b/src/main/java/brachy/modularui/theme/WidgetThemeMap.java @@ -1,8 +1,8 @@ package brachy.modularui.theme; -import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.objects.Object2ObjectLinkedOpenHashMap; -public class WidgetThemeMap extends Object2ObjectOpenHashMap, WidgetThemeEntry> { +public class WidgetThemeMap extends Object2ObjectLinkedOpenHashMap, WidgetThemeEntry> { @Override public WidgetThemeEntry put(WidgetThemeKey widgetThemeKey, WidgetThemeEntry widgetTheme) { @@ -13,14 +13,16 @@ public WidgetThemeEntry put(WidgetThemeKey widgetThemeKey, WidgetThemeEntr return super.put(widgetThemeKey, widgetTheme); } - @SuppressWarnings("unchecked") - public WidgetThemeEntry putTheme(WidgetThemeKey widgetThemeKey, - WidgetThemeEntry widgetTheme) { - return (WidgetThemeEntry) super.put(widgetThemeKey, widgetTheme); + public void putTheme(WidgetThemeKey key, WidgetThemeEntry widgetTheme) { + super.put(key, widgetTheme); } @SuppressWarnings("unchecked") public WidgetThemeEntry getTheme(WidgetThemeKey widgetThemeKey) { return (WidgetThemeEntry) super.get(widgetThemeKey); } + + public void register(WidgetThemeKey key, T widgetTheme, T hoverWidgetTheme) { + putTheme(key, new WidgetThemeEntry<>(key, widgetTheme, hoverWidgetTheme)); + } } diff --git a/src/main/java/brachy/modularui/theme/WidgetThemeMerger.java b/src/main/java/brachy/modularui/theme/WidgetThemeMerger.java new file mode 100644 index 0000000..7874090 --- /dev/null +++ b/src/main/java/brachy/modularui/theme/WidgetThemeMerger.java @@ -0,0 +1,56 @@ +package brachy.modularui.theme; + +import com.google.gson.JsonElement; +import com.google.gson.JsonNull; +import com.google.gson.JsonObject; + +import java.util.List; + +/** + * Handles merging widget theme JSONs into a single one and inherits values properly + * @param + */ +public class WidgetThemeMerger { + + private final List> fields; + + public WidgetThemeMerger(List> fields) { + this.fields = fields; + } + + public JsonObject merge(JsonObject input, ImmutableJson parent, ImmutableJson fallback) { + var inheritsList = getInheritsList(input); + for (WidgetThemeField field : this.fields) { + mergeField(field, input, parent, fallback, inheritsList); + } + return input; + } + + private void mergeField(WidgetThemeField field, JsonObject input, ImmutableJson parent, ImmutableJson fallback, JsonElement inheritsList) { + var key = field.name(); + if (input.has(key)) return; + if (field.canFallback() && !inherits(inheritsList, key) && fallback.has(key)) { + input.add(key, fallback.get(key)); + return; + } + if (parent.has(key)) { + input.add(key, parent.get(key)); + } + } + + private static JsonElement getInheritsList(JsonObject json) { + if (!json.has("inherit")) return JsonNull.INSTANCE; + return json.get("inherit"); + } + + protected static boolean inherits(JsonElement element, String property) { + if (element == JsonNull.INSTANCE) return false; + if (element.isJsonPrimitive()) return element.getAsString().equals(property); + if (element.isJsonArray()) { + for (JsonElement e : element.getAsJsonArray()) { + if (e.isJsonPrimitive() && e.getAsString().equals(property)) return true; + } + } + return false; + } +} diff --git a/src/main/java/brachy/modularui/theme/WidgetThemeParser.java b/src/main/java/brachy/modularui/theme/WidgetThemeParser.java deleted file mode 100755 index 5736b26..0000000 --- a/src/main/java/brachy/modularui/theme/WidgetThemeParser.java +++ /dev/null @@ -1,22 +0,0 @@ -package brachy.modularui.theme; - -import com.google.gson.JsonObject; -import org.jetbrains.annotations.NotNull; - -/** - * An interface used to parse json objects to widget themes. - */ -@FunctionalInterface -public interface WidgetThemeParser { - - /** - * Parses a json object to a widget theme, - * - * @param parent the widget theme from the parent of the currently parsed theme - * @param json the widget theme json data object - * @param fallback a fallback widget theme json data object - * @return the parsed widget theme - */ - @NotNull - T parse(T parent, JsonObject json, JsonObject fallback); -} diff --git a/src/main/java/brachy/modularui/utils/serialization/codec/CodecUtil.java b/src/main/java/brachy/modularui/utils/serialization/codec/CodecUtil.java index 3cbc4e0..45e45de 100644 --- a/src/main/java/brachy/modularui/utils/serialization/codec/CodecUtil.java +++ b/src/main/java/brachy/modularui/utils/serialization/codec/CodecUtil.java @@ -6,12 +6,16 @@ import com.mojang.serialization.Decoder; import com.mojang.serialization.DynamicOps; import com.mojang.serialization.Encoder; +import com.mojang.serialization.codecs.KeyDispatchCodec; +import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; import org.jetbrains.annotations.Nullable; +import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Objects; +import java.util.Set; import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Stream; @@ -74,17 +78,76 @@ public static Codec codecOf(Encoder encoder, Decoder... decoder) { } public static Codec dispatchNullable(Codec keyCodec, Function type, Function> codec) { - return dispatchNullable("type", keyCodec, type, codec); + return dispatchNullable("type", keyCodec, type, codec, true); } - public static Codec dispatchNullable(String key, Codec keyCodec, Function type, Function> codec) { - return keyCodec.partialDispatch(key, e -> { - A a = type.apply(e); - return a == null ? DataResult.error(() -> "No key found") : DataResult.success(a); - }, a -> { - Codec e = codec.apply(a); - return e == null ? DataResult.error(() -> "No codec found for key " + a) : DataResult.success(e); - }); + /** + * Creates a dispatch codec, but with nullable type and codec functions. + * If the functions return null, an error data result is returned instead of crashing. + * + * @see #dispatch(String, Codec, Function, Function, boolean) + */ + public static Codec dispatchNullable(String key, Codec keyCodec, + Function type, + Function> codec, boolean assumeMap) { + return dispatch(key, keyCodec, v -> { + K k = type.apply(v); + return k == null ? DataResult.error(() -> "No key found") : DataResult.success(k); + }, k -> { + Codec e = codec.apply(k); + return e == null ? DataResult.error(() -> "No codec found for key " + k) : DataResult.success(e); + }, assumeMap); + } + + /** + * Creates a dispatch codec with the option to assume map. + * {@link Codec#dispatch(Function, Function)} assumes the data in a structure like this for this example: + *
+     * {@code
+     *     {
+     *         "type": "pos2d",
+     *         "value: {
+     *             "x": 1,
+     *             "y": 2
+     *         }
+     *     }
+     * }
+     * 
+ * With assumeMap it would look like this: + *
+     * {@code
+     *     {
+     *         "type": "pos2d",
+     *         "x": 1,
+     *         "y": 2
+     *     }
+     * }
+     * 
+ * + * @param key key to get the type name, usually just "type" + * @param keyCodec codec for the key + * @param type function to get the key from a value + * @param codec function to get the codec from a key + * @param assumeMap if the codec should assume map like described above + * @param key type + * @param value type + */ + public static Codec dispatch(String key, Codec keyCodec, + Function> type, + Function>> codec, boolean assumeMap) { + return assumeMap ? + KeyDispatchCodec.unsafe(key, keyCodec, type, codec, v -> CodecUtil.getCodec(type, codec, v)).codec() : + new KeyDispatchCodec<>(key, keyCodec, type, codec).codec(); + + } + + @SuppressWarnings("unchecked") + private static DataResult> getCodec(final Function> type, + final Function>> encoder, + final V input) { + return type.apply(input) + .>flatMap(k -> encoder.apply(k).map(Function.identity())) + .map(c -> ((Encoder) c)); } public static
Encoder checked(Encoder codec, Predicate test) { @@ -132,8 +195,12 @@ public String toString() { return mapValues; } + public static Codec> setOf(Codec codec) { + return codec.listOf().xmap(ObjectOpenHashSet::new, ArrayList::new); + } + /** - * Creates a codec that accepts either a list or a single element and turns it into a list. + * Creates a codec that accepts either a data list or a single element and turns it into a list. */ public static Codec> listLike(Codec codec) { return chainedCodec(codec.flatComapMap(Collections::singletonList, list -> { @@ -141,4 +208,14 @@ public static Codec> listLike(Codec codec) { return DataResult.success(list.get(0)); }), codec.listOf()); } + + /** + * Creates a codec that accepts either a data list or a single element and turns it into a set. + */ + public static Codec> setLike(Codec codec) { + return chainedCodec(codec.flatComapMap(Collections::singleton, list -> { + if (list.size() != 1) return DataResult.error(() -> "List must contain exactly one element"); + return DataResult.success(list.iterator().next()); + }), setOf(codec)); + } } diff --git a/src/main/resources/assets/modularui/themes/context_menu.json b/src/main/resources/assets/modularui/themes/context_menu.json index b72b97f..111be58 100644 --- a/src/main/resources/assets/modularui/themes/context_menu.json +++ b/src/main/resources/assets/modularui/themes/context_menu.json @@ -3,7 +3,7 @@ "panel": { "background": { "type": "texture", - "id": "menu" + "name": "menu" } }, "button": { From 294a416052392e167f42057943becc8c1dd7a19b Mon Sep 17 00:00:00 2001 From: brachy84 Date: Tue, 19 May 2026 20:13:24 +0200 Subject: [PATCH 22/26] remove all kinds of manual json parsing --- .../java/brachy/modularui/ClientProxy.java | 2 - .../modularui/api/IJsonSerializable.java | 50 ---- .../modularui/api/drawable/IDrawable.java | 11 + .../brachy/modularui/api/drawable/Text.java | 21 +- .../drawable/AdaptableUITexture.java | 12 - .../brachy/modularui/drawable/Circle.java | 20 +- .../drawable/DrawableSerialization.java | 255 ------------------ .../modularui/drawable/DrawableStack.java | 49 +--- .../java/brachy/modularui/drawable/Icon.java | 33 +-- .../modularui/drawable/ItemDrawable.java | 3 +- .../brachy/modularui/drawable/Rectangle.java | 67 +---- .../brachy/modularui/drawable/Scrollbar.java | 19 +- .../modularui/drawable/TextureRegistry.java | 72 +++++ .../modularui/drawable/TiledUITexture.java | 14 +- .../brachy/modularui/drawable/UITexture.java | 87 +----- .../drawable/text/ModularComponent.java | 23 -- .../modularui/theme/SelectableTheme.java | 3 +- .../brachy/modularui/theme/ThemeBuilder.java | 9 +- .../modularui/theme/WidgetThemeBuilder.java | 3 +- .../brachy/modularui/utils/Alignment.java | 41 --- .../java/brachy/modularui/utils/Color.java | 74 ----- .../utils/serialization/json/JsonHelper.java | 39 --- .../brachy/modularui/widget/sizer/Box.java | 26 -- 23 files changed, 103 insertions(+), 830 deletions(-) delete mode 100755 src/main/java/brachy/modularui/api/IJsonSerializable.java delete mode 100644 src/main/java/brachy/modularui/drawable/DrawableSerialization.java create mode 100644 src/main/java/brachy/modularui/drawable/TextureRegistry.java diff --git a/src/main/java/brachy/modularui/ClientProxy.java b/src/main/java/brachy/modularui/ClientProxy.java index f620d34..197f250 100644 --- a/src/main/java/brachy/modularui/ClientProxy.java +++ b/src/main/java/brachy/modularui/ClientProxy.java @@ -8,7 +8,6 @@ import brachy.modularui.client.component.TooltipComponentIcon; import brachy.modularui.drawable.ClientTooltipComponentIcon; import brachy.modularui.drawable.DelegateIcon; -import brachy.modularui.drawable.DrawableSerialization; import brachy.modularui.drawable.HoverableIcon; import brachy.modularui.drawable.Icon; import brachy.modularui.drawable.InteractableIcon; @@ -55,7 +54,6 @@ public class ClientProxy extends CommonProxy { if (!ModularUI.isDataGen()) { CursorHandler.init(); AnimatorManager.init(); - DrawableSerialization.init(); } } diff --git a/src/main/java/brachy/modularui/api/IJsonSerializable.java b/src/main/java/brachy/modularui/api/IJsonSerializable.java deleted file mode 100755 index d04cc17..0000000 --- a/src/main/java/brachy/modularui/api/IJsonSerializable.java +++ /dev/null @@ -1,50 +0,0 @@ -package brachy.modularui.api; - -import com.mojang.serialization.Codec; -import com.mojang.serialization.DataResult; -import com.mojang.serialization.Dynamic; -import com.mojang.serialization.JsonOps; - -import com.google.gson.JsonObject; -import org.jetbrains.annotations.ApiStatus; - -@Deprecated -public interface IJsonSerializable> { - - /** - * Override this - * - * @return the codec to serialize this object with - */ - // TODO actually implement on subclasses - @ApiStatus.OverrideOnly - default Codec getCodec() { - return Codec.PASSTHROUGH.flatComapMap(dynamic -> { - loadFromJson(dynamic.cast(JsonOps.INSTANCE).getAsJsonObject()); - return (T) this; - }, object -> { - JsonObject jsonObject = new JsonObject(); - if (saveToJson(jsonObject)) { - return DataResult.success(new Dynamic<>(JsonOps.INSTANCE, jsonObject)); - } - return DataResult.error(() -> "Failed to serialize drawable %s".formatted(object)); - }); - } - - /** - * Reads extra json data after this drawable is created. - * - * @param json json to read from - */ - default void loadFromJson(JsonObject json) {} - - /** - * Writes all json data necessary so that deserializing it results in the same drawable. - * - * @param json json to write to - * @return if the drawable was serialized - */ - default boolean saveToJson(JsonObject json) { - return false; - } -} diff --git a/src/main/java/brachy/modularui/api/drawable/IDrawable.java b/src/main/java/brachy/modularui/api/drawable/IDrawable.java index b800871..1b433ee 100644 --- a/src/main/java/brachy/modularui/api/drawable/IDrawable.java +++ b/src/main/java/brachy/modularui/api/drawable/IDrawable.java @@ -14,8 +14,11 @@ import brachy.modularui.widget.Widget; import brachy.modularui.widget.sizer.Area; +import com.google.gson.JsonElement; + import com.mojang.serialization.Codec; import com.mojang.serialization.DataResult; +import com.mojang.serialization.JsonOps; import net.minecraftforge.api.distmarker.Dist; import net.minecraftforge.api.distmarker.OnlyIn; @@ -61,6 +64,14 @@ static IDrawable of(IDrawable... drawables) { }); Codec CODEC = CodecUtil.chainedCodec(CODEC_EMPTY_NONE, DrawableStack.CODEC, CODEC_DISPATCH); + static DataResult toJson(IDrawable drawable) { + return CODEC.encodeStart(JsonOps.INSTANCE, drawable); + } + + static JsonElement toJsonOrThrow(IDrawable drawable) { + return toJson(drawable).getOrThrow(false, s -> {}); + } + /** * Draws this drawable at the given position with the given size. It's the implementors responsibility to properly * apply the widget theme by calling {@link #applyColor(int)} before drawing. diff --git a/src/main/java/brachy/modularui/api/drawable/Text.java b/src/main/java/brachy/modularui/api/drawable/Text.java index 5b784e8..c6705d9 100644 --- a/src/main/java/brachy/modularui/api/drawable/Text.java +++ b/src/main/java/brachy/modularui/api/drawable/Text.java @@ -1,6 +1,5 @@ package brachy.modularui.api.drawable; -import brachy.modularui.api.IJsonSerializable; import brachy.modularui.drawable.text.DynamicComponent; import brachy.modularui.drawable.text.KeyIcon; import brachy.modularui.drawable.text.ModularComponent; @@ -9,14 +8,13 @@ import brachy.modularui.theme.WidgetTheme; import brachy.modularui.utils.Alignment; -import com.mojang.serialization.Codec; import net.minecraft.ChatFormatting; import net.minecraft.network.chat.Component; import net.minecraft.network.chat.MutableComponent; +import com.mojang.serialization.Codec; import net.minecraftforge.api.distmarker.Dist; import net.minecraftforge.api.distmarker.OnlyIn; -import com.google.gson.JsonObject; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -26,7 +24,7 @@ /** * This represents a piece of text in a GUI. */ -public interface Text extends IDrawable, IJsonSerializable { +public interface Text extends IDrawable { Codec CODEC = ModularComponent.CODEC; @@ -241,19 +239,4 @@ default KeyIcon asTextIcon() { default String getTypeName() { return "text"; } - - @Override - default void loadFromJson(JsonObject json) { - if (json.has("color") || json.has("shadow") || json.has("align") || json.has("alignment") || - json.has("scale")) { - /*StyledText styledText = this instanceof StyledText styledText1 ? styledText1 : withStyle(); - if (json.has("color")) { - styledText.color(JsonHelper.getInt(json, 0, "color")); - } - styledText.shadow(JsonHelper.getBoolean(json, false, "shadow")); - styledText.alignment( - JsonHelper.deserialize(json, Alignment.class, styledText.alignment(), "align", "alignment")); - styledText.scale(JsonHelper.getFloat(json, 1, "scale"));*/ - } - } } diff --git a/src/main/java/brachy/modularui/drawable/AdaptableUITexture.java b/src/main/java/brachy/modularui/drawable/AdaptableUITexture.java index e23b021..b971b9a 100644 --- a/src/main/java/brachy/modularui/drawable/AdaptableUITexture.java +++ b/src/main/java/brachy/modularui/drawable/AdaptableUITexture.java @@ -216,18 +216,6 @@ public void drawTiled(GuiContext context, float x, float y, float width, float h RenderSystem.disableBlend(); } - @Override - protected void saveTextureToJson(JsonObject json) { - super.saveToJson(json); - json.addProperty("imageWidth", this.imageWidth); - json.addProperty("imageHeight", this.imageHeight); - json.addProperty("bl", this.bl); - json.addProperty("br", this.br); - json.addProperty("bt", this.bt); - json.addProperty("bb", this.bb); - json.addProperty("tiled", this.tiled); - } - @Override protected AdaptableUITexture copy() { return new AdaptableUITexture(location, u0, v0, u1, v1, colorType, nonOpaque, diff --git a/src/main/java/brachy/modularui/drawable/Circle.java b/src/main/java/brachy/modularui/drawable/Circle.java index 369f7f6..658e95c 100644 --- a/src/main/java/brachy/modularui/drawable/Circle.java +++ b/src/main/java/brachy/modularui/drawable/Circle.java @@ -1,26 +1,23 @@ package brachy.modularui.drawable; import brachy.modularui.animation.IAnimatable; -import brachy.modularui.api.IJsonSerializable; import brachy.modularui.api.drawable.IDrawable; import brachy.modularui.screen.viewport.GuiContext; import brachy.modularui.theme.WidgetTheme; import brachy.modularui.utils.Color; import brachy.modularui.utils.Interpolations; import brachy.modularui.utils.serialization.codec.MutableObjectCodec; -import brachy.modularui.utils.serialization.json.JsonHelper; import com.mojang.serialization.Codec; import net.minecraftforge.api.distmarker.Dist; import net.minecraftforge.api.distmarker.OnlyIn; -import com.google.gson.JsonObject; import lombok.Getter; import lombok.Setter; import lombok.experimental.Accessors; @Accessors(fluent = true, chain = true) -public class Circle implements IDrawable, IJsonSerializable, IAnimatable { +public class Circle implements IDrawable, IAnimatable { public static final MutableObjectCodec CODEC = MutableObjectCodec.drawableBuilder(Circle::new) .addOpt("colorInner", Circle::colorInner, Circle::colorInner, Codec.INT, 0).alias("color") @@ -56,21 +53,6 @@ public void draw(GuiContext context, int x0, int y0, int width, int height, Widg this.colorInner, this.colorOuter, this.segments); } - @Override - public void loadFromJson(JsonObject json) { - this.colorInner = JsonHelper.getColor(json, Color.WHITE.main, "colorInner", "color"); - this.colorOuter = JsonHelper.getColor(json, Color.WHITE.main, "colorOuter", "color"); - this.segments = JsonHelper.getInt(json, 40, "segments"); - } - - @Override - public boolean saveToJson(JsonObject json) { - json.addProperty("colorInner", this.colorInner); - json.addProperty("colorOuter", this.colorOuter); - json.addProperty("segments", this.segments); - return true; - } - @Override public Circle interpolate(Circle start, Circle end, float t) { this.colorInner = Color.lerp(start.colorInner, end.colorInner, t); diff --git a/src/main/java/brachy/modularui/drawable/DrawableSerialization.java b/src/main/java/brachy/modularui/drawable/DrawableSerialization.java deleted file mode 100644 index d457c60..0000000 --- a/src/main/java/brachy/modularui/drawable/DrawableSerialization.java +++ /dev/null @@ -1,255 +0,0 @@ -package brachy.modularui.drawable; - -import brachy.modularui.ModularUI; -import brachy.modularui.ModularUIConfig; -import brachy.modularui.api.IJsonSerializable; -import brachy.modularui.api.drawable.IDrawable; -import brachy.modularui.api.drawable.Text; -import brachy.modularui.drawable.text.ModularComponent; -import brachy.modularui.utils.ObjectList; -import brachy.modularui.utils.serialization.json.JsonHelper; - -import net.minecraft.network.chat.Component; -import net.minecraft.network.chat.MutableComponent; - -import com.google.gson.JsonArray; -import com.google.gson.JsonDeserializationContext; -import com.google.gson.JsonDeserializer; -import com.google.gson.JsonElement; -import com.google.gson.JsonNull; -import com.google.gson.JsonObject; -import com.google.gson.JsonParseException; -import com.google.gson.JsonPrimitive; -import com.google.gson.JsonSerializationContext; -import com.google.gson.JsonSerializer; -import com.google.gson.JsonSyntaxException; -import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; -import org.jetbrains.annotations.ApiStatus; -import org.jetbrains.annotations.Nullable; - -import java.lang.reflect.Type; -import java.util.Map; -import java.util.function.Function; - -public class DrawableSerialization implements JsonSerializer, JsonDeserializer { - - private static final Map> DRAWABLE_TYPES = new Object2ObjectOpenHashMap<>(); - private static final Map, String> REVERSE_DRAWABLE_TYPES = new Object2ObjectOpenHashMap<>(); - private static final Map TEXTURES = new Object2ObjectOpenHashMap<>(); - private static final Map REVERSE_TEXTURES = new Object2ObjectOpenHashMap<>(); - - private static synchronized void registerTextureInternal(String name, UITexture texture) { - if (texture == null) return; - UITexture current = TEXTURES.put(name, texture); - REVERSE_TEXTURES.put(texture, name); - if (current != null && (ModularUI.isDev() || ModularUIConfig.DEBUG_UI.get())) { - ModularUI.LOGGER.warn("[DEBUG] Replacing texture with name '{}' and location '{}' with texture with location '{}'", name, current.location, texture.location); - } - } - - public static synchronized void registerTexture(String name, UITexture texture) { - String current = REVERSE_TEXTURES.get(texture); - if (current != null) { - if (name != null && !current.equals(name)) { - TEXTURES.put(name, texture); - REVERSE_TEXTURES.put(texture, name); - TEXTURES.remove(current); - } - return; - } - if (name == null) { - registerTextureAutoName(texture); - } else { - registerTextureInternal(name, texture); - } - } - - public static void registerTextureAutoName(UITexture texture) { - if (texture == null) return; - String[] p = texture.location.getPath().split("/"); - p = p[p.length - 1].split("\\."); - String baseName = texture.location.getNamespace() + ":" + p[0]; - String name = baseName; - int number = 0; - UITexture current; - while ((current = TEXTURES.get(name)) != null) { - if (current.equals(texture)) return; - number++; - name = baseName + "_" + number; - if (number == 20 && (ModularUI.isDev() || ModularUIConfig.DEBUG_UI.get())) { - ModularUI.LOGGER.warn("[DEBUG] Trying to register a UITexture with location '{}' for at least 20 times. This is likely a bug and should be fixed.", texture.location); - } else if (number == 10000) { - throw new IllegalStateException("Trying to register a UITexture with location '" + texture.location + "' 10000 times."); - } - } - registerTexture(name, texture); - } - - @Nullable - public static UITexture getTexture(String s) { - return TEXTURES.get(s); - } - - @Nullable - public static String getTextureId(UITexture texture) { - return REVERSE_TEXTURES.get(texture); - } - - public static > void registerDrawableType(String id, Class type, - Function creator) { - if (DRAWABLE_TYPES.containsKey(id)) { - throw new IllegalArgumentException("Drawable type '" + id + "' already exists!"); - } - DRAWABLE_TYPES.put(id, creator); - if (type != null) { - REVERSE_DRAWABLE_TYPES.put(type, id); - } - } - - @ApiStatus.Internal - public static void init() { - // empty, none and text are special cases - registerDrawableType("texture", UITexture.class, UITexture::parseFromJson); - registerDrawableType("color", Rectangle.class, json -> new Rectangle()); - registerDrawableType("rectangle", Rectangle.class, json -> new Rectangle()); - registerDrawableType("ellipse", Circle.class, json -> new Circle()); - registerDrawableType("icon", Icon.class, Icon::ofJson); - registerDrawableType("stack", DrawableStack.class, DrawableStack::parseJson); - registerDrawableType("scrollbar", Scrollbar.class, Scrollbar::ofJson); - } - - public static IDrawable deserialize(JsonElement json) { - return JsonHelper.DESERIALIZER.deserialize(json, IDrawable.class); - } - - public static JsonElement serialize(IDrawable drawable) { - return JsonHelper.SERIALIZER.serialize(drawable, IDrawable.class); - } - - @Override - public IDrawable deserialize(JsonElement element, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { - if (element.isJsonNull()) { - return IDrawable.EMPTY; - } - if (element.isJsonPrimitive()) { - if ("empty".equals(element.getAsString()) || "null".equals(element.getAsString())) { - return IDrawable.EMPTY; - } - if ("none".equals(element.getAsString())) { - return IDrawable.NONE; - } - } - if (element.isJsonArray()) { - return DrawableStack.parseJson(element.getAsJsonArray()); - } - if (!element.isJsonObject()) { - ModularUI.LOGGER.throwing(new JsonParseException("Drawable json should be an object or an array.")); - return IDrawable.EMPTY; - } - JsonObject json = element.getAsJsonObject(); - if (json.entrySet().isEmpty()) { - return IDrawable.EMPTY; - } - String type = JsonHelper.getString(json, "empty", "type"); - if ("text".equals(type)) { - ModularComponent key = parseText(json); - key.loadFromJson(json); - return key; - } - if (!DRAWABLE_TYPES.containsKey(type)) { - ModularUI.LOGGER - .throwing(new JsonParseException("Drawable type '" + type + "' is not json serializable!")); - return IDrawable.EMPTY; - } - IDrawable drawable = DRAWABLE_TYPES.get(type).apply(json); - ((IJsonSerializable) drawable).loadFromJson(json); - return drawable; - } - - @Override - public JsonElement serialize(IDrawable src, Type typeOfSrc, JsonSerializationContext context) { - if (src == IDrawable.EMPTY) return JsonNull.INSTANCE; - if (src == IDrawable.NONE) return new JsonPrimitive("none"); - if (src instanceof DrawableStack drawableStack) { - JsonArray jsonArray = new JsonArray(); - for (IDrawable drawable : drawableStack.drawables()) { - jsonArray.add(JsonHelper.serialize(drawable)); - } - return jsonArray; - } - JsonObject json = new JsonObject(); - if (src instanceof Text key) { - json.addProperty("type", "text"); - json.addProperty("text", Component.Serializer.toJson(key.getFormatted())); - } else if (!(src instanceof IJsonSerializable serializable)) { - throw new IllegalArgumentException("Can't serialize IDrawable of type '" + src.getClass().getSimpleName() + "' which doesn't implement IJsonSerializable!"); - } else { - Class type = src.getClass(); - String key = REVERSE_DRAWABLE_TYPES.get(type); - while (key == null && type != null && type != Object.class) { - type = type.getSuperclass(); - key = REVERSE_DRAWABLE_TYPES.get(type); - } - if (key == null) { - ModularUI.LOGGER.error("Serialization of drawable of type '{}' failed, because a key for the type could not be found!", - src.getClass().getSimpleName()); - return JsonNull.INSTANCE; - } - json.addProperty("type", key); - if (!serializable.saveToJson(json)) { - ModularUI.LOGGER.error("Serialization of drawable of type '{}' failed!", src.getClass().getSimpleName()); - } - } - return json; - } - - private static ModularComponent parseText(JsonObject json) throws JsonParseException { - JsonParseException exception; - try { - return Component.Serializer.fromJson(json).asModular(); - } catch (JsonSyntaxException e) { - exception = e; - } - JsonElement element = JsonHelper.getJsonElement(json, "text", "string", "key"); - if (element == null || element.isJsonNull()) { - return Text.str("No text found!"); - } else if (element.isJsonPrimitive()) { - String s = element.getAsString(); - if (s.startsWith("translate:")) { - return Text.lang(s.substring(10)); - } - return JsonHelper.getBoolean(json, false, "lang", "translate") ? Text.lang(s) : Text.str(s); - } else if (element.isJsonArray()) { - ObjectList strings = ObjectList.create(); - for (JsonElement element1 : element.getAsJsonArray()) { - strings.add(parseText(element1)); - } - strings.trim(); - return Text.comp(strings.elements()); - } - throw exception; - } - - private static Component parseText(JsonElement element) throws JsonParseException { - JsonParseException exception = new JsonParseException("Could not parse IKey from %s".formatted(element)); - try { - MutableComponent component = Component.Serializer.fromJson(element); - if (component != null) { - return component.asModular(); - } - } catch (JsonSyntaxException e) { - exception = e; - } - if (element.isJsonPrimitive()) { - String s = element.getAsString(); - if (s.startsWith("translate:")) { - return Text.lang(s.substring(10)); - } - return Text.str(s); - } - if (element.isJsonObject()) { - return parseText(element.getAsJsonObject()); - } - throw exception; - } -} diff --git a/src/main/java/brachy/modularui/drawable/DrawableStack.java b/src/main/java/brachy/modularui/drawable/DrawableStack.java index c2f2e37..feb7717 100644 --- a/src/main/java/brachy/modularui/drawable/DrawableStack.java +++ b/src/main/java/brachy/modularui/drawable/DrawableStack.java @@ -1,31 +1,22 @@ package brachy.modularui.drawable; -import brachy.modularui.ModularUI; -import brachy.modularui.api.IJsonSerializable; import brachy.modularui.api.drawable.IDrawable; import brachy.modularui.screen.viewport.GuiContext; import brachy.modularui.theme.WidgetTheme; import brachy.modularui.utils.serialization.codec.CodecUtil; -import brachy.modularui.utils.serialization.json.JsonHelper; import net.minecraft.util.ExtraCodecs; import com.mojang.serialization.Codec; import net.minecraftforge.api.distmarker.Dist; import net.minecraftforge.api.distmarker.OnlyIn; -import com.google.gson.JsonArray; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import com.google.gson.JsonParseException; - -import java.util.ArrayList; import java.util.Collections; import java.util.List; /** * A stack of {@link IDrawable} backed by an array which are drawn on top of each other. */ -public record DrawableStack(IDrawable... drawables) implements IDrawable, IJsonSerializable { +public record DrawableStack(IDrawable... drawables) implements IDrawable { public static final Codec CODEC = ExtraCodecs.lazyInitializedCodec(() -> CodecUtil.checkedEncoder(IDrawable.CODEC.listOf().xmap(DrawableStack::fromList, DrawableStack::toList), @@ -66,42 +57,4 @@ public boolean canApplyTheme() { } return false; } - - public static IDrawable parseJson(JsonObject json) { - JsonElement drawables = JsonHelper.getJsonElement(json, "drawables", "children"); - if (drawables != null && drawables.isJsonArray()) { - return parseJson(drawables.getAsJsonArray()); - } - ModularUI.LOGGER.throwing( - new JsonParseException("DrawableStack json should have an array named 'drawables' or 'children'.")); - return IDrawable.EMPTY; - } - - public static IDrawable parseJson(JsonArray drawables) { - List list = new ArrayList<>(); - for (JsonElement child : drawables) { - IDrawable drawable = JsonHelper.deserialize(child, IDrawable.class); - if (drawable != null) { - list.add(drawable); - } - } - if (list.isEmpty()) { - return IDrawable.EMPTY; - } - if (list.size() == 1) { - return list.get(0); - } - return new DrawableStack(list.toArray(IDrawable[]::new)); - } - - // this method should never be called, but the special casing code is copied here in case it does. - @Override - public boolean saveToJson(JsonObject json) { - JsonArray jsonArray = new JsonArray(); - for (IDrawable drawable : this.drawables()) { - jsonArray.add(JsonHelper.serialize(drawable)); - } - json.add("drawables", jsonArray); - return true; - } } diff --git a/src/main/java/brachy/modularui/drawable/Icon.java b/src/main/java/brachy/modularui/drawable/Icon.java index ead9156..3671138 100644 --- a/src/main/java/brachy/modularui/drawable/Icon.java +++ b/src/main/java/brachy/modularui/drawable/Icon.java @@ -1,27 +1,24 @@ package brachy.modularui.drawable; import brachy.modularui.ModularUI; -import brachy.modularui.api.IJsonSerializable; import brachy.modularui.api.drawable.IDrawable; import brachy.modularui.api.drawable.IIcon; import brachy.modularui.screen.viewport.GuiContext; import brachy.modularui.theme.WidgetTheme; import brachy.modularui.utils.Alignment; import brachy.modularui.utils.serialization.codec.MutableObjectCodec; -import brachy.modularui.utils.serialization.json.JsonHelper; import brachy.modularui.widget.sizer.Box; import com.mojang.serialization.Codec; import net.minecraftforge.api.distmarker.Dist; import net.minecraftforge.api.distmarker.OnlyIn; -import com.google.gson.JsonObject; import lombok.Getter; /** * A {@link IDrawable} wrapper with a fixed size and an alignment. */ -public class Icon implements IIcon, IJsonSerializable { +public class Icon implements IIcon { public static final MutableObjectCodec CODEC = MutableObjectCodec.drawableBuilder(Icon::new) .add("drawable", Icon::drawable, Icon::getDrawable, IDrawable.CODEC) @@ -184,34 +181,6 @@ public Icon margin(Box box) { return this; } - @Override - public void loadFromJson(JsonObject json) { - this.width = (json.has("autoWidth") || json.has("autoSize")) && - JsonHelper.getBoolean(json, true, "autoWidth", "autoSize") ? 0 : - JsonHelper.getInt(json, 0, "width", "w", "size"); - this.height = (json.has("autoHeight") || json.has("autoSize")) && - JsonHelper.getBoolean(json, true, "autoHeight", "autoSize") ? 0 : - JsonHelper.getInt(json, 0, "height", "h", "size"); - this.aspectRatio = JsonHelper.getFloat(json, 0, "aspectRatio"); - this.alignment = JsonHelper.deserialize(json, Alignment.class, Alignment.Center, "alignment", "align"); - this.margin.fromJson(json); - } - - public static Icon ofJson(JsonObject json) { - return JsonHelper.deserialize(json, IDrawable.class, IDrawable.EMPTY, "drawable", "icon").asIcon(); - } - - @Override - public boolean saveToJson(JsonObject json) { - json.add("drawable", JsonHelper.serialize(this.drawable)); - json.addProperty("width", this.width); - json.addProperty("height", this.height); - json.addProperty("aspectRatio", this.aspectRatio); - json.add("alignment", JsonHelper.serialize(this.alignment)); - this.margin.toJson(json); - return true; - } - @Override public String toString() { return getClass().getSimpleName() + "(" + this.drawable.getClass().getSimpleName() + ")"; diff --git a/src/main/java/brachy/modularui/drawable/ItemDrawable.java b/src/main/java/brachy/modularui/drawable/ItemDrawable.java index 144a96a..0abafc4 100755 --- a/src/main/java/brachy/modularui/drawable/ItemDrawable.java +++ b/src/main/java/brachy/modularui/drawable/ItemDrawable.java @@ -1,6 +1,5 @@ package brachy.modularui.drawable; -import brachy.modularui.api.IJsonSerializable; import brachy.modularui.api.drawable.IDrawable; import brachy.modularui.screen.viewport.GuiContext; import brachy.modularui.theme.WidgetTheme; @@ -27,7 +26,7 @@ import java.util.List; @Accessors(fluent = true, chain = true) -public class ItemDrawable implements IDrawable, IJsonSerializable { +public class ItemDrawable implements IDrawable { public static final Codec CODEC = MutableObjectCodec.drawableBuilder(ItemDrawable::new) .add("items", ItemDrawable::items, ItemDrawable::getItemList, CodecUtil.listLike(ItemStack.CODEC)).alias("item") diff --git a/src/main/java/brachy/modularui/drawable/Rectangle.java b/src/main/java/brachy/modularui/drawable/Rectangle.java index d96f2bb..01d27bb 100644 --- a/src/main/java/brachy/modularui/drawable/Rectangle.java +++ b/src/main/java/brachy/modularui/drawable/Rectangle.java @@ -3,31 +3,25 @@ import brachy.modularui.GTRenderTypes; import brachy.modularui.ModularUI; import brachy.modularui.animation.IAnimatable; -import brachy.modularui.api.IJsonSerializable; import brachy.modularui.api.drawable.IDrawable; import brachy.modularui.screen.viewport.GuiContext; import brachy.modularui.theme.WidgetTheme; import brachy.modularui.utils.Color; import brachy.modularui.utils.Interpolations; import brachy.modularui.utils.serialization.codec.MutableObjectCodec; -import brachy.modularui.utils.serialization.json.JsonHelper; import com.mojang.blaze3d.vertex.VertexConsumer; import com.mojang.serialization.Codec; import net.minecraftforge.api.distmarker.Dist; import net.minecraftforge.api.distmarker.OnlyIn; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; import lombok.Getter; import lombok.Setter; import lombok.experimental.Accessors; import org.joml.Matrix4f; -import java.util.function.IntConsumer; - @Accessors(fluent = true, chain = true) -public class Rectangle implements IDrawable, IJsonSerializable, IAnimatable { +public class Rectangle implements IDrawable, IAnimatable { public static final MutableObjectCodec CODEC = MutableObjectCodec.drawableBuilder(Rectangle::new) .addOpt("colorTopLeft", Rectangle::colorTL, Rectangle::colorTL, Color.CODEC, Color.WHITE.main) @@ -152,65 +146,6 @@ private static void v(Matrix4f pose, VertexConsumer buffer, float x, float y, in .endVertex(); } - @Override - public void loadFromJson(JsonObject json) { - if (json.has("color")) { - color(Color.ofJson(json.get("color"))); - } - if (json.has("colorTop")) { - int c = Color.ofJson(json.get("colorTop")); - this.colorTL = c; - this.colorTR = c; - } - if (json.has("colorBottom")) { - int c = Color.ofJson(json.get("colorBottom")); - this.colorBL = c; - this.colorBR = c; - } - if (json.has("colorLeft")) { - int c = Color.ofJson(json.get("colorLeft")); - this.colorTL = c; - this.colorBL = c; - } - if (json.has("colorRight")) { - int c = Color.ofJson(json.get("colorRight")); - this.colorTR = c; - this.colorBR = c; - } - setColor(json, val -> this.colorTL = val, "colorTopLeft", "colorTL"); - setColor(json, val -> this.colorTR = val, "colorTopRight", "colorTR"); - setColor(json, val -> this.colorBL = val, "colorBottomLeft", "colorBL"); - setColor(json, val -> this.colorBR = val, "colorBottomRight", "colorBR"); - this.cornerRadius = JsonHelper.getInt(json, 0, "cornerRadius"); - this.cornerSegments = JsonHelper.getInt(json, 10, "cornerSegments"); - if (JsonHelper.getBoolean(json, false, "solid")) { - this.borderThickness = 0; - } else if (JsonHelper.getBoolean(json, false, "hollow")) { - this.borderThickness = 1; - } else { - this.borderThickness = JsonHelper.getFloat(json, 0, "borderThickness"); - } - } - - @Override - public boolean saveToJson(JsonObject json) { - json.addProperty("colorTL", this.colorTL); - json.addProperty("colorTR", this.colorTR); - json.addProperty("colorBL", this.colorBL); - json.addProperty("colorBR", this.colorBR); - json.addProperty("cornerRadius", this.cornerRadius); - json.addProperty("cornerSegments", this.cornerSegments); - json.addProperty("borderThickness", this.borderThickness); - return true; - } - - private void setColor(JsonObject json, IntConsumer color, String... keys) { - JsonElement element = JsonHelper.getJsonElement(json, keys); - if (element != null) { - color.accept(Color.ofJson(element)); - } - } - @Override public Rectangle interpolate(Rectangle start, Rectangle end, float t) { this.cornerRadius = Interpolations.lerp(start.cornerRadius, end.cornerRadius, t); diff --git a/src/main/java/brachy/modularui/drawable/Scrollbar.java b/src/main/java/brachy/modularui/drawable/Scrollbar.java index c1713e7..4e803ab 100755 --- a/src/main/java/brachy/modularui/drawable/Scrollbar.java +++ b/src/main/java/brachy/modularui/drawable/Scrollbar.java @@ -1,17 +1,13 @@ package brachy.modularui.drawable; -import brachy.modularui.api.IJsonSerializable; import brachy.modularui.api.drawable.IDrawable; import brachy.modularui.screen.viewport.GuiContext; import brachy.modularui.theme.WidgetTheme; import brachy.modularui.utils.Color; -import brachy.modularui.utils.serialization.json.JsonHelper; import com.mojang.serialization.Codec; -import com.google.gson.JsonObject; - -public record Scrollbar(boolean striped) implements IDrawable, IJsonSerializable { +public record Scrollbar(boolean striped) implements IDrawable { public static final Scrollbar DEFAULT = new Scrollbar(false); public static final Scrollbar VANILLA = new Scrollbar(true); @@ -24,13 +20,6 @@ public static Scrollbar get(boolean striped) { Codec.BOOL.fieldOf("striped").xmap(Scrollbar::get, Scrollbar::striped).codec(), "scrollbar", "Scrollbar"); - public static Scrollbar ofJson(JsonObject json) { - if (JsonHelper.getBoolean(json, false, "striped", "vanilla")) { - return VANILLA; - } - return DEFAULT; - } - @Override public void draw(GuiContext context, int x, int y, int width, int height, WidgetTheme widgetTheme) { GuiDraw.drawRect(context.getGraphics(), x, y, width, height, Color.mix(0xFFEEEEEE, widgetTheme.getColor())); @@ -62,10 +51,4 @@ public void draw(GuiContext context, int x, int y, int width, int height, Widget public boolean canApplyTheme() { return true; } - - @Override - public boolean saveToJson(JsonObject json) { - json.addProperty("striped", this.striped); - return true; - } } diff --git a/src/main/java/brachy/modularui/drawable/TextureRegistry.java b/src/main/java/brachy/modularui/drawable/TextureRegistry.java new file mode 100644 index 0000000..1944fc8 --- /dev/null +++ b/src/main/java/brachy/modularui/drawable/TextureRegistry.java @@ -0,0 +1,72 @@ +package brachy.modularui.drawable; + +import brachy.modularui.ModularUI; +import brachy.modularui.ModularUIConfig; + +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import org.jetbrains.annotations.Nullable; + +import java.util.Map; + +public class TextureRegistry { + + private static final Map TEXTURES = new Object2ObjectOpenHashMap<>(); + private static final Map REVERSE_TEXTURES = new Object2ObjectOpenHashMap<>(); + + private static synchronized void registerTextureInternal(String name, UITexture texture) { + if (texture == null) return; + UITexture current = TEXTURES.put(name, texture); + REVERSE_TEXTURES.put(texture, name); + if (current != null && (ModularUI.isDev() || ModularUIConfig.DEBUG_UI.get())) { + ModularUI.LOGGER.warn("[DEBUG] Replacing texture with name '{}' and location '{}' with texture with location '{}'", name, current.location, texture.location); + } + } + + public static synchronized void registerTexture(String name, UITexture texture) { + String current = REVERSE_TEXTURES.get(texture); + if (current != null) { + if (name != null && !current.equals(name)) { + TEXTURES.put(name, texture); + REVERSE_TEXTURES.put(texture, name); + TEXTURES.remove(current); + } + return; + } + if (name == null) { + registerTextureAutoName(texture); + } else { + registerTextureInternal(name, texture); + } + } + + public static void registerTextureAutoName(UITexture texture) { + if (texture == null) return; + String[] p = texture.location.getPath().split("/"); + p = p[p.length - 1].split("\\."); + String baseName = texture.location.getNamespace() + ":" + p[0]; + String name = baseName; + int number = 0; + UITexture current; + while ((current = TEXTURES.get(name)) != null) { + if (current.equals(texture)) return; + number++; + name = baseName + "_" + number; + if (number == 20 && (ModularUI.isDev() || ModularUIConfig.DEBUG_UI.get())) { + ModularUI.LOGGER.warn("[DEBUG] Trying to register a UITexture with location '{}' for at least 20 times. This is likely a bug and should be fixed.", texture.location); + } else if (number == 10000) { + throw new IllegalStateException("Trying to register a UITexture with location '" + texture.location + "' 10000 times."); + } + } + registerTexture(name, texture); + } + + @Nullable + public static UITexture getTexture(String s) { + return TEXTURES.get(s); + } + + @Nullable + public static String getTextureId(UITexture texture) { + return REVERSE_TEXTURES.get(texture); + } +} diff --git a/src/main/java/brachy/modularui/drawable/TiledUITexture.java b/src/main/java/brachy/modularui/drawable/TiledUITexture.java index 1f57310..55a0ae7 100644 --- a/src/main/java/brachy/modularui/drawable/TiledUITexture.java +++ b/src/main/java/brachy/modularui/drawable/TiledUITexture.java @@ -2,14 +2,12 @@ import brachy.modularui.screen.viewport.GuiContext; -import lombok.Getter; -import lombok.experimental.Accessors; - import net.minecraft.client.renderer.GameRenderer; import net.minecraft.resources.ResourceLocation; import com.mojang.blaze3d.systems.RenderSystem; -import com.google.gson.JsonObject; +import lombok.Getter; +import lombok.experimental.Accessors; import java.util.Objects; @@ -45,14 +43,6 @@ public void draw(GuiContext context, float x, float y, float width, float height this.imageWidth, this.imageHeight, 0); } - @Override - protected void saveTextureToJson(JsonObject json) { - super.saveToJson(json); - json.addProperty("imageWidth", this.imageWidth); - json.addProperty("imageHeight", this.imageHeight); - json.addProperty("tiled", true); - } - @Override protected TiledUITexture copy() { return new TiledUITexture(location, u0, v0, u1, v1, colorType, nonOpaque, colorOverride, imageWidth, imageHeight); diff --git a/src/main/java/brachy/modularui/drawable/UITexture.java b/src/main/java/brachy/modularui/drawable/UITexture.java index ebbfaaa..cf9aac6 100644 --- a/src/main/java/brachy/modularui/drawable/UITexture.java +++ b/src/main/java/brachy/modularui/drawable/UITexture.java @@ -1,7 +1,6 @@ package brachy.modularui.drawable; import brachy.modularui.ModularUI; -import brachy.modularui.api.IJsonSerializable; import brachy.modularui.api.drawable.IDrawable; import brachy.modularui.screen.viewport.GuiContext; import brachy.modularui.theme.WidgetTheme; @@ -9,7 +8,6 @@ import brachy.modularui.utils.Interpolations; import brachy.modularui.utils.serialization.codec.CodecUtil; import brachy.modularui.utils.serialization.codec.MutableObjectCodec; -import brachy.modularui.utils.serialization.json.JsonHelper; import net.minecraft.resources.FileToIdConverter; import net.minecraft.resources.ResourceLocation; @@ -19,8 +17,6 @@ import net.minecraftforge.api.distmarker.Dist; import net.minecraftforge.api.distmarker.OnlyIn; -import com.google.gson.JsonObject; -import com.google.gson.JsonParseException; import lombok.Getter; import lombok.Setter; import lombok.experimental.Accessors; @@ -29,10 +25,10 @@ import java.util.Objects; @Accessors(fluent = true, chain = true) -public class UITexture implements IDrawable, IJsonSerializable { +public class UITexture implements IDrawable { public static final Codec CODEC_FROM_BUILDER = Builder.CODEC.xmap(Builder::buildForCodec, UITexture::toBuilder); - public static final Codec CODEC_FROM_NAME = ExtraCodecs.stringResolverCodec(DrawableSerialization::getTextureId, DrawableSerialization::getTexture); + public static final Codec CODEC_FROM_NAME = ExtraCodecs.stringResolverCodec(TextureRegistry::getTextureId, TextureRegistry::getTexture); public static final Codec CODEC = IDrawable.CODECS.register("texture", CodecUtil.chainedCodec(CODEC_FROM_NAME.fieldOf("name").codec(), CODEC_FROM_BUILDER)); @@ -156,7 +152,7 @@ public static UITexture fullImage(String mod, String location, ColorType colorTy } public UITexture register(String name) { - DrawableSerialization.registerTexture(name, this); + TextureRegistry.registerTexture(name, this); return this; } @@ -219,81 +215,6 @@ public void applyColor(int themeColor) { } } - public static UITexture parseFromJson(JsonObject json) { - String name = JsonHelper.getString(json, null, "name", "id"); - if (name != null) { - UITexture drawable = DrawableSerialization.getTexture(name); - if (drawable != null) return drawable; - ModularUI.LOGGER.error("Tried to parse UITexture from json, but no texture with name '{}' is registered!", name); - return GuiTextures.HELP; - } - Builder builder = builder(); - builder.location(JsonHelper.getString(json, ModularUI.MOD_ID + ":gui/widgets/error", "location")) - .imageSize(JsonHelper.getInt(json, -1, "imageWidth", "iw"), - JsonHelper.getInt(json, -1, "imageHeight", "ih")); - boolean mode1 = json.has("x") || json.has("y") || json.has("w") || json.has("h") || json.has("width") || - json.has("height"); - boolean mode2 = json.has("u0") || json.has("v0") || json.has("u1") || json.has("u1"); - if (mode1) { - if (mode2) { - throw new JsonParseException("Tried to specify x, y, w, h and u0, v0, u1, v1!"); - } - builder.subAreaXYWH(JsonHelper.getInt(json, 0, "x"), - JsonHelper.getInt(json, 0, "y"), - JsonHelper.getInt(json, builder.iw, "w", "width"), - JsonHelper.getInt(json, builder.ih, "h", "height")); - } else if (mode2) { - builder.subAreaUV(JsonHelper.getFloat(json, 0, "u0"), - JsonHelper.getFloat(json, 0, "v0"), - JsonHelper.getFloat(json, 1, "u1"), - JsonHelper.getFloat(json, 1, "v1")); - } - int bl = JsonHelper.getInt(json, 0, "bl", "borderLeft", "borderX", "border"); - int br = JsonHelper.getInt(json, 0, "br", "borderRight", "borderY", "border"); - int bt = JsonHelper.getInt(json, 0, "bt", "borderTop", "borderBottom", "border"); - int bb = JsonHelper.getInt(json, 0, "bb", "borderBottom", "borderTop", "border"); - if (bl > 0 || br > 0 || bt > 0 || bb > 0) { - builder.adaptable(bl, bt, br, bb); - } - if (JsonHelper.getBoolean(json, false, "tiled")) { - builder.tiled(); - } - String colorTypeName = JsonHelper.getString(json, null, "colorType", "color"); - if (colorTypeName != null) { - builder.colorType(ColorType.get(colorTypeName)); - } else if (JsonHelper.getBoolean(json, false, "canApplyTheme")) { - builder.canApplyTheme(); - } - if (JsonHelper.getBoolean(json, false, "nonOpaque")) { - builder.nonOpaque(); - } - UITexture uiTexture = builder.build(); - uiTexture.colorOverride = JsonHelper.getColor(json, 0, "colorOverride"); - return uiTexture; - } - - @Override - public boolean saveToJson(JsonObject json) { - String name = DrawableSerialization.getTextureId(this); - if (name != null) { - json.addProperty("id", name); - return true; - } - saveTextureToJson(json); - return true; - } - - protected void saveTextureToJson(JsonObject json) { - json.addProperty("location", this.location.toString()); - json.addProperty("u0", this.u0); - json.addProperty("v0", this.v0); - json.addProperty("u1", this.u1); - json.addProperty("v1", this.v1); - if (this.colorType != null) json.addProperty("colorType", this.colorType.getName()); - json.addProperty("nonOpaque", this.nonOpaque); - json.addProperty("colorOverride", this.colorOverride); - } - @Override public String getTypeName() { return "texture"; @@ -632,7 +553,7 @@ public UITexture build() { .resultOrPartial(s -> { throw new IllegalArgumentException(s); }).map(texture -> { - DrawableSerialization.registerTexture(this.name, texture); + TextureRegistry.registerTexture(this.name, texture); return texture; }).map(texture -> this.colorOverride != 0 ? texture.withColorOverride(this.colorOverride) : texture) .orElseThrow(); diff --git a/src/main/java/brachy/modularui/drawable/text/ModularComponent.java b/src/main/java/brachy/modularui/drawable/text/ModularComponent.java index 67b784b..3ae26a2 100644 --- a/src/main/java/brachy/modularui/drawable/text/ModularComponent.java +++ b/src/main/java/brachy/modularui/drawable/text/ModularComponent.java @@ -5,7 +5,6 @@ import brachy.modularui.theme.WidgetTheme; import brachy.modularui.utils.Alignment; import brachy.modularui.utils.serialization.codec.MutableObjectCodec; -import brachy.modularui.utils.serialization.json.JsonHelper; import brachy.modularui.widgets.TextWidget; import net.minecraft.ChatFormatting; @@ -24,7 +23,6 @@ import net.minecraft.util.ExtraCodecs; import com.mojang.serialization.Codec; -import com.google.gson.JsonObject; import lombok.Getter; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -37,7 +35,6 @@ public class ModularComponent extends MutableComponent implements Text { - // TODO: This currently doesn't not handle nested components correctly. I might have to write a complete custom codec for this. public static final MutableObjectCodec CODEC = MutableObjectCodec.drawableBuilder(ModularComponent.class, "Text") .wrapped(ExtraCodecs.COMPONENT.xmap(ModularComponent::of, mc -> mc)) .addOpt("alignment", ModularComponent::alignment, ModularComponent::getAlignment, Alignment.CODEC, Alignment.Center) @@ -226,24 +223,4 @@ public ModularComponent asModular() { public @NotNull ModularComponent withStyle(@NotNull UnaryOperator