diff --git a/build.gradle b/build.gradle index 359b7d5..a7075c0 100755 --- a/build.gradle +++ b/build.gradle @@ -86,3 +86,9 @@ tasks.withType(JavaCompile).configureEach { lombok { version = "1.18.38" } + +test { + useJUnitPlatform() + + systemProperty "unit.testing", "true" +} 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/ClientProxy.java b/src/main/java/brachy/modularui/ClientProxy.java index 2be93d7..52421f8 100644 --- a/src/main/java/brachy/modularui/ClientProxy.java +++ b/src/main/java/brachy/modularui/ClientProxy.java @@ -2,24 +2,29 @@ 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; 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; 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 +33,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 { @@ -47,7 +54,6 @@ public class ClientProxy extends CommonProxy { if (!ModularUI.isDataGen()) { CursorHandler.init(); AnimatorManager.init(); - DrawableSerialization.init(); } } @@ -58,6 +64,18 @@ protected void onInit(FMLCommonSetupEvent event) { // enable stencil bits, must call on render thread RenderSystem.recordRenderCall(() -> Minecraft.getInstance().getMainRenderTarget().enableStencil()); } + test(ModularComponent.CODEC.mutableCodec(), 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/ModularUI.java b/src/main/java/brachy/modularui/ModularUI.java index fa286ae..45b88e7 100644 --- a/src/main/java/brachy/modularui/ModularUI.java +++ b/src/main/java/brachy/modularui/ModularUI.java @@ -32,6 +32,7 @@ public class ModularUI { private static final ResourceLocation TEMPLATE_LOCATION = new ResourceLocation(MOD_ID, ""); public static final Logger LOGGER = LogManager.getLogger(NAME); + public static final boolean UNIT_TEST = Boolean.getBoolean("unit.testing"); public ModularUI() { DistExecutor.unsafeRunForDist(() -> ClientProxy::new, () -> CommonProxy::new); @@ -69,6 +70,10 @@ public static boolean isDev() { return !isProd(); } + public static boolean isTestEnv() { + return UNIT_TEST; + } + /** * @return if we're running data generation */ @@ -99,7 +104,7 @@ public static boolean isModLoaded(String modId) { * @return if the current thread is the client thread */ public static boolean isClientThread() { - return isClientSide() && Minecraft.getInstance().isSameThread(); + return isTestEnv() || (isClientSide() && Minecraft.getInstance().isSameThread()); } /** diff --git a/src/main/java/brachy/modularui/ModularUIConfig.java b/src/main/java/brachy/modularui/ModularUIConfig.java index 3264d8e..11994c1 100644 --- a/src/main/java/brachy/modularui/ModularUIConfig.java +++ b/src/main/java/brachy/modularui/ModularUIConfig.java @@ -188,7 +188,7 @@ public static final class Dev { private Dev() {} public static boolean debugUI() { - return DEBUG_UI.get(); + return ModularUI.isTestEnv() || DEBUG_UI.get(); } public static int textColor() { 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 88271d1..0000000 --- a/src/main/java/brachy/modularui/api/IJsonSerializable.java +++ /dev/null @@ -1,49 +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; - -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/ITheme.java b/src/main/java/brachy/modularui/api/ITheme.java index 85660f9..41d171f 100644 --- a/src/main/java/brachy/modularui/api/ITheme.java +++ b/src/main/java/brachy/modularui/api/ITheme.java @@ -6,16 +6,47 @@ 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) { + var mapBuilder = ops.mapBuilder(); + mapBuilder.add("id", ops.createString(input.getId())); + mapBuilder.add("parent", ops.createString(input.getParentTheme().getId())); + input.getFallback().encode(ops, mapBuilder, true); + 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, false); + set.add(entry.theme()); + set.add(entry.hoverTheme()); + } + return mapBuilder.build(prefix); + } + }; + /** * @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/api/MCHelper.java b/src/main/java/brachy/modularui/api/MCHelper.java index 2f4391e..ff20a5f 100644 --- a/src/main/java/brachy/modularui/api/MCHelper.java +++ b/src/main/java/brachy/modularui/api/MCHelper.java @@ -11,18 +11,23 @@ import net.minecraftforge.fml.relauncher.Side; import net.minecraftforge.fml.relauncher.SideOnly; +import org.jetbrains.annotations.Nullable; + import java.util.List; public class MCHelper { + @SuppressWarnings("DataFlowIssue") + @Nullable @SideOnly(Side.CLIENT) public static Minecraft getMc() { return Minecraft.getInstance(); } + @Nullable @SideOnly(Side.CLIENT) public static Player getPlayer() { - return getMc().player; + return getMc() == null ? null : getMc().player; } @SideOnly(Side.CLIENT) @@ -60,12 +65,12 @@ public static void setScreen(Screen screen) { @SideOnly(Side.CLIENT) public static Screen getCurrentScreen() { - return getMc().screen; + return getMc() == null ? null : getMc().screen; } @SideOnly(Side.CLIENT) public static Font getFont() { - return getMc().font; + return getMc() == null ? null : getMc().font; } public static List getItemToolTip(ItemStack item) { diff --git a/src/main/java/brachy/modularui/api/drawable/IDrawable.java b/src/main/java/brachy/modularui/api/drawable/IDrawable.java index 04fcfe9..c535ccf 100644 --- a/src/main/java/brachy/modularui/api/drawable/IDrawable.java +++ b/src/main/java/brachy/modularui/api/drawable/IDrawable.java @@ -9,12 +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.utils.serialization.codec.CodecUtil; import brachy.modularui.widget.Widget; import brachy.modularui.widget.sizer.Area; +import com.mojang.serialization.Codec; +import com.mojang.serialization.DataResult; +import com.mojang.serialization.JsonOps; +import com.mojang.serialization.MapCodec; import net.minecraftforge.api.distmarker.Dist; import net.minecraftforge.api.distmarker.OnlyIn; +import com.google.gson.JsonElement; import org.jetbrains.annotations.Nullable; /** @@ -34,6 +41,55 @@ static IDrawable of(IDrawable... drawables) { } } + /** + * An empty drawable. Does nothing. + */ + IDrawable EMPTY = new IDrawable() { + @Override + public void draw(GuiContext context, int x, int y, int width, int height, WidgetTheme widgetTheme) {} + + @Override + public String toString() { + return "IDrawable.EMPTY"; + } + }; + + /** + * An empty drawable used to mark hover textures as "should not be used"! + */ + IDrawable NONE = new IDrawable() { + @Override + public void draw(GuiContext context, int x, int y, int width, int height, WidgetTheme widgetTheme) {} + + @Override + public String toString() { + return "IDrawable.NONE"; + } + }; + + CodecRegistry CODECS = new CodecRegistry<>(); + MapCodec CODEC_DISPATCH = CodecUtil.dispatchNullable(Codec.STRING, IDrawable::getTypeName, CODECS::getNullableCodec); + 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( + CodecUtil.nullCodec(EMPTY), CODEC_EMPTY_NONE, + DrawableStack.CODEC, CODEC_DISPATCH.codec()); + + 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. @@ -164,15 +220,9 @@ default IDrawable getSubArea(float u0, float v0, float u1, float v1) { return new SubAreaDrawable(this).uv(u0, v0, u1, v1); } - /** - * 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) -> {}; + default String getTypeName() { + return getClass().getSimpleName(); + } static boolean isVisible(@Nullable IDrawable drawable) { if (drawable == null || drawable == EMPTY || drawable == NONE) return false; diff --git a/src/main/java/brachy/modularui/api/drawable/Text.java b/src/main/java/brachy/modularui/api/drawable/Text.java index 947745f..4a70acb 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; @@ -8,6 +7,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 net.minecraft.ChatFormatting; import net.minecraft.network.chat.Component; @@ -15,7 +15,6 @@ 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; @@ -25,7 +24,9 @@ /** * This represents a piece of text in a GUI. */ -public interface Text extends IDrawable, IJsonSerializable { +public interface Text extends IDrawable { + + MutableObjectCodec CODEC = ModularComponent.CODEC; int TEXT_COLOR = 0xFF404040; @@ -235,17 +236,7 @@ default KeyIcon asTextIcon() { } @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"));*/ - } + default String getTypeName() { + return "text"; } } diff --git a/src/main/java/brachy/modularui/api/widget/IWidget.java b/src/main/java/brachy/modularui/api/widget/IWidget.java index 93e85e0..85e5ea3 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::getNullableCodec); + 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/core/mixins/common/ComponentSerializerMixin.java b/src/main/java/brachy/modularui/core/mixins/common/ComponentSerializerMixin.java new file mode 100644 index 0000000..99050ba --- /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.mutableCodec().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.mutableCodec().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/core/mixins/common/TextColorMixin.java b/src/main/java/brachy/modularui/core/mixins/common/TextColorMixin.java new file mode 100644 index 0000000..bf5109b --- /dev/null +++ b/src/main/java/brachy/modularui/core/mixins/common/TextColorMixin.java @@ -0,0 +1,19 @@ +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.Redirect; + +@Mixin(TextColor.class) +public class TextColorMixin { + + /** + * @reason Minecraft uses Integer.parseInt which fails when alpha is specified because of signed vs. unsigned + */ + @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, radix); + } +} diff --git a/src/main/java/brachy/modularui/drawable/AdaptableUITexture.java b/src/main/java/brachy/modularui/drawable/AdaptableUITexture.java index 7013503..b971b9a 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)} @@ -203,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, @@ -226,6 +227,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/Circle.java b/src/main/java/brachy/modularui/drawable/Circle.java index 56ffbbb..4a6ae82 100644 --- a/src/main/java/brachy/modularui/drawable/Circle.java +++ b/src/main/java/brachy/modularui/drawable/Circle.java @@ -1,24 +1,33 @@ 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.json.JsonHelper; +import brachy.modularui.utils.serialization.codec.MutableObjectCodec; +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.ToString; import lombok.experimental.Accessors; +@ToString @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") + .addOpt("colorOuter", Circle::colorOuter, Circle::colorOuter, Codec.INT, 0).alias("color") + .addOpt("segments", Circle::segments, Circle::segments, Codec.INT, 40) + .build(); + + @Getter @Setter private int colorInner, colorOuter, segments; @@ -28,22 +37,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; @@ -62,21 +55,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); @@ -88,7 +66,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/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/DrawableSerialization.java b/src/main/java/brachy/modularui/drawable/DrawableSerialization.java deleted file mode 100644 index 570b705..0000000 --- a/src/main/java/brachy/modularui/drawable/DrawableSerialization.java +++ /dev/null @@ -1,256 +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("item", ItemDrawable.class, ItemDrawable::ofJson); - 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 86a74cb..cf0ec2d 100644 --- a/src/main/java/brachy/modularui/drawable/DrawableStack.java +++ b/src/main/java/brachy/modularui/drawable/DrawableStack.java @@ -1,31 +1,43 @@ 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.json.JsonHelper; +import brachy.modularui.utils.serialization.codec.CodecUtil; +import lombok.ToString; + +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), + 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; } @@ -47,42 +59,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/FluidDrawable.java b/src/main/java/brachy/modularui/drawable/FluidDrawable.java index f0622eb..578112e 100644 --- a/src/main/java/brachy/modularui/drawable/FluidDrawable.java +++ b/src/main/java/brachy/modularui/drawable/FluidDrawable.java @@ -3,29 +3,71 @@ 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 lombok.ToString; + +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; +@ToString +@Accessors(fluent = true, chain = true) public class FluidDrawable implements IDrawable { - private FluidStack fluid = null; + public static final MutableObjectCodec 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(); + + private FluidStack[] fluids = new FluidStack[0]; + @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 +86,66 @@ 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 FluidStack[] getFluids() { + return 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 diff --git a/src/main/java/brachy/modularui/drawable/Icon.java b/src/main/java/brachy/modularui/drawable/Icon.java index ff8b2fa..3671138 100644 --- a/src/main/java/brachy/modularui/drawable/Icon.java +++ b/src/main/java/brachy/modularui/drawable/Icon.java @@ -1,34 +1,43 @@ 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.json.JsonHelper; +import brachy.modularui.utils.serialization.codec.MutableObjectCodec; 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 { - 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 +94,11 @@ public IDrawable getWrappedDrawable() { return drawable; } + public Icon drawable(IDrawable drawable) { + this.drawable = drawable; + return this; + } + public Icon expandWidth() { return width(0); } @@ -160,32 +174,11 @@ public Icon marginBottom(int val) { 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; + public Icon margin(Box box) { + if (box != null && box != this.margin) { + Box.CODEC.copyFields(box, this.margin); + } + return this; } @Override 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..1237e64 100755 --- a/src/main/java/brachy/modularui/drawable/ItemDrawable.java +++ b/src/main/java/brachy/modularui/drawable/ItemDrawable.java @@ -1,140 +1,159 @@ 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.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 lombok.ToString; + +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; + +@ToString +@Accessors(fluent = true, chain = true) +public class ItemDrawable implements IDrawable { -public class ItemDrawable implements IDrawable, IJsonSerializable { + public static final MutableObjectCodec 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(); + private ItemStack[] items = new ItemStack[0]; @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 ItemStack[] getItems() { + return this.items; + } + + public void ingredient(Ingredient ingredient) { + items(ingredient.getItems()); } - public ItemDrawable setItem(@NotNull ItemStack item) { - this.item = item; + public ItemDrawable items(Collection items) { + return items(items.toArray(ItemStack[]::new)); + } + + 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 item(ItemLike item) { + return item(item.asItem(), 1, null); } - public ItemDrawable setItem(@NotNull Item item, int amount) { - return setItem(item, amount, null); + public ItemDrawable item(ItemLike item, int amount) { + return item(item, amount, null); } - public ItemDrawable setItem(@NotNull Item item, int amount, @Nullable CompoundTag nbt) { + 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/drawable/Rectangle.java b/src/main/java/brachy/modularui/drawable/Rectangle.java index a11c398..08e12e8 100644 --- a/src/main/java/brachy/modularui/drawable/Rectangle.java +++ b/src/main/java/brachy/modularui/drawable/Rectangle.java @@ -3,33 +3,55 @@ 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.json.JsonHelper; +import brachy.modularui.utils.serialization.codec.MutableObjectCodec; import com.mojang.blaze3d.vertex.VertexConsumer; +import com.mojang.serialization.Codec; + +import lombok.ToString; + 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; - +@ToString @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) + .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) + .addOpt("canApplyTheme", Rectangle::canApplyTheme, Rectangle::canApplyTheme, Codec.BOOL, false) + .build(); - private int cornerRadius, colorTL, colorTR, colorBL, colorBR; + @Getter + @Setter + private int colorTL, colorTR, colorBL, colorBR; + @Getter + private int cornerRadius; + @Getter @Setter private int cornerSegments; + @Getter + @Setter private float borderThickness; @Getter @Setter @@ -128,65 +150,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); @@ -206,4 +169,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/Scrollbar.java b/src/main/java/brachy/modularui/drawable/Scrollbar.java index f3b35c4..e9c16ae 100755 --- a/src/main/java/brachy/modularui/drawable/Scrollbar.java +++ b/src/main/java/brachy/modularui/drawable/Scrollbar.java @@ -1,26 +1,26 @@ 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.google.gson.JsonObject; +import com.mojang.serialization.Codec; +import com.mojang.serialization.MapCodec; -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); - public static Scrollbar ofJson(JsonObject json) { - if (JsonHelper.getBoolean(json, false, "striped", "vanilla")) { - return VANILLA; - } - return DEFAULT; + public static Scrollbar get(boolean striped) { + return striped ? VANILLA : DEFAULT; } + public static final MapCodec CODEC = IDrawable.CODECS.register( + Codec.BOOL.fieldOf("striped").xmap(Scrollbar::get, Scrollbar::striped), + "scrollbar", "Scrollbar"); + @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())); @@ -52,10 +52,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/SubAreaDrawable.java b/src/main/java/brachy/modularui/drawable/SubAreaDrawable.java index 76fcd5e..03f6cd9 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 MutableObjectCodec 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); } 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 d2c3393..55a0ae7 100644 --- a/src/main/java/brachy/modularui/drawable/TiledUITexture.java +++ b/src/main/java/brachy/modularui/drawable/TiledUITexture.java @@ -6,13 +6,15 @@ 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; +@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()} @@ -41,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); @@ -59,6 +53,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..eacc426 100644 --- a/src/main/java/brachy/modularui/drawable/UITexture.java +++ b/src/main/java/brachy/modularui/drawable/UITexture.java @@ -1,30 +1,40 @@ 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.Color; import brachy.modularui.utils.Interpolations; -import brachy.modularui.utils.serialization.json.JsonHelper; -import brachy.modularui.widget.sizer.Area; +import brachy.modularui.utils.serialization.codec.CodecUtil; +import brachy.modularui.utils.serialization.codec.MutableObjectCodec; + +import lombok.ToString; 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 com.mojang.serialization.MapCodec; 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; import java.util.Objects; +@ToString @Accessors(fluent = true, chain = true) -public class UITexture implements IDrawable, IJsonSerializable { +public class UITexture implements IDrawable { + + public static final MapCodec CODEC_FROM_BUILDER = Builder.CODEC.xmap(Builder::buildForCodec, UITexture::toBuilder); + public static final Codec CODEC_FROM_NAME = ExtraCodecs.stringResolverCodec(TextureRegistry::getTextureId, TextureRegistry::getTexture); + public static final MapCodec CODEC = IDrawable.CODECS.register("texture", + CodecUtil.chainedMapCodec(CODEC_FROM_NAME.fieldOf("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 +59,15 @@ 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; + @ToString.Include + @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 @@ -147,7 +157,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; } @@ -210,79 +220,17 @@ 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, defaultImageWidth, "imageWidth", "iw"), - JsonHelper.getInt(json, defaultImageHeight, "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; + public String getTypeName() { + return "texture"; } - 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); + public Builder toBuilder() { + return builder() + .location(this.location) + .subAreaUV(this.u0, this.v0, this.u1, this.v1) + .colorType(this.colorType) + .nonOpaque(this.nonOpaque); } @Override @@ -311,28 +259,48 @@ 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. */ + @Accessors(fluent = false) public static class Builder { - private ResourceLocation location; - 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; + public static final MutableObjectCodec CODEC = MutableObjectCodec.builder(UITexture::builder) + .add("location", Builder::location, Builder::getLocation, ResourceLocation.CODEC) + .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).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 = -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; + @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 +354,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 +535,16 @@ 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; + } + + public Builder colorOverride(int colorOverride) { + this.colorOverride = colorOverride; return this; } @@ -573,16 +554,28 @@ 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 -> { + TextureRegistry.registerTexture(this.name, texture); + return texture; + }).map(texture -> this.colorOverride != 0 ? texture.withColorOverride(this.colorOverride) : texture) + .orElseThrow(); + } + + 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 UITexture create() { + 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) throw new IllegalArgumentException("Image size must be > 0"); if (this.mode == Mode.FULL) { this.u0 = 0; this.v0 = 0; @@ -590,6 +583,10 @@ private UITexture 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"); + } float tw = 1f / this.iw, th = 1f / this.ih; this.u0 = this.x * tw; this.v0 = this.y * th; @@ -598,19 +595,81 @@ 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); + 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) { - return new TiledUITexture(this.location, this.u0, this.v0, this.u1, this.v1, - this.colorType, this.nonOpaque, 0, this.iw, this.ih); + 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)); } - 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/drawable/text/ModularComponent.java b/src/main/java/brachy/modularui/drawable/text/ModularComponent.java index a7ee775..3ae26a2 100644 --- a/src/main/java/brachy/modularui/drawable/text/ModularComponent.java +++ b/src/main/java/brachy/modularui/drawable/text/ModularComponent.java @@ -4,7 +4,7 @@ import brachy.modularui.screen.viewport.GuiContext; import brachy.modularui.theme.WidgetTheme; import brachy.modularui.utils.Alignment; -import brachy.modularui.utils.serialization.json.JsonHelper; +import brachy.modularui.utils.serialization.codec.MutableObjectCodec; import brachy.modularui.widgets.TextWidget; import net.minecraft.ChatFormatting; @@ -20,8 +20,9 @@ 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; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -34,6 +35,14 @@ public class ModularComponent extends MutableComponent implements Text { + 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 +88,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 +173,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; @@ -209,24 +223,4 @@ public ModularComponent asModular() { public @NotNull ModularComponent withStyle(@NotNull UnaryOperator