Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
980aa08
json stuff testing
brachy84 May 16, 2026
15cc89e
codec
brachy84 May 17, 2026
7360e2f
more codec stuff
brachy84 May 17, 2026
2f085c1
working codecs for Unit, DimensionSizer & StandardResizer
brachy84 May 17, 2026
75d61e3
more codecs
brachy84 May 17, 2026
226bdf4
some tweaks
brachy84 May 17, 2026
c7a508c
texture codec
brachy84 May 18, 2026
045d3bd
drawable codec & some fixes
brachy84 May 18, 2026
6ac36f1
fix color conversion inaccuracy
brachy84 May 18, 2026
fbbfe77
unit tests for codecs & colors
brachy84 May 18, 2026
951e8b3
ModularComponent codec
brachy84 May 18, 2026
fc47917
Redirect instead of Inject
brachy84 May 18, 2026
526ee03
rip radix
brachy84 May 18, 2026
e4527ad
someone is not going to like this
brachy84 May 18, 2026
9858c29
dont encode unset fields & restructure a little bit
brachy84 May 18, 2026
ab82c6c
Icon codec
brachy84 May 18, 2026
224c9e8
ItemDrawable codec & merge IngredientDrawable
brachy84 May 18, 2026
d53a0d0
make FluidDrawable consistent with ItemDrawable
brachy84 May 18, 2026
e822dae
last drawable codec
brachy84 May 18, 2026
0e02593
some fixes
brachy84 May 19, 2026
d0eab8d
widget theme json parsing via codec
brachy84 May 19, 2026
294a416
remove all kinds of manual json parsing
brachy84 May 19, 2026
7a931c1
map codec shenanigans
brachy84 May 19, 2026
f09f7d3
Merge branch '1.20.1' into editor
brachy84 May 20, 2026
630d0a3
everything is better with MapCodec and RecordBuilder
brachy84 May 20, 2026
aebda5c
toString on some drawables
brachy84 May 20, 2026
fd55f3a
first widget codec
brachy84 May 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -86,3 +86,9 @@ tasks.withType(JavaCompile).configureEach {
lombok {
version = "1.18.38"
}

test {
useJUnitPlatform()

systemProperty "unit.testing", "true"
}
4 changes: 4 additions & 0 deletions dependencies.gradle
Original file line number Diff line number Diff line change
@@ -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())
Expand Down
22 changes: 20 additions & 2 deletions src/main/java/brachy/modularui/ClientProxy.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 {
Expand All @@ -47,7 +54,6 @@ public class ClientProxy extends CommonProxy {
if (!ModularUI.isDataGen()) {
CursorHandler.init();
AnimatorManager.init();
DrawableSerialization.init();
}
}

Expand All @@ -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 <A> void test(Codec<A> 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) {
Expand Down
7 changes: 6 additions & 1 deletion src/main/java/brachy/modularui/ModularUI.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -69,6 +70,10 @@ public static boolean isDev() {
return !isProd();
}

public static boolean isTestEnv() {
return UNIT_TEST;
}

/**
* @return if we're running data generation
*/
Expand Down Expand Up @@ -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());
}

/**
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/brachy/modularui/ModularUIConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
49 changes: 0 additions & 49 deletions src/main/java/brachy/modularui/api/IJsonSerializable.java

This file was deleted.

33 changes: 32 additions & 1 deletion src/main/java/brachy/modularui/api/ITheme.java
Original file line number Diff line number Diff line change
Expand Up @@ -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<ITheme> ENCODER = new Encoder<>() {
@Override
public <T> DataResult<T> encode(ITheme input, DynamicOps<T> 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<String, Set<WidgetTheme>> 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.
*/
Expand Down
86 changes: 51 additions & 35 deletions src/main/java/brachy/modularui/api/IThemeApi.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<WidgetTheme> 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<WidgetTheme> PANEL = get().widgetThemeKeyBuilder("panel", WidgetTheme.class)
.defaultTheme(WidgetTheme.darkTextNoShadow(176, 166, GuiTextures.MC_BACKGROUND))
.fieldsOf(FALLBACK)
.register();

WidgetThemeKey<WidgetTheme> 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<WidgetTheme> 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<WidgetTheme> SCROLLBAR = get().widgetThemeKeyBuilder("scrollbar", WidgetTheme.class)
.defaultTheme(WidgetTheme.darkTextNoShadow(4, 4, Scrollbar.VANILLA))
.fieldsOf(FALLBACK)
.register();

WidgetThemeKey<SlotTheme> 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<SlotTheme> FLUID_SLOT = get().widgetThemeKeyBuilder("fluidSlot", SlotTheme.class)
.defaultTheme(new SlotTheme(GuiTextures.SLOT_FLUID))
.fieldsOf(ITEM_SLOT)
.register();

WidgetThemeKey<TextFieldTheme> 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<SelectableTheme> 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
Expand All @@ -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
Expand Down Expand Up @@ -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
<T extends WidgetTheme> WidgetThemeKey<T> registerWidgetTheme(String id, T defaultTheme, T defaultHoverTheme,
WidgetThemeParser<T> parser);
WidgetThemeMerger<T> merger, WidgetThemeCodec<T> codec);

default <T extends WidgetTheme> WidgetThemeKeyBuilder<T> widgetThemeKeyBuilder(String id, Class<T> type) {
return new WidgetThemeKeyBuilder<>(id);
return new WidgetThemeKeyBuilder<>(id, type);
}

@UnmodifiableView
Expand Down
Loading